diff --git a/packages/client-runtime/src/state/serverConfigProjection.test.ts b/packages/client-runtime/src/state/serverConfigProjection.test.ts index 5ed28107a321a..33921ecc29764 100644 --- a/packages/client-runtime/src/state/serverConfigProjection.test.ts +++ b/packages/client-runtime/src/state/serverConfigProjection.test.ts @@ -1,9 +1,18 @@ -import type { ServerConfig, ServerConfigStreamEvent } from "@t3tools/contracts"; +import { + DEFAULT_SERVER_SETTINGS, + ServerSettings, + type ServerConfig, + type ServerConfigStreamEvent, +} from "@t3tools/contracts"; import * as Option from "effect/Option"; +import * as Schema from "effect/Schema"; import { describe, expect, it } from "vite-plus/test"; import { applyServerConfigProjection, reuseUnchanged } from "./serverConfigProjection.ts"; +const encodeSettings = Schema.encodeSync(ServerSettings); +const decodeSettings = Schema.decodeSync(ServerSettings); + describe("reuseUnchanged", () => { const provider = (checkedAt: string, skills: ReadonlyArray) => ({ instanceId: "claude", @@ -53,4 +62,20 @@ describe("applyServerConfigProjection settingsUpdated", () => { expect(nextSettings.providerInstances.codex).toBe(settings.providerInstances.codex); expect(nextSettings.providerInstances.claude.config.launchArgs).toBe("--verbose"); }); + + it("keeps the settings object when an update repeats the current settings", () => { + const wire = encodeSettings(DEFAULT_SERVER_SETTINGS); + const decode = () => decodeSettings(structuredClone(wire)); + const settings = decode(); + const current = Option.some({ + config: { settings } as unknown as ServerConfig, + latestEvent: {} as ServerConfigStreamEvent, + source: "live" as const, + }); + const next = applyServerConfigProjection(current, { + type: "settingsUpdated", + payload: { settings: decode() }, + } as ServerConfigStreamEvent); + expect(Option.getOrThrow(next).config).toBe(Option.getOrThrow(current).config); + }); }); diff --git a/packages/client-runtime/src/state/serverConfigProjection.ts b/packages/client-runtime/src/state/serverConfigProjection.ts index 7d388f889a37a..86a7f5cb8a575 100644 --- a/packages/client-runtime/src/state/serverConfigProjection.ts +++ b/packages/client-runtime/src/state/serverConfigProjection.ts @@ -1,4 +1,5 @@ import type { ServerConfig, ServerConfigStreamEvent } from "@t3tools/contracts"; +import * as Equal from "effect/Equal"; import * as Option from "effect/Option"; export interface ServerConfigProjection { @@ -44,6 +45,8 @@ export function reuseUnchanged(previous: unknown, next: T): T { }); return (same ? previous : merged) as T; } + // Decoded settings carry Durations, which are fresh instances on every push. + if (Equal.isEqual(next)) return (Equal.equals(previous, next) ? previous : next) as T; if (Object.getPrototypeOf(next) !== Object.prototype) return next; const before = previous as Record; const after = next as Record; @@ -104,14 +107,17 @@ export function applyServerConfigProjection( source: "live", })); case "settingsUpdated": - return Option.map(current, (projection) => ({ - config: { - ...projection.config, - settings: reuseUnchanged(projection.config.settings, event.payload.settings), - }, - latestEvent: event, - source: "live", - })); + return Option.map(current, (projection) => { + const settings = reuseUnchanged(projection.config.settings, event.payload.settings); + return { + config: + settings === projection.config.settings + ? projection.config + : { ...projection.config, settings }, + latestEvent: event, + source: "live", + }; + }); case "environmentThemesUpdated": return Option.map(current, (projection) => ({ config: {