swarm-ui: key message rows on an arrival id and follow the tail on it
At the 2000-message cap the count stops changing, so the page stopped following the newest message, and every row's index key shifted on each arrival, remounting all of them. Each message now gets an id when it arrives; the rows key on it and the scroll follows the newest id. Rows are memoised on the message, and the events of one frame join the list in a single update.
This commit is contained in:
parent
447498fef7
commit
bdeec3f744
4 changed files with 114 additions and 19 deletions
|
|
@ -3,9 +3,13 @@
|
|||
// start; the list therefore empties each time the connection opens, and the
|
||||
// replay refills it. Not `useSwarmTermStream`: that one keeps its rows across
|
||||
// a reconnect, which here would list every message twice.
|
||||
import { useEffect, useState } from "preact/hooks";
|
||||
import type { SwarmMessage } from "./messages.js";
|
||||
import { messagesUrl, newest } from "./messages.js";
|
||||
//
|
||||
// Each message gets an id as it arrives, and the events that arrive within one
|
||||
// frame join the list in a single update, so a long replay is one render per
|
||||
// frame instead of one per message.
|
||||
import { useEffect, useRef, useState } from "preact/hooks";
|
||||
import type { ArrivedMessage, SwarmMessage } from "./messages.js";
|
||||
import { messagesUrl, newest, numbered } from "./messages.js";
|
||||
|
||||
export type ConnectionState = "connecting" | "open" | "reconnecting" | "closed";
|
||||
|
||||
|
|
@ -13,17 +17,37 @@ export type ConnectionState = "connecting" | "open" | "reconnecting" | "closed";
|
|||
export const MAX_MESSAGES = 2000;
|
||||
|
||||
export function useMessageStream(sinceMinutes: number): {
|
||||
messages: SwarmMessage[];
|
||||
messages: ArrivedMessage[];
|
||||
connection: ConnectionState;
|
||||
} {
|
||||
const [messages, setMessages] = useState<SwarmMessage[]>([]);
|
||||
const [messages, setMessages] = useState<ArrivedMessage[]>([]);
|
||||
const [connection, setConnection] = useState<ConnectionState>("connecting");
|
||||
// The next id to hand out; it never goes back, so an id is not reused after
|
||||
// a reconnect either.
|
||||
const nextId = useRef(0);
|
||||
|
||||
useEffect(() => {
|
||||
setMessages([]);
|
||||
setConnection("connecting");
|
||||
const es = new EventSource(messagesUrl(sinceMinutes));
|
||||
let pending: SwarmMessage[] = [];
|
||||
let frame: number | null = null;
|
||||
|
||||
function drop() {
|
||||
pending = [];
|
||||
if (frame !== null) cancelAnimationFrame(frame);
|
||||
frame = null;
|
||||
}
|
||||
function flush() {
|
||||
frame = null;
|
||||
const batch = numbered(pending, nextId.current);
|
||||
pending = [];
|
||||
nextId.current += batch.length;
|
||||
setMessages((prev) => newest([...prev, ...batch], MAX_MESSAGES));
|
||||
}
|
||||
|
||||
es.onopen = () => {
|
||||
drop();
|
||||
setMessages([]);
|
||||
setConnection("open");
|
||||
};
|
||||
|
|
@ -34,14 +58,18 @@ export function useMessageStream(sinceMinutes: number): {
|
|||
} catch {
|
||||
return;
|
||||
}
|
||||
setMessages((prev) => newest([...prev, m], MAX_MESSAGES));
|
||||
pending.push(m);
|
||||
if (frame === null) frame = requestAnimationFrame(flush);
|
||||
};
|
||||
es.onerror = () => {
|
||||
setConnection(
|
||||
es.readyState === 0 /* CONNECTING */ ? "reconnecting" : "closed",
|
||||
);
|
||||
};
|
||||
return () => es.close();
|
||||
return () => {
|
||||
drop();
|
||||
es.close();
|
||||
};
|
||||
}, [sinceMinutes]);
|
||||
|
||||
return { messages, connection };
|
||||
|
|
|
|||
Loading…
Reference in a new issue