189 lines · 6.4 KB · TypeScript
RawDownload
import { Option, Schema } from "effect";import {  normalizeExecutorServerConnection,  type ExecutorServerConnection,  type ExecutorServerConnectionInput,} from "./server-connection"; export const EXECUTOR_SERVER_PROFILES_STORAGE_KEY = "executor.serverConnections.v1"; export interface ExecutorServerProfilesSnapshot {  readonly activeKey: string | null;  readonly profiles: readonly ExecutorServerConnection[];} export interface ExecutorServerProfileStorage {  readonly getItem: (key: string) => string | null;  readonly setItem: (key: string, value: string) => void;} const PersistedBasicAuth = Schema.Struct({  kind: Schema.Literal("basic"),  username: Schema.optional(Schema.String),  password: Schema.String,}); const PersistedBearerAuth = Schema.Struct({  kind: Schema.Literal("bearer"),  token: Schema.String,}); const PersistedAuth = Schema.Union([PersistedBasicAuth, PersistedBearerAuth]); const PersistedConnection = Schema.Struct({  kind: Schema.optional(Schema.Literals(["http", "desktop-sidecar"])),  key: Schema.optional(Schema.String),  origin: Schema.optional(Schema.String),  apiBaseUrl: Schema.optional(Schema.String),  displayName: Schema.optional(Schema.String),  auth: Schema.optional(PersistedAuth),}); const PersistedProfiles = Schema.Struct({  version: Schema.Literal(1),  activeKey: Schema.optional(Schema.NullOr(Schema.String)),  profiles: Schema.Array(PersistedConnection),}); const decodeProfilesJson = Schema.decodeUnknownOption(Schema.fromJsonString(PersistedProfiles)); const EMPTY_PROFILES: ExecutorServerProfilesSnapshot = {  activeKey: null,  profiles: [],}; const hasHttpScheme = (value: string): boolean => /^https?:\/\//.test(value.trim()); const canParseOrigin = (origin: string | undefined): boolean => {  if (origin === undefined) return true;  const trimmed = origin.trim();  if (!trimmed) return true;  return URL.canParse(hasHttpScheme(trimmed) ? trimmed : `http://${trimmed}`);}; const canParseApiBaseUrl = (apiBaseUrl: string | undefined): boolean => {  if (apiBaseUrl === undefined) return true;  const trimmed = apiBaseUrl.trim();  if (!trimmed) return true;  return hasHttpScheme(trimmed) && URL.canParse(trimmed);}; const normalizeConnectionOption = (  input: ExecutorServerConnectionInput,): ExecutorServerConnection | null => {  if (!canParseOrigin(input.origin) || !canParseApiBaseUrl(input.apiBaseUrl)) return null;  return normalizeExecutorServerConnection(input);}; const asConnectionInput = (  connection: ExecutorServerConnection,): ExecutorServerConnectionInput => ({  kind: connection.kind,  key: connection.key,  origin: connection.origin,  apiBaseUrl: connection.apiBaseUrl,  displayName: connection.displayName,  ...(connection.auth ? { auth: connection.auth } : {}),}); export const parseExecutorServerProfilesSnapshot = (  raw: string | null | undefined,): ExecutorServerProfilesSnapshot => {  if (!raw) return EMPTY_PROFILES;  const decoded = decodeProfilesJson(raw);  if (Option.isNone(decoded)) return EMPTY_PROFILES;  return normalizeExecutorServerProfilesSnapshot(decoded.value);}; export const serializeExecutorServerProfilesSnapshot = (  snapshot: ExecutorServerProfilesSnapshot,): string => {  // Custom server auth is intentionally persisted with the profile so local-dev  // and advanced remote endpoints do not force reauth on every reload. Desktop  // stores this payload through its Electron store; web falls back to  // localStorage for the same format.  const persisted = {    version: 1,    activeKey: snapshot.activeKey,    profiles: snapshot.profiles.map(asConnectionInput),  };  return JSON.stringify(persisted);}; export const normalizeExecutorServerProfilesSnapshot = (input: {  readonly activeKey?: string | null;  readonly profiles?: readonly ExecutorServerConnectionInput[];}): ExecutorServerProfilesSnapshot => {  const deduped = new Map<string, ExecutorServerConnection>();  for (const profile of input.profiles ?? []) {    const normalized = normalizeConnectionOption(profile);    if (!normalized) continue;    deduped.set(normalized.key, normalized);  }   const profiles = [...deduped.values()];  const activeKey =    input.activeKey && deduped.has(input.activeKey) ? input.activeKey : (profiles[0]?.key ?? null);   return { activeKey, profiles };}; export const readExecutorServerProfiles = (  storage: ExecutorServerProfileStorage | null | undefined,  storageKey = EXECUTOR_SERVER_PROFILES_STORAGE_KEY,): ExecutorServerProfilesSnapshot => {  if (!storage) return EMPTY_PROFILES;  return parseExecutorServerProfilesSnapshot(storage.getItem(storageKey));}; export const writeExecutorServerProfiles = (  storage: ExecutorServerProfileStorage | null | undefined,  snapshot: ExecutorServerProfilesSnapshot,  storageKey = EXECUTOR_SERVER_PROFILES_STORAGE_KEY,): void => {  if (!storage) return;  storage.setItem(storageKey, serializeExecutorServerProfilesSnapshot(snapshot));}; export const getActiveExecutorServerProfile = (  snapshot: ExecutorServerProfilesSnapshot,): ExecutorServerConnection | null =>  snapshot.profiles.find((profile) => profile.key === snapshot.activeKey) ?? null; export const upsertExecutorServerProfile = (  snapshot: ExecutorServerProfilesSnapshot,  input: ExecutorServerConnectionInput,  options: {    readonly makeActive?: boolean;  } = {},): ExecutorServerProfilesSnapshot | null => {  const normalized = normalizeConnectionOption(input);  if (!normalized) return null;  const profiles = new Map(snapshot.profiles.map((profile) => [profile.key, profile]));  profiles.set(normalized.key, normalized);  const activeKey =    options.makeActive === false ? (snapshot.activeKey ?? normalized.key) : normalized.key;  return normalizeExecutorServerProfilesSnapshot({    activeKey,    profiles: [...profiles.values()],  });}; export const selectExecutorServerProfile = (  snapshot: ExecutorServerProfilesSnapshot,  key: string,): ExecutorServerProfilesSnapshot => {  if (!snapshot.profiles.some((profile) => profile.key === key)) return snapshot;  return { ...snapshot, activeKey: key };}; export const removeExecutorServerProfile = (  snapshot: ExecutorServerProfilesSnapshot,  key: string,): ExecutorServerProfilesSnapshot => {  const profiles = snapshot.profiles.filter((profile) => profile.key !== key);  return normalizeExecutorServerProfilesSnapshot({    activeKey: snapshot.activeKey === key ? null : snapshot.activeKey,    profiles,  });};