| author | |
| committer | |
| log | e25db3a572a6718a1f3c5a64529395cc88cd5249 |
| tree | f40153716283936e95729073be4d9c006ba8cd43 |
| parent | b8305afa29309e52045987caab91db9b7e481ac0 |
| signature | Signed by PGP key B5690EEEBB952194 |
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Julius Marminge <juliusmarminge@users.noreply.github.com>5 files changed, 55 insertions(+), 42 deletions(-)
apps/server/src/atomicWrite.ts created+25| ... | ... | @@ -0,0 +1,25 @@ |
| 1 | import { Effect, FileSystem, Path } from "effect"; | |
| 2 | import * as Random from "effect/Random"; | |
| 3 | ||
| 4 | export const writeFileStringAtomically = (input: { | |
| 5 | readonly filePath: string; | |
| 6 | readonly contents: string; | |
| 7 | }) => | |
| 8 | Effect.scoped( | |
| 9 | Effect.gen(function* () { | |
| 10 | const fs = yield* FileSystem.FileSystem; | |
| 11 | const path = yield* Path.Path; | |
| 12 | const tempFileId = yield* Random.nextUUIDv4; | |
| 13 | const targetDirectory = path.dirname(input.filePath); | |
| 14 | ||
| 15 | yield* fs.makeDirectory(targetDirectory, { recursive: true }); | |
| 16 | const tempDirectory = yield* fs.makeTempDirectoryScoped({ | |
| 17 | directory: targetDirectory, | |
| 18 | prefix: `${path.basename(input.filePath)}.`, | |
| 19 | }); | |
| 20 | const tempPath = path.join(tempDirectory, `${tempFileId}.tmp`); | |
| 21 | ||
| 22 | yield* fs.writeFileString(tempPath, input.contents); | |
| 23 | yield* fs.rename(tempPath, input.filePath); | |
| 24 | }), | |
| 25 | ); |
apps/server/src/keybindings.ts+10-6| ... | ... | @@ -46,6 +46,7 @@ import { |
| 46 | 46 | } from "effect"; |
| 47 | 47 | import * as Semaphore from "effect/Semaphore"; |
| 48 | 48 | import { ServerConfig } from "./config.ts"; |
| 49 | import { writeFileStringAtomically } from "./atomicWrite.ts"; | |
| 49 | 50 | import { fromLenientJson } from "@t3tools/shared/schemaJson"; |
| 50 | 51 | |
| 51 | 52 | type WhenToken = |
| ... | ... | @@ -670,14 +671,17 @@ const makeKeybindings = Effect.gen(function* () { |
| 670 | 671 | }); |
| 671 | 672 | |
| 672 | 673 | const writeConfigAtomically = (rules: readonly KeybindingRule[]) => { |
| 673 | const tempPath = `${keybindingsConfigPath}.${process.pid}.${Date.now()}.tmp`; | |
| 674 | ||
| 675 | 674 | return Schema.encodeEffect(KeybindingsConfigPrettyJson)(rules).pipe( |
| 676 | 675 | Effect.map((encoded) => `${encoded}\n`), |
| 677 | Effect.tap(() => fs.makeDirectory(path.dirname(keybindingsConfigPath), { recursive: true })), | |
| 678 | Effect.tap((encoded) => fs.writeFileString(tempPath, encoded)), | |
| 679 | Effect.flatMap(() => fs.rename(tempPath, keybindingsConfigPath)), | |
| 680 | Effect.ensuring(fs.remove(tempPath, { force: true }).pipe(Effect.ignore({ log: true }))), | |
| 676 | Effect.flatMap((encoded) => | |
| 677 | writeFileStringAtomically({ | |
| 678 | filePath: keybindingsConfigPath, | |
| 679 | contents: encoded, | |
| 680 | }).pipe( | |
| 681 | Effect.provideService(FileSystem.FileSystem, fs), | |
| 682 | Effect.provideService(Path.Path, path), | |
| 683 | ), | |
| 684 | ), | |
| 681 | 685 | Effect.mapError( |
| 682 | 686 | (cause) => |
| 683 | 687 | new KeybindingsConfigError({ |
apps/server/src/provider/providerStatusCache.ts+8-20| ... | ... | @@ -1,6 +1,8 @@ |
| 1 | 1 | import * as nodePath from "node:path"; |
| 2 | 2 | import { type ServerProvider, ServerProvider as ServerProviderSchema } from "@t3tools/contracts"; |
| 3 | import { Cause, Effect, FileSystem, Path, Schema } from "effect"; | |
| 3 | import { Cause, Effect, FileSystem, Schema } from "effect"; | |
| 4 | ||
| 5 | import { writeFileStringAtomically } from "../atomicWrite.ts"; | |
| 4 | 6 | |
| 5 | 7 | export const PROVIDER_CACHE_IDS = [ |
| 6 | 8 | "codex", |
| ... | ... | @@ -96,22 +98,8 @@ export const readProviderStatusCache = (filePath: string) => |
| 96 | 98 | export const writeProviderStatusCache = (input: { |
| 97 | 99 | readonly filePath: string; |
| 98 | 100 | readonly provider: ServerProvider; |
| 99 | }) => { | |
| 100 | const tempPath = `${input.filePath}.${process.pid}.${Date.now()}.tmp`; | |
| 101 | return Effect.gen(function* () { | |
| 102 | const fs = yield* FileSystem.FileSystem; | |
| 103 | const path = yield* Path.Path; | |
| 104 | const encoded = `${JSON.stringify(input.provider, null, 2)}\n`; | |
| 105 | ||
| 106 | yield* fs.makeDirectory(path.dirname(input.filePath), { recursive: true }); | |
| 107 | yield* fs.writeFileString(tempPath, encoded); | |
| 108 | yield* fs.rename(tempPath, input.filePath); | |
| 109 | }).pipe( | |
| 110 | Effect.ensuring( | |
| 111 | Effect.gen(function* () { | |
| 112 | const fs = yield* FileSystem.FileSystem; | |
| 113 | yield* fs.remove(tempPath, { force: true }).pipe(Effect.ignore({ log: true })); | |
| 114 | }), | |
| 115 | ), | |
| 116 | ); | |
| 117 | }; | |
| 101 | }) => | |
| 102 | writeFileStringAtomically({ | |
| 103 | filePath: input.filePath, | |
| 104 | contents: `${JSON.stringify(input.provider, null, 2)}\n`, | |
| 105 | }); |
apps/server/src/serverRuntimeState.ts+5-10| ... | ... | @@ -1,5 +1,6 @@ |
| 1 | import { Effect, FileSystem, Option, Path, Schema } from "effect"; | |
| 1 | import { Effect, FileSystem, Option, Schema } from "effect"; | |
| 2 | 2 | |
| 3 | import { writeFileStringAtomically } from "./atomicWrite.ts"; | |
| 3 | 4 | import { type ServerConfigShape } from "./config.ts"; |
| 4 | 5 | import { formatHostForUrl, isWildcardHost } from "./startupAccess.ts"; |
| 5 | 6 | |
| ... | ... | @@ -42,15 +43,9 @@ export const persistServerRuntimeState = (input: { |
| 42 | 43 | readonly path: string; |
| 43 | 44 | readonly state: PersistedServerRuntimeState; |
| 44 | 45 | }) => |
| 45 | Effect.gen(function* () { | |
| 46 | const fs = yield* FileSystem.FileSystem; | |
| 47 | const pathService = yield* Path.Path; | |
| 48 | const tempPath = `${input.path}.${process.pid}.${Date.now()}.tmp`; | |
| 49 | return yield* fs.makeDirectory(pathService.dirname(input.path), { recursive: true }).pipe( | |
| 50 | Effect.flatMap(() => fs.writeFileString(tempPath, `${JSON.stringify(input.state)}\n`)), | |
| 51 | Effect.flatMap(() => fs.rename(tempPath, input.path)), | |
| 52 | Effect.ensuring(fs.remove(tempPath, { force: true }).pipe(Effect.ignore({ log: true }))), | |
| 53 | ); | |
| 46 | writeFileStringAtomically({ | |
| 47 | filePath: input.path, | |
| 48 | contents: `${JSON.stringify(input.state)}\n`, | |
| 54 | 49 | }); |
| 55 | 50 | |
| 56 | 51 | export const clearPersistedServerRuntimeState = (path: string) => |
apps/server/src/serverSettings.ts+7-6| ... | ... | @@ -39,6 +39,7 @@ import { |
| 39 | 39 | Cause, |
| 40 | 40 | } from "effect"; |
| 41 | 41 | import * as Semaphore from "effect/Semaphore"; |
| 42 | import { writeFileStringAtomically } from "./atomicWrite.ts"; | |
| 42 | 43 | import { ServerConfig } from "./config.ts"; |
| 43 | 44 | import { type DeepPartial, deepMerge } from "@t3tools/shared/Struct"; |
| 44 | 45 | import { fromLenientJson } from "@t3tools/shared/schemaJson"; |
| ... | ... | @@ -233,14 +234,14 @@ const makeServerSettings = Effect.gen(function* () { |
| 233 | 234 | const getSettingsFromCache = Cache.get(settingsCache, cacheKey); |
| 234 | 235 | |
| 235 | 236 | const writeSettingsAtomically = (settings: ServerSettings) => { |
| 236 | const tempPath = `${settingsPath}.${process.pid}.${Date.now()}.tmp`; | |
| 237 | 237 | const sparseSettings = stripDefaultServerSettings(settings, DEFAULT_SERVER_SETTINGS) ?? {}; |
| 238 | 238 | |
| 239 | return Effect.succeed(`${JSON.stringify(sparseSettings, null, 2)}\n`).pipe( | |
| 240 | Effect.tap(() => fs.makeDirectory(pathService.dirname(settingsPath), { recursive: true })), | |
| 241 | Effect.tap((encoded) => fs.writeFileString(tempPath, encoded)), | |
| 242 | Effect.flatMap(() => fs.rename(tempPath, settingsPath)), | |
| 243 | Effect.ensuring(fs.remove(tempPath, { force: true }).pipe(Effect.ignore({ log: true }))), | |
| 239 | return writeFileStringAtomically({ | |
| 240 | filePath: settingsPath, | |
| 241 | contents: `${JSON.stringify(sparseSettings, null, 2)}\n`, | |
| 242 | }).pipe( | |
| 243 | Effect.provideService(FileSystem.FileSystem, fs), | |
| 244 | Effect.provideService(Path.Path, pathService), | |
| 244 | 245 | Effect.mapError( |
| 245 | 246 | (cause) => |
| 246 | 247 | new ServerSettingsError({ |