patchrelay 0.74.8 → 0.75.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/build-info.json +3 -3
- package/dist/cli/watch/use-detail-stream.js +64 -6
- package/dist/http.js +13 -0
- package/dist/service.js +16 -0
- package/dist/telemetry.js +16 -0
- package/package.json +1 -1
package/dist/build-info.json
CHANGED
|
@@ -1,4 +1,7 @@
|
|
|
1
1
|
import { useEffect, useRef } from "react";
|
|
2
|
+
const DETAIL_REHYDRATE_INTERVAL_MS = 3000;
|
|
3
|
+
const FEED_REHYDRATE_LIMIT = 100;
|
|
4
|
+
const MAX_CACHED_FEED_EVENTS = 300;
|
|
2
5
|
export function useDetailStream(options) {
|
|
3
6
|
const optionsRef = useRef(options);
|
|
4
7
|
optionsRef.current = options;
|
|
@@ -14,10 +17,24 @@ export function useDetailStream(options) {
|
|
|
14
17
|
if (bearerToken) {
|
|
15
18
|
headers.authorization = `Bearer ${bearerToken}`;
|
|
16
19
|
}
|
|
17
|
-
|
|
20
|
+
const feedState = {
|
|
21
|
+
lastFeedEventId: undefined,
|
|
22
|
+
feedEvents: [],
|
|
23
|
+
};
|
|
24
|
+
let inFlight = false;
|
|
25
|
+
const runRehydrate = () => {
|
|
26
|
+
if (inFlight)
|
|
27
|
+
return;
|
|
28
|
+
inFlight = true;
|
|
29
|
+
void rehydrate(baseUrl, issueKey, headers, abortController.signal, dispatch, feedState)
|
|
30
|
+
.finally(() => {
|
|
31
|
+
inFlight = false;
|
|
32
|
+
});
|
|
33
|
+
};
|
|
34
|
+
runRehydrate();
|
|
18
35
|
const intervalId = setInterval(() => {
|
|
19
|
-
|
|
20
|
-
},
|
|
36
|
+
runRehydrate();
|
|
37
|
+
}, DETAIL_REHYDRATE_INTERVAL_MS);
|
|
21
38
|
return () => {
|
|
22
39
|
clearInterval(intervalId);
|
|
23
40
|
abortController.abort();
|
|
@@ -25,17 +42,22 @@ export function useDetailStream(options) {
|
|
|
25
42
|
}, [options.issueKey, options.active]);
|
|
26
43
|
}
|
|
27
44
|
// ─── Rehydration ──────────────────────────────────────────────────
|
|
28
|
-
async function rehydrate(baseUrl, issueKey, headers, signal, dispatch) {
|
|
45
|
+
async function rehydrate(baseUrl, issueKey, headers, signal, dispatch, feedState) {
|
|
29
46
|
try {
|
|
30
47
|
const url = new URL(`/api/issues/${encodeURIComponent(issueKey)}`, baseUrl);
|
|
31
|
-
const
|
|
48
|
+
const feedUrl = buildFeedUrl(baseUrl, issueKey, feedState.lastFeedEventId);
|
|
49
|
+
const [response, newFeedEvents] = await Promise.all([
|
|
50
|
+
fetch(url, { headers: { ...headers, accept: "application/json" }, signal }),
|
|
51
|
+
fetchFeedEvents(feedUrl, headers, signal),
|
|
52
|
+
]);
|
|
32
53
|
if (!response.ok)
|
|
33
54
|
return;
|
|
55
|
+
updateFeedState(feedState, newFeedEvents);
|
|
34
56
|
const data = await response.json();
|
|
35
57
|
dispatch({
|
|
36
58
|
type: "timeline-rehydrate",
|
|
37
59
|
runs: Array.isArray(data.runs) ? data.runs : [],
|
|
38
|
-
feedEvents:
|
|
60
|
+
feedEvents: feedState.feedEvents,
|
|
39
61
|
liveThread: data.liveThread ?? null,
|
|
40
62
|
activeRunId: data.activeRun?.id ?? null,
|
|
41
63
|
activeRunStartedAt: data.activeRun?.startedAt ?? null,
|
|
@@ -46,3 +68,39 @@ async function rehydrate(baseUrl, issueKey, headers, signal, dispatch) {
|
|
|
46
68
|
// Rehydration is best-effort
|
|
47
69
|
}
|
|
48
70
|
}
|
|
71
|
+
function buildFeedUrl(baseUrl, issueKey, afterId) {
|
|
72
|
+
const feedUrl = new URL(`/api/issues/${encodeURIComponent(issueKey)}/feed`, baseUrl);
|
|
73
|
+
feedUrl.searchParams.set("limit", String(FEED_REHYDRATE_LIMIT));
|
|
74
|
+
if (afterId !== undefined) {
|
|
75
|
+
feedUrl.searchParams.set("afterId", String(afterId));
|
|
76
|
+
}
|
|
77
|
+
return feedUrl;
|
|
78
|
+
}
|
|
79
|
+
async function fetchFeedEvents(url, headers, signal) {
|
|
80
|
+
try {
|
|
81
|
+
const response = await fetch(url, { headers: { ...headers, accept: "application/json" }, signal });
|
|
82
|
+
if (!response.ok)
|
|
83
|
+
return [];
|
|
84
|
+
const data = await response.json();
|
|
85
|
+
return Array.isArray(data.events) ? data.events : [];
|
|
86
|
+
}
|
|
87
|
+
catch {
|
|
88
|
+
return [];
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
function updateFeedState(feedState, newEvents) {
|
|
92
|
+
if (newEvents.length === 0)
|
|
93
|
+
return;
|
|
94
|
+
const byId = new Map();
|
|
95
|
+
for (const event of feedState.feedEvents) {
|
|
96
|
+
byId.set(event.id, event);
|
|
97
|
+
}
|
|
98
|
+
for (const event of newEvents) {
|
|
99
|
+
byId.set(event.id, event);
|
|
100
|
+
}
|
|
101
|
+
const feedEvents = [...byId.values()]
|
|
102
|
+
.sort((left, right) => left.id - right.id)
|
|
103
|
+
.slice(-MAX_CACHED_FEED_EVENTS);
|
|
104
|
+
feedState.feedEvents = feedEvents;
|
|
105
|
+
feedState.lastFeedEventId = feedEvents.at(-1)?.id;
|
|
106
|
+
}
|
package/dist/http.js
CHANGED
|
@@ -264,6 +264,19 @@ export async function buildHttpServer(config, service, logger) {
|
|
|
264
264
|
}
|
|
265
265
|
return reply.send({ ok: true, ...result });
|
|
266
266
|
});
|
|
267
|
+
app.get("/api/issues/:issueKey/feed", async (request, reply) => {
|
|
268
|
+
const issueKey = request.params.issueKey;
|
|
269
|
+
const afterId = getPositiveIntegerQueryParam(request, "afterId");
|
|
270
|
+
const requestedLimit = getPositiveIntegerQueryParam(request, "limit");
|
|
271
|
+
const result = service.listIssueFeedEvents(issueKey, {
|
|
272
|
+
...(afterId !== undefined ? { afterId } : {}),
|
|
273
|
+
limit: requestedLimit ? Math.min(requestedLimit, 100) : 100,
|
|
274
|
+
});
|
|
275
|
+
if (!result) {
|
|
276
|
+
return reply.code(404).send({ ok: false, reason: "issue_not_found" });
|
|
277
|
+
}
|
|
278
|
+
return reply.send({ ok: true, events: result.events });
|
|
279
|
+
});
|
|
267
280
|
app.get("/api/issues/:issueKey/live", async (request, reply) => {
|
|
268
281
|
const issueKey = request.params.issueKey;
|
|
269
282
|
const result = await service.getActiveRunStatus(issueKey);
|
package/dist/service.js
CHANGED
|
@@ -338,6 +338,22 @@ export class PatchRelayService {
|
|
|
338
338
|
async getIssueOverview(issueKey) {
|
|
339
339
|
return await this.queryService.getIssueOverview(issueKey);
|
|
340
340
|
}
|
|
341
|
+
listIssueFeedEvents(issueKey, options) {
|
|
342
|
+
const session = this.db.issueSessions.getIssueSessionByKey(issueKey);
|
|
343
|
+
const issue = this.db.issues.getIssueByKey(issueKey);
|
|
344
|
+
const projectId = session?.projectId ?? issue?.projectId;
|
|
345
|
+
const resolvedIssueKey = session?.issueKey ?? issue?.issueKey ?? issueKey;
|
|
346
|
+
if (!projectId)
|
|
347
|
+
return undefined;
|
|
348
|
+
return {
|
|
349
|
+
events: this.db.operatorFeed.list({
|
|
350
|
+
issueKey: resolvedIssueKey,
|
|
351
|
+
projectId,
|
|
352
|
+
...(options?.afterId !== undefined ? { afterId: options.afterId } : {}),
|
|
353
|
+
limit: Math.min(options?.limit ?? 100, 100),
|
|
354
|
+
}),
|
|
355
|
+
};
|
|
356
|
+
}
|
|
341
357
|
async getActiveRunStatus(issueKey) {
|
|
342
358
|
return await this.orchestrator.getActiveRunStatus(issueKey);
|
|
343
359
|
}
|
package/dist/telemetry.js
CHANGED
|
@@ -78,8 +78,24 @@ export class OperatorFeedTelemetrySink {
|
|
|
78
78
|
};
|
|
79
79
|
}
|
|
80
80
|
return undefined;
|
|
81
|
+
case "health.invariant":
|
|
82
|
+
return {
|
|
83
|
+
level: event.status === "observed" ? "warn" : "info",
|
|
84
|
+
kind: "workflow",
|
|
85
|
+
...(event.issueKey ? { issueKey: event.issueKey } : {}),
|
|
86
|
+
...(event.projectId ? { projectId: event.projectId } : {}),
|
|
87
|
+
...(event.runType ? { stage: event.runType } : {}),
|
|
88
|
+
status: `health_${event.status}`,
|
|
89
|
+
summary: event.status === "observed"
|
|
90
|
+
? `Health warning: ${formatInvariant(event.invariant)}`
|
|
91
|
+
: `Health repaired: ${formatInvariant(event.invariant)}`,
|
|
92
|
+
...(event.detail ? { detail: event.detail } : {}),
|
|
93
|
+
};
|
|
81
94
|
default:
|
|
82
95
|
return undefined;
|
|
83
96
|
}
|
|
84
97
|
}
|
|
85
98
|
}
|
|
99
|
+
function formatInvariant(invariant) {
|
|
100
|
+
return invariant.replaceAll("_", " ");
|
|
101
|
+
}
|