diff options
| author | Fuwn <[email protected]> | 2026-03-03 09:11:38 -0800 |
|---|---|---|
| committer | Fuwn <[email protected]> | 2026-03-03 09:12:31 -0800 |
| commit | 0b3f4af0d8061fefcdeaba1352ea8176f34b1cbd (patch) | |
| tree | 5305af0876b70d0207df44febab27fb65236622d /src/lib | |
| parent | refactor(effect): harden settings and media cache json parsing (diff) | |
| download | due.moe-0b3f4af0d8061fefcdeaba1352ea8176f34b1cbd.tar.xz due.moe-0b3f4af0d8061fefcdeaba1352ea8176f34b1cbd.zip | |
refactor(effect): migrate svelte json hotspots to typed decoders
Diffstat (limited to 'src/lib')
| -rw-r--r-- | src/lib/Effect/json.test.ts | 19 | ||||
| -rw-r--r-- | src/lib/Effect/json.ts | 31 | ||||
| -rw-r--r-- | src/lib/Events/AniListBadges/EasterEvent2025/EasterEgg.svelte | 23 | ||||
| -rw-r--r-- | src/lib/Tools/DumpProfile.svelte | 27 |
4 files changed, 79 insertions, 21 deletions
diff --git a/src/lib/Effect/json.test.ts b/src/lib/Effect/json.test.ts index b315fe61..c13824fe 100644 --- a/src/lib/Effect/json.test.ts +++ b/src/lib/Effect/json.test.ts @@ -2,7 +2,9 @@ import { describe, expect, it } from "vitest"; import { parseJsonStringOrDefault, parseJsonStringOrThrow, + parseJsonStringWithSchemaOrDefault, } from "$lib/Effect/json"; +import { Schema } from "effect"; describe("effect json parsing", () => { it("parses valid json strings", () => { @@ -21,4 +23,21 @@ describe("effect json parsing", () => { ok: false, }); }); + + it("decodes json with a schema and returns fallback on schema mismatch", () => { + expect( + parseJsonStringWithSchemaOrDefault( + "[1,2,3]", + Schema.Array(Schema.Number), + [], + ), + ).toEqual([1, 2, 3]); + expect( + parseJsonStringWithSchemaOrDefault( + `["a",2]`, + Schema.Array(Schema.Number), + [], + ), + ).toEqual([]); + }); }); diff --git a/src/lib/Effect/json.ts b/src/lib/Effect/json.ts index b905cc41..24414fb0 100644 --- a/src/lib/Effect/json.ts +++ b/src/lib/Effect/json.ts @@ -1,4 +1,8 @@ -import { Effect, Result } from "effect"; +import { Effect, Result, Schema } from "effect"; + +type SyncDecodingSchema = Schema.Top & { + readonly DecodingServices: never; +}; export const parseJsonStringEffect = (value: string) => Effect.try({ @@ -26,3 +30,28 @@ export const parseJsonStringOrDefault = <T>(value: string, fallback: T): T => { onFailure: () => fallback, }); }; + +export const parseJsonStringWithSchemaOrDefault = < + S extends SyncDecodingSchema, +>( + value: string, + schema: S, + fallback: S["Type"], +): S["Type"] => { + const parsed = parseJsonStringEither(value); + + return Result.match(parsed, { + onSuccess: (decoded) => { + const decodedWithSchema = Result.try({ + try: () => Schema.decodeUnknownSync(schema)(decoded), + catch: () => fallback, + }); + + return Result.match(decodedWithSchema, { + onSuccess: (value) => value, + onFailure: () => fallback, + }); + }, + onFailure: () => fallback, + }); +}; diff --git a/src/lib/Events/AniListBadges/EasterEvent2025/EasterEgg.svelte b/src/lib/Events/AniListBadges/EasterEvent2025/EasterEgg.svelte index ab9698d4..c48003d7 100644 --- a/src/lib/Events/AniListBadges/EasterEvent2025/EasterEgg.svelte +++ b/src/lib/Events/AniListBadges/EasterEvent2025/EasterEgg.svelte @@ -2,16 +2,24 @@ import { onMount, tick } from "svelte"; import { browser } from "$app/environment"; import Popup from "$lib/Layout/Popup.svelte"; +import { parseJsonStringWithSchemaOrDefault } from "$lib/Effect/json"; +import { Schema } from "effect"; export let targetID = "easter-target"; export let id: number; let visible = false; let showPopup = false; +const clickedEggsSchema = Schema.Array(Schema.Number); +const readClickedEggs = () => [ + ...parseJsonStringWithSchemaOrDefault( + localStorage.getItem("easter2025ClickedEggs") || "[]", + clickedEggsSchema, + [], + ), +]; -$: eggCount = browser - ? JSON.parse(localStorage.getItem("easter2025ClickedEggs") || "[]").length - : 0; +$: eggCount = browser ? readClickedEggs().length : 0; onMount(() => { let intervalId: number | undefined; @@ -23,8 +31,7 @@ onMount(() => { if (!targetElement) return; - const storedClickedEggs = localStorage.getItem("easter2025ClickedEggs"); - const clickedEggs = storedClickedEggs ? JSON.parse(storedClickedEggs) : []; + const clickedEggs = readClickedEggs(); const eggVisual = document.getElementById(`egg-visual-${targetID}-${id}`); const eggClick = document.getElementById(`egg-click-${targetID}-${id}`); const pageWidth = document.documentElement.clientWidth; @@ -60,8 +67,7 @@ onMount(() => { const handleClick = (event: MouseEvent) => { if (event.button === 0) { - const storedClickedEggs = localStorage.getItem("easter2025ClickedEggs"); - const clickedEggs = storedClickedEggs ? JSON.parse(storedClickedEggs) : []; + const clickedEggs = readClickedEggs(); if (!clickedEggs.includes(id)) { clickedEggs.push(id); @@ -92,8 +98,7 @@ const copyCode = (source: string) => { const onLeavePopup = () => { showPopup = false; - const storedClickedEggs = localStorage.getItem("easter2025ClickedEggs"); - const clickedEggs = storedClickedEggs ? JSON.parse(storedClickedEggs) : []; + const clickedEggs = readClickedEggs(); clickedEggs.push(-1); localStorage.setItem("easter2025ClickedEggs", JSON.stringify(clickedEggs)); diff --git a/src/lib/Tools/DumpProfile.svelte b/src/lib/Tools/DumpProfile.svelte index a876f587..88605ffc 100644 --- a/src/lib/Tools/DumpProfile.svelte +++ b/src/lib/Tools/DumpProfile.svelte @@ -2,6 +2,7 @@ import Spacer from "$lib/Layout/Spacer.svelte"; import { dumpUser } from "$lib/Data/AniList/user"; import RateLimited from "$lib/Error/RateLimited.svelte"; +import { parseJsonStringOrDefault } from "$lib/Effect/json"; import Skeleton from "$lib/Loading/Skeleton.svelte"; import InputTemplate from "./InputTemplate.svelte"; import LZString from "lz-string"; @@ -9,19 +10,23 @@ import LZString from "lz-string"; let submission = ""; // Credit: @hoh -const decodeJSON = (about: string): JSON | null => { +const decodeJSON = (about: string): unknown | null => { const match = (about || "").match(/^\[\]\(json([A-Za-z0-9+/=]+)\)/); - if (match) - try { - return JSON.parse(atob(match[1])); - } catch { - try { - return JSON.parse(LZString.decompressFromBase64(match[1])); - } catch { - return null; - } - } + if (match) { + const directDecoded = parseJsonStringOrDefault<unknown | null>( + atob(match[1]), + null, + ); + + if (directDecoded !== null) return directDecoded; + + const decompressed = LZString.decompressFromBase64(match[1]); + + if (!decompressed) return null; + + return parseJsonStringOrDefault<unknown | null>(decompressed, null); + } return null; }; |