trails/apps/planner/app/lib/host-election.ts
Ullrich Schäfer 55806fc84a
Fix waypoint drag, host election, segment routing, debug panel
Waypoint drag:
- Use observeDeep instead of observe to detect Y.Map property changes

Host election:
- Extracted to host-election.ts with 8 unit tests
- Simple rule: lowest real client ID is always host, deterministic
- Always sets role (host or participant), no missing roles
- Filters server docs (empty awareness state) from election

BRouter routing:
- Route segment-by-segment like bikerouter.de (one request per
  waypoint pair), guarantees route passes through every waypoint
- Merge GeoJSON segments with accumulated stats
- 8 unit tests for segment merging and waypoint splitting

Awareness cleanup:
- Track client IDs per WebSocket connection
- Immediately remove awareness on WebSocket close

Debug panel:
- Visibility + expanded state persisted in localStorage
- Hotkey (Shift+Cmd+Option+T) toggles visibility
- Labels server docs as (server)
- All logic self-contained in YjsDebugPanel.tsx

User identity:
- Color and name persisted in localStorage across reloads

Tests: 32 total (16 existing + 8 host election + 8 routing)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-23 08:52:52 +01:00

26 lines
764 B
TypeScript

/**
* Determines which client should be the routing host.
*
* Simple rule: the real client (has user state) with the lowest
* client ID is always the host. Deterministic, no race conditions.
*/
export function electHost(
states: Map<number, Record<string, unknown>>,
localId: number,
): { isHost: boolean; role: "host" | "participant" } {
let lowestRealId = Infinity;
states.forEach((state, clientId) => {
if (state.user) {
if (clientId < lowestRealId) lowestRealId = clientId;
}
});
// No real clients found (shouldn't happen, but defensive)
if (lowestRealId === Infinity) {
return { isHost: true, role: "host" };
}
const isHost = localId === lowestRealId;
return { isHost, role: isHost ? "host" : "participant" };
}