Unified · Split
| @@ -4,6 +4,7 @@ import * as Effect from "effect/Effect"; · 3 unmodified lines | ||
| 4 | 4 | import * as Exit from "effect/Exit"; |
| 5 | 5 | import * as Fiber from "effect/Fiber"; |
| 6 | 6 | import * as Layer from "effect/Layer"; |
| 7 | +import * as Option from "effect/Option"; | |
| 7 | 8 | import * as PubSub from "effect/PubSub"; |
| 8 | 9 | import * as Ref from "effect/Ref"; |
| 9 | 10 | import * as Schema from "effect/Schema"; |
| @@ -752,7 +753,7 @@ it.layer(Layer.mergeAll(NodeServices.layer, ServerSettingsService.layerTest(), T · 742 unmodified lines | ||
| 752 | 753 | ]); |
| 753 | 754 | yield* PubSub.publish(changes, refreshedProvider); |
| 754 | 755 | |
| 755 | − let cachedProvider = yield* readProviderStatusCache(filePath); | |
| 756 | + let cachedProvider = Option.getOrUndefined(yield* readProviderStatusCache(filePath)); | |
| 756 | 757 | for ( |
| 757 | 758 | let attempt = 0; |
| 758 | 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 | 762 | yield* TestClock.adjust("10 millis"); |
| 762 | 763 | yield* Effect.yieldNow; |
| 763 | − cachedProvider = yield* readProviderStatusCache(filePath); | |
| 764 | + cachedProvider = Option.getOrUndefined(yield* readProviderStatusCache(filePath)); | |
| 764 | 765 | } |
| 765 | 766 | |
| 766 | 767 | assert.deepStrictEqual(cachedProvider, { |
| @@ -34,6 +34,7 @@ import * as Effect from "effect/Effect"; · 33 unmodified lines | ||
| 34 | 34 | import * as Equal from "effect/Equal"; |
| 35 | 35 | import * as FileSystem from "effect/FileSystem"; |
| 36 | 36 | import * as Layer from "effect/Layer"; |
| 37 | +import * as Option from "effect/Option"; | |
| 37 | 38 | import * as Path from "effect/Path"; |
| 38 | 39 | import * as PubSub from "effect/PubSub"; |
| 39 | 40 | import * as Ref from "effect/Ref"; |
| @@ -237,25 +238,27 @@ export const ProviderRegistryLive = Layer.effect( · 197 unmodified lines | ||
| 237 | 238 | } |
| 238 | 239 | return yield* readProviderStatusCache(filePath).pipe( |
| 239 | 240 | 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 | + ), | |
| 259 | 262 | ); |
| 260 | 263 | }), |
| 261 | 264 | { concurrency: "unbounded" }, |
| @@ -9,6 +9,7 @@ import { createModelCapabilities } from "@t3tools/shared/model"; · 8 unmodified lines | ||
| 9 | 9 | import { assert, it } from "@effect/vitest"; |
| 10 | 10 | import * as Effect from "effect/Effect"; |
| 11 | 11 | import * as FileSystem from "effect/FileSystem"; |
| 12 | +import * as Option from "effect/Option"; | |
| 12 | 13 | |
| 13 | 14 | import { |
| 14 | 15 | hydrateCachedProvider, |
| @@ -81,9 +82,41 @@ it.layer(NodeServices.layer)("providerStatusCache", (it) => { · 66 unmodified lines | ||
| 81 | 82 | provider: openCodeProvider, |
| 82 | 83 | }); |
| 83 | 84 | |
| 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); | |
| 87 | 120 | }), |
| 88 | 121 | ); |
| 89 | 122 | |
| @@ -4,17 +4,19 @@ import { · 3 unmodified lines | ||
| 4 | 4 | type ServerProvider, |
| 5 | 5 | ServerProvider as ServerProviderSchema, |
| 6 | 6 | } from "@t3tools/contracts"; |
| 7 | +import { fromJsonStringPretty } from "@t3tools/shared/schemaJson"; | |
| 7 | 8 | import * as Cause from "effect/Cause"; |
| 8 | 9 | import * as Effect from "effect/Effect"; |
| 9 | 10 | import * as FileSystem from "effect/FileSystem"; |
| 11 | +import * as Option from "effect/Option"; | |
| 10 | 12 | import * as Path from "effect/Path"; |
| 11 | 13 | import * as Schema from "effect/Schema"; |
| 12 | 14 | |
| 13 | 15 | import { writeFileStringAtomically } from "../atomicWrite.ts"; |
| 14 | 16 | |
| 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); | |
| 18 | 20 | |
| 19 | 21 | const mergeProviderModels = ( |
| 20 | 22 | fallbackModels: ReadonlyArray<ServerProvider["models"][number]>, |
| @@ -120,13 +122,13 @@ export const readProviderStatusCache = (filePath: string) => · 99 unmodified lines | ||
| 120 | 122 | const fs = yield* FileSystem.FileSystem; |
| 121 | 123 | const exists = yield* fs.exists(filePath).pipe(Effect.orElseSucceed(() => false)); |
| 122 | 124 | if (!exists) { |
| 123 | − return undefined; | |
| 125 | + return Option.none<ServerProvider>(); | |
| 124 | 126 | } |
| 125 | 127 | |
| 126 | 128 | const raw = yield* fs.readFileString(filePath).pipe(Effect.orElseSucceed(() => "")); |
| 127 | 129 | const trimmed = raw.trim(); |
| 128 | 130 | if (trimmed.length === 0) { |
| 129 | − return undefined; | |
| 131 | + return Option.none<ServerProvider>(); | |
| 130 | 132 | } |
| 131 | 133 | |
| 132 | 134 | return yield* decodeProviderStatusCache(trimmed).pipe( |
| @@ -135,8 +137,8 @@ export const readProviderStatusCache = (filePath: string) => · 2 unmodified lines | ||
| 135 | 137 | Effect.logWarning("failed to parse provider status cache, ignoring", { |
| 136 | 138 | path: filePath, |
| 137 | 139 | 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)), | |
| 140 | 142 | }), |
| 141 | 143 | ); |
| 142 | 144 | }); |
| @@ -146,8 +148,12 @@ export const writeProviderStatusCache = (input: { · 3 unmodified lines | ||
| 146 | 148 | readonly provider: ServerProvider; |
| 147 | 149 | }) => { |
| 148 | 150 | 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 | + ); | |
| 153 | 159 | }; |