import { ORCHESTRATION_WS_METHODS, WS_METHODS } from "@t3tools/contracts"; import * as Cause from "effect/Cause"; import * as Context from "effect/Context"; import type * as Duration from "effect/Duration"; import * as Effect from "effect/Effect"; import * as Option from "effect/Option"; import * as Schema from "effect/Schema"; import * as Stream from "effect/Stream"; import * as SubscriptionRef from "effect/SubscriptionRef"; import { RpcClientError } from "effect/unstable/rpc"; import { EnvironmentSupervisor } from "../connection/supervisor.ts"; import type { WsRpcProtocolClient } from "../rpc/protocol.ts"; import type { RpcSession } from "../rpc/session.ts"; export class EnvironmentRpcUnavailableError extends Schema.TaggedErrorClass()( "EnvironmentRpcUnavailableError", { environmentId: Schema.String, message: Schema.String, }, ) {} export interface EnvironmentRpcRequestObservation { readonly environmentId: string; readonly method: string; } export class EnvironmentRpcRequestObserver extends Context.Reference<{ readonly observe: ( request: EnvironmentRpcRequestObservation, ) => Effect.Effect>; }>("@t3tools/client-runtime/rpc/EnvironmentRpcRequestObserver", { defaultValue: () => ({ observe: () => Effect.succeed(Effect.void), }), }) {} export type EnvironmentRpcTag = keyof WsRpcProtocolClient & string; type RpcMethod = WsRpcProtocolClient[TTag]; export type EnvironmentSubscriptionRpcTag = | typeof ORCHESTRATION_WS_METHODS.subscribeShell | typeof ORCHESTRATION_WS_METHODS.subscribeThread | typeof WS_METHODS.subscribeAuthAccess | typeof WS_METHODS.subscribeServerConfig | typeof WS_METHODS.subscribeServerLifecycle | typeof WS_METHODS.subscribeTerminalEvents | typeof WS_METHODS.subscribeTerminalMetadata | typeof WS_METHODS.subscribePreviewEvents | typeof WS_METHODS.subscribeDiscoveredLocalServers | typeof WS_METHODS.previewAutomationConnect | typeof WS_METHODS.subscribeVcsStatus | typeof WS_METHODS.terminalAttach; export type EnvironmentStreamCommandRpcTag = | typeof WS_METHODS.cloudInstallRelayClient | typeof WS_METHODS.gitRunStackedAction; export type EnvironmentStreamRpcTag = | EnvironmentSubscriptionRpcTag | EnvironmentStreamCommandRpcTag; export type EnvironmentUnaryRpcTag = Exclude; const isRpcClientError = Schema.is(RpcClientError.RpcClientError); export type EnvironmentRpcInput = Parameters>[0]; export type EnvironmentRpcSuccess = RpcMethod extends (input: any, options?: any) => Effect.Effect ? A : never; export type EnvironmentRpcFailure = RpcMethod extends (input: any, options?: any) => Effect.Effect ? E : never; export type EnvironmentRpcStreamValue = RpcMethod extends (input: any, options?: any) => Stream.Stream ? A : never; export type EnvironmentRpcStreamFailure = RpcMethod extends (input: any, options?: any) => Stream.Stream ? E : never; const currentSession = Effect.fn("EnvironmentRpc.currentSession")(function* () { const supervisor = yield* EnvironmentSupervisor; return yield* SubscriptionRef.get(supervisor.session).pipe( Effect.flatMap( Option.match({ onNone: () => Effect.fail( new EnvironmentRpcUnavailableError({ environmentId: supervisor.target.environmentId, message: `${supervisor.target.label} is not connected.`, }), ), onSome: Effect.succeed, }), ), ); }); export const request = Effect.fn("EnvironmentRpc.request")(function* < TTag extends EnvironmentUnaryRpcTag, >(tag: TTag, input: EnvironmentRpcInput) { const supervisor = yield* EnvironmentSupervisor; yield* Effect.annotateCurrentSpan({ "environment.id": supervisor.target.environmentId, "rpc.method": tag, }); const session = yield* currentSession(); const observer = yield* EnvironmentRpcRequestObserver; const method = session.client[tag] as ( input: EnvironmentRpcInput, ) => Effect.Effect, EnvironmentRpcFailure>; const completeObservation = yield* observer.observe({ environmentId: supervisor.target.environmentId, method: tag, }); return yield* method(input).pipe(Effect.ensuring(completeObservation)); }); export function runStream( tag: TTag, input: EnvironmentRpcInput, ): Stream.Stream< EnvironmentRpcStreamValue, EnvironmentRpcStreamFailure | EnvironmentRpcUnavailableError, EnvironmentSupervisor > { return Stream.unwrap( currentSession().pipe( Effect.map((session) => { const method = session.client[tag] as ( input: EnvironmentRpcInput, ) => Stream.Stream, EnvironmentRpcStreamFailure>; return method(input); }), ), ).pipe( Stream.withSpan("EnvironmentRpc.runStream", { attributes: { "rpc.method": tag }, }), ); } interface SubscriptionOptions { readonly onExpectedFailure?: ( cause: Cause.Cause>, ) => Effect.Effect; readonly retryExpectedFailureAfter?: Duration.Input; readonly resubscribe?: Stream.Stream; } export function subscribeDynamic( tag: TTag, makeInput: (session: RpcSession) => Effect.Effect>, options?: SubscriptionOptions, ): Stream.Stream< EnvironmentRpcStreamValue, EnvironmentRpcStreamFailure, EnvironmentSupervisor > { return Stream.unwrap( EnvironmentSupervisor.pipe( Effect.map((supervisor) => { const sessionChanges = SubscriptionRef.changes(supervisor.session); const sessions = options?.resubscribe === undefined ? sessionChanges : Stream.merge( sessionChanges, options.resubscribe.pipe( Stream.mapEffect(() => SubscriptionRef.get(supervisor.session)), ), ); return sessions.pipe( Stream.switchMap( Option.match({ onNone: () => Stream.empty, onSome: (session) => { const method = session.client[tag] as ( input: EnvironmentRpcInput, ) => Stream.Stream< EnvironmentRpcStreamValue, EnvironmentRpcStreamFailure >; const subscribeToSession = (): Stream.Stream< EnvironmentRpcStreamValue, EnvironmentRpcStreamFailure > => Stream.suspend(() => Stream.unwrap( makeInput(session).pipe( Effect.map((input) => method(input).pipe( Stream.catchCause((cause) => { const hasOnlyExpectedFailures = cause.reasons.length > 0 && cause.reasons.every((reason) => reason._tag === "Fail"); const isTransportFailure = hasOnlyExpectedFailures && cause.reasons.every( (reason) => reason._tag === "Fail" && isRpcClientError(reason.error), ); if (isTransportFailure) { return Stream.fromEffect( Effect.logWarning( "Durable RPC subscription lost its transport; waiting for the next session.", { cause: Cause.pretty(cause), method: tag, environmentId: supervisor.target.environmentId, }, ), ).pipe(Stream.drain); } if ( hasOnlyExpectedFailures && options?.onExpectedFailure !== undefined ) { const handled = Stream.fromEffect( options.onExpectedFailure(cause), ).pipe(Stream.drain); if (options.retryExpectedFailureAfter === undefined) { return handled; } return handled.pipe( Stream.concat( Stream.fromEffect( Effect.sleep(options.retryExpectedFailureAfter), ).pipe(Stream.drain), ), Stream.concat(subscribeToSession()), ); } return Stream.failCause(cause); }), ), ), ), ), ); return subscribeToSession(); }, }), ), ); }), ), ).pipe( Stream.withSpan("EnvironmentRpc.subscribe", { attributes: { "rpc.method": tag }, }), ); } export function subscribe( tag: TTag, input: EnvironmentRpcInput, options?: SubscriptionOptions, ): Stream.Stream< EnvironmentRpcStreamValue, EnvironmentRpcStreamFailure, EnvironmentSupervisor > { return subscribeDynamic(tag, () => Effect.succeed(input), options); } export const config = Effect.gen(function* () { const session = yield* currentSession(); return yield* session.initialConfig; }).pipe(Effect.withSpan("EnvironmentRpc.config"));