Coalesce the event bus, and fix the socket that ended on a client frame
A three-node cascade publishes 13-16 events and each one crossed to the
event loop on its own. They are one `call_soon_threadsafe` now — whatever
was published between two turns of the loop goes over together — and every
subscriber still receives every event, oldest still dropped first when one
falls behind.
The socket end of the same path:
- **any frame from the client ended its stream.** `receive_text` was
awaited once, outside the loop, so a keepalive — or anything else a
client decided to say — satisfied it and was read as the client going
away. It is recreated per iteration; only a disconnect ends the stream.
- events go out in one frame per wave (`{"type": "batch", "events": [...]}`,
capped at 64), serialised once with orjson rather than per client with
the stdlib's `json.dumps` through `send_json`. The client unpacks a batch
and still understands single frames, so an older engine behind a newer
bundle keeps working.
- authenticating and building the snapshot happen on a thread. Both were on
the event loop: one is a database round trip, the other reads the whole
of state, per connect and again per `dashboard_changed` per panel.
`Pipeline.values()` — what that snapshot is — no longer SCANs the whole
Redis namespace. It scanned five bookkeeping keys for every message to find
the messages; `RedisState` keeps a set of the names beside them and answers
from it. Maintained wherever a message is written, so a seeded value or a
deleted flow keeps it exact.
On the client, while in the same file:
- a `node_health` event invalidates the flow's detail. The canvas draws
health from the server-derived `issues`, so a node going down or
recovering only showed on mount, navigation or a rebuild. The store had
a health map of its own that nothing ever read; it and `useNodeHealth`
are gone rather than wired up, since the server's view is the one the
canvas already uses.
- a reconnect invalidates the five key families this socket feeds instead
of the entire cache, and the backoff is jittered. The usual reason a
socket dropped is the engine restarting, so every tab and every wall
panel refetched everything, together, at the moment it was least able to
answer.
- a frame that will not parse costs the frame, not the connection. It was
the one unguarded `JSON.parse` in the app; an exception there escaped to
`window.onerror` and left whatever it had already applied behind.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01M6hPWS6YEbT1P8LxhhFb2T
This commit is contained in:
@@ -29,11 +29,6 @@ export type LiveStatus = {
|
||||
/** Why it is queued — what it is waiting for, and on which machine. */
|
||||
detail?: string | null
|
||||
}
|
||||
/** How a node's connection is doing, which is not how its last run went. */
|
||||
export type NodeHealth = {
|
||||
health: "ok" | "down" | "unknown"
|
||||
detail?: string | null
|
||||
}
|
||||
/**
|
||||
* A node's last failure, kept after it has run again.
|
||||
*
|
||||
@@ -72,7 +67,6 @@ const ENGINE_EVENT_LIMIT = 100
|
||||
const values = new Map<string, LiveValue>()
|
||||
const statuses = new Map<string, LiveStatus>()
|
||||
const failures = new Map<string, NodeFailure>()
|
||||
const health = new Map<string, NodeHealth>()
|
||||
let engineEvents: EngineEvent[] = []
|
||||
// How many times this page has seen a node emit. The number itself means
|
||||
// nothing; a change is what restarts the pulse.
|
||||
@@ -194,13 +188,6 @@ export const liveStore = {
|
||||
path: { name: flow, node_id: node },
|
||||
}).catch(() => {})
|
||||
},
|
||||
setHealth(nodeId: string, entry: NodeHealth) {
|
||||
health.set(nodeId, entry)
|
||||
notify(`health:${nodeId}`)
|
||||
},
|
||||
getHealth(nodeId: string) {
|
||||
return health.get(nodeId)
|
||||
},
|
||||
recordEngineEvent(event: EngineEvent) {
|
||||
// A new array each time, so the hook's snapshot comparison sees the change.
|
||||
engineEvents = [...engineEvents, event].slice(-ENGINE_EVENT_LIMIT)
|
||||
@@ -271,8 +258,6 @@ export const liveStore = {
|
||||
notify(`emit:${key}`)
|
||||
emits.clear()
|
||||
priorEmits.clear()
|
||||
for (const key of health.keys()) notify(`health:${key}`)
|
||||
health.clear()
|
||||
engineEvents = []
|
||||
notify("engine")
|
||||
logLines = []
|
||||
@@ -304,14 +289,6 @@ export function useNodeFailure(nodeId: string): NodeFailure | undefined {
|
||||
)
|
||||
}
|
||||
|
||||
/** How the node's connection is doing, once it has said anything about it. */
|
||||
export function useNodeHealth(nodeId: string): NodeHealth | undefined {
|
||||
return useSyncExternalStore(
|
||||
(listener) => subscribeKey(`health:${nodeId}`, listener),
|
||||
() => health.get(nodeId),
|
||||
)
|
||||
}
|
||||
|
||||
/** The last hundred things the engine said about itself, oldest first. */
|
||||
export function useEngineEvents(): EngineEvent[] {
|
||||
return useSyncExternalStore(
|
||||
|
||||
Reference in New Issue
Block a user