Skip to content

Commit 46e42b1

Browse files
authored
Merge pull request #1363 from chhsiao1981/PACS-sse
pacs with sse
2 parents 4ef3268 + 0185707 commit 46e42b1

File tree

10 files changed

+347
-167
lines changed

10 files changed

+347
-167
lines changed

src/api/lonk/LonkSubscriber.ts

Lines changed: 12 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -145,21 +145,25 @@ class LonkSubscriber {
145145
}
146146
}
147147

148-
function isSubscribed(msg: { [key: string]: any }): msg is LonkSubscription {
148+
export const isSubscribed = (msg: {
149+
[key: string]: any;
150+
}): msg is LonkSubscription => {
149151
return "subscribed" in msg && msg.subscribed === true;
150-
}
152+
};
151153

152-
function isDone(msg: { [key: string]: any }): msg is LonkDone {
154+
export const isDone = (msg: { [key: string]: any }): msg is LonkDone => {
153155
return "done" in msg && msg.done === true;
154-
}
156+
};
155157

156-
function isProgress(msg: { [key: string]: any }): msg is LonkProgress {
158+
export const isProgress = (msg: {
159+
[key: string]: any;
160+
}): msg is LonkProgress => {
157161
return "ndicom" in msg && Number.isInteger(msg.ndicom);
158-
}
162+
};
159163

160-
function isError(msg: { [key: string]: any }): msg is LonkError {
164+
export const isError = (msg: { [key: string]: any }): msg is LonkError => {
161165
return "error" in msg;
162-
}
166+
};
163167

164168
export default LonkSubscriber;
165169
export type { LonkHandlers };

src/components/Pacs/PacsApp.tsx

Lines changed: 80 additions & 98 deletions
Original file line numberDiff line numberDiff line change
@@ -5,25 +5,28 @@
55
*/
66

77
import { PageSection } from "@patternfly/react-core";
8-
import { App } from "antd";
9-
import type React from "react";
8+
import config from "config";
9+
import { access } from "fs";
1010
import { useEffect, useState } from "react";
1111
import {
12+
genUUID,
1213
getRoot,
1314
getRootID,
15+
getState,
1416
type ModuleToFunc,
17+
StateType,
1518
useReducer,
1619
} from "react-reducer-utils";
17-
import { useLonk } from "../../api/lonk/index.ts";
20+
import { useLocation, useSearchParams } from "react-router-dom";
21+
import type { Lonk, LonkMessageData } from "../../api/lonk/types.ts";
1822
import type { PACSqueryCore } from "../../api/pfdcm/index.ts";
1923
import * as DoPacs from "../../reducers/pacs";
2024
import ErrorScreen from "./components/ErrorScreen.tsx";
2125
import PacsLoadingScreen from "./components/PacsLoadingScreen.tsx";
22-
import { getSeriesDescription } from "./components/utils.ts";
2326
import { DEFAULT_PREFERENCES } from "./defaultPreferences.ts";
2427
import styles from "./PacsApp.module.css";
2528
import PacsView from "./PacsView.tsx";
26-
import { type PacsState, SeriesPullState } from "./types.ts";
29+
import { type PacsState, QUERY_PROMPT, SearchMode } from "./types.ts";
2730
import { createFeedWithSeriesInstanceUID, errorCodeIsNot4xx } from "./utils.ts";
2831

2932
type TDoPacs = ModuleToFunc<typeof DoPacs>;
@@ -76,22 +79,25 @@ export default () => {
7679
// ========================================
7780
// CLIENTS AND MISC
7881
// ========================================
79-
const { message } = App.useApp();
80-
81-
const [statePacs, doPacs] = useReducer<DoPacs.State, TDoPacs>(DoPacs);
82+
const [statePacs, doPacs] = useReducer<DoPacs.State, TDoPacs>(
83+
DoPacs,
84+
StateType.LOCAL,
85+
);
86+
const [searchParams, setSearchParams] = useSearchParams();
87+
const location = useLocation();
88+
const [pacsID, _] = useState(genUUID());
8289

83-
const pacsID = getRootID(statePacs);
84-
const pacs = getRoot(statePacs) ?? DoPacs.defaultState;
90+
const pacs = getState(statePacs, pacsID) ?? DoPacs.defaultState;
8591

8692
const {
8793
expandedStudyUids,
8894
expandedSeries,
8995
studies,
9096
services,
97+
service,
9198
isGetServices,
92-
seriesMap,
9399
isLoadingStudies,
94-
wsUrl,
100+
isExpandedAllDone,
95101
} = pacs;
96102

97103
// ========================================
@@ -101,7 +107,7 @@ export default () => {
101107
/**
102108
* Indicates a fatal error with the WebSocket.
103109
*/
104-
const [wsError, setWsError] = useState<React.ReactNode | null>(null);
110+
const [wsError, setWsError] = useState("");
105111
// TODO create a settings component for changing preferences
106112
const [preferences, setPreferences] = useState(DEFAULT_PREFERENCES);
107113

@@ -115,78 +121,6 @@ export default () => {
115121
const state: PacsState = { preferences, studies };
116122

117123
const error = wsError || pacs.errmsg;
118-
// ========================================
119-
// LONK WEBSOCKET
120-
// ========================================
121-
const lonk = useLonk({
122-
url: wsUrl,
123-
onDone: async (pacs_name: string, SeriesInstanceUID: string) => {
124-
doPacs.updateReceiveState(pacsID, pacs_name, SeriesInstanceUID, {
125-
pullState: SeriesPullState.WAITING_OR_COMPLETE,
126-
done: true,
127-
});
128-
doPacs.queryCubeSeriesStateBySeriesUID(
129-
pacsID,
130-
pacs_name,
131-
SeriesInstanceUID,
132-
);
133-
134-
await createFeedWithSeriesInstanceUID(SeriesInstanceUID);
135-
136-
return;
137-
},
138-
onProgress: (
139-
pacs_name: string,
140-
SeriesInstanceUID: string,
141-
ndicom: number,
142-
) => {
143-
doPacs.updateReceiveState(
144-
pacsID,
145-
pacs_name,
146-
SeriesInstanceUID,
147-
{ receivedCount: ndicom },
148-
(theOrig: number, theNew: number) => theOrig < theNew,
149-
);
150-
},
151-
onLonkError: (
152-
pacs_name: string,
153-
SeriesInstanceUID: string,
154-
error: string,
155-
) => {
156-
doPacs.pushReceiveStateError(pacsID, pacs_name, SeriesInstanceUID, error);
157-
158-
const desc = getSeriesDescription(
159-
pacs_name,
160-
SeriesInstanceUID,
161-
seriesMap,
162-
);
163-
message.error(
164-
<>There was an error while receiving the series "{desc}"</>,
165-
);
166-
},
167-
onMessageError: (data: any, error: string) => {
168-
message.error(
169-
<>
170-
A <em>LONK</em> error occurred, please check the console.
171-
</>,
172-
);
173-
},
174-
heartbeat: false,
175-
retryOnError: true,
176-
reconnectAttempts: 3,
177-
reconnectInterval: 3000,
178-
shouldReconnect: errorCodeIsNot4xx,
179-
onReconnectStop: () => {
180-
console.error("PacsApp.lonk: onReconnectStop");
181-
setWsError(<>The WebSocket is disconnected.</>);
182-
},
183-
onWebsocketError: () => {
184-
console.error("PacsApp.lonk: onWebsocketError");
185-
message.error(
186-
<>There was an error with the WebSocket. Reconnecting&hellip;</>,
187-
);
188-
},
189-
});
190124

191125
// ========================================
192126
// CALLBACKS
@@ -216,35 +150,83 @@ export default () => {
216150
// ========================================
217151

218152
// init
219-
// biome-ignore lint/correctness/useExhaustiveDependencies: doPacs.init
220153
useEffect(() => {
154+
doPacs.init(pacsID);
155+
156+
doPacs.updateServiceQueryBySearchParams(pacsID, location, searchParams);
157+
}, [
158+
pacsID,
159+
doPacs.init,
160+
doPacs.updateServiceQueryBySearchParams,
161+
location,
162+
searchParams,
163+
]);
164+
165+
useEffect(() => {
166+
if (!pacsID) {
167+
return;
168+
}
169+
170+
if (!location.pathname.startsWith("/pacs")) {
171+
return;
172+
}
173+
221174
// set document title.
222175
const originalTitle = document.title;
223176
document.title = "ChRIS PACS";
224177

225-
// doPacs
226-
doPacs.init();
178+
doPacs.updateServiceQueryBySearchParams(pacsID, location, searchParams);
227179

228180
return () => {
229181
document.title = originalTitle;
230182
};
231-
}, []);
183+
}, [
184+
pacsID,
185+
location,
186+
location.pathname,
187+
searchParams,
188+
doPacs.updateServiceQueryBySearchParams,
189+
]);
232190

233191
// Subscribe to all expanded series
234192
// biome-ignore lint/correctness/useExhaustiveDependencies: updateReceiveState
235193
useEffect(() => {
236-
for (const { pacs_name, SeriesInstanceUID } of expandedSeries) {
237-
lonk
238-
.subscribe(pacs_name, SeriesInstanceUID)
239-
.then(({ pacs_name, SeriesInstanceUID }) => {
240-
doPacs.updateReceiveState(pacsID, pacs_name, SeriesInstanceUID, {
241-
subscribed: true,
242-
});
243-
});
194+
if (wsError) {
195+
return;
244196
}
197+
198+
if (isExpandedAllDone) {
199+
return;
200+
}
201+
202+
if (!expandedSeries.length) {
203+
return;
204+
}
205+
206+
const series_uids = expandedSeries
207+
.map((each) => each.SeriesInstanceUID)
208+
.join(",");
209+
210+
const url = `${config.API_ROOT}/pacs/sse/?pacs_name=${service}&series_uids=${series_uids}`;
211+
const eventSource = new EventSource(url);
212+
213+
eventSource.onmessage = (event) => {
214+
const data: Lonk<LonkMessageData> = JSON.parse(event.data);
215+
doPacs.processLonkMsg(pacsID, data);
216+
};
217+
218+
eventSource.onerror = (err) => {
219+
console.error("PacsApp.eventSource.onerror: err:", err);
220+
setWsError(`event error: ${err}`);
221+
};
222+
223+
return () => {
224+
console.info("PacsApp.eventSource: to return");
225+
eventSource.close();
226+
};
245227
// Note: we are subscribing to series, but never unsubscribing.
246228
// This is mostly harmless.
247-
}, [expandedSeries]);
229+
}, [expandedSeries, wsError, isExpandedAllDone]);
248230

249231
// ========================================
250232
// RENDER

src/components/Pacs/PacsView.tsx

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { Empty, Flex, Spin, Typography } from "antd";
22
import type { CSSProperties } from "react";
3-
import type { DispatchFuncMap } from "react-reducer-utils";
3+
import type { DispatchFuncMap, ModuleToFunc } from "react-reducer-utils";
44
import type { PACSqueryCore } from "../../api/pfdcm";
55
import type * as DoPacs from "../../reducers/pacs";
66
import PacsInput, {
@@ -11,6 +11,8 @@ import PacsStudiesView, {
1111
} from "./components/PacsStudiesView.tsx";
1212
import type { PacsState } from "./types.ts";
1313

14+
type TDoPacs = ModuleToFunc<typeof DoPacs>;
15+
1416
type Props = Pick<PacsInputProps, "services" | "onSubmit"> &
1517
Pick<PacsStudiesViewProps, "expandedStudyUids"> & {
1618
onRetrieve: (service: string, query: PACSqueryCore) => void;
@@ -23,7 +25,7 @@ type Props = Pick<PacsInputProps, "services" | "onSubmit"> &
2325

2426
pacsID: string;
2527
pacs: DoPacs.State;
26-
doPacs: DispatchFuncMap;
28+
doPacs: DispatchFuncMap<DoPacs.State, TDoPacs>;
2729
};
2830

2931
/**
@@ -48,6 +50,7 @@ export default (props: Props) => {
4850
} = props;
4951

5052
const service = pacs.service;
53+
5154
const setService = (service: string) => {
5255
doPacs.setService(pacsID, service);
5356
};

src/components/Pacs/components/PacsInput.tsx

Lines changed: 2 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -3,20 +3,14 @@ import { Col, Row, Segmented } from "antd";
33
import type { ReadonlyNonEmptyArray } from "fp-ts/ReadonlyNonEmptyArray";
44
import { useEffect, useState } from "react";
55
import { useLocation, useSearchParams } from "react-router-dom";
6-
import type { PACSqueryCore } from "../../../api/pfdcm";
76
import OperationButton from "../../NewLibrary/components/operations/OperationButton";
8-
import type { PacsStudyState } from "../types";
7+
import { type PacsStudyState, QUERY_PROMPT, SearchMode } from "../types";
98
import PacsInputText from "./PacsInputText";
109
import ScreenSizeSpan from "./ScreenSizeSpan";
1110
import ServiceDropdown from "./ServiceDropdown";
1211

1312
import { downloadStudiesToCSV } from "./utils";
1413

15-
enum SearchMode {
16-
MRN = "mrn",
17-
AccessNo = "accno",
18-
}
19-
2014
export type Props = {
2115
services: ReadonlyNonEmptyArray<string>;
2216
service: string;
@@ -92,8 +86,7 @@ export default (props: Props) => {
9286
return;
9387
}
9488

95-
const prompt =
96-
searchMode === SearchMode.MRN ? "PatientID" : "AccessionNumber";
89+
const prompt = QUERY_PROMPT[searchMode];
9790

9891
console.info(
9992
"PacsInput: to onSubmit: service:",

src/components/Pacs/components/SeriesRow.tsx

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -128,7 +128,13 @@ const SeriesRow: React.FC<SeriesRowProps> = ({
128128
const showModal = () => {
129129
if (!hasMultipleSelected) {
130130
setFeedName(
131-
generateFeedName({ info, errors, pullState, inCube, receivedCount }),
131+
generateFeedName({
132+
info,
133+
errors,
134+
pullState,
135+
inCube,
136+
receivedCount,
137+
}),
132138
);
133139
}
134140
setIsModalOpen(true);
@@ -183,7 +189,7 @@ const SeriesRow: React.FC<SeriesRowProps> = ({
183189
};
184190

185191
const contextMenuItems = getSeriesContextMenuItems(
186-
{ info, errors, pullState, inCube, receivedCount },
192+
{ info, errors, pullState, inCube, receivedCount: receivedCount },
187193
selected,
188194
selectedSeries.length,
189195
contextMenuHandlers,
@@ -254,7 +260,7 @@ const SeriesRow: React.FC<SeriesRowProps> = ({
254260
errors,
255261
pullState,
256262
inCube,
257-
receivedCount,
263+
receivedCount: receivedCount,
258264
});
259265
} else {
260266
message.info("Please retrieve the series first");

0 commit comments

Comments
 (0)