fix(app): persist defensiveness (#12973)
This commit is contained in:
@@ -99,4 +99,9 @@ describe("persist localStorage resilience", () => {
|
|||||||
|
|
||||||
expect(storage.getItem("direct-value")).toBe('{"value":5}')
|
expect(storage.getItem("direct-value")).toBe('{"value":5}')
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test("normalizer rejects malformed JSON payloads", () => {
|
||||||
|
const result = persistTesting.normalize({ value: "ok" }, '{"value":"\\x"}')
|
||||||
|
expect(result).toBeUndefined()
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -195,6 +195,14 @@ function parse(value: string) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function normalize(defaults: unknown, raw: string, migrate?: (value: unknown) => unknown) {
|
||||||
|
const parsed = parse(raw)
|
||||||
|
if (parsed === undefined) return
|
||||||
|
const migrated = migrate ? migrate(parsed) : parsed
|
||||||
|
const merged = merge(defaults, migrated)
|
||||||
|
return JSON.stringify(merged)
|
||||||
|
}
|
||||||
|
|
||||||
function workspaceStorage(dir: string) {
|
function workspaceStorage(dir: string) {
|
||||||
const head = dir.slice(0, 12) || "workspace"
|
const head = dir.slice(0, 12) || "workspace"
|
||||||
const sum = checksum(dir) ?? "0"
|
const sum = checksum(dir) ?? "0"
|
||||||
@@ -291,6 +299,7 @@ function localStorageDirect(): SyncStorage {
|
|||||||
export const PersistTesting = {
|
export const PersistTesting = {
|
||||||
localStorageDirect,
|
localStorageDirect,
|
||||||
localStorageWithPrefix,
|
localStorageWithPrefix,
|
||||||
|
normalize,
|
||||||
}
|
}
|
||||||
|
|
||||||
export const Persist = {
|
export const Persist = {
|
||||||
@@ -358,12 +367,11 @@ export function persisted<T>(
|
|||||||
getItem: (key) => {
|
getItem: (key) => {
|
||||||
const raw = current.getItem(key)
|
const raw = current.getItem(key)
|
||||||
if (raw !== null) {
|
if (raw !== null) {
|
||||||
const parsed = parse(raw)
|
const next = normalize(defaults, raw, config.migrate)
|
||||||
if (parsed === undefined) return raw
|
if (next === undefined) {
|
||||||
|
current.removeItem(key)
|
||||||
const migrated = config.migrate ? config.migrate(parsed) : parsed
|
return null
|
||||||
const merged = merge(defaults, migrated)
|
}
|
||||||
const next = JSON.stringify(merged)
|
|
||||||
if (raw !== next) current.setItem(key, next)
|
if (raw !== next) current.setItem(key, next)
|
||||||
return next
|
return next
|
||||||
}
|
}
|
||||||
@@ -372,16 +380,13 @@ export function persisted<T>(
|
|||||||
const legacyRaw = legacyStore.getItem(legacyKey)
|
const legacyRaw = legacyStore.getItem(legacyKey)
|
||||||
if (legacyRaw === null) continue
|
if (legacyRaw === null) continue
|
||||||
|
|
||||||
current.setItem(key, legacyRaw)
|
const next = normalize(defaults, legacyRaw, config.migrate)
|
||||||
|
if (next === undefined) {
|
||||||
|
legacyStore.removeItem(legacyKey)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
current.setItem(key, next)
|
||||||
legacyStore.removeItem(legacyKey)
|
legacyStore.removeItem(legacyKey)
|
||||||
|
|
||||||
const parsed = parse(legacyRaw)
|
|
||||||
if (parsed === undefined) return legacyRaw
|
|
||||||
|
|
||||||
const migrated = config.migrate ? config.migrate(parsed) : parsed
|
|
||||||
const merged = merge(defaults, migrated)
|
|
||||||
const next = JSON.stringify(merged)
|
|
||||||
if (legacyRaw !== next) current.setItem(key, next)
|
|
||||||
return next
|
return next
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -405,12 +410,11 @@ export function persisted<T>(
|
|||||||
getItem: async (key) => {
|
getItem: async (key) => {
|
||||||
const raw = await current.getItem(key)
|
const raw = await current.getItem(key)
|
||||||
if (raw !== null) {
|
if (raw !== null) {
|
||||||
const parsed = parse(raw)
|
const next = normalize(defaults, raw, config.migrate)
|
||||||
if (parsed === undefined) return raw
|
if (next === undefined) {
|
||||||
|
await current.removeItem(key).catch(() => undefined)
|
||||||
const migrated = config.migrate ? config.migrate(parsed) : parsed
|
return null
|
||||||
const merged = merge(defaults, migrated)
|
}
|
||||||
const next = JSON.stringify(merged)
|
|
||||||
if (raw !== next) await current.setItem(key, next)
|
if (raw !== next) await current.setItem(key, next)
|
||||||
return next
|
return next
|
||||||
}
|
}
|
||||||
@@ -421,16 +425,13 @@ export function persisted<T>(
|
|||||||
const legacyRaw = await legacyStore.getItem(legacyKey)
|
const legacyRaw = await legacyStore.getItem(legacyKey)
|
||||||
if (legacyRaw === null) continue
|
if (legacyRaw === null) continue
|
||||||
|
|
||||||
await current.setItem(key, legacyRaw)
|
const next = normalize(defaults, legacyRaw, config.migrate)
|
||||||
|
if (next === undefined) {
|
||||||
|
await legacyStore.removeItem(legacyKey).catch(() => undefined)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
await current.setItem(key, next)
|
||||||
await legacyStore.removeItem(legacyKey)
|
await legacyStore.removeItem(legacyKey)
|
||||||
|
|
||||||
const parsed = parse(legacyRaw)
|
|
||||||
if (parsed === undefined) return legacyRaw
|
|
||||||
|
|
||||||
const migrated = config.migrate ? config.migrate(parsed) : parsed
|
|
||||||
const merged = merge(defaults, migrated)
|
|
||||||
const next = JSON.stringify(merged)
|
|
||||||
if (legacyRaw !== next) await current.setItem(key, next)
|
|
||||||
return next
|
return next
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -116,6 +116,15 @@ function parseRecord(value: unknown) {
|
|||||||
return value as Record<string, unknown>
|
return value as Record<string, unknown>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function parseStored(value: unknown) {
|
||||||
|
if (typeof value !== "string") return value
|
||||||
|
try {
|
||||||
|
return JSON.parse(value) as unknown
|
||||||
|
} catch {
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function pickLocale(value: unknown): Locale | null {
|
function pickLocale(value: unknown): Locale | null {
|
||||||
const direct = parseLocale(value)
|
const direct = parseLocale(value)
|
||||||
if (direct) return direct
|
if (direct) return direct
|
||||||
@@ -169,7 +178,7 @@ export function initI18n(): Promise<Locale> {
|
|||||||
if (!store) return state.locale
|
if (!store) return state.locale
|
||||||
|
|
||||||
const raw = await store.get("language").catch(() => null)
|
const raw = await store.get("language").catch(() => null)
|
||||||
const value = typeof raw === "string" ? JSON.parse(raw) : raw
|
const value = parseStored(raw)
|
||||||
const next = pickLocale(value) ?? state.locale
|
const next = pickLocale(value) ?? state.locale
|
||||||
|
|
||||||
state.locale = next
|
state.locale = next
|
||||||
|
|||||||
Reference in New Issue
Block a user