// Disposable derived state (cache.json): corrupt or absent falls back to `empty` and // never fails a read; every `modify` is write-through with the same atomic-write // discipline as config. Held in a Ref so reads are cheap and mutations are ordered. import * as fs from "node:fs"; import { Effect, Ref, Schema } from "effect"; import type { ConfigWriteError } from "./errors.js"; import { atomicWriteFile, ensureStateDir, statePath } from "./paths.js"; import { PluginInfo } from "./host-client.js"; export interface CacheStore { readonly get: Effect.Effect; /** Atomically update the cache (Ref + write-through). Returns the `modify` result. */ readonly modify: ( f: (current: S["Type"]) => readonly [A, S["Type"]], ) => Effect.Effect; /** `modify` without a result. */ readonly update: ( f: (current: S["Type"]) => S["Type"], ) => Effect.Effect; /** Absolute path of the cache file (status views). */ readonly path: string; } export const makeCacheStore = (opts: { readonly schema: S; readonly empty: S["Type"]; readonly fileName?: string; }): Effect.Effect, never, PluginInfo> => Effect.gen(function* () { const info = yield* PluginInfo; const file = statePath(info.name, opts.fileName ?? "cache.json"); const initial = yield* Effect.suspend(() => { try { const parsed = JSON.parse(fs.readFileSync(file, "utf8")) as unknown; return Schema.decodeUnknownEffect(opts.schema)(parsed).pipe( Effect.orElseSucceed(() => opts.empty), ) as Effect.Effect; } catch { return Effect.succeed(opts.empty); } }); const ref = yield* Ref.make(initial); const persist = (value: S["Type"]) => ensureStateDir(info.name).pipe( Effect.flatMap(() => atomicWriteFile(file, JSON.stringify(value)), ), ); const modify = (f: (current: S["Type"]) => readonly [A, S["Type"]]) => Ref.modify(ref, (current) => { const [a, next] = f(current); return [[a, next] as const, next] as const; }).pipe( Effect.flatMap(([a, next]) => persist(next).pipe(Effect.as(a)), ), ); return { get: Ref.get(ref), modify, update: (f) => modify((c) => [undefined, f(c)] as const), path: file, } satisfies CacheStore; });