import { ClientSettingsSchema, type ClientSettings } from "@t3tools/contracts"; import { fromLenientJson } from "@t3tools/shared/schemaJson"; import * as Context from "effect/Context"; import * as Crypto from "effect/Crypto"; import * as Effect from "effect/Effect"; import * as FileSystem from "effect/FileSystem"; import * as Layer from "effect/Layer"; import * as Option from "effect/Option"; import * as Path from "effect/Path"; import * as Schema from "effect/Schema"; import * as Ref from "effect/Ref"; import * as DesktopEnvironment from "../app/DesktopEnvironment.ts"; const ClientSettingsDocumentSchema = Schema.Struct({ settings: ClientSettingsSchema, }); const ClientSettingsJson = fromLenientJson(ClientSettingsSchema); const LegacyClientSettingsDocumentJson = fromLenientJson(ClientSettingsDocumentSchema); const decodeLegacyClientSettingsDocumentJson = Schema.decodeEffect( LegacyClientSettingsDocumentJson, ); const decodeClientSettingsJsonValue = Schema.decodeEffect(ClientSettingsJson); const decodeClientSettingsJson = (raw: string): Effect.Effect => decodeLegacyClientSettingsDocumentJson(raw).pipe( Effect.map((document) => document.settings), Effect.catchTags({ SchemaError: () => decodeClientSettingsJsonValue(raw), }), ); const encodeClientSettingsJson = Schema.encodeEffect(ClientSettingsJson); const DesktopClientSettingsWriteOperation = Schema.Literals([ "create-temporary-file-name", "encode-document", "create-directory", "write-temporary-file", "replace-settings-file", ]); export class DesktopClientSettingsWriteError extends Schema.TaggedErrorClass()( "DesktopClientSettingsWriteError", { operation: DesktopClientSettingsWriteOperation, path: Schema.String, cause: Schema.Defect(), }, ) { override get message(): string { return `Desktop client settings write failed during ${this.operation} at ${this.path}.`; } } export class DesktopClientSettings extends Context.Service< DesktopClientSettings, { readonly get: Effect.Effect>; readonly set: ( settings: ClientSettings, ) => Effect.Effect; } >()("@t3tools/desktop/settings/DesktopClientSettings") {} const readClientSettings = ( fileSystem: FileSystem.FileSystem, settingsPath: string, ): Effect.Effect> => fileSystem.readFileString(settingsPath).pipe( Effect.map(Option.some), Effect.catchTags({ PlatformError: (cause) => cause.reason._tag === "NotFound" ? Effect.succeed(Option.none()) : Effect.logWarning("Could not read desktop client settings.", cause).pipe( Effect.annotateLogs({ settingsPath }), Effect.as(Option.none()), ), }), Effect.flatMap( Option.match({ onNone: () => Effect.succeed(Option.none()), onSome: (raw) => decodeClientSettingsJson(raw).pipe( Effect.map((settings) => Option.some(settings)), Effect.catchTags({ SchemaError: (cause) => Effect.logWarning("Could not decode desktop client settings.", cause).pipe( Effect.annotateLogs({ settingsPath }), Effect.as(Option.none()), ), }), ), }), ), ); const writeClientSettings = Effect.fnUntraced(function* (input: { readonly fileSystem: FileSystem.FileSystem; readonly path: Path.Path; readonly settingsPath: string; readonly settings: ClientSettings; readonly suffix: string; }): Effect.fn.Return { const directory = input.path.dirname(input.settingsPath); const tempPath = `${input.settingsPath}.${process.pid}.${input.suffix}.tmp`; const encoded = yield* encodeClientSettingsJson(input.settings).pipe( Effect.mapError( (cause) => new DesktopClientSettingsWriteError({ operation: "encode-document", path: input.settingsPath, cause, }), ), ); yield* input.fileSystem.makeDirectory(directory, { recursive: true }).pipe( Effect.mapError( (cause) => new DesktopClientSettingsWriteError({ operation: "create-directory", path: directory, cause, }), ), ); yield* input.fileSystem.writeFileString(tempPath, `${encoded}\n`).pipe( Effect.mapError( (cause) => new DesktopClientSettingsWriteError({ operation: "write-temporary-file", path: tempPath, cause, }), ), ); yield* input.fileSystem.rename(tempPath, input.settingsPath).pipe( Effect.mapError( (cause) => new DesktopClientSettingsWriteError({ operation: "replace-settings-file", path: input.settingsPath, cause, }), ), ); }); export const make = Effect.gen(function* () { const environment = yield* DesktopEnvironment.DesktopEnvironment; const fileSystem = yield* FileSystem.FileSystem; const path = yield* Path.Path; const crypto = yield* Crypto.Crypto; return DesktopClientSettings.of({ get: readClientSettings(fileSystem, environment.clientSettingsPath).pipe( Effect.withSpan("desktop.clientSettings.get"), ), set: (settings) => crypto.randomUUIDv4.pipe( Effect.map((uuid) => uuid.replace(/-/g, "")), Effect.mapError( (cause) => new DesktopClientSettingsWriteError({ operation: "create-temporary-file-name", path: environment.clientSettingsPath, cause, }), ), Effect.flatMap((suffix) => writeClientSettings({ fileSystem, path, settingsPath: environment.clientSettingsPath, settings, suffix, }), ), Effect.withSpan("desktop.clientSettings.set"), ), }); }); export const layer = Layer.effect(DesktopClientSettings, make); export const layerTest = (initialSettings: Option.Option = Option.none()) => Layer.effect( DesktopClientSettings, Effect.gen(function* () { const settingsRef = yield* Ref.make(initialSettings); return DesktopClientSettings.of({ get: Ref.get(settingsRef), set: (settings) => Ref.set(settingsRef, Option.some(settings)), }); }), );