From 169ababd60c12aee3507ca7540e95fdfa002333b Mon Sep 17 00:00:00 2001 From: LukeParkerDev <10430890+Hona@users.noreply.github.com> Date: Sat, 19 Sep 2026 21:47:51 +1000 Subject: [PATCH] refactor(app): port command, notification, highlights, home, file-view and workspace-tip stores to plain codecs Session import keeps validating against the shared Effect schema through a dynamic import, so the shared schema and Effect load only when a file is imported. Persisted notification errors are checked structurally instead of through the shared schema. composer/schema.ts carries Effect twins of the file selection shapes until it is ported. --- packages/app/src/composer/schema.ts | 16 +++++++- packages/app/src/home/projects/controller.tsx | 20 ++++++---- packages/app/src/new-session/view.tsx | 12 +++--- .../src/runtime/persistence/consumers.test.ts | 12 +++--- packages/app/src/settings/models/models.tsx | 8 ++-- .../app/src/shell/commands/command.test.ts | 7 ++-- packages/app/src/shell/commands/command.tsx | 21 +++++----- .../shell/notifications/notification.test.ts | 8 ++-- .../src/shell/notifications/notification.tsx | 39 ++++++++++++------- .../app/src/shell/updates/highlights.test.ts | 8 ++-- packages/app/src/shell/updates/highlights.tsx | 9 +++-- packages/app/src/workspaces/files/types.ts | 24 ++++++------ .../app/src/workspaces/files/view-cache.ts | 16 ++++---- 13 files changed, 118 insertions(+), 82 deletions(-) diff --git a/packages/app/src/composer/schema.ts b/packages/app/src/composer/schema.ts index 6738a7882ae..2aa1320f9db 100644 --- a/packages/app/src/composer/schema.ts +++ b/packages/app/src/composer/schema.ts @@ -3,7 +3,19 @@ import { checksum } from "@opencode/util/encode" import { SessionMessage } from "@opencode/schema/session-message" import { Skill } from "@opencode/schema/skill" import { Persistence } from "@/runtime/persistence/schema" -import { FileSelection, SelectedLineRange } from "@/workspaces/files/types" +// Effect twins of workspaces/files/types until this module moves to plain codecs. +const SelectedLineRange = Persistence.struct({ + start: Schema.Number, + end: Schema.Number, + side: Persistence.optional(Schema.Literals(["additions", "deletions"])), + endSide: Persistence.optional(Schema.Literals(["additions", "deletions"])), +}) +const FileSelection = Persistence.struct({ + startLine: Schema.Number, + startChar: Schema.Number, + endLine: Schema.Number, + endChar: Schema.Number, +}) const PartBase = { content: Schema.String, @@ -228,3 +240,5 @@ export const PromptHistoryEntry = Schema.Union([HistoryEntry, HistoryPrompt]).pi export type PromptHistoryEntry = typeof PromptHistoryEntry.Type export const PromptHistoryState = Persistence.struct({ entries: Persistence.array(PromptHistoryEntry) }) + + diff --git a/packages/app/src/home/projects/controller.tsx b/packages/app/src/home/projects/controller.tsx index 070c4e95c16..1d824c35db1 100644 --- a/packages/app/src/home/projects/controller.tsx +++ b/packages/app/src/home/projects/controller.tsx @@ -11,15 +11,15 @@ import { Persist, persisted } from "@/runtime/persistence/storage" import { showToast } from "@/shell/notifications/toast" import { useDialog } from "@opencode/ui/context/dialog" import { createResource } from "solid-js" -import { Schema } from "effect" -import { Persistence } from "@/runtime/persistence/schema" +import { Codec } from "@/runtime/persistence/codec" + import type { HomeController } from "../model" import { useGlobal } from "@/runtime/server/runtime" -import { SessionTransfer } from "@opencode/schema/session-transfer" + import { useSshAuthenticate } from "@/servers/ssh/authenticate" -export const HomeServersSchema = Schema.Struct({ - collapsed: Persistence.record(Persistence.fallback(Schema.Boolean, () => false)), +export const HomeServersSchema = Codec.struct({ + collapsed: Codec.lenientRecord(Codec.fallback(Codec.boolean, () => false)), }) export function createHomeProjectsController(home: HomeController) { @@ -114,8 +114,13 @@ export function createHomeProjectsController(home: HomeController) { extensions: ["json"], }, async (file) => { - const data = await Schema.decodeUnknownPromise(Schema.fromJsonString(SessionTransfer.Data))( - await file.text(), + // Validating an imported file is the one place the shared Effect schema is needed here. + const [{ Schema }, { SessionTransfer }] = await Promise.all([ + import("effect"), + import("@opencode/schema/session-transfer"), + ]) + const data = await Schema.decodeUnknownPromise(Schema.fromJsonString(SessionTransfer.Data))( + await file.text(), ) const api = home.server.context(conn).sdk.api.session const imported = await api.import({ @@ -184,3 +189,4 @@ export function createHomeProjectsController(home: HomeController) { } export type HomeProjectsController = ReturnType + diff --git a/packages/app/src/new-session/view.tsx b/packages/app/src/new-session/view.tsx index aef0818d715..5e371eb5b61 100644 --- a/packages/app/src/new-session/view.tsx +++ b/packages/app/src/new-session/view.tsx @@ -5,7 +5,7 @@ import { Show, Suspense, createMemo, createSignal, lazy, onMount } from "solid-j import { createStore } from "solid-js/store" import { makeEventListener } from "@solid-primitives/event-listener" import { debounce } from "@solid-primitives/scheduled" -import { Schema } from "effect" +import { Codec } from "@/runtime/persistence/codec" import createPresence from "solid-presence" import { Composer } from "@/composer/composer" import { ComposerDropzone } from "@/composer/dropzone" @@ -21,7 +21,6 @@ import { useWorkspaceLocation } from "@/workspaces/location" import { useProviders } from "@/providers/catalog/providers" import { NEW_SESSION_CONTENT_WIDTH } from "@/new-session/layout" import { Persist, persisted } from "@/runtime/persistence/storage" -import { Persistence } from "@/runtime/persistence/schema" import type { NewSessionWorkspaceController } from "./workspace/controller" import { NewSessionWordmark } from "./wordmark" import { SummaryPopover } from "@/session/summary/popover" @@ -34,12 +33,12 @@ const NewSessionSummary = lazy(async () => { const providerTipDismissalDuration = 30 * 24 * 60 * 60 * 1000 -export const WorkspaceOnboardingSchema = Persistence.struct({ - used: Schema.Boolean, +export const WorkspaceOnboardingSchema = Codec.struct({ + used: Codec.boolean, }) -export const ProviderTipSchema = Persistence.struct({ - dismissedAt: Schema.Finite, +export const ProviderTipSchema = Codec.struct({ + dismissedAt: Codec.number, }) export const WorkspaceTipSchema = ProviderTipSchema @@ -267,3 +266,4 @@ function NewSessionTips(props: { workspaceEligible: boolean; onWorkspace: () => ) } + diff --git a/packages/app/src/runtime/persistence/consumers.test.ts b/packages/app/src/runtime/persistence/consumers.test.ts index ed70db0a7b6..4ab9a98ca21 100644 --- a/packages/app/src/runtime/persistence/consumers.test.ts +++ b/packages/app/src/runtime/persistence/consumers.test.ts @@ -3,6 +3,7 @@ import { Schema } from "effect" import { WorkspaceOnboardingSchema, ProviderTipSchema, WorkspaceTipSchema } from "@/new-session/view" import { ModelSelectionSchema } from "@/providers/models/selection" import { Persistence } from "@/runtime/persistence/schema" +import { Codec } from "@/runtime/persistence/codec" import { FileViewsSchema } from "@/workspaces/files/view-cache" import { languageSchema } from "@/runtime/i18n/language" import { HomeServersSchema } from "@/home/projects/controller" @@ -10,9 +11,9 @@ import { ModelProvidersSchema } from "@/settings/models/models" describe("persisted consumer schemas", () => { test("onboarding and provider tip retain defaults and validate stored values", () => { - const onboarding = Schema.decodeUnknownSync(Persistence.withInitial(WorkspaceOnboardingSchema, { used: false })) - const tip = Schema.decodeUnknownSync(Persistence.withInitial(ProviderTipSchema, { dismissedAt: 0 })) - const workspaceTip = Schema.decodeUnknownSync(Persistence.withInitial(WorkspaceTipSchema, { dismissedAt: 0 })) + const onboarding = ((input: unknown) => Codec.decodeOrThrow(Codec.withInitial(WorkspaceOnboardingSchema, { used: false }), input)) + const tip = ((input: unknown) => Codec.decodeOrThrow(Codec.withInitial(ProviderTipSchema, { dismissedAt: 0 }), input)) + const workspaceTip = ((input: unknown) => Codec.decodeOrThrow(Codec.withInitial(WorkspaceTipSchema, { dismissedAt: 0 }), input)) expect(onboarding({})).toEqual({ used: false }) expect(onboarding({ used: "true" })).toEqual({ used: false }) expect(onboarding({ used: true })).toEqual({ used: true }) @@ -25,7 +26,7 @@ describe("persisted consumer schemas", () => { test("collapse records recover malformed entries without losing valid siblings", () => { for (const schema of [HomeServersSchema, ModelProvidersSchema]) { - const decode = Schema.decodeUnknownSync(Persistence.withInitial(schema, { collapsed: {} })) + const decode = (input: unknown) => Codec.decodeOrThrow(Codec.withInitial(schema, { collapsed: {} }), input) expect(decode({})).toEqual({ collapsed: {} }) expect(decode({ collapsed: [] })).toEqual({ collapsed: {} }) expect(decode({ collapsed: { open: false, closed: true, invalid: "false" } })).toEqual({ @@ -76,7 +77,7 @@ describe("persisted consumer schemas", () => { }) test("file views validate scroll positions and line sides independently", () => { - const decode = Schema.decodeUnknownSync(Persistence.withInitial(FileViewsSchema, { file: {} })) + const decode = ((input: unknown) => Codec.decodeOrThrow(Codec.withInitial(FileViewsSchema, { file: {} }), input)) expect(decode({})).toEqual({ file: {} }) const state = decode({ file: { @@ -111,3 +112,4 @@ describe("persisted consumer schemas", () => { expect(decode({ locale: "ar" })).toEqual({ locale: "ar" }) }) }) + diff --git a/packages/app/src/settings/models/models.tsx b/packages/app/src/settings/models/models.tsx index 1d1fc843b1d..457d9c1379f 100644 --- a/packages/app/src/settings/models/models.tsx +++ b/packages/app/src/settings/models/models.tsx @@ -5,8 +5,7 @@ import { Icon } from "@opencode/ui/icon" import { IconButton } from "@opencode/ui/icon-button" import { TextInput } from "@opencode/ui/text-input" import { type Component, createEffect, For, on, onCleanup, Show } from "solid-js" -import { Schema } from "effect" -import { Persistence } from "@/runtime/persistence/schema" +import { Codec } from "@/runtime/persistence/codec" import { useLanguage } from "@/runtime/i18n/language" import { useModels } from "@/providers/models/models" import { useServerSDK } from "@/runtime/server/client" @@ -20,8 +19,8 @@ type ModelItem = ReturnType["list"]>[number] const PROVIDER_ICON_SIZE = 16 -export const ModelProvidersSchema = Schema.Struct({ - collapsed: Persistence.record(Persistence.fallback(Schema.Boolean, () => false)), +export const ModelProvidersSchema = Codec.struct({ + collapsed: Codec.lenientRecord(Codec.fallback(Codec.boolean, () => false)), }) export const SettingsModels: Component<{ active?: boolean; autofocus?: boolean }> = (props) => { @@ -211,3 +210,4 @@ export const SettingsModels: Component<{ active?: boolean; autofocus?: boolean } ) } + diff --git a/packages/app/src/shell/commands/command.test.ts b/packages/app/src/shell/commands/command.test.ts index 4e53f4f9446..d53f09d7e20 100644 --- a/packages/app/src/shell/commands/command.test.ts +++ b/packages/app/src/shell/commands/command.test.ts @@ -1,5 +1,5 @@ import { describe, expect, test } from "bun:test" -import { Schema } from "effect" +import { Codec } from "@/runtime/persistence/codec" import { activeCommandRegistrations, addCommandRegistration, @@ -10,12 +10,12 @@ import { } from "./command" test("command catalog persistence validates metadata and omits executable fields", () => { - const decode = Schema.decodeUnknownSync(CommandCatalog) + const decode = ((input: unknown) => Codec.decodeOrThrow(CommandCatalog, input)) const catalog = decode({ open: { title: "Open", keybind: "mod+o", hidden: false, onSelect: "invalid" } }) expect(catalog).toEqual({ open: { title: "Open", keybind: "mod+o", hidden: false } }) expect(decode({})).toEqual({}) expect(() => decode({ open: { title: 1 } })).toThrow() - expect(decode(Schema.encodeSync(CommandCatalog)(catalog))).toEqual(catalog) + expect(decode(CommandCatalog.encode(catalog))).toEqual(catalog) }) const paletteOptions: CommandOption[] = [ @@ -79,3 +79,4 @@ describe("resolveKeybindOption", () => { expect(resolveKeybindOption([fallback, contextual], new KeyboardEvent("keydown"))).toBe(fallback) }) }) + diff --git a/packages/app/src/shell/commands/command.tsx b/packages/app/src/shell/commands/command.tsx index 4ce001a1a35..9ceeecb3744 100644 --- a/packages/app/src/shell/commands/command.tsx +++ b/packages/app/src/shell/commands/command.tsx @@ -2,8 +2,8 @@ import { createSimpleContext } from "@opencode/ui/context" import { useDialog } from "@opencode/ui/context/dialog" import { type Accessor, batch, createEffect, createMemo, onCleanup, onMount } from "solid-js" import { createStore, reconcile } from "solid-js/store" -import { Schema } from "effect" -import { Persistence } from "@/runtime/persistence/schema" +import { Codec } from "@/runtime/persistence/codec" + import { makeEventListener } from "@solid-primitives/event-listener" import { useLanguage } from "@/runtime/i18n/language" import { useSettings } from "@/settings/model" @@ -109,16 +109,16 @@ export function resolveKeybindOption(candidates: CommandOption[] | undefined, ev type CommandSource = "palette" | "keybind" | "slash" -export const CommandCatalogItem = Persistence.struct({ - title: Schema.String, - description: Schema.optional(Schema.String), - category: Schema.optional(Schema.String), - keybind: Schema.optional(Schema.String), - slash: Schema.optional(Schema.String), - hidden: Schema.optional(Schema.Boolean), +export const CommandCatalogItem = Codec.struct({ + title: Codec.string, + description: Codec.optional(Codec.string), + category: Codec.optional(Codec.string), + keybind: Codec.optional(Codec.string), + slash: Codec.optional(Codec.string), + hidden: Codec.optional(Codec.boolean), }) export type CommandCatalogItem = typeof CommandCatalogItem.Type -export const CommandCatalog = Schema.Record(Schema.String, Schema.mutableKey(CommandCatalogItem)) +export const CommandCatalog = Codec.record(CommandCatalogItem) export type CommandCatalog = typeof CommandCatalog.Type export type CommandRegistration = { @@ -480,3 +480,4 @@ export const { use: useCommand, provider: CommandProvider } = createSimpleContex } }, }) + diff --git a/packages/app/src/shell/notifications/notification.test.ts b/packages/app/src/shell/notifications/notification.test.ts index e86d5b2df62..146d6d14493 100644 --- a/packages/app/src/shell/notifications/notification.test.ts +++ b/packages/app/src/shell/notifications/notification.test.ts @@ -1,23 +1,22 @@ import { expect, test } from "bun:test" -import { Schema } from "effect" import type { ServerConnection } from "@/runtime/server/registry" import type { Tab } from "@/shell/tabs/tabs" import { NotificationStore, openNotificationSession, type Notification } from "./notification" -import { Persistence } from "@/runtime/persistence/schema" +import { Codec } from "@/runtime/persistence/codec" test("notification persistence validates and salvages individual notifications", () => { const valid: Notification[] = [ { type: "turn-complete", time: 123, viewed: false, session: "session-1" }, { type: "error", time: 124, viewed: true, error: { type: "api", message: "failed", status: 500 } }, ] - const decode = Schema.decodeUnknownSync(Persistence.withInitial(NotificationStore, { list: [] })) + const decode = ((input: unknown) => Codec.decodeOrThrow(Codec.withInitial(NotificationStore, { list: [] }), input)) const store = decode({ list: [valid[0], null, { type: "unknown", time: 123, viewed: false }, { ...valid[1], error: "invalid" }, valid[1]], }) expect(store.list).toEqual(valid) expect(decode({})).toEqual({ list: [] }) expect(decode({ list: {} })).toEqual({ list: [] }) - expect(decode(Schema.encodeSync(NotificationStore)(store))).toEqual(store) + expect(decode(NotificationStore.encode(store))).toEqual(store) }) test("opens notification sessions through the tab router", () => { @@ -41,3 +40,4 @@ test("opens notification sessions through the tab router", () => { expect(calls).toEqual(["add:session-1", "route:session-1", "select:session-1"]) }) + diff --git a/packages/app/src/shell/notifications/notification.tsx b/packages/app/src/shell/notifications/notification.tsx index 55e435fb23e..32d20485611 100644 --- a/packages/app/src/shell/notifications/notification.tsx +++ b/packages/app/src/shell/notifications/notification.tsx @@ -1,6 +1,6 @@ import { createStore, reconcile } from "solid-js/store" -import { Schema } from "effect" -import { SessionError } from "@opencode/schema/session-error" +import { Codec } from "@/runtime/persistence/codec" +import type { SessionError } from "@opencode/schema/session-error" import { type Accessor, batch, createEffect, createMemo, createRoot, getOwner, onCleanup } from "solid-js" import { createSimpleContext } from "@opencode/ui/context" import type { ServerSDK } from "@/runtime/server/client" @@ -10,7 +10,7 @@ import { useLanguage } from "@/runtime/i18n/language" import { useSettings } from "@/settings/model" import { decode64 } from "@/runtime/persistence/base64" import { Persist, persisted } from "@/runtime/persistence/storage" -import { Persistence } from "@/runtime/persistence/schema" + import { playSoundById } from "@/shell/notifications/sound" import type { createNotificationCoordinator } from "@/shell/notifications/coordinator" import { useGlobal } from "@/runtime/server/runtime" @@ -20,19 +20,28 @@ import { requireServerKey, sessionHref } from "@/shell/routes/session" import type { ServerScope } from "@/runtime/server/scope" import { useServer } from "@/runtime/server/current" -const NotificationBase = { - directory: Schema.optional(Schema.String), - session: Schema.optional(Schema.String), - metadata: Schema.optional(Schema.Unknown), - time: Schema.Finite, - viewed: Schema.Boolean, -} -export const Notification = Schema.Union([ - Persistence.struct({ ...NotificationBase, type: Schema.Literal("turn-complete") }), - Persistence.struct({ ...NotificationBase, type: Schema.Literal("error"), error: SessionError.Error }), +const NotificationBase = { + directory: Codec.optional(Codec.string), + session: Codec.optional(Codec.string), + metadata: Codec.optional(Codec.unknown), + time: Codec.number, + viewed: Codec.boolean, +} +// The error payload is whatever the server reported; shape-checking it here would load the shared +// Effect schema into the renderer's startup path for a value the server already validated. +const StoredSessionError = Codec.make( + (value) => + typeof value === "object" && value !== null && "name" in value && "message" in value + ? (value as unknown as SessionError.Error) + : Codec.INVALID, + (value) => value, +) +export const Notification = Codec.union([ + Codec.struct({ ...NotificationBase, type: Codec.literal("turn-complete") }), + Codec.struct({ ...NotificationBase, type: Codec.literal("error"), error: StoredSessionError }), ]) export type Notification = typeof Notification.Type -export const NotificationStore = Persistence.struct({ list: Persistence.array(Notification) }) +export const NotificationStore = Codec.struct({ list: Codec.lenientArray(Notification) }) type NotificationIndex = { session: { @@ -368,3 +377,5 @@ export const useNotification = () => { const server = useServer() return server.ctx.notification } + + diff --git a/packages/app/src/shell/updates/highlights.test.ts b/packages/app/src/shell/updates/highlights.test.ts index 9406ce8a387..e03afe620b7 100644 --- a/packages/app/src/shell/updates/highlights.test.ts +++ b/packages/app/src/shell/updates/highlights.test.ts @@ -1,13 +1,13 @@ import { expect, test } from "bun:test" -import { Schema } from "effect" import { HighlightsStore } from "./highlights" -import { Persistence } from "@/runtime/persistence/schema" +import { Codec } from "@/runtime/persistence/codec" test("highlight persistence defaults missing or invalid versions and round-trips valid versions", () => { - const decode = Schema.decodeUnknownSync(Persistence.withInitial(HighlightsStore, { version: undefined })) + const decode = ((input: unknown) => Codec.decodeOrThrow(Codec.withInitial(HighlightsStore, { version: undefined }), input)) expect(decode({})).toEqual({ version: undefined }) expect(decode({ version: null })).toEqual({ version: undefined }) const value = decode({ version: "1.2.3", legacy: true }) expect(value).toEqual({ version: "1.2.3" }) - expect(Schema.encodeSync(HighlightsStore)(value)).toEqual(value) + expect(HighlightsStore.encode(value)).toEqual(value) }) + diff --git a/packages/app/src/shell/updates/highlights.tsx b/packages/app/src/shell/updates/highlights.tsx index 8896833235e..32ac9c81d64 100644 --- a/packages/app/src/shell/updates/highlights.tsx +++ b/packages/app/src/shell/updates/highlights.tsx @@ -1,18 +1,18 @@ import { createEffect, onCleanup } from "solid-js" import { createStore } from "solid-js/store" -import { Schema } from "effect" +import { Codec } from "@/runtime/persistence/codec" import { createSimpleContext } from "@opencode/ui/context" import { useDialog } from "@opencode/ui/context/dialog" import { usePlatform } from "@/runtime/platform/platform" import { useSettings } from "@/settings/model" import { persisted } from "@/runtime/persistence/storage" -import { Persistence } from "@/runtime/persistence/schema" + import { DialogReleaseNotes, type Highlight } from "@/shell/updates/release-notes" const CHANGELOG_URL = "https://opencode.ai/changelog.json" -export const HighlightsStore = Persistence.struct({ - version: Schema.UndefinedOr(Schema.String), +export const HighlightsStore = Codec.struct({ + version: Codec.undefinedOr(Codec.string), }) type ParsedRelease = { @@ -233,3 +233,4 @@ export const { use: useHighlights, provider: HighlightsProvider } = createSimple } }, }) + diff --git a/packages/app/src/workspaces/files/types.ts b/packages/app/src/workspaces/files/types.ts index fa09fc04b09..dfbf41471cb 100644 --- a/packages/app/src/workspaces/files/types.ts +++ b/packages/app/src/workspaces/files/types.ts @@ -1,20 +1,19 @@ import type { FileContent } from "@/runtime/server/types" -import { Schema } from "effect" -import { Persistence } from "@/runtime/persistence/schema" +import { Codec } from "@/runtime/persistence/codec" -export const FileSelection = Persistence.struct({ - startLine: Schema.Number, - startChar: Schema.Number, - endLine: Schema.Number, - endChar: Schema.Number, +export const FileSelection = Codec.struct({ + startLine: Codec.number, + startChar: Codec.number, + endLine: Codec.number, + endChar: Codec.number, }) export type FileSelection = typeof FileSelection.Type -export const SelectedLineRange = Persistence.struct({ - start: Schema.Number, - end: Schema.Number, - side: Persistence.optional(Schema.Literals(["additions", "deletions"])), - endSide: Persistence.optional(Schema.Literals(["additions", "deletions"])), +export const SelectedLineRange = Codec.struct({ + start: Codec.number, + end: Codec.number, + side: Codec.lenientOptional(Codec.literals(["additions", "deletions"])), + endSide: Codec.lenientOptional(Codec.literals(["additions", "deletions"])), }) export type SelectedLineRange = typeof SelectedLineRange.Type @@ -44,3 +43,4 @@ export function selectionFromLines(range: SelectedLineRange): FileSelection { endChar: 0, } } + diff --git a/packages/app/src/workspaces/files/view-cache.ts b/packages/app/src/workspaces/files/view-cache.ts index 44cbb7c1304..d67388f2b6f 100644 --- a/packages/app/src/workspaces/files/view-cache.ts +++ b/packages/app/src/workspaces/files/view-cache.ts @@ -1,8 +1,7 @@ import { createEffect, createRoot } from "solid-js" import { produce } from "solid-js/store" -import { Schema } from "effect" +import { Codec } from "@/runtime/persistence/codec" import { Persist, persisted } from "@/runtime/persistence/storage" -import { Persistence } from "@/runtime/persistence/schema" import { createScopedCache } from "@/runtime/server/scoped-cache" import { SelectedLineRange } from "./types" import type { ServerScope } from "@/runtime/server/scope" @@ -11,14 +10,14 @@ const WORKSPACE_KEY = "__workspace__" const MAX_FILE_VIEW_SESSIONS = 20 const MAX_VIEW_FILES = 500 -const FileViewSchema = Persistence.struct({ - scrollTop: Persistence.optional(Schema.Finite), - scrollLeft: Persistence.optional(Schema.Finite), - selectedLines: Persistence.optional(Schema.NullOr(SelectedLineRange)), +const FileViewSchema = Codec.struct({ + scrollTop: Codec.lenientOptional(Codec.number), + scrollLeft: Codec.lenientOptional(Codec.number), + selectedLines: Codec.lenientOptional(Codec.nullOr(SelectedLineRange)), }) -export const FileViewsSchema = Schema.Struct({ - file: Persistence.record(Persistence.fallback(FileViewSchema, () => ({}))), +export const FileViewsSchema = Codec.struct({ + file: Codec.lenientRecord(Codec.fallback(FileViewSchema, () => ({}))), }) function normalizeSelectedLines(range: SelectedLineRange): SelectedLineRange { @@ -150,3 +149,4 @@ export function createFileViewCache(scope: ServerScope) { clear: () => cache.clear(), } } +