Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 25 additions & 0 deletions apps/server/src/atomicWrite.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { Effect, FileSystem, Path } from "effect";
import * as Random from "effect/Random";

export const writeFileStringAtomically = (input: {
readonly filePath: string;
readonly contents: string;
}) =>
Effect.scoped(
Effect.gen(function* () {
const fs = yield* FileSystem.FileSystem;
const path = yield* Path.Path;
const tempFileId = yield* Random.nextUUIDv4;
const targetDirectory = path.dirname(input.filePath);

yield* fs.makeDirectory(targetDirectory, { recursive: true });
const tempDirectory = yield* fs.makeTempDirectoryScoped({
directory: targetDirectory,
prefix: `${path.basename(input.filePath)}.`,
});
const tempPath = path.join(tempDirectory, `${tempFileId}.tmp`);

yield* fs.writeFileString(tempPath, input.contents);
yield* fs.rename(tempPath, input.filePath);
}),
);
16 changes: 10 additions & 6 deletions apps/server/src/keybindings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ import {
} from "effect";
import * as Semaphore from "effect/Semaphore";
import { ServerConfig } from "./config.ts";
import { writeFileStringAtomically } from "./atomicWrite.ts";
import { fromLenientJson } from "@t3tools/shared/schemaJson";

type WhenToken =
Expand Down Expand Up @@ -670,14 +671,17 @@ const makeKeybindings = Effect.gen(function* () {
});

const writeConfigAtomically = (rules: readonly KeybindingRule[]) => {
const tempPath = `${keybindingsConfigPath}.${process.pid}.${Date.now()}.tmp`;

return Schema.encodeEffect(KeybindingsConfigPrettyJson)(rules).pipe(
Effect.map((encoded) => `${encoded}\n`),
Effect.tap(() => fs.makeDirectory(path.dirname(keybindingsConfigPath), { recursive: true })),
Effect.tap((encoded) => fs.writeFileString(tempPath, encoded)),
Effect.flatMap(() => fs.rename(tempPath, keybindingsConfigPath)),
Effect.ensuring(fs.remove(tempPath, { force: true }).pipe(Effect.ignore({ log: true }))),
Effect.flatMap((encoded) =>
writeFileStringAtomically({
filePath: keybindingsConfigPath,
contents: encoded,
}).pipe(
Effect.provideService(FileSystem.FileSystem, fs),
Effect.provideService(Path.Path, path),
),
),
Effect.mapError(
(cause) =>
new KeybindingsConfigError({
Expand Down
45 changes: 45 additions & 0 deletions apps/server/src/provider/providerStatusCache.test.ts
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Usless test @cursor, remove

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import * as NodeServices from "@effect/platform-node/NodeServices";
import type { ServerProvider } from "@t3tools/contracts";
import { assert, it } from "@effect/vitest";
import { Effect, FileSystem } from "effect";
import { afterEach, vi } from "vitest";

import {
hydrateCachedProvider,
Expand All @@ -27,6 +28,10 @@ const makeProvider = (
...overrides,
});

afterEach(() => {
vi.restoreAllMocks();
});

it.layer(NodeServices.layer)("providerStatusCache", (it) => {
it.effect("writes and reads provider status snapshots", () =>
Effect.gen(function* () {
Expand Down Expand Up @@ -73,6 +78,46 @@ it.layer(NodeServices.layer)("providerStatusCache", (it) => {
}),
);

it.effect("supports overlapping writes for the same provider cache file", () =>
Effect.gen(function* () {
const fs = yield* FileSystem.FileSystem;
const tempDir = yield* fs.makeTempDirectoryScoped({ prefix: "t3-provider-cache-overlap-" });
const filePath = resolveProviderStatusCachePath({
cacheDir: tempDir,
provider: "opencode",
});
const stableNow = 1_776_841_093_506;
vi.spyOn(Date, "now").mockReturnValue(stableNow);

const initialProvider = makeProvider("opencode", {
auth: { status: "unknown", type: "opencode" },
});
const updatedProvider = makeProvider("opencode", {
version: "1.0.1",
auth: { status: "unknown", type: "opencode" },
});

const results = yield* Effect.all(
[
Effect.exit(writeProviderStatusCache({ filePath, provider: initialProvider })),
Effect.exit(writeProviderStatusCache({ filePath, provider: updatedProvider })),
],
{ concurrency: "unbounded" },
);

assert.strictEqual(results[0]._tag, "Success");
assert.strictEqual(results[1]._tag, "Success");

const persistedProvider = yield* readProviderStatusCache(filePath);
assert.deepStrictEqual(
[initialProvider, updatedProvider].some(
(provider) => JSON.stringify(provider) === JSON.stringify(persistedProvider),
),
true,
);
}),
);

it("hydrates cached provider status while preserving current settings-derived models", () => {
const cachedCodex = makeProvider("codex", {
checkedAt: "2026-04-10T12:00:00.000Z",
Expand Down
28 changes: 8 additions & 20 deletions apps/server/src/provider/providerStatusCache.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import * as nodePath from "node:path";
import { type ServerProvider, ServerProvider as ServerProviderSchema } from "@t3tools/contracts";
import { Cause, Effect, FileSystem, Path, Schema } from "effect";
import { Cause, Effect, FileSystem, Schema } from "effect";

import { writeFileStringAtomically } from "../atomicWrite.ts";

export const PROVIDER_CACHE_IDS = [
"codex",
Expand Down Expand Up @@ -96,22 +98,8 @@ export const readProviderStatusCache = (filePath: string) =>
export const writeProviderStatusCache = (input: {
readonly filePath: string;
readonly provider: ServerProvider;
}) => {
const tempPath = `${input.filePath}.${process.pid}.${Date.now()}.tmp`;
return Effect.gen(function* () {
const fs = yield* FileSystem.FileSystem;
const path = yield* Path.Path;
const encoded = `${JSON.stringify(input.provider, null, 2)}\n`;

yield* fs.makeDirectory(path.dirname(input.filePath), { recursive: true });
yield* fs.writeFileString(tempPath, encoded);
yield* fs.rename(tempPath, input.filePath);
}).pipe(
Effect.ensuring(
Effect.gen(function* () {
const fs = yield* FileSystem.FileSystem;
yield* fs.remove(tempPath, { force: true }).pipe(Effect.ignore({ log: true }));
}),
),
);
};
}) =>
writeFileStringAtomically({
filePath: input.filePath,
contents: `${JSON.stringify(input.provider, null, 2)}\n`,
});
15 changes: 5 additions & 10 deletions apps/server/src/serverRuntimeState.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { Effect, FileSystem, Option, Path, Schema } from "effect";
import { Effect, FileSystem, Option, Schema } from "effect";

import { writeFileStringAtomically } from "./atomicWrite.ts";
import { type ServerConfigShape } from "./config.ts";
import { formatHostForUrl, isWildcardHost } from "./startupAccess.ts";

Expand Down Expand Up @@ -42,15 +43,9 @@ export const persistServerRuntimeState = (input: {
readonly path: string;
readonly state: PersistedServerRuntimeState;
}) =>
Effect.gen(function* () {
const fs = yield* FileSystem.FileSystem;
const pathService = yield* Path.Path;
const tempPath = `${input.path}.${process.pid}.${Date.now()}.tmp`;
return yield* fs.makeDirectory(pathService.dirname(input.path), { recursive: true }).pipe(
Effect.flatMap(() => fs.writeFileString(tempPath, `${JSON.stringify(input.state)}\n`)),
Effect.flatMap(() => fs.rename(tempPath, input.path)),
Effect.ensuring(fs.remove(tempPath, { force: true }).pipe(Effect.ignore({ log: true }))),
);
writeFileStringAtomically({
filePath: input.path,
contents: `${JSON.stringify(input.state)}\n`,
});

export const clearPersistedServerRuntimeState = (path: string) =>
Expand Down
13 changes: 7 additions & 6 deletions apps/server/src/serverSettings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ import {
Cause,
} from "effect";
import * as Semaphore from "effect/Semaphore";
import { writeFileStringAtomically } from "./atomicWrite.ts";
import { ServerConfig } from "./config.ts";
import { type DeepPartial, deepMerge } from "@t3tools/shared/Struct";
import { fromLenientJson } from "@t3tools/shared/schemaJson";
Expand Down Expand Up @@ -233,14 +234,14 @@ const makeServerSettings = Effect.gen(function* () {
const getSettingsFromCache = Cache.get(settingsCache, cacheKey);

const writeSettingsAtomically = (settings: ServerSettings) => {
const tempPath = `${settingsPath}.${process.pid}.${Date.now()}.tmp`;
const sparseSettings = stripDefaultServerSettings(settings, DEFAULT_SERVER_SETTINGS) ?? {};

return Effect.succeed(`${JSON.stringify(sparseSettings, null, 2)}\n`).pipe(
Effect.tap(() => fs.makeDirectory(pathService.dirname(settingsPath), { recursive: true })),
Effect.tap((encoded) => fs.writeFileString(tempPath, encoded)),
Effect.flatMap(() => fs.rename(tempPath, settingsPath)),
Effect.ensuring(fs.remove(tempPath, { force: true }).pipe(Effect.ignore({ log: true }))),
return writeFileStringAtomically({
filePath: settingsPath,
contents: `${JSON.stringify(sparseSettings, null, 2)}\n`,
}).pipe(
Effect.provideService(FileSystem.FileSystem, fs),
Effect.provideService(Path.Path, pathService),
Effect.mapError(
(cause) =>
new ServerSettingsError({
Expand Down
Loading