mirror of
https://github.com/getpaseo/paseo.git
synced 2026-11-04 23:44:41 +00:00
* perf(app): restore live sessions immediately after resume Foregrounding now reconnects every host without waiting for scheduled backoff, while cached turns, timelines, agents, and workspaces remain internally consistent until authoritative catch-up arrives. Replica persistence is keyed and incremental so ordinary updates avoid whole-directory serialization. * fix(app): preserve turn ownership through session updates Stream turn closure now wakes queued-message delivery, while same-turn snapshots retain local cancellation state. Offline liveness remains visible until an authoritative close arrives. * fix(app): preserve pending chat cache writes and trim resume refactor Directory baseline replacement must preserve independently accepted timeline changes until the deferred flush. Keep the reconnect and persistence fixes while removing the session-data facade and broad read-hook migration. * fix(app): initialize reconnect policy from current app state Initialize visibility before host bootstrap so a background mount cannot leave reconnect enabled. Seed sidebar fixtures through the workspace owner so cache hydration preserves their rows. * test(app): verify submission settlement across reconnect Keep submission acknowledgement separate from turn liveness: disconnect preserves the running turn until authoritative catch-up. Wait for confirmed running chrome before exercising queued cancellation so the alternate-send shortcut cannot race turn startup. * docs: remove cache implementation recap
265 lines
7.6 KiB
TypeScript
265 lines
7.6 KiB
TypeScript
import type { AppState } from "react-native";
|
|
import type { ActiveWorkspaceSelection } from "@/stores/navigation-active-workspace-store";
|
|
import type {
|
|
DaemonStartCondition,
|
|
DaemonStartResult,
|
|
StartDaemonIfEnabledInput,
|
|
} from "@/runtime/daemon-start-service";
|
|
import type { Href } from "expo-router";
|
|
import {
|
|
buildHostRootRoute,
|
|
buildHostWorkspaceRoute,
|
|
buildOpenProjectRoute,
|
|
} from "@/utils/host-routes";
|
|
|
|
export interface HostRuntimeBootstrapStore {
|
|
boot: () => Promise<void>;
|
|
}
|
|
|
|
export interface HostRuntimeBootstrapDaemonStartService {
|
|
startIfEnabled: (input: StartDaemonIfEnabledInput) => Promise<DaemonStartResult>;
|
|
}
|
|
|
|
export interface StartHostRuntimeBootstrapInput {
|
|
store: HostRuntimeBootstrapStore;
|
|
daemonStartService: HostRuntimeBootstrapDaemonStartService;
|
|
shouldStartDaemon: DaemonStartCondition;
|
|
}
|
|
|
|
export function startHostRuntimeBootstrap(input: StartHostRuntimeBootstrapInput): void {
|
|
const registryReady = input.store.boot();
|
|
void input.daemonStartService.startIfEnabled({
|
|
shouldStart: async () => {
|
|
await registryReady;
|
|
return typeof input.shouldStartDaemon === "boolean"
|
|
? input.shouldStartDaemon
|
|
: input.shouldStartDaemon();
|
|
},
|
|
});
|
|
}
|
|
|
|
const WELCOME_ROUTE: Href = "/welcome";
|
|
|
|
export type StartupBlocker =
|
|
| { kind: "none" }
|
|
| { kind: "managed-daemon-starting" }
|
|
| { kind: "managed-daemon-error"; message: string };
|
|
|
|
export interface ResolveStartupBlockerInput {
|
|
isDesktopRuntime: boolean;
|
|
anyOnlineHostServerId: string | null;
|
|
daemonStartIsRunning: boolean;
|
|
daemonStartError: string | null;
|
|
}
|
|
|
|
export function resolveStartupBlocker(input: ResolveStartupBlockerInput): StartupBlocker {
|
|
if (!input.isDesktopRuntime) {
|
|
return { kind: "none" };
|
|
}
|
|
|
|
if (input.anyOnlineHostServerId) {
|
|
return { kind: "none" };
|
|
}
|
|
|
|
if (input.daemonStartError) {
|
|
return { kind: "managed-daemon-error", message: input.daemonStartError };
|
|
}
|
|
|
|
if (input.daemonStartIsRunning) {
|
|
return { kind: "managed-daemon-starting" };
|
|
}
|
|
|
|
return { kind: "none" };
|
|
}
|
|
|
|
export function resolveStartupNavigationReady(input: { startupBlocker: StartupBlocker }): boolean {
|
|
return input.startupBlocker.kind !== "managed-daemon-starting";
|
|
}
|
|
|
|
export function shouldRunStartupGiveUpTimer(input: {
|
|
startupBlocker: StartupBlocker;
|
|
anyOnlineHostServerId: string | null;
|
|
hasGivenUpWaitingForHost: boolean;
|
|
}): boolean {
|
|
if (input.anyOnlineHostServerId) {
|
|
return false;
|
|
}
|
|
if (input.hasGivenUpWaitingForHost) {
|
|
return false;
|
|
}
|
|
return input.startupBlocker.kind === "none";
|
|
}
|
|
|
|
export type StartupRegistryStatus = "loading" | "ready";
|
|
|
|
export interface IndexStartupRouteTarget {
|
|
kind: "index";
|
|
pathname: string;
|
|
}
|
|
|
|
export interface HostStartupRouteTarget {
|
|
kind: "host";
|
|
serverId: string | null;
|
|
}
|
|
|
|
export type StartupRouteTarget = IndexStartupRouteTarget | HostStartupRouteTarget;
|
|
|
|
interface ResolveStartupRouteBaseInput {
|
|
startupBlocker: StartupBlocker;
|
|
hostRegistryStatus: StartupRegistryStatus;
|
|
hosts: readonly { serverId: string }[];
|
|
}
|
|
|
|
export interface ResolveIndexStartupRouteInput extends ResolveStartupRouteBaseInput {
|
|
route: IndexStartupRouteTarget;
|
|
anyOnlineHostServerId: string | null;
|
|
workspaceSelection: ActiveWorkspaceSelection | null;
|
|
workspaceSelectionStatus: WorkspaceSelectionStatus;
|
|
isWorkspaceSelectionLoaded: boolean;
|
|
hasGivenUpWaitingForHost: boolean;
|
|
}
|
|
|
|
export interface ResolveHostStartupRouteInput extends ResolveStartupRouteBaseInput {
|
|
route: HostStartupRouteTarget;
|
|
}
|
|
|
|
export type ResolveStartupRouteInput = ResolveIndexStartupRouteInput | ResolveHostStartupRouteInput;
|
|
|
|
export type StartupRouteDecision =
|
|
| { kind: "render" }
|
|
| { kind: "splash" }
|
|
| { kind: "redirect"; href: Href };
|
|
|
|
export type WorkspaceSelectionStatus = "unknown" | "exists" | "missing";
|
|
|
|
function shouldRestoreWorkspaceSelection(input: {
|
|
workspaceSelection: ActiveWorkspaceSelection | null;
|
|
workspaceSelectionStatus: WorkspaceSelectionStatus;
|
|
}): input is {
|
|
workspaceSelection: ActiveWorkspaceSelection;
|
|
workspaceSelectionStatus: Exclude<WorkspaceSelectionStatus, "missing">;
|
|
} {
|
|
return input.workspaceSelection !== null && input.workspaceSelectionStatus !== "missing";
|
|
}
|
|
|
|
export function resolveWorkspaceSelectionStatus(input: {
|
|
hasHydratedWorkspaces: boolean;
|
|
workspaceExists: boolean;
|
|
}): WorkspaceSelectionStatus {
|
|
if (input.workspaceExists) {
|
|
return "exists";
|
|
}
|
|
return input.hasHydratedWorkspaces ? "missing" : "unknown";
|
|
}
|
|
|
|
export function resolveHostIndexRoute(input: {
|
|
serverId: string;
|
|
workspaceSelection: ActiveWorkspaceSelection | null;
|
|
workspaceSelectionStatus: WorkspaceSelectionStatus;
|
|
}): Href {
|
|
if (
|
|
input.workspaceSelection?.serverId === input.serverId &&
|
|
shouldRestoreWorkspaceSelection(input)
|
|
) {
|
|
return buildHostWorkspaceRoute(input.serverId, input.workspaceSelection.workspaceId);
|
|
}
|
|
return buildOpenProjectRoute();
|
|
}
|
|
|
|
function isIndexPathname(pathname: string) {
|
|
return pathname === "/" || pathname === "";
|
|
}
|
|
|
|
function hostExists(hosts: readonly { serverId: string }[], serverId: string | null): boolean {
|
|
if (!serverId) {
|
|
return false;
|
|
}
|
|
return hosts.some((host) => host.serverId === serverId);
|
|
}
|
|
|
|
function resolveReadyIndexStartupRoute(input: ResolveIndexStartupRouteInput): StartupRouteDecision {
|
|
if (!isIndexPathname(input.route.pathname)) {
|
|
return { kind: "render" };
|
|
}
|
|
|
|
if (!input.isWorkspaceSelectionLoaded) {
|
|
return { kind: "splash" };
|
|
}
|
|
|
|
if (
|
|
shouldRestoreWorkspaceSelection(input) &&
|
|
hostExists(input.hosts, input.workspaceSelection.serverId)
|
|
) {
|
|
// Native cold launch must enter the host boundary first. The host index
|
|
// owns workspace restore after its local dynamic params exist.
|
|
return {
|
|
kind: "redirect",
|
|
href: buildHostRootRoute(input.workspaceSelection.serverId),
|
|
};
|
|
}
|
|
|
|
if (input.anyOnlineHostServerId) {
|
|
return { kind: "redirect", href: buildHostRootRoute(input.anyOnlineHostServerId) };
|
|
}
|
|
|
|
const savedHostServerId = input.hosts[0]?.serverId ?? null;
|
|
if (savedHostServerId) {
|
|
return { kind: "redirect", href: buildHostRootRoute(savedHostServerId) };
|
|
}
|
|
|
|
if (input.hasGivenUpWaitingForHost) {
|
|
return { kind: "redirect", href: WELCOME_ROUTE };
|
|
}
|
|
|
|
return { kind: "splash" };
|
|
}
|
|
|
|
function resolveReadyHostStartupRoute(input: ResolveHostStartupRouteInput): StartupRouteDecision {
|
|
if (hostExists(input.hosts, input.route.serverId)) {
|
|
return { kind: "render" };
|
|
}
|
|
|
|
const fallbackServerId = input.hosts[0]?.serverId ?? null;
|
|
if (fallbackServerId) {
|
|
return { kind: "redirect", href: buildOpenProjectRoute() };
|
|
}
|
|
|
|
return { kind: "redirect", href: WELCOME_ROUTE };
|
|
}
|
|
|
|
function isHostStartupRouteInput(
|
|
input: ResolveStartupRouteInput,
|
|
): input is ResolveHostStartupRouteInput {
|
|
return input.route.kind === "host";
|
|
}
|
|
|
|
export function resolveStartupRoute(input: ResolveStartupRouteInput): StartupRouteDecision {
|
|
if (isHostStartupRouteInput(input)) {
|
|
if (input.startupBlocker.kind !== "none" || input.hostRegistryStatus === "loading") {
|
|
return { kind: "render" };
|
|
}
|
|
return resolveReadyHostStartupRoute(input);
|
|
}
|
|
|
|
if (input.startupBlocker.kind !== "none") {
|
|
return { kind: "splash" };
|
|
}
|
|
|
|
if (input.hostRegistryStatus === "loading") {
|
|
return { kind: "splash" };
|
|
}
|
|
|
|
return resolveReadyIndexStartupRoute(input);
|
|
}
|
|
|
|
export function bindHostRuntimeAppState(
|
|
store: { setAppVisible: (visible: boolean) => void },
|
|
appState: Pick<typeof AppState, "currentState" | "addEventListener">,
|
|
): () => void {
|
|
const subscription = appState.addEventListener("change", (state) => {
|
|
store.setAppVisible(state === "active");
|
|
});
|
|
store.setAppVisible(appState.currentState === "active");
|
|
return () => subscription.remove();
|
|
}
|