174 lines · 4.6 KB · TypeScript
RawDownload
import type {  EnvironmentId,  OrchestrationShellSnapshot,  OrchestrationShellStreamEvent,  ServerConfig,  ServerLifecycleWelcomePayload,  TerminalEvent,} from "@t3tools/contracts"; import type { KnownEnvironment } from "./knownEnvironment";import type { WsRpcClient } from "./wsRpcClient"; export interface EnvironmentConnection {  readonly kind: "primary" | "saved";  readonly environmentId: EnvironmentId;  readonly knownEnvironment: KnownEnvironment;  readonly client: WsRpcClient;  readonly ensureBootstrapped: () => Promise<void>;  readonly reconnect: () => Promise<void>;  readonly dispose: () => Promise<void>;} interface OrchestrationHandlers {  readonly applyShellEvent: (    event: OrchestrationShellStreamEvent,    environmentId: EnvironmentId,  ) => void;  readonly syncShellSnapshot: (    snapshot: OrchestrationShellSnapshot,    environmentId: EnvironmentId,  ) => void;  readonly applyTerminalEvent: (event: TerminalEvent, environmentId: EnvironmentId) => void;} interface EnvironmentConnectionInput extends OrchestrationHandlers {  readonly kind: "primary" | "saved";  readonly knownEnvironment: KnownEnvironment;  readonly client: WsRpcClient;  readonly refreshMetadata?: () => Promise<void>;  readonly onConfigSnapshot?: (config: ServerConfig) => void;  readonly onWelcome?: (payload: ServerLifecycleWelcomePayload) => void;  readonly onShellResubscribe?: (environmentId: EnvironmentId) => void;} function createBootstrapGate() {  let resolve: (() => void) | null = null;  let reject: ((error: unknown) => void) | null = null;  let promise = new Promise<void>((nextResolve, nextReject) => {    resolve = nextResolve;    reject = nextReject;  });   return {    wait: () => promise,    resolve: () => {      resolve?.();      resolve = null;      reject = null;    },    reject: (error: unknown) => {      reject?.(error);      resolve = null;      reject = null;    },    reset: () => {      promise = new Promise<void>((nextResolve, nextReject) => {        resolve = nextResolve;        reject = nextReject;      });    },  };} export function createEnvironmentConnection(  input: EnvironmentConnectionInput,): EnvironmentConnection {  const environmentId = input.knownEnvironment.environmentId;   if (!environmentId) {    throw new Error(      `Known environment ${input.knownEnvironment.label} is missing its environmentId.`,    );  }   let disposed = false;  const bootstrapGate = createBootstrapGate();   const observeEnvironmentIdentity = (nextEnvironmentId: EnvironmentId, source: string) => {    if (environmentId !== nextEnvironmentId) {      throw new Error(        `Environment connection ${environmentId} changed identity to ${nextEnvironmentId} via ${source}.`,      );    }  };   const unsubLifecycle = input.client.server.subscribeLifecycle((event) => {    if (event.type !== "welcome") {      return;    }     observeEnvironmentIdentity(event.payload.environment.environmentId, "server lifecycle welcome");    input.onWelcome?.(event.payload);  });   const unsubConfig = input.client.server.subscribeConfig((event) => {    if (event.type !== "snapshot") {      return;    }     observeEnvironmentIdentity(event.config.environment.environmentId, "server config snapshot");    input.onConfigSnapshot?.(event.config);  });   const unsubShell = input.client.orchestration.subscribeShell(    (item) => {      if (disposed) {        return;      }       if (item.kind === "snapshot") {        input.syncShellSnapshot(item.snapshot, environmentId);        bootstrapGate.resolve();        return;      }       input.applyShellEvent(item, environmentId);    },    {      onResubscribe: () => {        if (disposed) {          return;        }         bootstrapGate.reset();        input.onShellResubscribe?.(environmentId);      },    },  );   const unsubTerminalEvent = input.client.terminal.onEvent((event) => {    input.applyTerminalEvent(event, environmentId);  });   const cleanup = () => {    disposed = true;    unsubShell();    unsubTerminalEvent();    unsubLifecycle();    unsubConfig();  };   return {    kind: input.kind,    environmentId,    knownEnvironment: input.knownEnvironment,    client: input.client,    ensureBootstrapped: () => bootstrapGate.wait(),    reconnect: async () => {      bootstrapGate.reset();      try {        await input.client.reconnect();        await input.refreshMetadata?.();        await bootstrapGate.wait();      } catch (error) {        bootstrapGate.reject(error);        throw error;      }    },    dispose: async () => {      cleanup();      await input.client.dispose();    },  };}