import { type AuthSessionRole, type EnvironmentId, type OrchestrationEvent, type OrchestrationReadModel, type ServerConfig, type TerminalEvent, ThreadId, } from "@t3tools/contracts"; import { type QueryClient } from "@tanstack/react-query"; import { Throttler } from "@tanstack/react-pacer"; import { createKnownEnvironment, getKnownEnvironmentWsBaseUrl, scopedProjectKey, scopedThreadKey, scopeProjectRef, scopeThreadRef, } from "@t3tools/client-runtime"; import { markPromotedDraftThreadByRef, markPromotedDraftThreadsByRef, useComposerDraftStore, } from "~/composerDraftStore"; import { ensureLocalApi } from "~/localApi"; import { collectActiveTerminalThreadIds } from "~/lib/terminalStateCleanup"; import { deriveOrchestrationBatchEffects } from "~/orchestrationEventEffects"; import { projectQueryKeys } from "~/lib/projectReactQuery"; import { providerQueryKeys } from "~/lib/providerReactQuery"; import { getPrimaryKnownEnvironment } from "../primary"; import { bootstrapRemoteBearerSession, fetchRemoteEnvironmentDescriptor, fetchRemoteSessionState, resolveRemoteWebSocketConnectionUrl, } from "../remote/api"; import { resolveRemotePairingTarget } from "../remote/target"; import { getSavedEnvironmentRecord, hasSavedEnvironmentRegistryHydrated, listSavedEnvironmentRecords, persistSavedEnvironmentRecord, readSavedEnvironmentBearerToken, removeSavedEnvironmentBearerToken, type SavedEnvironmentRecord, useSavedEnvironmentRegistryStore, useSavedEnvironmentRuntimeStore, waitForSavedEnvironmentRegistryHydration, writeSavedEnvironmentBearerToken, } from "./catalog"; import { createEnvironmentConnection, type EnvironmentConnection } from "./connection"; import { useStore, selectProjectsAcrossEnvironments, selectThreadByRef, selectThreadsAcrossEnvironments, } from "~/store"; import { useTerminalStateStore } from "~/terminalStateStore"; import { useUiStateStore } from "~/uiStateStore"; import { WsTransport } from "../../rpc/wsTransport"; import { createWsRpcClient, type WsRpcClient } from "../../rpc/wsRpcClient"; type EnvironmentServiceState = { readonly queryClient: QueryClient; readonly queryInvalidationThrottler: Throttler<() => void>; refCount: number; stop: () => void; }; const environmentConnections = new Map(); const environmentConnectionListeners = new Set<() => void>(); let activeService: EnvironmentServiceState | null = null; let needsProviderInvalidation = false; function emitEnvironmentConnectionRegistryChange() { for (const listener of environmentConnectionListeners) { listener(); } } function getRuntimeErrorFields(error: unknown) { return { lastError: error instanceof Error ? error.message : String(error), lastErrorAt: new Date().toISOString(), } as const; } function isoNow(): string { return new Date().toISOString(); } function setRuntimeConnecting(environmentId: EnvironmentId) { useSavedEnvironmentRuntimeStore.getState().patch(environmentId, { connectionState: "connecting", lastError: null, lastErrorAt: null, }); } function setRuntimeConnected(environmentId: EnvironmentId) { const connectedAt = isoNow(); useSavedEnvironmentRuntimeStore.getState().patch(environmentId, { connectionState: "connected", authState: "authenticated", connectedAt, disconnectedAt: null, lastError: null, lastErrorAt: null, }); useSavedEnvironmentRegistryStore.getState().markConnected(environmentId, connectedAt); } function setRuntimeDisconnected(environmentId: EnvironmentId, reason?: string | null) { useSavedEnvironmentRuntimeStore.getState().patch(environmentId, { connectionState: "disconnected", disconnectedAt: isoNow(), ...(reason && reason.trim().length > 0 ? { lastError: reason, lastErrorAt: isoNow(), } : {}), }); } function setRuntimeError(environmentId: EnvironmentId, error: unknown) { useSavedEnvironmentRuntimeStore.getState().patch(environmentId, { connectionState: "error", ...getRuntimeErrorFields(error), }); } function coalesceOrchestrationUiEvents( events: ReadonlyArray, ): OrchestrationEvent[] { if (events.length < 2) { return [...events]; } const coalesced: OrchestrationEvent[] = []; for (const event of events) { const previous = coalesced.at(-1); if ( previous?.type === "thread.message-sent" && event.type === "thread.message-sent" && previous.payload.threadId === event.payload.threadId && previous.payload.messageId === event.payload.messageId ) { coalesced[coalesced.length - 1] = { ...event, payload: { ...event.payload, attachments: event.payload.attachments ?? previous.payload.attachments, createdAt: previous.payload.createdAt, text: !event.payload.streaming && event.payload.text.length > 0 ? event.payload.text : previous.payload.text + event.payload.text, }, }; continue; } coalesced.push(event); } return coalesced; } function reconcileSnapshotDerivedState() { const storeState = useStore.getState(); const threads = selectThreadsAcrossEnvironments(storeState); const projects = selectProjectsAcrossEnvironments(storeState); useUiStateStore.getState().syncProjects( projects.map((project) => ({ key: scopedProjectKey(scopeProjectRef(project.environmentId, project.id)), cwd: project.cwd, })), ); useUiStateStore.getState().syncThreads( threads.map((thread) => ({ key: scopedThreadKey(scopeThreadRef(thread.environmentId, thread.id)), seedVisitedAt: thread.updatedAt ?? thread.createdAt, })), ); markPromotedDraftThreadsByRef( threads.map((thread) => scopeThreadRef(thread.environmentId, thread.id)), ); const activeThreadKeys = collectActiveTerminalThreadIds({ snapshotThreads: threads.map((thread) => ({ key: scopedThreadKey(scopeThreadRef(thread.environmentId, thread.id)), deletedAt: null, archivedAt: thread.archivedAt, })), draftThreadKeys: useComposerDraftStore.getState().listDraftThreadKeys(), }); useTerminalStateStore.getState().removeOrphanedTerminalStates(activeThreadKeys); } export function shouldApplyTerminalEvent(input: { serverThreadArchivedAt: string | null | undefined; hasDraftThread: boolean; }): boolean { if (input.serverThreadArchivedAt !== undefined) { return input.serverThreadArchivedAt === null; } return input.hasDraftThread; } function applyRecoveredEventBatch( events: ReadonlyArray, environmentId: EnvironmentId, ) { if (events.length === 0) { return; } const batchEffects = deriveOrchestrationBatchEffects(events); const uiEvents = coalesceOrchestrationUiEvents(events); const needsProjectUiSync = events.some( (event) => event.type === "project.created" || event.type === "project.meta-updated" || event.type === "project.deleted", ); if (batchEffects.needsProviderInvalidation) { needsProviderInvalidation = true; void activeService?.queryInvalidationThrottler.maybeExecute(); } useStore.getState().applyOrchestrationEvents(uiEvents, environmentId); if (needsProjectUiSync) { const projects = selectProjectsAcrossEnvironments(useStore.getState()); useUiStateStore.getState().syncProjects( projects.map((project) => ({ key: scopedProjectKey(scopeProjectRef(project.environmentId, project.id)), cwd: project.cwd, })), ); } const needsThreadUiSync = events.some( (event) => event.type === "thread.created" || event.type === "thread.deleted", ); if (needsThreadUiSync) { const threads = selectThreadsAcrossEnvironments(useStore.getState()); useUiStateStore.getState().syncThreads( threads.map((thread) => ({ key: scopedThreadKey(scopeThreadRef(thread.environmentId, thread.id)), seedVisitedAt: thread.updatedAt ?? thread.createdAt, })), ); } const draftStore = useComposerDraftStore.getState(); for (const threadId of batchEffects.promoteDraftThreadIds) { markPromotedDraftThreadByRef(scopeThreadRef(environmentId, threadId)); } for (const threadId of batchEffects.clearDeletedThreadIds) { draftStore.clearDraftThread(scopeThreadRef(environmentId, threadId)); useUiStateStore .getState() .clearThreadUi(scopedThreadKey(scopeThreadRef(environmentId, threadId))); } for (const threadId of batchEffects.removeTerminalStateThreadIds) { useTerminalStateStore.getState().removeTerminalState(scopeThreadRef(environmentId, threadId)); } } function createEnvironmentConnectionHandlers() { return { applyEventBatch: applyRecoveredEventBatch, syncSnapshot: (snapshot: OrchestrationReadModel, environmentId: EnvironmentId) => { useStore.getState().syncServerReadModel(snapshot, environmentId); reconcileSnapshotDerivedState(); }, applyTerminalEvent: (event: TerminalEvent, environmentId: EnvironmentId) => { const threadRef = scopeThreadRef(environmentId, ThreadId.makeUnsafe(event.threadId)); const serverThread = selectThreadByRef(useStore.getState(), threadRef); const hasDraftThread = useComposerDraftStore.getState().getDraftThreadByRef(threadRef) !== null; if ( !shouldApplyTerminalEvent({ serverThreadArchivedAt: serverThread?.archivedAt, hasDraftThread, }) ) { return; } useTerminalStateStore.getState().applyTerminalEvent(threadRef, event); }, }; } function createPrimaryEnvironmentClient( knownEnvironment: ReturnType, ) { const wsBaseUrl = getKnownEnvironmentWsBaseUrl(knownEnvironment); if (!wsBaseUrl) { throw new Error( `Unable to resolve websocket URL for ${knownEnvironment?.label ?? "primary environment"}.`, ); } return createWsRpcClient(new WsTransport(wsBaseUrl)); } function createSavedEnvironmentClient( record: SavedEnvironmentRecord, bearerToken: string, ): WsRpcClient { useSavedEnvironmentRuntimeStore.getState().ensure(record.environmentId); return createWsRpcClient( new WsTransport( () => resolveRemoteWebSocketConnectionUrl({ wsBaseUrl: record.wsBaseUrl, httpBaseUrl: record.httpBaseUrl, bearerToken, }), { onAttempt: () => { setRuntimeConnecting(record.environmentId); }, onOpen: () => { setRuntimeConnected(record.environmentId); }, onError: (message: string) => { useSavedEnvironmentRuntimeStore.getState().patch(record.environmentId, { connectionState: "error", lastError: message, lastErrorAt: isoNow(), }); }, onClose: (details: { readonly code: number; readonly reason: string }) => { setRuntimeDisconnected(record.environmentId, details.reason); }, }, ), ); } async function refreshSavedEnvironmentMetadata( record: SavedEnvironmentRecord, bearerToken: string, client: WsRpcClient, roleHint?: AuthSessionRole | null, configHint?: ServerConfig | null, ): Promise { const [serverConfig, sessionState] = await Promise.all([ configHint ? Promise.resolve(configHint) : client.server.getConfig(), fetchRemoteSessionState({ httpBaseUrl: record.httpBaseUrl, bearerToken, }), ]); useSavedEnvironmentRuntimeStore.getState().patch(record.environmentId, { authState: sessionState.authenticated ? "authenticated" : "requires-auth", descriptor: serverConfig.environment, serverConfig, role: sessionState.authenticated ? (sessionState.role ?? roleHint ?? null) : null, }); } function registerConnection(connection: EnvironmentConnection): EnvironmentConnection { const existing = environmentConnections.get(connection.environmentId); if (existing && existing !== connection) { throw new Error(`Environment ${connection.environmentId} already has an active connection.`); } environmentConnections.set(connection.environmentId, connection); emitEnvironmentConnectionRegistryChange(); return connection; } async function removeConnection(environmentId: EnvironmentId): Promise { const connection = environmentConnections.get(environmentId); if (!connection) { return false; } environmentConnections.delete(environmentId); emitEnvironmentConnectionRegistryChange(); await connection.dispose(); return true; } function createPrimaryEnvironmentConnection(): EnvironmentConnection { const knownEnvironment = getPrimaryKnownEnvironment(); if (!knownEnvironment?.environmentId) { throw new Error("Unable to resolve the primary environment."); } const existing = environmentConnections.get(knownEnvironment.environmentId); if (existing) { return existing; } return registerConnection( createEnvironmentConnection({ kind: "primary", knownEnvironment, client: createPrimaryEnvironmentClient(knownEnvironment), ...createEnvironmentConnectionHandlers(), }), ); } async function ensureSavedEnvironmentConnection( record: SavedEnvironmentRecord, options?: { readonly client?: WsRpcClient; readonly bearerToken?: string; readonly role?: AuthSessionRole | null; readonly serverConfig?: ServerConfig | null; }, ): Promise { const existing = environmentConnections.get(record.environmentId); if (existing) { return existing; } const bearerToken = options?.bearerToken ?? (await readSavedEnvironmentBearerToken(record.environmentId)); if (!bearerToken) { useSavedEnvironmentRuntimeStore.getState().patch(record.environmentId, { authState: "requires-auth", role: null, connectionState: "disconnected", lastError: "Saved environment is missing its saved credential. Pair it again.", lastErrorAt: isoNow(), }); throw new Error("Saved environment is missing its saved credential."); } const client = options?.client ?? createSavedEnvironmentClient(record, bearerToken); const knownEnvironment = createKnownEnvironment({ id: record.environmentId, label: record.label, source: "manual", target: { httpBaseUrl: record.httpBaseUrl, wsBaseUrl: record.wsBaseUrl, }, }); const connection = createEnvironmentConnection({ kind: "saved", knownEnvironment: { ...knownEnvironment, environmentId: record.environmentId, }, client, refreshMetadata: async () => { await refreshSavedEnvironmentMetadata(record, bearerToken, client); }, onConfigSnapshot: (config) => { useSavedEnvironmentRuntimeStore.getState().patch(record.environmentId, { descriptor: config.environment, serverConfig: config, }); }, onWelcome: (payload) => { useSavedEnvironmentRuntimeStore.getState().patch(record.environmentId, { descriptor: payload.environment, }); }, ...createEnvironmentConnectionHandlers(), }); registerConnection(connection); try { await refreshSavedEnvironmentMetadata( record, bearerToken, client, options?.role ?? null, options?.serverConfig ?? null, ); return connection; } catch (error) { setRuntimeError(record.environmentId, error); await removeConnection(record.environmentId).catch(() => false); throw error; } } async function syncSavedEnvironmentConnections( records: ReadonlyArray, ): Promise { const expectedEnvironmentIds = new Set(records.map((record) => record.environmentId)); const staleEnvironmentIds = [...environmentConnections.values()] .filter((connection) => connection.kind === "saved") .map((connection) => connection.environmentId) .filter((environmentId) => !expectedEnvironmentIds.has(environmentId)); await Promise.all( staleEnvironmentIds.map((environmentId) => disconnectSavedEnvironment(environmentId)), ); await Promise.all( records.map((record) => ensureSavedEnvironmentConnection(record).catch(() => undefined)), ); } function stopActiveService() { activeService?.stop(); activeService = null; } export function subscribeEnvironmentConnections(listener: () => void): () => void { environmentConnectionListeners.add(listener); return () => { environmentConnectionListeners.delete(listener); }; } export function listEnvironmentConnections(): ReadonlyArray { return [...environmentConnections.values()]; } export function readEnvironmentConnection( environmentId: EnvironmentId, ): EnvironmentConnection | null { return environmentConnections.get(environmentId) ?? null; } export function requireEnvironmentConnection(environmentId: EnvironmentId): EnvironmentConnection { const connection = readEnvironmentConnection(environmentId); if (!connection) { throw new Error(`No websocket client registered for environment ${environmentId}.`); } return connection; } export function getPrimaryEnvironmentConnection(): EnvironmentConnection { return createPrimaryEnvironmentConnection(); } export async function disconnectSavedEnvironment(environmentId: EnvironmentId): Promise { const connection = environmentConnections.get(environmentId); if (connection?.kind !== "saved") { return; } useSavedEnvironmentRuntimeStore.getState().clear(environmentId); await removeConnection(environmentId).catch(() => false); } export async function reconnectSavedEnvironment(environmentId: EnvironmentId): Promise { const record = getSavedEnvironmentRecord(environmentId); if (!record) { throw new Error("Saved environment not found."); } const connection = environmentConnections.get(environmentId); if (!connection) { await ensureSavedEnvironmentConnection(record); return; } setRuntimeConnecting(environmentId); try { await connection.reconnect(); } catch (error) { setRuntimeError(environmentId, error); throw error; } } export async function removeSavedEnvironment(environmentId: EnvironmentId): Promise { useSavedEnvironmentRegistryStore.getState().remove(environmentId); await removeSavedEnvironmentBearerToken(environmentId); await disconnectSavedEnvironment(environmentId); } export async function addSavedEnvironment(input: { readonly label: string; readonly pairingUrl?: string; readonly host?: string; readonly pairingCode?: string; }): Promise { const resolvedTarget = resolveRemotePairingTarget({ ...(input.pairingUrl !== undefined ? { pairingUrl: input.pairingUrl } : {}), ...(input.host !== undefined ? { host: input.host } : {}), ...(input.pairingCode !== undefined ? { pairingCode: input.pairingCode } : {}), }); const descriptor = await fetchRemoteEnvironmentDescriptor({ httpBaseUrl: resolvedTarget.httpBaseUrl, }); const environmentId = descriptor.environmentId; if (environmentConnections.has(environmentId)) { throw new Error("This environment is already connected."); } const bearerSession = await bootstrapRemoteBearerSession({ httpBaseUrl: resolvedTarget.httpBaseUrl, credential: resolvedTarget.credential, }); const record: SavedEnvironmentRecord = { environmentId, label: input.label.trim() || descriptor.label, wsBaseUrl: resolvedTarget.wsBaseUrl, httpBaseUrl: resolvedTarget.httpBaseUrl, createdAt: isoNow(), lastConnectedAt: isoNow(), }; await persistSavedEnvironmentRecord(record); const didPersistBearerToken = await writeSavedEnvironmentBearerToken( environmentId, bearerSession.sessionToken, ); if (!didPersistBearerToken) { await ensureLocalApi().persistence.setSavedEnvironmentRegistry( listSavedEnvironmentRecords().map((entry) => ({ environmentId: entry.environmentId, label: entry.label, httpBaseUrl: entry.httpBaseUrl, wsBaseUrl: entry.wsBaseUrl, createdAt: entry.createdAt, lastConnectedAt: entry.lastConnectedAt, })), ); throw new Error("Unable to persist saved environment credentials."); } await ensureSavedEnvironmentConnection(record, { bearerToken: bearerSession.sessionToken, role: bearerSession.role, }); useSavedEnvironmentRegistryStore.getState().upsert(record); return record; } export async function ensureEnvironmentConnectionBootstrapped( environmentId: EnvironmentId, ): Promise { await environmentConnections.get(environmentId)?.ensureBootstrapped(); } export function startEnvironmentConnectionService(queryClient: QueryClient): () => void { if (activeService?.queryClient === queryClient) { activeService.refCount += 1; return () => { if (!activeService || activeService.queryClient !== queryClient) { return; } activeService.refCount -= 1; if (activeService.refCount === 0) { stopActiveService(); } }; } stopActiveService(); needsProviderInvalidation = false; const queryInvalidationThrottler = new Throttler( () => { if (!needsProviderInvalidation) { return; } needsProviderInvalidation = false; void queryClient.invalidateQueries({ queryKey: providerQueryKeys.all }); void queryClient.invalidateQueries({ queryKey: projectQueryKeys.all }); }, { wait: 100, leading: false, trailing: true, }, ); createPrimaryEnvironmentConnection(); const unsubscribeSavedEnvironments = useSavedEnvironmentRegistryStore.subscribe(() => { if (!hasSavedEnvironmentRegistryHydrated()) { return; } void syncSavedEnvironmentConnections(listSavedEnvironmentRecords()); }); void waitForSavedEnvironmentRegistryHydration() .then(() => syncSavedEnvironmentConnections(listSavedEnvironmentRecords())) .catch(() => undefined); activeService = { queryClient, queryInvalidationThrottler, refCount: 1, stop: () => { unsubscribeSavedEnvironments(); queryInvalidationThrottler.cancel(); }, }; return () => { if (!activeService || activeService.queryClient !== queryClient) { return; } activeService.refCount -= 1; if (activeService.refCount === 0) { stopActiveService(); } }; } export async function resetEnvironmentServiceForTests(): Promise { stopActiveService(); await Promise.all( [...environmentConnections.keys()].map((environmentId) => removeConnection(environmentId)), ); }