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
44 import * as Exit from "effect/Exit";
55 import * as Fiber from "effect/Fiber";
66 import * as Layer from "effect/Layer";
7+import * as Option from "effect/Option";
78 import * as PubSub from "effect/PubSub";
89 import * as Ref from "effect/Ref";
910 import * as Schema from "effect/Schema";
@@ -752,7 +753,7 @@ it.layer(Layer.mergeAll(NodeServices.layer, ServerSettingsService.layerTest(), T · 742 unmodified lines
752753 ]);
753754 yield* PubSub.publish(changes, refreshedProvider);
754755
755− let cachedProvider = yield* readProviderStatusCache(filePath);
756+ let cachedProvider = Option.getOrUndefined(yield* readProviderStatusCache(filePath));
756757 for (
757758 let attempt = 0;
758759 attempt < 50 && cachedProvider?.checkedAt !== refreshedProvider.checkedAt;
@@ -760,7 +761,7 @@ it.layer(Layer.mergeAll(NodeServices.layer, ServerSettingsService.layerTest(), T · 1 unmodified line
760761 ) {
761762 yield* TestClock.adjust("10 millis");
762763 yield* Effect.yieldNow;
763− cachedProvider = yield* readProviderStatusCache(filePath);
764+ cachedProvider = Option.getOrUndefined(yield* readProviderStatusCache(filePath));
764765 }
765766
766767 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
3434 import * as Equal from "effect/Equal";
3535 import * as FileSystem from "effect/FileSystem";
3636 import * as Layer from "effect/Layer";
37+import * as Option from "effect/Option";
3738 import * as Path from "effect/Path";
3839 import * as PubSub from "effect/PubSub";
3940 import * as Ref from "effect/Ref";
@@ -237,25 +238,27 @@ export const ProviderRegistryLive = Layer.effect( · 197 unmodified lines
237238 }
238239 return yield* readProviderStatusCache(filePath).pipe(
239240 Effect.provideService(FileSystem.FileSystem, fileSystem),
240− Effect.flatMap((cachedProvider) => {
241− if (cachedProvider === undefined) {
242− return Effect.void.pipe(Effect.as(undefined as ServerProvider | undefined));
243− }
244− const correlation = {
245− cachedProvider,
246− fallbackProvider,
247− } as const;
248− if (!isCachedProviderCorrelated(correlation)) {
249− return Effect.logWarning("provider status cache identity mismatch, ignoring", {
250− path: filePath,
251− instanceId: source.instanceId,
252− cachedInstanceId: cachedProvider.instanceId ?? null,
253− driver: source.driverKind,
254− cachedDriver: cachedProvider.driver ?? null,
255− }).pipe(Effect.as(undefined as ServerProvider | undefined));
256− }
257− return Effect.succeed(hydrateCachedProvider(correlation));
258− }),
241+ Effect.flatMap((cachedProvider) =>
242+ Option.match(cachedProvider, {
243+ onNone: () => Effect.succeed(undefined as ServerProvider | undefined),
244+ onSome: (cachedProvider) => {
245+ const correlation = {
246+ cachedProvider,
247+ fallbackProvider,
248+ } as const;
249+ if (!isCachedProviderCorrelated(correlation)) {
250+ return Effect.logWarning("provider status cache identity mismatch, ignoring", {
251+ path: filePath,
252+ instanceId: source.instanceId,
253+ cachedInstanceId: cachedProvider.instanceId ?? null,
254+ driver: source.driverKind,
255+ cachedDriver: cachedProvider.driver ?? null,
256+ }).pipe(Effect.as(undefined as ServerProvider | undefined));
257+ }
258+ return Effect.succeed(hydrateCachedProvider(correlation));
259+ },
260+ }),
261+ ),
259262 );
260263 }),
261264 { concurrency: "unbounded" },
apps/server/src/provider/providerStatusCache.test.ts+36−3
@@ -9,6 +9,7 @@ import { createModelCapabilities } from "@t3tools/shared/model"; · 8 unmodified lines
99 import { assert, it } from "@effect/vitest";
1010 import * as Effect from "effect/Effect";
1111 import * as FileSystem from "effect/FileSystem";
12+import * as Option from "effect/Option";
1213
1314 import {
1415 hydrateCachedProvider,
@@ -81,9 +82,41 @@ it.layer(NodeServices.layer)("providerStatusCache", (it) => { · 66 unmodified lines
8182 provider: openCodeProvider,
8283 });
8384
84− assert.deepStrictEqual(yield* readProviderStatusCache(codexPath), codexProvider);
85− assert.deepStrictEqual(yield* readProviderStatusCache(claudePath), claudeProvider);
86− assert.deepStrictEqual(yield* readProviderStatusCache(openCodePath), openCodeProvider);
85+ assert.deepStrictEqual(Option.getOrNull(yield* readProviderStatusCache(codexPath)), codexProvider);
86+ assert.deepStrictEqual(
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);
87120 }),
88121 );
89122
apps/server/src/provider/providerStatusCache.ts+17−11
@@ -4,17 +4,19 @@ import { · 3 unmodified lines
44 type ServerProvider,
55 ServerProvider as ServerProviderSchema,
66 } from "@t3tools/contracts";
7+import { fromJsonStringPretty } from "@t3tools/shared/schemaJson";
78 import * as Cause from "effect/Cause";
89 import * as Effect from "effect/Effect";
910 import * as FileSystem from "effect/FileSystem";
11+import * as Option from "effect/Option";
1012 import * as Path from "effect/Path";
1113 import * as Schema from "effect/Schema";
1214
1315 import { writeFileStringAtomically } from "../atomicWrite.ts";
1416
15−const decodeProviderStatusCache = Schema.decodeUnknownEffect(
16− Schema.fromJsonString(ServerProviderSchema),
17−);
17+const ProviderStatusCacheJson = fromJsonStringPretty(ServerProviderSchema);
18+const decodeProviderStatusCache = Schema.decodeUnknownEffect(ProviderStatusCacheJson);
19+const encodeProviderStatusCache = Schema.encodeEffect(ProviderStatusCacheJson);
1820
1921 const mergeProviderModels = (
2022 fallbackModels: ReadonlyArray<ServerProvider["models"][number]>,
@@ -120,13 +122,13 @@ export const readProviderStatusCache = (filePath: string) => · 99 unmodified lines
120122 const fs = yield* FileSystem.FileSystem;
121123 const exists = yield* fs.exists(filePath).pipe(Effect.orElseSucceed(() => false));
122124 if (!exists) {
123− return undefined;
125+ return Option.none<ServerProvider>();
124126 }
125127
126128 const raw = yield* fs.readFileString(filePath).pipe(Effect.orElseSucceed(() => ""));
127129 const trimmed = raw.trim();
128130 if (trimmed.length === 0) {
129− return undefined;
131+ return Option.none<ServerProvider>();
130132 }
131133
132134 return yield* decodeProviderStatusCache(trimmed).pipe(
@@ -135,8 +137,8 @@ export const readProviderStatusCache = (filePath: string) => · 2 unmodified lines
135137 Effect.logWarning("failed to parse provider status cache, ignoring", {
136138 path: filePath,
137139 issues: Cause.pretty(cause),
138− }).pipe(Effect.as(undefined)),
139− onSuccess: Effect.succeed,
140+ }).pipe(Effect.as(Option.none<ServerProvider>())),
141+ onSuccess: (provider) => Effect.succeed(Option.some(provider)),
140142 }),
141143 );
142144 });
@@ -146,8 +148,12 @@ export const writeProviderStatusCache = (input: { · 3 unmodified lines
146148 readonly provider: ServerProvider;
147149 }) => {
148150 const { updateState: _updateState, ...cacheableProvider } = input.provider;
149− return writeFileStringAtomically({
150− filePath: input.filePath,
151− contents: `${JSON.stringify(cacheableProvider, null, 2)}\n`,
152− });
151+ return encodeProviderStatusCache(cacheableProvider).pipe(
152+ Effect.flatMap((contents) =>
153+ writeFileStringAtomically({
154+ filePath: input.filePath,
155+ contents: `${contents}\n`,
156+ }),
157+ ),
158+ );
153159 };