Remove primary host plumbing
This commit is contained in:
@@ -1,18 +1,13 @@
|
||||
import { createContext, useCallback, useContext, useEffect, useMemo, useRef, useState } from "react";
|
||||
import type { ReactNode } from "react";
|
||||
import AsyncStorage from "@react-native-async-storage/async-storage";
|
||||
import { useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import { useDaemonRegistry, type DaemonProfile } from "./daemon-registry-context";
|
||||
import { trackAnalyticsEvent } from "@/utils/analytics";
|
||||
import type { SessionContextValue } from "./session-context";
|
||||
|
||||
export interface SessionDirectoryEntry {
|
||||
getSnapshot: () => SessionContextValue | null;
|
||||
subscribe: (listener: () => void) => () => void;
|
||||
}
|
||||
|
||||
export type SessionAccessorRole = "primary" | "background";
|
||||
type SessionAccessorRegistry = Map<string, Map<SessionAccessorRole, SessionDirectoryEntry>>;
|
||||
type SessionAccessorRegistry = Map<string, SessionDirectoryEntry>;
|
||||
|
||||
export type ConnectionState =
|
||||
| { status: "idle"; lastError: null; lastOnlineAt: string | null }
|
||||
@@ -35,33 +30,18 @@ export type DaemonConnectionRecord = {
|
||||
} & ConnectionState;
|
||||
|
||||
interface DaemonConnectionsContextValue {
|
||||
activeDaemonId: string | null;
|
||||
activeDaemon: DaemonProfile | null;
|
||||
connectionStates: Map<string, DaemonConnectionRecord>;
|
||||
isLoading: boolean;
|
||||
setActiveDaemonId: (daemonId: string, options?: SetActiveDaemonOptions) => void;
|
||||
updateConnectionStatus: (daemonId: string, update: ConnectionStateUpdate) => void;
|
||||
sessionAccessors: Map<string, SessionDirectoryEntry>;
|
||||
sessionAccessorRoles: Map<string, Set<SessionAccessorRole>>;
|
||||
registerSessionAccessor: (
|
||||
daemonId: string,
|
||||
entry: SessionDirectoryEntry,
|
||||
role?: SessionAccessorRole
|
||||
) => void;
|
||||
unregisterSessionAccessor: (daemonId: string, role?: SessionAccessorRole) => void;
|
||||
registerSessionAccessor: (daemonId: string, entry: SessionDirectoryEntry) => void;
|
||||
unregisterSessionAccessor: (daemonId: string) => void;
|
||||
subscribeToSessionDirectory: (listener: () => void) => () => void;
|
||||
notifySessionDirectoryChange: () => void;
|
||||
}
|
||||
|
||||
const DaemonConnectionsContext = createContext<DaemonConnectionsContextValue | null>(null);
|
||||
|
||||
const ACTIVE_DAEMON_STORAGE_KEY = "@paseo:active-daemon-id";
|
||||
const ACTIVE_DAEMON_QUERY_KEY = ["active-daemon-id"];
|
||||
|
||||
export type SetActiveDaemonOptions = {
|
||||
source?: string;
|
||||
};
|
||||
|
||||
function createDefaultConnectionState(): ConnectionState {
|
||||
return {
|
||||
status: "idle",
|
||||
@@ -132,25 +112,10 @@ export function useDaemonConnections(): DaemonConnectionsContextValue {
|
||||
|
||||
export function DaemonConnectionsProvider({ children }: { children: ReactNode }) {
|
||||
const { daemons, isLoading: registryLoading } = useDaemonRegistry();
|
||||
const [activeDaemonId, setActiveDaemonIdState] = useState<string | null>(null);
|
||||
const [connectionStates, setConnectionStates] = useState<Map<string, DaemonConnectionRecord>>(new Map());
|
||||
const [sessionAccessorRegistry, setSessionAccessorRegistry] = useState<SessionAccessorRegistry>(new Map());
|
||||
const queryClient = useQueryClient();
|
||||
const activeDaemonPreference = useQuery({
|
||||
queryKey: ACTIVE_DAEMON_QUERY_KEY,
|
||||
queryFn: loadActiveDaemonPreference,
|
||||
staleTime: Infinity,
|
||||
gcTime: Infinity,
|
||||
});
|
||||
const sessionDirectoryListenersRef = useRef<Set<() => void>>(new Set());
|
||||
|
||||
useEffect(() => {
|
||||
if (activeDaemonPreference.isPending) {
|
||||
return;
|
||||
}
|
||||
setActiveDaemonIdState(activeDaemonPreference.data ?? null);
|
||||
}, [activeDaemonPreference.data, activeDaemonPreference.isPending]);
|
||||
|
||||
// Ensure connection states stay in sync with registry entries
|
||||
useEffect(() => {
|
||||
setConnectionStates((prev) => {
|
||||
@@ -172,77 +137,18 @@ export function DaemonConnectionsProvider({ children }: { children: ReactNode })
|
||||
let changed = false;
|
||||
const next: SessionAccessorRegistry = new Map();
|
||||
|
||||
for (const [daemonId, roleMap] of prev.entries()) {
|
||||
for (const [daemonId, entry] of prev.entries()) {
|
||||
if (!validDaemonIds.has(daemonId)) {
|
||||
changed = true;
|
||||
continue;
|
||||
}
|
||||
next.set(daemonId, roleMap);
|
||||
next.set(daemonId, entry);
|
||||
}
|
||||
|
||||
return changed ? next : prev;
|
||||
});
|
||||
}, [daemons]);
|
||||
|
||||
// Keep active daemon aligned with registry default
|
||||
const persistActiveDaemonId = useCallback(async (daemonId: string | null) => {
|
||||
queryClient.setQueryData<string | null>(ACTIVE_DAEMON_QUERY_KEY, daemonId);
|
||||
try {
|
||||
if (daemonId) {
|
||||
await AsyncStorage.setItem(ACTIVE_DAEMON_STORAGE_KEY, daemonId);
|
||||
} else {
|
||||
await AsyncStorage.removeItem(ACTIVE_DAEMON_STORAGE_KEY);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("[DaemonConnections] Failed to persist active daemon", error);
|
||||
}
|
||||
}, [queryClient]);
|
||||
|
||||
const setActiveDaemonId = useCallback(
|
||||
(daemonId: string, options?: SetActiveDaemonOptions) => {
|
||||
setActiveDaemonIdState((previous) => {
|
||||
if (previous !== daemonId) {
|
||||
trackAnalyticsEvent({
|
||||
type: "daemon_active_changed",
|
||||
daemonId,
|
||||
previousDaemonId: previous,
|
||||
source: options?.source,
|
||||
});
|
||||
}
|
||||
return daemonId;
|
||||
});
|
||||
void persistActiveDaemonId(daemonId);
|
||||
},
|
||||
[persistActiveDaemonId]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (activeDaemonPreference.isPending) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (daemons.length === 0) {
|
||||
setActiveDaemonIdState(null);
|
||||
void persistActiveDaemonId(null);
|
||||
return;
|
||||
}
|
||||
|
||||
if (activeDaemonId && daemons.some((daemon) => daemon.id === activeDaemonId)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const fallback = daemons[0];
|
||||
if (fallback) {
|
||||
setActiveDaemonId(fallback.id, { source: "auto_fallback" });
|
||||
}
|
||||
}, [daemons, activeDaemonId, activeDaemonPreference.isPending, persistActiveDaemonId, setActiveDaemonId]);
|
||||
|
||||
const activeDaemon = useMemo(() => {
|
||||
if (!activeDaemonId) {
|
||||
return null;
|
||||
}
|
||||
return daemons.find((daemon) => daemon.id === activeDaemonId) ?? null;
|
||||
}, [activeDaemonId, daemons]);
|
||||
|
||||
const updateConnectionStatus = useCallback(
|
||||
(daemonId: string, update: ConnectionStateUpdate) => {
|
||||
@@ -269,41 +175,28 @@ export function DaemonConnectionsProvider({ children }: { children: ReactNode })
|
||||
[]
|
||||
);
|
||||
|
||||
const registerSessionAccessor = useCallback(
|
||||
(daemonId: string, entry: SessionDirectoryEntry, role: SessionAccessorRole = "primary") => {
|
||||
setSessionAccessorRegistry((prev) => {
|
||||
const next = new Map(prev);
|
||||
const existing = new Map(next.get(daemonId) ?? []);
|
||||
existing.set(role, entry);
|
||||
next.set(daemonId, existing);
|
||||
return next;
|
||||
});
|
||||
},
|
||||
[]
|
||||
);
|
||||
const registerSessionAccessor = useCallback((daemonId: string, entry: SessionDirectoryEntry) => {
|
||||
setSessionAccessorRegistry((prev) => {
|
||||
const next = new Map(prev);
|
||||
const hasExisting = next.has(daemonId);
|
||||
next.set(daemonId, entry);
|
||||
if (hasExisting) {
|
||||
console.warn(`[DaemonConnections] Duplicate session accessor detected for "${daemonId}". Overwriting.`);
|
||||
}
|
||||
return next;
|
||||
});
|
||||
}, []);
|
||||
|
||||
const unregisterSessionAccessor = useCallback(
|
||||
(daemonId: string, role: SessionAccessorRole = "primary") => {
|
||||
setSessionAccessorRegistry((prev) => {
|
||||
const existing = prev.get(daemonId);
|
||||
if (!existing) {
|
||||
return prev;
|
||||
}
|
||||
|
||||
const nextRoleMap = new Map(existing);
|
||||
nextRoleMap.delete(role);
|
||||
|
||||
const next = new Map(prev);
|
||||
if (nextRoleMap.size === 0) {
|
||||
next.delete(daemonId);
|
||||
} else {
|
||||
next.set(daemonId, nextRoleMap);
|
||||
}
|
||||
return next;
|
||||
});
|
||||
},
|
||||
[]
|
||||
);
|
||||
const unregisterSessionAccessor = useCallback((daemonId: string) => {
|
||||
setSessionAccessorRegistry((prev) => {
|
||||
if (!prev.has(daemonId)) {
|
||||
return prev;
|
||||
}
|
||||
const next = new Map(prev);
|
||||
next.delete(daemonId);
|
||||
return next;
|
||||
});
|
||||
}, []);
|
||||
|
||||
const subscribeToSessionDirectory = useCallback((listener: () => void) => {
|
||||
sessionDirectoryListenersRef.current.add(listener);
|
||||
@@ -323,43 +216,13 @@ export function DaemonConnectionsProvider({ children }: { children: ReactNode })
|
||||
}
|
||||
}, []);
|
||||
|
||||
const { sessionAccessors, sessionAccessorRoles } = useMemo(() => {
|
||||
const flattened = new Map<string, SessionDirectoryEntry>();
|
||||
const roleSets = new Map<string, Set<SessionAccessorRole>>();
|
||||
|
||||
sessionAccessorRegistry.forEach((roleMap, daemonId) => {
|
||||
if (roleMap.size === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const roles = new Set<SessionAccessorRole>();
|
||||
roleMap.forEach((_entry, role) => roles.add(role));
|
||||
roleSets.set(daemonId, roles);
|
||||
|
||||
const primaryEntry = roleMap.get("primary");
|
||||
if (primaryEntry) {
|
||||
flattened.set(daemonId, primaryEntry);
|
||||
return;
|
||||
}
|
||||
|
||||
const backgroundEntry = roleMap.get("background");
|
||||
if (backgroundEntry) {
|
||||
flattened.set(daemonId, backgroundEntry);
|
||||
}
|
||||
});
|
||||
|
||||
return { sessionAccessors: flattened, sessionAccessorRoles: roleSets };
|
||||
}, [sessionAccessorRegistry]);
|
||||
const sessionAccessors = useMemo(() => new Map(sessionAccessorRegistry), [sessionAccessorRegistry]);
|
||||
|
||||
const value: DaemonConnectionsContextValue = {
|
||||
activeDaemonId,
|
||||
activeDaemon,
|
||||
connectionStates,
|
||||
isLoading: registryLoading || activeDaemonPreference.isPending,
|
||||
setActiveDaemonId,
|
||||
isLoading: registryLoading,
|
||||
updateConnectionStatus,
|
||||
sessionAccessors,
|
||||
sessionAccessorRoles,
|
||||
registerSessionAccessor,
|
||||
unregisterSessionAccessor,
|
||||
subscribeToSessionDirectory,
|
||||
@@ -372,13 +235,3 @@ export function DaemonConnectionsProvider({ children }: { children: ReactNode })
|
||||
</DaemonConnectionsContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
async function loadActiveDaemonPreference(): Promise<string | null> {
|
||||
try {
|
||||
const stored = await AsyncStorage.getItem(ACTIVE_DAEMON_STORAGE_KEY);
|
||||
return stored ?? null;
|
||||
} catch (error) {
|
||||
console.error("[DaemonConnections] Failed to read active daemon preference", error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user