mirror of
https://gitee.com/zhangsken/opencode.git
synced 2026-10-16 10:30:01 +08:00
refactor(app): declare open legacy persistence shapes
This commit is contained in:
@@ -45,7 +45,7 @@ const Current = Persistence.struct({ session: SessionsSchema })
|
||||
|
||||
export const ModelSelectionSchema = Persistence.migrate(
|
||||
Current,
|
||||
Schema.Struct({
|
||||
Persistence.legacy({
|
||||
session: Persistence.optional(Schema.Record(Schema.String, Schema.Unknown)),
|
||||
pick: Persistence.optional(Schema.Record(Schema.String, Schema.Unknown)),
|
||||
}).pipe(
|
||||
|
||||
@@ -32,7 +32,7 @@ describe("persistence schemas", () => {
|
||||
|
||||
test("legacy migration observes missing fields before initial defaults are applied", () => {
|
||||
const current = Persistence.struct({ mode: Schema.Literals(["compact", "full"]), enabled: Schema.Boolean })
|
||||
const stored = Schema.Struct({
|
||||
const stored = Persistence.legacy({
|
||||
mode: Schema.optional(Schema.Unknown),
|
||||
expanded: Schema.optional(Schema.Boolean),
|
||||
}).pipe(
|
||||
@@ -53,6 +53,38 @@ describe("persistence schemas", () => {
|
||||
expect(Schema.encodeSync(schema)(decode({ expanded: true }))).toEqual({ mode: "full", enabled: true })
|
||||
})
|
||||
|
||||
test("legacy read shapes carry unknown nested fields through to the current schema", () => {
|
||||
const current = Persistence.struct({
|
||||
general: Persistence.struct({
|
||||
mode: Schema.Literals(["compact", "full"]),
|
||||
theme: Schema.String,
|
||||
nested: Persistence.struct({ keep: Schema.Boolean }),
|
||||
}),
|
||||
other: Persistence.struct({ flag: Schema.Boolean }),
|
||||
})
|
||||
const stored = Persistence.legacy({
|
||||
general: Persistence.optional(Persistence.legacy({ expanded: Persistence.optional(Schema.Boolean) })),
|
||||
}).pipe(
|
||||
Schema.decode({
|
||||
decode: SchemaGetter.transform((value) => {
|
||||
const general = value.general
|
||||
if (!general || general.expanded === undefined) return value
|
||||
return { ...value, general: { ...general, mode: general.expanded ? "full" : "compact" } }
|
||||
}),
|
||||
encode: SchemaGetter.passthrough(),
|
||||
}),
|
||||
)
|
||||
const schema = Persistence.withInitial(Persistence.migrate(current, stored), {
|
||||
general: { mode: "compact", theme: "light", nested: { keep: false } },
|
||||
other: { flag: false },
|
||||
})
|
||||
const decode = Schema.decodeUnknownSync(schema)
|
||||
expect(decode({ general: { expanded: true, theme: "dark", nested: { keep: true } }, other: { flag: true } })).toEqual({
|
||||
general: { mode: "full", theme: "dark", nested: { keep: true } },
|
||||
other: { flag: true },
|
||||
})
|
||||
})
|
||||
|
||||
test("initial merging preserves field codecs and replaces arrays rather than merging indexes", () => {
|
||||
const current = Persistence.struct({
|
||||
amount: Schema.NumberFromString.check(Schema.isFinite()),
|
||||
|
||||
@@ -23,11 +23,7 @@ export function withInitial<S extends Schema.ConstraintCodec<object, unknown>>(
|
||||
initial: NoInfer<S["Type"]>,
|
||||
) {
|
||||
const schema = isMigrated(definition) ? definition.current : definition
|
||||
const read = isMigrated(definition)
|
||||
? SchemaParser.decodeUnknownResult(
|
||||
Schema.make<Schema.ConstraintDecoder<unknown>>(preserveExcess(definition.read.ast)),
|
||||
)
|
||||
: Result.succeed<unknown>
|
||||
const read = isMigrated(definition) ? SchemaParser.decodeUnknownResult(definition.read) : Result.succeed<unknown>
|
||||
const encode = Schema.encodeUnknownSync(schema)
|
||||
return Schema.Unknown.pipe(
|
||||
Schema.decode<Schema.Unknown>({
|
||||
@@ -40,43 +36,10 @@ export function withInitial<S extends Schema.ConstraintCodec<object, unknown>>(
|
||||
)
|
||||
}
|
||||
|
||||
// Migration decoders must carry fields they do not know yet into the current schema. Effect 4.0
|
||||
// no longer exposes the parser-level `preserve` option, so make closed structs open recursively.
|
||||
function preserveExcess(ast: SchemaAST.AST): SchemaAST.AST {
|
||||
const encoding = ast.encoding && preserveEncoding(ast.encoding)
|
||||
const recurred = "recur" in ast && typeof ast.recur === "function" ? ast.recur(preserveExcess) : ast
|
||||
const next = withEncoding(recurred, encoding)
|
||||
if (next._tag !== "Objects") return next
|
||||
const indexSignatures =
|
||||
next.propertySignatures.length === 0 || next.indexSignatures.length > 0
|
||||
? next.indexSignatures
|
||||
: [
|
||||
new SchemaAST.IndexSignature(Schema.String.ast, Schema.Unknown.ast),
|
||||
new SchemaAST.IndexSignature(Schema.Symbol.ast, Schema.Unknown.ast),
|
||||
]
|
||||
return new SchemaAST.Objects(
|
||||
next.propertySignatures,
|
||||
indexSignatures,
|
||||
next.annotations,
|
||||
next.checks,
|
||||
encoding,
|
||||
next.context,
|
||||
next.encodingChecks,
|
||||
)
|
||||
}
|
||||
|
||||
function preserveEncoding(encoding: SchemaAST.Encoding): SchemaAST.Encoding {
|
||||
return [
|
||||
new SchemaAST.Link(preserveExcess(encoding[0].to), encoding[0].transformation),
|
||||
...encoding.slice(1).map((link) => new SchemaAST.Link(preserveExcess(link.to), link.transformation)),
|
||||
]
|
||||
}
|
||||
|
||||
function withEncoding(ast: SchemaAST.AST, encoding: SchemaAST.Encoding | undefined): SchemaAST.AST {
|
||||
if (ast.encoding === encoding) return ast
|
||||
const descriptors = Object.getOwnPropertyDescriptors(ast)
|
||||
descriptors.encoding.value = encoding
|
||||
return Object.create(Object.getPrototypeOf(ast), descriptors)
|
||||
// A legacy read shape declares only the fields it migrates. Decoding strips unknown keys, so every
|
||||
// struct level in a `migrate` read schema must stay open for the current fields it does not name.
|
||||
export function legacy<const Fields extends Schema.Struct.Fields>(fields: Fields) {
|
||||
return Schema.StructWithRest(Schema.Struct(fields), [Schema.Record(Schema.String, Schema.Unknown)])
|
||||
}
|
||||
|
||||
// Object-level codecs own their recovery. Plain structs can recover fields independently.
|
||||
|
||||
@@ -53,7 +53,7 @@ const State = Persistence.struct({
|
||||
export function serverState(canonicalLocalServer: () => string | undefined = () => undefined) {
|
||||
return Persistence.migrate(
|
||||
State,
|
||||
Schema.Struct({ projects: Projects, lastProject: LastProject }).pipe(
|
||||
Persistence.legacy({ projects: Projects, lastProject: LastProject }).pipe(
|
||||
Schema.decode({
|
||||
decode: SchemaGetter.transform((value) => {
|
||||
const canonical = canonicalLocalServer()
|
||||
|
||||
@@ -185,9 +185,9 @@ function legacyTimelineActivity(value: boolean | "hidden" | "compact" | "full" |
|
||||
|
||||
export const settingsPersistence = Persistence.migrate(
|
||||
settingsSchema,
|
||||
Schema.Struct({
|
||||
Persistence.legacy({
|
||||
general: Persistence.optional(
|
||||
Schema.Struct({
|
||||
Persistence.legacy({
|
||||
// Keep invalid explicit values distinct from absent values so legacy preferences cannot replace them.
|
||||
timelineDetail: Schema.optional(
|
||||
Schema.NullOr(
|
||||
|
||||
@@ -176,16 +176,16 @@ export const layoutSchema = Persistence.struct({
|
||||
|
||||
export const layoutPersistence = Persistence.migrate(
|
||||
layoutSchema,
|
||||
Schema.Struct({
|
||||
Persistence.legacy({
|
||||
sidebar: Persistence.optional(
|
||||
Schema.Struct({
|
||||
Persistence.legacy({
|
||||
workspaces: Persistence.optional(Schema.Union([Schema.Boolean, Schema.Record(Schema.String, Schema.Boolean)])),
|
||||
workspacesDefault: Persistence.optional(Schema.Boolean),
|
||||
}),
|
||||
),
|
||||
review: Persistence.optional(Schema.Struct({ panelOpened: Persistence.optional(Schema.Boolean) })),
|
||||
review: Persistence.optional(Persistence.legacy({ panelOpened: Persistence.optional(Schema.Boolean) })),
|
||||
fileTree: Persistence.optional(
|
||||
Schema.Struct({
|
||||
Persistence.legacy({
|
||||
opened: Persistence.optional(Schema.Boolean),
|
||||
width: Persistence.optional(Schema.Finite),
|
||||
tab: Persistence.optional(Schema.Literals(["changes", "all"])),
|
||||
|
||||
Reference in New Issue
Block a user