122 lines · 4.7 KB · TypeScript
RawDownload
/** * Shared server settings. * * Every server keeps its own `settings.json`, but some keys are user * preferences that only live on the server because the server has to act on * them (auto-settlement runs with no client attached). A user does not want * those to differ per machine. Clients write these keys to every connected * environment, and warn when a connected environment still holds a different * value so the user can push their current value out. */import type { EnvironmentId, ServerSettings, ServerSettingsPatch } from "@t3tools/contracts";import * as Equal from "effect/Equal";import * as Struct from "effect/Struct"; import type { EnvironmentPresentation } from "../connection/presentation.ts"; /** Server keys that hold a user preference rather than machine config. */export const SHARED_SERVER_SETTING_KEYS = [  "sidebarAutoSettleAfterDays",  "sidebarAutoSettleOnMerge",  "defaultThreadEnvMode",  "newWorktreesStartFromOrigin",  "sourceControlWritingStyle",] as const satisfies ReadonlyArray<keyof ServerSettings & keyof ServerSettingsPatch>; export type SharedServerSettingKey = (typeof SHARED_SERVER_SETTING_KEYS)[number]; const SHARED_KEY_SET = new Set<string>(SHARED_SERVER_SETTING_KEYS); /** Split a server patch into the keys every environment should receive and the primary-only rest. */export function splitSharedServerPatch(patch: ServerSettingsPatch): {  sharedPatch: ServerSettingsPatch;  localPatch: ServerSettingsPatch;} {  const sharedPatch: Record<string, unknown> = {};  const localPatch: Record<string, unknown> = {};  for (const [key, value] of Object.entries(patch)) {    if (SHARED_KEY_SET.has(key)) {      sharedPatch[key] = value;    } else {      localPatch[key] = value;    }  }  return {    sharedPatch: sharedPatch as ServerSettingsPatch,    localPatch: localPatch as ServerSettingsPatch,  };} /** The shared subset of one environment's settings, as a patch that can be written elsewhere. */export function pickSharedServerSettings(settings: ServerSettings): ServerSettingsPatch {  return Struct.pick(settings, SHARED_SERVER_SETTING_KEYS);} /** * Whether an environment can hold every shared key right now. Gated on the * auto-settlement capability because it is the newest of the shared keys: a * server that has it has all of them. Older servers drop unknown keys on * write, so a mismatch against them could never clear, and their decoded * defaults must not be treated as real values. */export function supportsSharedSettings(environment: EnvironmentPresentation): boolean {  return (    environment.connection.phase === "connected" &&    environment.serverConfig?.environment.capabilities.threadAutoSettlement === true  );} /** * One line per environment that rejected a settings write, each with its own * reason. Two environments can fail for different reasons (one dropped its * session, one refused the scope), so the reasons are never collapsed. */export function describeRejectedSettingsWrites(  failed: ReadonlyArray<{ readonly label: string; readonly error: unknown }>,): string {  return failed    .map(      ({ label, error }) =>        `${label}: ${error instanceof Error ? error.message : "The server rejected the change."}`,    )    .join("\n");} export interface SharedSettingsEnvironment {  readonly environmentId: EnvironmentId;  readonly label: string;  readonly connected: boolean;  readonly settings: ServerSettings | null;} /** * Connected environments whose shared settings differ from the primary * environment's. Offline environments are skipped: nothing can be read from * or written to them, and the warning would never clear. With no primary * settings loaded there is nothing to compare against, so nothing is * reported. Callers must pass the real loaded settings, never a default * fallback, or "apply to all" would push defaults over real values. */export function findSharedSettingsMismatches(input: {  readonly primaryEnvironmentId: EnvironmentId | null;  readonly primarySettings: ServerSettings | null;  readonly environments: ReadonlyArray<SharedSettingsEnvironment>;}): ReadonlyArray<{ readonly environmentId: EnvironmentId; readonly label: string }> {  if (input.primaryEnvironmentId === null || input.primarySettings === null) {    return [];  }  const expected = pickSharedServerSettings(input.primarySettings);  return input.environments.flatMap((environment) => {    if (      environment.environmentId === input.primaryEnvironmentId ||      !environment.connected ||      environment.settings === null    ) {      return [];    }    const actual = pickSharedServerSettings(environment.settings);    return Equal.equals(actual, expected)      ? []      : [{ environmentId: environment.environmentId, label: environment.label }];  });}