import * as Context from "effect/Context"; import * as Effect from "effect/Effect"; import * as Exit from "effect/Exit"; import * as Fiber from "effect/Fiber"; import * as Layer from "effect/Layer"; import * as Scope from "effect/Scope"; import * as FetchHttpClient from "effect/http/FetchHttpClient"; import * as HttpClient from "effect/http/HttpClient"; import type { ConnectionCatalogEntry, ConnectionRoute } from "./catalog.ts"; import type { ConnectionAttemptError, ConnectionAttemptStage, PreparedConnection, } from "./model.ts"; import { ConnectionTransientError } from "./model.ts"; import * as ConnectionResolver from "./resolver.ts"; import { connectionRoutes, routeEntry, routeHttpBaseUrl, sendsTokenInClear, usableRoutes, } from "./routes.ts"; import * as RpcSession from "../rpc/session.ts"; import { fetchRemoteEnvironmentDescriptor } from "../environment/descriptor.ts"; export type ConnectionDriverProgress = | { readonly stage: "preparing"; } | { readonly stage: Exclude; readonly prepared: PreparedConnection; }; export interface EnvironmentConnectionLease { readonly prepared: PreparedConnection; readonly session: RpcSession.RpcSession; } export type RouteCheck = "answered" | "silent" | "unchecked"; const ROUTE_CHECK_TIMEOUT_MS = 2_500; // Preferred direct routes share one head start, so a relay connection does not // wait the full probe timeout for a LAN the device has already left. A route // that misses it connects over relay, then upgrades with a second reconnect. const ROUTE_CHECK_HEAD_START_MS = 500; export class ConnectionDriver extends Context.Service< ConnectionDriver, { readonly connect: ( entry: ConnectionCatalogEntry, reportProgress: (progress: ConnectionDriverProgress) => Effect.Effect, ) => Effect.Effect; readonly preflight: ( entry: ConnectionCatalogEntry, route: ConnectionRoute, ) => Effect.Effect; /** Asks the host behind a live session to answer a fresh request on the session's own origin. */ readonly checkHost: (prepared: PreparedConnection) => Effect.Effect; } >()("@t3tools/client-runtime/connection/driver/ConnectionDriver") {} export const connectOverRoutes = Effect.fn("ConnectionDriver.connectOverRoutes")(function* ( entry: ConnectionCatalogEntry, checkRoute: (route: ConnectionRoute) => Effect.Effect, connectRoute: ( route: ConnectionRoute, ) => Effect.Effect, ) { const routes = usableRoutes(entry); const checks = routes.length === 1 ? [] : yield* Effect.forEach(routes, (route) => Effect.forkChild(checkRoute(route))); const headStart = yield* Effect.forkChild(Effect.sleep(ROUTE_CHECK_HEAD_START_MS)); const attemptScope = yield* Scope.Scope; let transient: ConnectionAttemptError | undefined; let blocked: ConnectionAttemptError | undefined; // Each route gets its own scope so a half-open session closes before the next try. const attempt = Effect.fnUntraced(function* (route: ConnectionRoute) { const routeScope = yield* Scope.fork(attemptScope); const result = yield* connectRoute(route).pipe( Scope.provide(routeScope), Effect.onExit((exit) => (Exit.isSuccess(exit) ? Effect.void : Scope.close(routeScope, exit))), Effect.result, ); if (result._tag === "Failure") { if (result.failure._tag === "ConnectionTransientError") transient ??= result.failure; else blocked ??= result.failure; } return result; }); const silent: Array<{ readonly route: ConnectionRoute; readonly index: number }> = []; for (const [index, route] of routes.entries()) { const check = checks.length === 0 ? "unchecked" : yield* Effect.raceFirst( Fiber.join(checks[index]!), Fiber.join(headStart).pipe(Effect.as("silent")), ); if (check === "silent") { silent.push({ route, index }); continue; } const result = yield* attempt(route); if (result._tag === "Success") return result.success; // An incompatible server is the same server on every route. if (result.failure.reason === "unsupported") return yield* result.failure; } // A slow route that answered after the head start goes before one that is // still dead, which can hold an attempt open far longer than its probe. const late = yield* Effect.forEach(silent, ({ index }) => Fiber.join(checks[index]!), { concurrency: "unbounded", }); const retry = [ ...silent.filter((_, position) => late[position] !== "silent"), ...silent.filter((_, position) => late[position] === "silent"), ]; for (const { route } of retry) { const result = yield* attempt(route); if (result._tag === "Success") return result.success; if (result.failure.reason === "unsupported") return yield* result.failure; } return yield* ( transient ?? blocked ?? new ConnectionTransientError({ reason: "endpoint-unavailable", detail: `${entry.target.label} did not answer on any saved route.`, }) ); }); /** @public */ export const make = Effect.gen(function* () { const resolver = yield* ConnectionResolver.ConnectionResolver; const sessions = yield* RpcSession.RpcSessionFactory; const httpClient = yield* HttpClient.HttpClient; // The descriptor is public, so this sends no credential to whatever // answers at a saved LAN address on a different network. const checkDescriptor = (httpBaseUrl: string, environmentId: string) => fetchRemoteEnvironmentDescriptor({ httpBaseUrl, timeoutMs: ROUTE_CHECK_TIMEOUT_MS, }).pipe( Effect.map((descriptor): RouteCheck => descriptor.environmentId === environmentId ? "answered" : "silent", ), Effect.orElseSucceed((): RouteCheck => "silent"), // A cached descriptor would answer for a host that cannot be reached. Effect.provideService(FetchHttpClient.RequestInit, { cache: "no-store" }), Effect.provideService(HttpClient.HttpClient, httpClient), ); const checkRoute = (entry: ConnectionCatalogEntry, route: ConnectionRoute) => { const httpBaseUrl = routeHttpBaseUrl(route); if (httpBaseUrl === null) return Effect.succeed("unchecked"); return checkDescriptor(httpBaseUrl, entry.target.environmentId).pipe( Effect.withSpan("ConnectionDriver.checkRoute", { attributes: { "connection.target.kind": route.target._tag }, }), ); }; const checkHost = (prepared: PreparedConnection) => checkDescriptor(prepared.httpBaseUrl, prepared.environmentId).pipe( Effect.withSpan("ConnectionDriver.checkHost", { attributes: { "connection.target.kind": prepared.target._tag }, }), ); const connect = Effect.fn("ConnectionDriver.connect")(function* ( entry: ConnectionCatalogEntry, reportProgress: (progress: ConnectionDriverProgress) => Effect.Effect, ) { const target = entry.target; yield* Effect.annotateCurrentSpan({ "connection.environment.id": target.environmentId, "connection.target.kind": target._tag, "connection.route.count": connectionRoutes(entry).length, }); yield* reportProgress({ stage: "preparing" }); return yield* connectOverRoutes( entry, (route) => checkRoute(entry, route), Effect.fnUntraced(function* (route) { const prepared = yield* resolver.prepare(routeEntry(entry, route)); yield* reportProgress({ stage: "opening", prepared }); const session = yield* sessions.connect(prepared); yield* reportProgress({ stage: "synchronizing", prepared }); yield* session.ready; return { prepared, session } satisfies EnvironmentConnectionLease; }), ); }); const preflight = (entry: ConnectionCatalogEntry, route: ConnectionRoute) => (sendsTokenInClear(connectionRoutes(entry), route) ? Effect.succeed("silent" as const) : checkRoute(entry, route) ).pipe( Effect.flatMap((check) => check === "answered" ? resolver.prepare(routeEntry(entry, route)).pipe( Effect.as(true), Effect.orElseSucceed(() => false), ) : Effect.succeed(false), ), Effect.withSpan("ConnectionDriver.preflight"), ); return ConnectionDriver.of({ connect, preflight, checkHost }); }); export const layer = Layer.effect(ConnectionDriver, make);