diff --git a/.changeset/tolerant-server-config.md b/.changeset/tolerant-server-config.md new file mode 100644 index 0000000..c171671 --- /dev/null +++ b/.changeset/tolerant-server-config.md @@ -0,0 +1,5 @@ +--- +"t3code-cli": patch +--- + +Keep RPC connections compatible with server config fields the CLI does not use. diff --git a/package.json b/package.json index 4bdae86..c2feb82 100644 --- a/package.json +++ b/package.json @@ -117,7 +117,7 @@ "devDependencies": { "@changesets/cli": "^2.31.1", "@effect/vitest": "catalog:", - "@t3tools/client-runtime": "workspace:*", + "@t3tools/client-runtime": "link:upstream-t3code/packages/client-runtime", "@t3tools/contracts": "workspace:*", "@total-typescript/shoehorn": "^0.1.2", "@types/node": "^26.1.2", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 3b77f72..ae0ac8b 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -6,9 +6,33 @@ settings: catalogs: default: + '@effect/platform-node': + specifier: 4.0.0-beta.78 + version: 4.0.0-beta.78 '@effect/vitest': specifier: 4.0.0-beta.78 version: 4.0.0-beta.78 + '@noble/curves': + specifier: 2.2.0 + version: 2.2.0 + '@noble/hashes': + specifier: 2.2.0 + version: 2.2.0 + '@types/node': + specifier: ^26.1.2 + version: 26.1.2 + effect: + specifier: 4.0.0-beta.78 + version: 4.0.0-beta.78 + jose: + specifier: 6.2.4 + version: 6.2.4 + vite-plus: + specifier: 0.2.6 + version: 0.2.6 + yaml: + specifier: ^2.9.0 + version: 2.9.0 patchedDependencies: '@effect/vitest@4.0.0-beta.78': @@ -46,9 +70,9 @@ importers: version: 2.31.1(@types/node@26.1.2) '@effect/vitest': specifier: 'catalog:' - version: 4.0.0-beta.78(patch_hash=74fd480109c3bd975255bf5573a64718451bcae02b523c432bc18e9aa27b52fd)(effect@4.0.0-beta.78)(vitest@4.1.10(@types/node@26.1.2)(@vitest/browser-preview@4.1.10)(vite@8.0.14(@types/node@26.1.2)(yaml@2.9.0))) + version: 4.0.0-beta.78(patch_hash=74fd480109c3bd975255bf5573a64718451bcae02b523c432bc18e9aa27b52fd)(effect@4.0.0-beta.78)(vitest@4.1.10) '@t3tools/client-runtime': - specifier: workspace:* + specifier: link:upstream-t3code/packages/client-runtime version: link:upstream-t3code/packages/client-runtime '@t3tools/contracts': specifier: workspace:* @@ -2332,11 +2356,6 @@ snapshots: - bufferutil - utf-8-validate - '@effect/vitest@4.0.0-beta.78(patch_hash=74fd480109c3bd975255bf5573a64718451bcae02b523c432bc18e9aa27b52fd)(effect@4.0.0-beta.78)(vitest@4.1.10(@types/node@26.1.2)(@vitest/browser-preview@4.1.10)(vite@8.0.14(@types/node@26.1.2)(yaml@2.9.0)))': - dependencies: - effect: 4.0.0-beta.78 - vitest: 4.1.10(@types/node@26.1.2)(@vitest/browser-preview@4.1.10)(vite@8.0.14(@types/node@26.1.2)(yaml@2.9.0)) - '@effect/vitest@4.0.0-beta.78(patch_hash=74fd480109c3bd975255bf5573a64718451bcae02b523c432bc18e9aa27b52fd)(effect@4.0.0-beta.78)(vitest@4.1.10)': dependencies: effect: 4.0.0-beta.78 diff --git a/src/rpc/layer.ts b/src/rpc/layer.ts index 604b57a..1528422 100644 --- a/src/rpc/layer.ts +++ b/src/rpc/layer.ts @@ -1,4 +1,3 @@ -import { RpcSessionFactory } from "@t3tools/client-runtime/rpc"; import * as Context from "effect/Context"; import * as Effect from "effect/Effect"; import * as Exit from "effect/Exit"; @@ -11,6 +10,7 @@ import * as SynchronizedRef from "effect/SynchronizedRef"; import { T3PreparedConnectionProvider } from "../connection/prepared.ts"; import { RpcError } from "./error.ts"; +import { T3RpcSessionFactory } from "./session.ts"; import { T3Rpc, type WsClient } from "./service.ts"; const connectionRetrySchedule = Schedule.exponential("100 millis").pipe( @@ -27,7 +27,7 @@ type Connection = { export const makeT3RpcLayer = Effect.fn("makeT3RpcLayer")(function* () { const preparedConnectionProvider = yield* T3PreparedConnectionProvider; - const sessions = yield* RpcSessionFactory; + const sessions = yield* T3RpcSessionFactory; const parentScope = yield* Scope.Scope; const connection = yield* SynchronizedRef.make(Option.none()); const openConnection = Effect.fn("T3RpcLive.openConnection")(function* () { diff --git a/src/rpc/service.ts b/src/rpc/service.ts index 54a765c..82206ec 100644 --- a/src/rpc/service.ts +++ b/src/rpc/service.ts @@ -1,10 +1,11 @@ -import type { RpcSession } from "@t3tools/client-runtime/rpc"; import * as Context from "effect/Context"; import type * as Effect from "effect/Effect"; +import type { RpcClient, RpcClientError } from "effect/unstable/rpc"; import type { RpcError } from "./error.ts"; +import type { CliWsRpcGroup } from "./ws-group.ts"; -export type WsClient = RpcSession["client"]; +export type WsClient = RpcClient.FromGroup; export type T3RpcService = { readonly getClient: Effect.Effect; diff --git a/src/rpc/session.ts b/src/rpc/session.ts index 9d722a4..a161c5e 100644 --- a/src/rpc/session.ts +++ b/src/rpc/session.ts @@ -1,8 +1,3 @@ -import { - makeWsRpcProtocolClient, - type RpcSession, - RpcSessionFactory, -} from "@t3tools/client-runtime/rpc"; import { ConnectionBlockedError, type ConnectionAttemptError, @@ -10,22 +5,47 @@ import { type PreparedConnection, } from "@t3tools/client-runtime/connection"; import { 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 type { WsClient as CliWsClient } from "./service.ts"; +import type { CliServerConfig } from "./ws-group.ts"; +import { CliWsRpcGroup } from "./ws-group.ts"; + +interface T3RpcSession { + readonly client: CliWsClient; + readonly initialConfig: Effect.Effect; + readonly ready: Effect.Effect; + readonly probe: Effect.Effect; + readonly closed: Effect.Effect; +} + +export class T3RpcSessionFactory extends Context.Service< + T3RpcSessionFactory, + { + readonly connect: ( + connection: PreparedConnection, + ) => Effect.Effect; + } +>()("t3cli/T3RpcSessionFactory") {} + type SessionRpcError = - | Effect.Error> - | Effect.Error>; + | Effect.Error> + | Effect.Error>; + +const makeClient = RpcClient.make(CliWsRpcGroup); const makeT3RpcSessionFactory = Effect.fn("makeT3RpcSessionFactory")(function* () { const webSocketConstructor = yield* Socket.WebSocketConstructor; - const connect: RpcSessionFactory["Service"]["connect"] = Effect.fnUntraced(function* ( + const connect: T3RpcSessionFactory["Service"]["connect"] = Effect.fnUntraced(function* ( connection: PreparedConnection, ) { yield* Effect.annotateCurrentSpan({ @@ -72,7 +92,7 @@ const makeT3RpcSessionFactory = Effect.fn("makeT3RpcSessionFactory")(function* ( const protocolContext = yield* Layer.build(protocolLayer).pipe( Effect.withSpan("environment.websocket.connect"), ); - const client = yield* makeWsRpcProtocolClient.pipe(Effect.provide(protocolContext)); + const client = yield* makeClient.pipe(Effect.provide(protocolContext)); const initialConfig = yield* Effect.cached( client[WS_METHODS.serverGetConfig]({}).pipe( catchSessionRpcErrors, @@ -82,7 +102,7 @@ const makeT3RpcSessionFactory = Effect.fn("makeT3RpcSessionFactory")(function* ( const probe = initialConfig.pipe( Effect.flatMap((config) => Effect.gen(function* () { - if (config.environment.capabilities.connectionProbe === true) { + if (config.environment.capabilities.connectionProbe) { return yield* client[WS_METHODS.serverProbe]({}); } return yield* client[WS_METHODS.serverGetConfig]({}); @@ -102,13 +122,13 @@ const makeT3RpcSessionFactory = Effect.fn("makeT3RpcSessionFactory")(function* ( ), probe, closed: Deferred.await(disconnected), - } satisfies RpcSession; + } satisfies T3RpcSession; }); - return RpcSessionFactory.of({ connect }); + return T3RpcSessionFactory.of({ connect }); }); -export const T3RpcSessionFactoryLive = Layer.effect(RpcSessionFactory, makeT3RpcSessionFactory()); +export const T3RpcSessionFactoryLive = Layer.effect(T3RpcSessionFactory, makeT3RpcSessionFactory()); function catchSessionRpcErrors( effect: Effect.Effect, diff --git a/src/rpc/ws-group.ts b/src/rpc/ws-group.ts new file mode 100644 index 0000000..39d512d --- /dev/null +++ b/src/rpc/ws-group.ts @@ -0,0 +1,53 @@ +import { + EnvironmentAuthorizationError, + KeybindingsConfigError, + ServerProviders, + ServerSettingsError, + WS_METHODS, + WsOrchestrationDispatchCommandRpc, + WsOrchestrationGetArchivedShellSnapshotRpc, + WsOrchestrationSubscribeShellRpc, + WsOrchestrationSubscribeThreadRpc, + WsServerProbeRpc, + WsSubscribeTerminalEventsRpc, + WsSubscribeTerminalMetadataRpc, + WsTerminalAttachRpc, + WsTerminalCloseRpc, + WsTerminalOpenRpc, + WsTerminalResizeRpc, + WsTerminalWriteRpc, +} from "@t3tools/contracts"; +import * as Schema from "effect/Schema"; +import { Rpc, RpcGroup } from "effect/unstable/rpc"; + +export const CliServerConfig = Schema.Struct({ + environment: Schema.Struct({ + capabilities: Schema.Struct({ + connectionProbe: Schema.Boolean, + }), + }), + providers: ServerProviders, +}); +export type CliServerConfig = typeof CliServerConfig.Type; + +export const WsServerGetConfigRpc = Rpc.make(WS_METHODS.serverGetConfig, { + payload: Schema.Struct({}), + success: CliServerConfig, + error: Schema.Union([KeybindingsConfigError, ServerSettingsError, EnvironmentAuthorizationError]), +}); + +export const CliWsRpcGroup = RpcGroup.make( + WsTerminalOpenRpc, + WsTerminalAttachRpc, + WsTerminalWriteRpc, + WsTerminalResizeRpc, + WsTerminalCloseRpc, + WsSubscribeTerminalEventsRpc, + WsSubscribeTerminalMetadataRpc, + WsOrchestrationDispatchCommandRpc, + WsOrchestrationGetArchivedShellSnapshotRpc, + WsOrchestrationSubscribeShellRpc, + WsOrchestrationSubscribeThreadRpc, + WsServerProbeRpc, + WsServerGetConfigRpc, +); diff --git a/upstream-t3code b/upstream-t3code index 8b8da91..cb4714c 160000 --- a/upstream-t3code +++ b/upstream-t3code @@ -1 +1 @@ -Subproject commit 8b8da9193c710ffdbb156f57fc0ec24d06479eff +Subproject commit cb4714ceba5b76b3afa34add7061e90a570600ce