Deepen three architectural seams: FIT consolidation, host election extraction, injectable db

- Extract shared fitToGpx into connected-services/fit.ts (FIT is a Garmin
  open standard used by Wahoo, Coros, Garmin — not provider-specific)
- Add importActivity() to sync/imports.server.ts so providers call one
  function instead of createActivity + recordImport separately; eliminates
  direct dependency on activities.server.ts from provider adapters
- Update wahoo importer + webhook to use both shared helpers
- Extract useHostElection(yjs) hook from useRouting so host election is
  independently testable without mounting the full routing stack
- Add setDb() to journal db.ts for module-level injection in unit tests

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Ullrich Schäfer 2026-05-10 15:52:31 +02:00
parent 48d97be8f1
commit e387e1f798
No known key found for this signature in database
GPG key ID: A32FF691A0F752D9
8 changed files with 104 additions and 122 deletions

View file

@ -0,0 +1,28 @@
import { useEffect, useState } from "react";
import { electHost } from "./host-election.ts";
import type { YjsState } from "./use-yjs.ts";
export function useHostElection(yjs: YjsState | null): boolean {
const [isHost, setIsHost] = useState(false);
useEffect(() => {
if (!yjs) return;
const checkHost = () => {
const states = yjs.awareness.getStates() as Map<number, Record<string, unknown>>;
const localId = yjs.awareness.clientID;
const { isHost: amHost, role } = electHost(states, localId);
setIsHost(amHost);
yjs.awareness.setLocalStateField("role", role);
};
yjs.awareness.on("change", checkHost);
checkHost();
return () => {
yjs.awareness.off("change", checkHost);
};
}, [yjs]);
return isHost;
}

View file

@ -1,7 +1,7 @@
import { useCallback, useEffect, useRef, useState } from "react";
import * as Y from "yjs";
import type { YjsState } from "./use-yjs.ts";
import { electHost } from "./host-election.ts";
import { useHostElection } from "./use-host-election.ts";
import {
hashNoGoAreas,
mergeGeoJsonSegments,
@ -51,7 +51,7 @@ function restoreWaypoints(yjs: YjsState, snapshot: WaypointData[], restoringRef:
export type RouteError = "no_route" | "failed" | "rate_limit" | null;
export function useRouting(yjs: YjsState | null, sessionId: string) {
const [isHost, setIsHost] = useState(false);
const isHost = useHostElection(yjs);
const [computing, setComputing] = useState(false);
const [routeError, setRouteError] = useState<RouteError>(null);
const [routeStats, setRouteStats] = useState<RouteStats>({});
@ -67,26 +67,6 @@ export function useRouting(yjs: YjsState | null, sessionId: string) {
// instead of N-1 on every recompute.
const segmentCacheRef = useRef<SegmentCache>(new SegmentCache());
// Host election via Yjs awareness
useEffect(() => {
if (!yjs) return;
const checkHost = () => {
const states = yjs.awareness.getStates() as Map<number, Record<string, unknown>>;
const localId = yjs.awareness.clientID;
const { isHost: amHost, role } = electHost(states, localId);
setIsHost(amHost);
yjs.awareness.setLocalStateField("role", role);
};
yjs.awareness.on("change", checkHost);
checkHost();
return () => {
yjs.awareness.off("change", checkHost);
};
}, [yjs]);
const computeRoute = useCallback(
async (waypoints: WaypointData[]) => {
if (!yjs || !isHost || waypoints.length < 2) return;