u
This commit is contained in:
@@ -12,10 +12,11 @@ import CloseIcon from '@mui/icons-material/Close';
|
|||||||
import DeleteSweepIcon from '@mui/icons-material/DeleteSweep';
|
import DeleteSweepIcon from '@mui/icons-material/DeleteSweep';
|
||||||
import DragIndicatorIcon from '@mui/icons-material/DragIndicator';
|
import DragIndicatorIcon from '@mui/icons-material/DragIndicator';
|
||||||
import SensorsIcon from '@mui/icons-material/Sensors';
|
import SensorsIcon from '@mui/icons-material/Sensors';
|
||||||
import sseAutomat from '../automats/sseAutomat';
|
import sseAutomat, { getSseClientId } from '../automats/sseAutomat';
|
||||||
|
|
||||||
const PANEL_WIDTH = 420;
|
const PANEL_WIDTH = 420;
|
||||||
const PANEL_HEIGHT = 380;
|
const PANEL_HEIGHT = 380;
|
||||||
|
const RAW_EVENT_LIMIT = 500;
|
||||||
const pulseAnimationA = keyframes`
|
const pulseAnimationA = keyframes`
|
||||||
0% { transform: scale(1); box-shadow: 0 0 0 0 rgba(255, 255, 255, 0); }
|
0% { transform: scale(1); box-shadow: 0 0 0 0 rgba(255, 255, 255, 0); }
|
||||||
35% { transform: scale(1.16); box-shadow: 0 0 0 8px rgba(255, 255, 255, 0.55); }
|
35% { transform: scale(1.16); box-shadow: 0 0 0 8px rgba(255, 255, 255, 0.55); }
|
||||||
@@ -42,11 +43,15 @@ class SseMonitor extends PureComponent {
|
|||||||
this.state = {
|
this.state = {
|
||||||
status: sseAutomat.state.status,
|
status: sseAutomat.state.status,
|
||||||
companyId: sseAutomat.state.companyId,
|
companyId: sseAutomat.state.companyId,
|
||||||
events: sseAutomat.state.events,
|
events: [],
|
||||||
open: false,
|
open: false,
|
||||||
position: null,
|
position: null,
|
||||||
eventPulse: 0,
|
eventPulse: 0,
|
||||||
};
|
};
|
||||||
|
this.nextRawId = 0;
|
||||||
|
this.rawAbort = null;
|
||||||
|
this.rawReconnectTimer = null;
|
||||||
|
this.rawBuffer = '';
|
||||||
this.panelRef = createRef();
|
this.panelRef = createRef();
|
||||||
this.handleToggle = this.handleToggle.bind(this);
|
this.handleToggle = this.handleToggle.bind(this);
|
||||||
this.handleKeyDown = this.handleKeyDown.bind(this);
|
this.handleKeyDown = this.handleKeyDown.bind(this);
|
||||||
@@ -61,14 +66,15 @@ class SseMonitor extends PureComponent {
|
|||||||
this.unsubscribe = sseAutomat.subscribe(this, (state) => ({
|
this.unsubscribe = sseAutomat.subscribe(this, (state) => ({
|
||||||
status: state.status,
|
status: state.status,
|
||||||
companyId: state.companyId,
|
companyId: state.companyId,
|
||||||
events: state.events,
|
|
||||||
}));
|
}));
|
||||||
window.addEventListener('keydown', this.handleKeyDown);
|
window.addEventListener('keydown', this.handleKeyDown);
|
||||||
|
this.startRawStream();
|
||||||
}
|
}
|
||||||
|
|
||||||
componentWillUnmount() {
|
componentWillUnmount() {
|
||||||
this.unsubscribe?.();
|
this.unsubscribe?.();
|
||||||
window.removeEventListener('keydown', this.handleKeyDown);
|
window.removeEventListener('keydown', this.handleKeyDown);
|
||||||
|
this.stopRawStream();
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidUpdate(prevProps, prevState) {
|
componentDidUpdate(prevProps, prevState) {
|
||||||
@@ -77,6 +83,96 @@ class SseMonitor extends PureComponent {
|
|||||||
if (currentEventId != null && currentEventId !== previousEventId) {
|
if (currentEventId != null && currentEventId !== previousEventId) {
|
||||||
this.setState((state) => ({ eventPulse: state.eventPulse + 1 }));
|
this.setState((state) => ({ eventPulse: state.eventPulse + 1 }));
|
||||||
}
|
}
|
||||||
|
if (prevState.companyId !== this.state.companyId) {
|
||||||
|
this.startRawStream();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pushRawEvent(type, data) {
|
||||||
|
const entry = {
|
||||||
|
id: ++this.nextRawId,
|
||||||
|
type: type || 'message',
|
||||||
|
data: data ?? '',
|
||||||
|
lastEventId: '',
|
||||||
|
time: Date.now(),
|
||||||
|
};
|
||||||
|
this.setState((state) => ({
|
||||||
|
events: [...state.events.slice(-(RAW_EVENT_LIMIT - 1)), entry],
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
handleRawChunk(text) {
|
||||||
|
this.rawBuffer += text;
|
||||||
|
let boundary;
|
||||||
|
while ((boundary = this.rawBuffer.indexOf('\n\n')) !== -1) {
|
||||||
|
const frame = this.rawBuffer.slice(0, boundary);
|
||||||
|
this.rawBuffer = this.rawBuffer.slice(boundary + 2);
|
||||||
|
let type = '';
|
||||||
|
const dataLines = [];
|
||||||
|
let comment = '';
|
||||||
|
for (const line of frame.split('\n')) {
|
||||||
|
if (line.startsWith(':')) {
|
||||||
|
comment = line.slice(1).trim();
|
||||||
|
} else if (line.startsWith('event:')) {
|
||||||
|
type = line.slice(6).trim();
|
||||||
|
} else if (line.startsWith('data:')) {
|
||||||
|
dataLines.push(line.slice(5).trimStart());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// No filtering: show every frame, including heartbeats and
|
||||||
|
// events no automat listens for (brand, modelconversation, ...).
|
||||||
|
if (!type && comment) {
|
||||||
|
this.pushRawEvent(comment, '');
|
||||||
|
} else if (type || dataLines.length > 0) {
|
||||||
|
this.pushRawEvent(type || 'message', dataLines.join('\n'));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
stopRawStream() {
|
||||||
|
if (this.rawReconnectTimer) {
|
||||||
|
clearTimeout(this.rawReconnectTimer);
|
||||||
|
this.rawReconnectTimer = null;
|
||||||
|
}
|
||||||
|
if (this.rawAbort) {
|
||||||
|
try {
|
||||||
|
this.rawAbort.abort();
|
||||||
|
} catch {}
|
||||||
|
this.rawAbort = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async startRawStream() {
|
||||||
|
this.stopRawStream();
|
||||||
|
this.rawBuffer = '';
|
||||||
|
const controller = new AbortController();
|
||||||
|
this.rawAbort = controller;
|
||||||
|
try {
|
||||||
|
const url = new URL('/api/events', window.location.origin);
|
||||||
|
try {
|
||||||
|
const cid = getSseClientId();
|
||||||
|
if (cid) url.searchParams.set('cid', `${cid}-monitor`);
|
||||||
|
} catch {}
|
||||||
|
const res = await fetch(url.href, {
|
||||||
|
credentials: 'include',
|
||||||
|
signal: controller.signal,
|
||||||
|
headers: { Accept: 'text/event-stream' },
|
||||||
|
});
|
||||||
|
if (!res.ok || !res.body) throw new Error(`SSE monitor HTTP ${res.status}`);
|
||||||
|
const reader = res.body.getReader();
|
||||||
|
const decoder = new TextDecoder();
|
||||||
|
for (;;) {
|
||||||
|
const { done, value } = await reader.read();
|
||||||
|
if (done) break;
|
||||||
|
this.handleRawChunk(decoder.decode(value, { stream: true }));
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
if (controller.signal.aborted) return;
|
||||||
|
this.pushRawEvent('error', String(err?.message ?? err));
|
||||||
|
}
|
||||||
|
if (!controller.signal.aborted && !this.rawReconnectTimer) {
|
||||||
|
this.rawReconnectTimer = setTimeout(() => this.startRawStream(), 3000);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
handleKeyDown(event) {
|
handleKeyDown(event) {
|
||||||
@@ -94,7 +190,7 @@ class SseMonitor extends PureComponent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
handleClear() {
|
handleClear() {
|
||||||
sseAutomat.actions.clearEvents();
|
this.setState({ events: [] });
|
||||||
}
|
}
|
||||||
|
|
||||||
handlePointerDown(event) {
|
handlePointerDown(event) {
|
||||||
|
|||||||
Reference in New Issue
Block a user