| 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,6 +46,7 @@ import { |
| 46 | } from "effect"; | 46 | } from "effect"; |
| 47 | import * as Semaphore from "effect/Semaphore"; | 47 | import * as Semaphore from "effect/Semaphore"; |
| 48 | import { ServerConfig } from "./config.ts"; | 48 | import { ServerConfig } from "./config.ts"; |
| 49 | import { writeFileStringAtomically } from "./atomicWrite.ts"; | ||
| 49 | import { fromLenientJson } from "@t3tools/shared/schemaJson"; | 50 | import { fromLenientJson } from "@t3tools/shared/schemaJson"; |
| 50 | 51 | ||
| 51 | type WhenToken = | 52 | type WhenToken = |
| ... | @@ -670,14 +671,17 @@ const makeKeybindings = Effect.gen(function* () { | ... | @@ -670,14 +671,17 @@ const makeKeybindings = Effect.gen(function* () { |
| 670 | }); | 671 | }); |
| 671 | 672 | ||
| 672 | const writeConfigAtomically = (rules: readonly KeybindingRule[]) => { | 673 | const writeConfigAtomically = (rules: readonly KeybindingRule[]) => { |
| 673 | const tempPath = `${keybindingsConfigPath}.${process.pid}.${Date.now()}.tmp`; | ||
| 674 | |||
| 675 | return Schema.encodeEffect(KeybindingsConfigPrettyJson)(rules).pipe( | 674 | return Schema.encodeEffect(KeybindingsConfigPrettyJson)(rules).pipe( |
| 676 | Effect.map((encoded) => `${encoded}\n`), | 675 | Effect.map((encoded) => `${encoded}\n`), |
| 677 | Effect.tap(() => fs.makeDirectory(path.dirname(keybindingsConfigPath), { recursive: true })), | 676 | Effect.flatMap((encoded) => |
| 678 | Effect.tap((encoded) => fs.writeFileString(tempPath, encoded)), | 677 | writeFileStringAtomically({ |
| 679 | Effect.flatMap(() => fs.rename(tempPath, keybindingsConfigPath)), | 678 | filePath: keybindingsConfigPath, |
| 680 | Effect.ensuring(fs.remove(tempPath, { force: true }).pipe(Effect.ignore({ log: true }))), | 679 | contents: encoded, |
| 680 | }).pipe( | ||
| 681 | Effect.provideService(FileSystem.FileSystem, fs), | ||
| 682 | Effect.provideService(Path.Path, path), | ||
| 683 | ), | ||
| 684 | ), | ||
| 681 | Effect.mapError( | 685 | Effect.mapError( |
| 682 | (cause) => | 686 | (cause) => |
| 683 | new KeybindingsConfigError({ | 687 | new KeybindingsConfigError({ |
apps/server/src/provider/providerStatusCache.ts+8-20| ... | @@ -1,6 +1,8 @@ | ... | @@ -1,6 +1,8 @@ |
| 1 | import * as nodePath from "node:path"; | 1 | import * as nodePath from "node:path"; |
| 2 | import { type ServerProvider, ServerProvider as ServerProviderSchema } from "@t3tools/contracts"; | 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 | export const PROVIDER_CACHE_IDS = [ | 7 | export const PROVIDER_CACHE_IDS = [ |
| 6 | "codex", | 8 | "codex", |
| ... | @@ -96,22 +98,8 @@ export const readProviderStatusCache = (filePath: string) => | ... | @@ -96,22 +98,8 @@ export const readProviderStatusCache = (filePath: string) => |
| 96 | export const writeProviderStatusCache = (input: { | 98 | export const writeProviderStatusCache = (input: { |
| 97 | readonly filePath: string; | 99 | readonly filePath: string; |
| 98 | readonly provider: ServerProvider; | 100 | readonly provider: ServerProvider; |
| 99 | }) => { | 101 | }) => |
| 100 | const tempPath = `${input.filePath}.${process.pid}.${Date.now()}.tmp`; | 102 | writeFileStringAtomically({ |
| 101 | return Effect.gen(function* () { | 103 | filePath: input.filePath, |
| 102 | const fs = yield* FileSystem.FileSystem; | 104 | contents: `${JSON.stringify(input.provider, null, 2)}\n`, |
| 103 | const path = yield* Path.Path; | 105 | }); |
| 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 | }; |
apps/server/src/serverRuntimeState.ts+5-10| ... | @@ -1,5 +1,6 @@ | ... | @@ -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 | import { type ServerConfigShape } from "./config.ts"; | 4 | import { type ServerConfigShape } from "./config.ts"; |
| 4 | import { formatHostForUrl, isWildcardHost } from "./startupAccess.ts"; | 5 | import { formatHostForUrl, isWildcardHost } from "./startupAccess.ts"; |
| 5 | 6 | ||
| ... | @@ -42,15 +43,9 @@ export const persistServerRuntimeState = (input: { | ... | @@ -42,15 +43,9 @@ export const persistServerRuntimeState = (input: { |
| 42 | readonly path: string; | 43 | readonly path: string; |
| 43 | readonly state: PersistedServerRuntimeState; | 44 | readonly state: PersistedServerRuntimeState; |
| 44 | }) => | 45 | }) => |
| 45 | Effect.gen(function* () { | 46 | writeFileStringAtomically({ |
| 46 | const fs = yield* FileSystem.FileSystem; | 47 | filePath: input.path, |
| 47 | const pathService = yield* Path.Path; | 48 | contents: `${JSON.stringify(input.state)}\n`, |
| 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 | ); | ||
| 54 | }); | 49 | }); |
| 55 | 50 | ||
| 56 | export const clearPersistedServerRuntimeState = (path: string) => | 51 | export const clearPersistedServerRuntimeState = (path: string) => |
apps/server/src/serverSettings.ts+7-6| ... | @@ -39,6 +39,7 @@ import { | ... | @@ -39,6 +39,7 @@ import { |
| 39 | Cause, | 39 | Cause, |
| 40 | } from "effect"; | 40 | } from "effect"; |
| 41 | import * as Semaphore from "effect/Semaphore"; | 41 | import * as Semaphore from "effect/Semaphore"; |
| 42 | import { writeFileStringAtomically } from "./atomicWrite.ts"; | ||
| 42 | import { ServerConfig } from "./config.ts"; | 43 | import { ServerConfig } from "./config.ts"; |
| 43 | import { type DeepPartial, deepMerge } from "@t3tools/shared/Struct"; | 44 | import { type DeepPartial, deepMerge } from "@t3tools/shared/Struct"; |
| 44 | import { fromLenientJson } from "@t3tools/shared/schemaJson"; | 45 | import { fromLenientJson } from "@t3tools/shared/schemaJson"; |
| ... | @@ -233,14 +234,14 @@ const makeServerSettings = Effect.gen(function* () { | ... | @@ -233,14 +234,14 @@ const makeServerSettings = Effect.gen(function* () { |
| 233 | const getSettingsFromCache = Cache.get(settingsCache, cacheKey); | 234 | const getSettingsFromCache = Cache.get(settingsCache, cacheKey); |
| 234 | 235 | ||
| 235 | const writeSettingsAtomically = (settings: ServerSettings) => { | 236 | const writeSettingsAtomically = (settings: ServerSettings) => { |
| 236 | const tempPath = `${settingsPath}.${process.pid}.${Date.now()}.tmp`; | ||
| 237 | const sparseSettings = stripDefaultServerSettings(settings, DEFAULT_SERVER_SETTINGS) ?? {}; | 237 | const sparseSettings = stripDefaultServerSettings(settings, DEFAULT_SERVER_SETTINGS) ?? {}; |
| 238 | 238 | ||
| 239 | return Effect.succeed(`${JSON.stringify(sparseSettings, null, 2)}\n`).pipe( | 239 | return writeFileStringAtomically({ |
| 240 | Effect.tap(() => fs.makeDirectory(pathService.dirname(settingsPath), { recursive: true })), | 240 | filePath: settingsPath, |
| 241 | Effect.tap((encoded) => fs.writeFileString(tempPath, encoded)), | 241 | contents: `${JSON.stringify(sparseSettings, null, 2)}\n`, |
| 242 | Effect.flatMap(() => fs.rename(tempPath, settingsPath)), | 242 | }).pipe( |
| 243 | Effect.ensuring(fs.remove(tempPath, { force: true }).pipe(Effect.ignore({ log: true }))), | 243 | Effect.provideService(FileSystem.FileSystem, fs), |
| 244 | Effect.provideService(Path.Path, pathService), | ||
| 244 | Effect.mapError( | 245 | Effect.mapError( |
| 245 | (cause) => | 246 | (cause) => |
| 246 | new ServerSettingsError({ | 247 | new ServerSettingsError({ |