import { type ServerConfig, WS_METHODS } from "@t3tools/contracts"; import * as Context from "effect/Context"; import * as Deferred from "effect/Deferred"; import * as Effect from "effect/Effect"; import * as Layer from "effect/Layer"; import * as Schedule from "effect/Schedule"; import type * as Scope from "effect/Scope"; import * as RpcClient from "effect/unstable/rpc/RpcClient"; import * as RpcSerialization from "effect/unstable/rpc/RpcSerialization"; import * as Socket from "effect/unstable/socket/Socket"; import { makeWsRpcProtocolClient, type WsRpcProtocolClient } from "./protocol.ts"; import type { ConnectionAttemptError, ConnectionTransientError, PreparedConnection, } from "../connection/model.ts"; import { ConnectionBlockedError, ConnectionTransientError as ConnectionTransientErrorClass, } from "../connection/model.ts"; const SOCKET_OPEN_TIMEOUT = "15 seconds"; export interface RpcSession { readonly client: WsRpcProtocolClient; readonly initialConfig: Effect.Effect; readonly ready: Effect.Effect; readonly probe: Effect.Effect; readonly closed: Effect.Effect; } export class RpcSessionFactory extends Context.Service< RpcSessionFactory, { readonly connect: ( connection: PreparedConnection, ) => Effect.Effect; } >()("@t3tools/client-runtime/rpc/session/RpcSessionFactory") {} type InitialConfigError = Effect.Error< ReturnType >; type ProbeError = Effect.Error>; function mapSessionRpcError(error: InitialConfigError | ProbeError): ConnectionAttemptError { switch (error._tag) { case "EnvironmentAuthorizationError": return new ConnectionBlockedError({ reason: "permission", detail: error.message, }); case "KeybindingsConfigParseError": case "ServerSettingsError": return new ConnectionTransientErrorClass({ reason: "remote-unavailable", detail: error.message, }); case "RpcClientError": return new ConnectionTransientErrorClass({ reason: "transport", detail: error.message, }); } } export const make = Effect.gen(function* () { const webSocketConstructor = yield* Socket.WebSocketConstructor; const connect = Effect.fnUntraced(function* (connection: PreparedConnection) { yield* Effect.annotateCurrentSpan({ "connection.environment.id": connection.environmentId, }); const connected = yield* Deferred.make(); const disconnected = yield* Deferred.make(); const hooks = RpcClient.ConnectionHooks.of({ onConnect: Deferred.succeed(connected, undefined).pipe(Effect.asVoid), onDisconnect: Deferred.isDone(connected).pipe( Effect.flatMap((wasConnected) => Deferred.fail( disconnected, new ConnectionTransientErrorClass({ reason: "transport", detail: wasConnected ? `${connection.label} disconnected.` : `${connection.label} could not establish a WebSocket connection.`, }), ), ), Effect.asVoid, ), }); const socketLayer = Socket.layerWebSocket(connection.socketUrl, { openTimeout: SOCKET_OPEN_TIMEOUT, }).pipe(Layer.provide(Layer.succeed(Socket.WebSocketConstructor, webSocketConstructor))); const protocolLayer = Layer.effect( RpcClient.Protocol, RpcClient.makeProtocolSocket({ retryTransientErrors: false, retryPolicy: Schedule.recurs(0), }), ).pipe( Layer.provide( Layer.mergeAll( socketLayer, RpcSerialization.layerJson, Layer.succeed(RpcClient.ConnectionHooks, hooks), ), ), ); const protocolContext = yield* Layer.build(protocolLayer).pipe( Effect.withSpan("environment.websocket.connect"), ); const client = yield* makeWsRpcProtocolClient.pipe(Effect.provide(protocolContext)); const initialConfig = yield* Effect.cached( client[WS_METHODS.serverGetConfig]({}).pipe( Effect.mapError(mapSessionRpcError), Effect.withSpan("environment.initialSync"), ), ); const probe = initialConfig.pipe( Effect.flatMap((config) => (config.environment.capabilities.connectionProbe === true ? client[WS_METHODS.serverProbe]({}) : client[WS_METHODS.serverGetConfig]({}) ).pipe(Effect.mapError(mapSessionRpcError)), ), Effect.asVoid, Effect.withSpan("clientRuntime.connection.rpcSession.probe"), ); return { client, initialConfig, ready: Deferred.await(connected).pipe( Effect.andThen(initialConfig), Effect.asVoid, Effect.raceFirst(Deferred.await(disconnected)), ), probe, closed: Deferred.await(disconnected), } satisfies RpcSession; }); return RpcSessionFactory.of({ connect }); }); export const layer = Layer.effect(RpcSessionFactory, make);