Intermediate commit
This commit is contained in:
parent
0e9e49df16
commit
3d4cfeff1a
65 changed files with 5507 additions and 333 deletions
|
|
@ -49,6 +49,35 @@ function buildRunsPayload(runs: ReturnType<typeof buildRun>[]) {
|
|||
};
|
||||
}
|
||||
|
||||
class FakeEventSource {
|
||||
static instances: FakeEventSource[] = [];
|
||||
|
||||
public readonly url: string;
|
||||
public closed = false;
|
||||
private listeners = new Map<string, Array<(event: { data: string }) => void>>();
|
||||
|
||||
constructor(url: string) {
|
||||
this.url = url;
|
||||
FakeEventSource.instances.push(this);
|
||||
}
|
||||
|
||||
addEventListener(eventType: string, callback: (event: { data: string }) => void): void {
|
||||
const existing = this.listeners.get(eventType) ?? [];
|
||||
this.listeners.set(eventType, [...existing, callback]);
|
||||
}
|
||||
|
||||
emit(eventType: string, payload: unknown): void {
|
||||
const callbacks = this.listeners.get(eventType) ?? [];
|
||||
for (const callback of callbacks) {
|
||||
callback({ data: JSON.stringify(payload) });
|
||||
}
|
||||
}
|
||||
|
||||
close(): void {
|
||||
this.closed = true;
|
||||
}
|
||||
}
|
||||
|
||||
describe("run status store", () => {
|
||||
const mockedListRuns = vi.mocked(listRuns);
|
||||
const mockedTriggerManualRun = vi.mocked(triggerManualRun);
|
||||
|
|
@ -309,4 +338,42 @@ describe("run status store", () => {
|
|||
|
||||
expect(store.latestRun?.new_publication_count).toBe(5);
|
||||
});
|
||||
|
||||
it("applies identifier_updated SSE events to live publications", () => {
|
||||
const previousEventSource = (globalThis as any).EventSource;
|
||||
FakeEventSource.instances = [];
|
||||
(globalThis as any).EventSource = FakeEventSource as any;
|
||||
try {
|
||||
const store = useRunStatusStore();
|
||||
store.setLatestRun(buildRun({ id: 314, status: "running", end_dt: null }));
|
||||
|
||||
const stream = FakeEventSource.instances[0];
|
||||
expect(stream).toBeDefined();
|
||||
stream.emit("publication_discovered", {
|
||||
publication_id: 22,
|
||||
scholar_profile_id: 7,
|
||||
scholar_label: "Ada Lovelace",
|
||||
title: "Optimization Notes",
|
||||
pub_url: null,
|
||||
first_seen_at: "2026-02-26T10:00:00Z",
|
||||
});
|
||||
expect(store.livePublications).toHaveLength(1);
|
||||
expect(store.livePublications[0].display_identifier).toBeNull();
|
||||
|
||||
stream.emit("identifier_updated", {
|
||||
publication_id: 22,
|
||||
display_identifier: {
|
||||
kind: "doi",
|
||||
value: "10.1000/xyz",
|
||||
label: "DOI: 10.1000/xyz",
|
||||
url: "https://doi.org/10.1000/xyz",
|
||||
confidence_score: 0.95,
|
||||
},
|
||||
});
|
||||
expect(store.livePublications[0].display_identifier?.kind).toBe("doi");
|
||||
expect(store.livePublications[0].display_identifier?.value).toBe("10.1000/xyz");
|
||||
} finally {
|
||||
(globalThis as any).EventSource = previousEventSource;
|
||||
}
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -40,6 +40,8 @@ let eventSource: EventSource | null = null;
|
|||
let activeStreamRunId: number | null = null;
|
||||
const ACTIVE_STATUSES = new Set(["running", "resolving"]);
|
||||
|
||||
type StreamDisplayIdentifier = PublicationItem["display_identifier"];
|
||||
|
||||
function parseRunId(value: unknown): number | null {
|
||||
if (typeof value === "number" && Number.isFinite(value)) {
|
||||
return value;
|
||||
|
|
@ -86,6 +88,46 @@ function parsePublicationCount(value: unknown, fallback: number): number {
|
|||
return fallback;
|
||||
}
|
||||
|
||||
function parseDisplayIdentifier(value: unknown): StreamDisplayIdentifier {
|
||||
if (!value || typeof value !== "object") {
|
||||
return null;
|
||||
}
|
||||
const payload = value as Record<string, unknown>;
|
||||
if (typeof payload.kind !== "string" || typeof payload.value !== "string" || typeof payload.label !== "string") {
|
||||
return null;
|
||||
}
|
||||
if (typeof payload.confidence_score !== "number" || !Number.isFinite(payload.confidence_score)) {
|
||||
return null;
|
||||
}
|
||||
const url = typeof payload.url === "string" ? payload.url : null;
|
||||
return {
|
||||
kind: payload.kind,
|
||||
value: payload.value,
|
||||
label: payload.label,
|
||||
url,
|
||||
confidence_score: payload.confidence_score,
|
||||
};
|
||||
}
|
||||
|
||||
function withUpdatedDisplayIdentifier(
|
||||
items: PublicationItem[],
|
||||
update: {
|
||||
publicationId: number;
|
||||
displayIdentifier: StreamDisplayIdentifier;
|
||||
},
|
||||
): PublicationItem[] {
|
||||
const { publicationId, displayIdentifier } = update;
|
||||
let changed = false;
|
||||
const next = items.map((item) => {
|
||||
if (item.publication_id !== publicationId) {
|
||||
return item;
|
||||
}
|
||||
changed = true;
|
||||
return { ...item, display_identifier: displayIdentifier };
|
||||
});
|
||||
return changed ? next : items;
|
||||
}
|
||||
|
||||
function reconcileRunCounters(previous: RunListItem | null, next: RunListItem | null): RunListItem | null {
|
||||
if (previous === null || next === null) {
|
||||
return next;
|
||||
|
|
@ -200,7 +242,7 @@ export const useRunStatusStore = defineStore("runStatus", {
|
|||
this.updateEventSource();
|
||||
},
|
||||
updateEventSource(): void {
|
||||
const targetRunId = this.latestRun?.status === "running" ? this.latestRun.id : null;
|
||||
const targetRunId = isActiveStatus(this.latestRun?.status) ? this.latestRun?.id ?? null : null;
|
||||
if (activeStreamRunId === targetRunId) {
|
||||
return;
|
||||
}
|
||||
|
|
@ -251,6 +293,25 @@ export const useRunStatusStore = defineStore("runStatus", {
|
|||
console.error("Failed to parse SSE event", err);
|
||||
}
|
||||
});
|
||||
eventSource.addEventListener("identifier_updated", (e) => {
|
||||
try {
|
||||
const data = JSON.parse(e.data);
|
||||
const publicationId = parseRunId(data?.publication_id);
|
||||
const displayIdentifier = parseDisplayIdentifier(data?.display_identifier);
|
||||
if (publicationId === null || displayIdentifier === null) {
|
||||
return;
|
||||
}
|
||||
this.livePublications = withUpdatedDisplayIdentifier(
|
||||
this.livePublications,
|
||||
{
|
||||
publicationId,
|
||||
displayIdentifier,
|
||||
},
|
||||
);
|
||||
} catch (err) {
|
||||
console.error("Failed to parse SSE event", err);
|
||||
}
|
||||
});
|
||||
eventSource.onerror = () => {
|
||||
// Reconnecting is handled automatically by EventSource,
|
||||
// but if it's permanently closed, we could do something here.
|
||||
|
|
@ -429,6 +490,7 @@ export const useRunStatusStore = defineStore("runStatus", {
|
|||
this.lastErrorRequestId = null;
|
||||
this.lastSyncAt = null;
|
||||
this.safetyState = createDefaultSafetyState();
|
||||
this.livePublications = [];
|
||||
},
|
||||
},
|
||||
});
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue