aboutsummaryrefslogtreecommitdiff
path: root/src/lib
diff options
context:
space:
mode:
authorFuwn <[email protected]>2026-03-03 09:11:38 -0800
committerFuwn <[email protected]>2026-03-03 09:12:31 -0800
commit0b3f4af0d8061fefcdeaba1352ea8176f34b1cbd (patch)
tree5305af0876b70d0207df44febab27fb65236622d /src/lib
parentrefactor(effect): harden settings and media cache json parsing (diff)
downloaddue.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.ts19
-rw-r--r--src/lib/Effect/json.ts31
-rw-r--r--src/lib/Events/AniListBadges/EasterEvent2025/EasterEgg.svelte23
-rw-r--r--src/lib/Tools/DumpProfile.svelte27
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;
};