History

Use Option for provider status cache

Cursor Agent committed 5 months ago

Commit 83682421 parent cf07d064 files · +78 −35Browse files

Unified · Split

4 files
  1. apps/server/src/provider/Layers/ProviderRegistry.test.ts +3 −2
  2. apps/server/src/provider/Layers/ProviderRegistry.ts +22 −19
  3. apps/server/src/provider/providerStatusCache.test.ts +36 −3
  4. apps/server/src/provider/providerStatusCache.ts +17 −11
apps/server/src/provider/Layers/ProviderRegistry.test.ts+3−2
@@ -4,6 +4,7 @@ import * as Effect from "effect/Effect"; · 3 unmodified lines
4import * as Exit from "effect/Exit";4import * as Exit from "effect/Exit";
5import * as Fiber from "effect/Fiber";5import * as Fiber from "effect/Fiber";
6import * as Layer from "effect/Layer";6import * as Layer from "effect/Layer";
7import * as Option from "effect/Option";
7import * as PubSub from "effect/PubSub";8import * as PubSub from "effect/PubSub";
8import * as Ref from "effect/Ref";9import * as Ref from "effect/Ref";
9import * as Schema from "effect/Schema";10import * as Schema from "effect/Schema";
@@ -752,7 +753,7 @@ it.layer(Layer.mergeAll(NodeServices.layer, ServerSettingsService.layerTest(), T · 742 unmodified lines
752 ]);753 ]);
753 yield* PubSub.publish(changes, refreshedProvider);754 yield* PubSub.publish(changes, refreshedProvider);
754 755
755 let cachedProvider = yield* readProviderStatusCache(filePath);756 let cachedProvider = Option.getOrUndefined(yield* readProviderStatusCache(filePath));
756 for (757 for (
757 let attempt = 0;758 let attempt = 0;
758 attempt < 50 && cachedProvider?.checkedAt !== refreshedProvider.checkedAt;759 attempt < 50 && cachedProvider?.checkedAt !== refreshedProvider.checkedAt;
@@ -760,7 +761,7 @@ it.layer(Layer.mergeAll(NodeServices.layer, ServerSettingsService.layerTest(), T · 1 unmodified line
760 ) {761 ) {
761 yield* TestClock.adjust("10 millis");762 yield* TestClock.adjust("10 millis");
762 yield* Effect.yieldNow;763 yield* Effect.yieldNow;
763 cachedProvider = yield* readProviderStatusCache(filePath);764 cachedProvider = Option.getOrUndefined(yield* readProviderStatusCache(filePath));
764 }765 }
765 766
766 assert.deepStrictEqual(cachedProvider, {767 assert.deepStrictEqual(cachedProvider, {
apps/server/src/provider/Layers/ProviderRegistry.ts+22−19
@@ -34,6 +34,7 @@ import * as Effect from "effect/Effect"; · 33 unmodified lines
34import * as Equal from "effect/Equal";34import * as Equal from "effect/Equal";
35import * as FileSystem from "effect/FileSystem";35import * as FileSystem from "effect/FileSystem";
36import * as Layer from "effect/Layer";36import * as Layer from "effect/Layer";
37import * as Option from "effect/Option";
37import * as Path from "effect/Path";38import * as Path from "effect/Path";
38import * as PubSub from "effect/PubSub";39import * as PubSub from "effect/PubSub";
39import * as Ref from "effect/Ref";40import * as Ref from "effect/Ref";
@@ -237,25 +238,27 @@ export const ProviderRegistryLive = Layer.effect( · 197 unmodified lines
237 }238 }
238 return yield* readProviderStatusCache(filePath).pipe(239 return yield* readProviderStatusCache(filePath).pipe(
239 Effect.provideService(FileSystem.FileSystem, fileSystem),240 Effect.provideService(FileSystem.FileSystem, fileSystem),
240 Effect.flatMap((cachedProvider) => {241 Effect.flatMap((cachedProvider) =>
241 if (cachedProvider === undefined) {242 Option.match(cachedProvider, {
242 return Effect.void.pipe(Effect.as(undefined as ServerProvider | undefined));243 onNone: () => Effect.succeed(undefined as ServerProvider | undefined),
243 }244 onSome: (cachedProvider) => {
244 const correlation = {245 const correlation = {
245 cachedProvider,246 cachedProvider,
246 fallbackProvider,247 fallbackProvider,
247 } as const;248 } as const;
248 if (!isCachedProviderCorrelated(correlation)) {249 if (!isCachedProviderCorrelated(correlation)) {
249 return Effect.logWarning("provider status cache identity mismatch, ignoring", {250 return Effect.logWarning("provider status cache identity mismatch, ignoring", {
250 path: filePath,251 path: filePath,
251 instanceId: source.instanceId,252 instanceId: source.instanceId,
252 cachedInstanceId: cachedProvider.instanceId ?? null,253 cachedInstanceId: cachedProvider.instanceId ?? null,
253 driver: source.driverKind,254 driver: source.driverKind,
254 cachedDriver: cachedProvider.driver ?? null,255 cachedDriver: cachedProvider.driver ?? null,
255 }).pipe(Effect.as(undefined as ServerProvider | undefined));256 }).pipe(Effect.as(undefined as ServerProvider | undefined));
256 }257 }
257 return Effect.succeed(hydrateCachedProvider(correlation));258 return Effect.succeed(hydrateCachedProvider(correlation));
258 }),259 },
260 }),
261 ),
259 );262 );
260 }),263 }),
261 { concurrency: "unbounded" },264 { concurrency: "unbounded" },
apps/server/src/provider/providerStatusCache.test.ts+36−3
@@ -9,6 +9,7 @@ import { createModelCapabilities } from "@t3tools/shared/model"; · 8 unmodified lines
9import { assert, it } from "@effect/vitest";9import { assert, it } from "@effect/vitest";
10import * as Effect from "effect/Effect";10import * as Effect from "effect/Effect";
11import * as FileSystem from "effect/FileSystem";11import * as FileSystem from "effect/FileSystem";
12import * as Option from "effect/Option";
12 13
13import {14import {
14 hydrateCachedProvider,15 hydrateCachedProvider,
@@ -81,9 +82,41 @@ it.layer(NodeServices.layer)("providerStatusCache", (it) => { · 66 unmodified lines
81 provider: openCodeProvider,82 provider: openCodeProvider,
82 });83 });
83 84
84 assert.deepStrictEqual(yield* readProviderStatusCache(codexPath), codexProvider);85 assert.deepStrictEqual(Option.getOrNull(yield* readProviderStatusCache(codexPath)), codexProvider);
85 assert.deepStrictEqual(yield* readProviderStatusCache(claudePath), claudeProvider);86 assert.deepStrictEqual(
86 assert.deepStrictEqual(yield* readProviderStatusCache(openCodePath), openCodeProvider);87 Option.getOrNull(yield* readProviderStatusCache(claudePath)),
88 claudeProvider,
89 );
90 assert.deepStrictEqual(
91 Option.getOrNull(yield* readProviderStatusCache(openCodePath)),
92 openCodeProvider,
93 );
94 }),
95 );
96
97 it.effect("returns none for missing, empty, or invalid cache files", () =>
98 Effect.gen(function* () {
99 const fs = yield* FileSystem.FileSystem;
100 const tempDir = yield* fs.makeTempDirectoryScoped({ prefix: "t3-provider-cache-invalid-" });
101 const missingPath = yield* resolveProviderStatusCachePath({
102 cacheDir: tempDir,
103 instanceId: defaultInstanceIdForDriver(CODEX_DRIVER),
104 });
105 const emptyPath = yield* resolveProviderStatusCachePath({
106 cacheDir: tempDir,
107 instanceId: ProviderInstanceId.make("codex_empty"),
108 });
109 const invalidPath = yield* resolveProviderStatusCachePath({
110 cacheDir: tempDir,
111 instanceId: ProviderInstanceId.make("codex_invalid"),
112 });
113
114 yield* fs.writeFileString(emptyPath, "\n");
115 yield* fs.writeFileString(invalidPath, "{ invalid json");
116
117 assert.strictEqual(Option.isNone(yield* readProviderStatusCache(missingPath)), true);
118 assert.strictEqual(Option.isNone(yield* readProviderStatusCache(emptyPath)), true);
119 assert.strictEqual(Option.isNone(yield* readProviderStatusCache(invalidPath)), true);
87 }),120 }),
88 );121 );
89 122
apps/server/src/provider/providerStatusCache.ts+17−11
@@ -4,17 +4,19 @@ import { · 3 unmodified lines
4 type ServerProvider,4 type ServerProvider,
5 ServerProvider as ServerProviderSchema,5 ServerProvider as ServerProviderSchema,
6} from "@t3tools/contracts";6} from "@t3tools/contracts";
7import { fromJsonStringPretty } from "@t3tools/shared/schemaJson";
7import * as Cause from "effect/Cause";8import * as Cause from "effect/Cause";
8import * as Effect from "effect/Effect";9import * as Effect from "effect/Effect";
9import * as FileSystem from "effect/FileSystem";10import * as FileSystem from "effect/FileSystem";
11import * as Option from "effect/Option";
10import * as Path from "effect/Path";12import * as Path from "effect/Path";
11import * as Schema from "effect/Schema";13import * as Schema from "effect/Schema";
12 14
13import { writeFileStringAtomically } from "../atomicWrite.ts";15import { writeFileStringAtomically } from "../atomicWrite.ts";
14 16
15const decodeProviderStatusCache = Schema.decodeUnknownEffect(17const ProviderStatusCacheJson = fromJsonStringPretty(ServerProviderSchema);
16 Schema.fromJsonString(ServerProviderSchema),18const decodeProviderStatusCache = Schema.decodeUnknownEffect(ProviderStatusCacheJson);
17);19const encodeProviderStatusCache = Schema.encodeEffect(ProviderStatusCacheJson);
18 20
19const mergeProviderModels = (21const mergeProviderModels = (
20 fallbackModels: ReadonlyArray<ServerProvider["models"][number]>,22 fallbackModels: ReadonlyArray<ServerProvider["models"][number]>,
@@ -120,13 +122,13 @@ export const readProviderStatusCache = (filePath: string) => · 99 unmodified lines
120 const fs = yield* FileSystem.FileSystem;122 const fs = yield* FileSystem.FileSystem;
121 const exists = yield* fs.exists(filePath).pipe(Effect.orElseSucceed(() => false));123 const exists = yield* fs.exists(filePath).pipe(Effect.orElseSucceed(() => false));
122 if (!exists) {124 if (!exists) {
123 return undefined;125 return Option.none<ServerProvider>();
124 }126 }
125 127
126 const raw = yield* fs.readFileString(filePath).pipe(Effect.orElseSucceed(() => ""));128 const raw = yield* fs.readFileString(filePath).pipe(Effect.orElseSucceed(() => ""));
127 const trimmed = raw.trim();129 const trimmed = raw.trim();
128 if (trimmed.length === 0) {130 if (trimmed.length === 0) {
129 return undefined;131 return Option.none<ServerProvider>();
130 }132 }
131 133
132 return yield* decodeProviderStatusCache(trimmed).pipe(134 return yield* decodeProviderStatusCache(trimmed).pipe(
@@ -135,8 +137,8 @@ export const readProviderStatusCache = (filePath: string) => · 2 unmodified lines
135 Effect.logWarning("failed to parse provider status cache, ignoring", {137 Effect.logWarning("failed to parse provider status cache, ignoring", {
136 path: filePath,138 path: filePath,
137 issues: Cause.pretty(cause),139 issues: Cause.pretty(cause),
138 }).pipe(Effect.as(undefined)),140 }).pipe(Effect.as(Option.none<ServerProvider>())),
139 onSuccess: Effect.succeed,141 onSuccess: (provider) => Effect.succeed(Option.some(provider)),
140 }),142 }),
141 );143 );
142 });144 });
@@ -146,8 +148,12 @@ export const writeProviderStatusCache = (input: { · 3 unmodified lines
146 readonly provider: ServerProvider;148 readonly provider: ServerProvider;
147}) => {149}) => {
148 const { updateState: _updateState, ...cacheableProvider } = input.provider;150 const { updateState: _updateState, ...cacheableProvider } = input.provider;
149 return writeFileStringAtomically({151 return encodeProviderStatusCache(cacheableProvider).pipe(
150 filePath: input.filePath,152 Effect.flatMap((contents) =>
151 contents: `${JSON.stringify(cacheableProvider, null, 2)}\n`,153 writeFileStringAtomically({
152 });154 filePath: input.filePath,
155 contents: `${contents}\n`,
156 }),
157 ),
158 );
153};159};