diff --git a/packages/core/src/models-dev.ts b/packages/core/src/models-dev.ts index b5533ea56ec..fe844ead9aa 100644 --- a/packages/core/src/models-dev.ts +++ b/packages/core/src/models-dev.ts @@ -636,8 +636,8 @@ export const layer = (options?: Options) => // population. The payload has outgrown some KV backends' per-value // limits (Durable Object SQLite caps values at 2 MB and api.json // passed it in Aug 2026); a boot without a cache hit just refetches. - const writeCache = Effect.fn("ModelsDev.writeCache")(function* (text: string) { - yield* kv.set(key, { updatedAt: Date.now(), digest: bodyDigest(text), body: text }).pipe( + const writeCache = Effect.fn("ModelsDev.writeCache")(function* (text: string, digest = bodyDigest(text)) { + yield* kv.set(key, { updatedAt: Date.now(), digest, body: text }).pipe( Effect.catchCauseIf( (cause) => !Cause.hasInterruptsOnly(cause), (cause) => Effect.logWarning("Failed to cache models.dev catalog", { cause }), @@ -681,12 +681,13 @@ export const layer = (options?: Options) => const stored = yield* loadFromCache() if (!force && stored && Date.now() - stored.updatedAt < Duration.toMillis(ttl)) return const text = yield* fetchApi() + const digest = bodyDigest(text) // models.dev rarely changes between polls; skip the cache write, // invalidation, and Refreshed event for a byte-identical body so // downstream catalog.updated listeners stay quiet. - if (!force && stored?.digest === bodyDigest(text)) return + if (!force && stored?.digest === digest) return yield* decodeCatalog(text) - yield* writeCache(text) + yield* writeCache(text, digest) yield* invalidate yield* bus.publish(ModelsDev.Event.Refreshed, {}) }), diff --git a/packages/core/test/models.test.ts b/packages/core/test/models.test.ts index 74e5dad9920..e41fa71559f 100644 --- a/packages/core/test/models.test.ts +++ b/packages/core/test/models.test.ts @@ -297,7 +297,10 @@ describe("ModelsDev Service", () => { const context = yield* Layer.build(buildLayer(state, cache, { fetch: true, snapshot: false })) const result = yield* ModelsDev.Service.use((s) => s.get()).pipe(Effect.provide(context)) expect(result).toEqual(fixture2Snapshot) - expect(cache.values.get(cacheKey)).toMatchObject({ body: JSON.stringify(fixture2) }) + expect(cache.values.get(cacheKey)).toMatchObject({ + body: JSON.stringify(fixture2), + digest: bodyDigest(JSON.stringify(fixture2)), + }) const final = yield* Ref.get(state) expect(final.calls.length).toBe(1) }), @@ -387,7 +390,10 @@ describe("ModelsDev Service", () => { ) expect(result.before).toEqual(fixtureSnapshot) expect(result.after).toEqual(fixture2Snapshot) - expect(cache.values.get(cacheKey)).toMatchObject({ body: JSON.stringify(fixture2) }) + expect(cache.values.get(cacheKey)).toMatchObject({ + body: JSON.stringify(fixture2), + digest: bodyDigest(JSON.stringify(fixture2)), + }) const final = yield* Ref.get(state) expect(final.calls.length).toBe(1) expect(final.calls[0].url).toContain("/api.json") @@ -440,6 +446,10 @@ describe("ModelsDev Service", () => { const final = yield* Ref.get(state) expect(final.calls.length).toBe(1) expect(after).toEqual(fixture2Snapshot) + expect(cache.values.get(cacheKey)).toMatchObject({ + body: JSON.stringify(fixture2), + digest: bodyDigest(JSON.stringify(fixture2)), + }) }), )