Rhys Sullivanfmtc92c57d6 months agoHistory
72 lines · 2.9 KB · TypeScript
RawDownload
// ---------------------------------------------------------------------------// ConfigFileSink — best-effort write-back of source changes to executor.jsonc.//// Plugins (openapi, graphql, mcp) call `sink.upsertSource` after their DB// writes so the committable file stays in sync with runtime state. Errors// are logged and swallowed — a failed file write must never fail a DB// mutation, and the next successful mutation (or a boot-time sync) will// eventually reconcile.//// The FileSystem layer is injected so library code here doesn't pick a// platform binding. The host app provides NodeFileSystem (or BunFileSystem).// --------------------------------------------------------------------------- import { Effect } from "effect";import type { Layer } from "effect";import type { FileSystem } from "effect"; import { SECRET_REF_PREFIX, type ConfigHeaderValue, type SourceConfig } from "./schema";import { addSourceToConfig, removeSourceFromConfig } from "./write"; // Translate a plugin-side header value (`{ secretId, prefix? }` for secret// refs) into the config file's `secret-public-ref:<id>` string form.type PluginHeaderValue = string | { secretId: string; prefix?: string }; export const headerToConfigValue = (value: PluginHeaderValue): ConfigHeaderValue => {  if (typeof value === "string") return value;  const ref = `${SECRET_REF_PREFIX}${value.secretId}`;  return value.prefix ? { value: ref, prefix: value.prefix } : ref;}; export const headersToConfigValues = (  headers: Record<string, PluginHeaderValue> | undefined,): Record<string, ConfigHeaderValue> | undefined => {  if (!headers) return undefined;  const out: Record<string, ConfigHeaderValue> = {};  for (const [k, v] of Object.entries(headers)) out[k] = headerToConfigValue(v);  return out;}; export interface ConfigFileSink {  readonly upsertSource: (source: SourceConfig) => Effect.Effect<void>;  readonly removeSource: (namespace: string) => Effect.Effect<void>;} export interface ConfigFileSinkOptions {  readonly path: string;  readonly fsLayer: Layer.Layer<FileSystem.FileSystem>;  /** Called when a file operation fails. Defaults to console.warn. */  readonly onError?: (op: "upsert" | "remove", err: unknown) => void;} const defaultOnError = (op: "upsert" | "remove", err: unknown): void => {  console.warn(`[config-sink] ${op} failed`, err);}; export const makeFileConfigSink = (options: ConfigFileSinkOptions): ConfigFileSink => {  const { path, fsLayer, onError = defaultOnError } = options;   return {    upsertSource: (source) =>      addSourceToConfig(path, source).pipe(        Effect.provide(fsLayer),        Effect.catch((err: unknown) => Effect.sync(() => onError("upsert", err))),      ),     removeSource: (namespace) =>      removeSourceFromConfig(path, namespace).pipe(        Effect.provide(fsLayer),        Effect.catch((err: unknown) => Effect.sync(() => onError("remove", err))),      ),  };};