From 6f324a3dcdf02f9832285979d22dcd7fb427a864 Mon Sep 17 00:00:00 2001
From: Lhcfl <Lhcfl@outlook.com>
Date: Tue, 23 Apr 2024 16:56:39 +0800
Subject: [PATCH] refactor: move ColdDeviceStorage into a module

---
 packages/client/src/cold-store.ts  | 121 +++++++++++++++++++++++++++++
 packages/client/src/store.ts       | 116 ++-------------------------
 packages/client/src/stream.ts      |   1 +
 packages/client/src/theme-store.ts |   3 +-
 packages/firefish-js/src/api.ts    |  20 +++--
 5 files changed, 146 insertions(+), 115 deletions(-)
 create mode 100644 packages/client/src/cold-store.ts

diff --git a/packages/client/src/cold-store.ts b/packages/client/src/cold-store.ts
new file mode 100644
index 0000000000..63e7782264
--- /dev/null
+++ b/packages/client/src/cold-store.ts
@@ -0,0 +1,121 @@
+import { ref as vueRef } from "vue";
+import type { UnwrapRef } from "vue";
+
+// TODO: 他のタブと永続化されたstateを同期
+
+const PREFIX = "miux:";
+
+interface Plugin {
+	id: string;
+	name: string;
+	active: boolean;
+	configData: Record<string, unknown>;
+	token: string;
+	ast: unknown[];
+}
+
+import darkTheme from "@/themes/d-rosepine.json5";
+/**
+ * Storage for configuration information that does not need to be constantly loaded into memory (non-reactive)
+ */
+import lightTheme from "@/themes/l-rosepinedawn.json5";
+
+const ColdStoreDefault = {
+	lightTheme,
+	darkTheme,
+	syncDeviceDarkMode: true,
+	plugins: [] as Plugin[],
+	mediaVolume: 0.5,
+	vibrate: false,
+	sound_masterVolume: 0.3,
+	sound_note: { type: "none", volume: 0 },
+	sound_noteMy: { type: "syuilo/up", volume: 1 },
+	sound_notification: { type: "syuilo/pope2", volume: 1 },
+	sound_chat: { type: "syuilo/pope1", volume: 1 },
+	sound_chatBg: { type: "syuilo/waon", volume: 1 },
+	sound_antenna: { type: "syuilo/triple", volume: 1 },
+	sound_channel: { type: "syuilo/square-pico", volume: 1 },
+};
+
+const watchers: {
+	key: string;
+	callback: (value) => void;
+}[] = [];
+
+function get<T extends keyof typeof ColdStoreDefault>(
+	key: T,
+): (typeof ColdStoreDefault)[T] {
+	// TODO: indexedDBにする
+	//       ただしその際はnullチェックではなくキー存在チェックにしないとダメ
+	//       (indexedDBはnullを保存できるため、ユーザーが意図してnullを格納した可能性がある)
+	const value = localStorage.getItem(PREFIX + key);
+	if (value == null) {
+		return ColdStoreDefault[key];
+	} else {
+		return JSON.parse(value);
+	}
+}
+
+function set<T extends keyof typeof ColdStoreDefault>(
+	key: T,
+	value: (typeof ColdStoreDefault)[T],
+): void {
+	// 呼び出し側のバグ等で undefined が来ることがある
+	// undefined を文字列として localStorage に入れると参照する際の JSON.parse でコケて不具合の元になるため無視
+	if (value === undefined) {
+		console.error(`attempt to store undefined value for key '${key}'`);
+		return;
+	}
+
+	localStorage.setItem(PREFIX + key, JSON.stringify(value));
+
+	for (const watcher of watchers) {
+		if (watcher.key === key) watcher.callback(value);
+	}
+}
+
+function watch<T extends keyof typeof ColdStoreDefault>(
+	key: T,
+	callback: (value: (typeof ColdStoreDefault)[T]) => void,
+) {
+	watchers.push({ key, callback });
+}
+
+// TODO: VueのcustomRef使うと良い感じになるかも
+function ref<T extends keyof typeof ColdStoreDefault>(key: T) {
+	const v = get(key);
+	const r = vueRef(v);
+	// TODO: このままではwatcherがリークするので開放する方法を考える
+	watch(key, (v) => {
+		r.value = v as UnwrapRef<typeof v>;
+	});
+	return r;
+}
+
+/**
+ * 特定のキーの、簡易的なgetter/setterを作ります
+ * 主にvue場で設定コントロールのmodelとして使う用
+ */
+function makeGetterSetter<K extends keyof typeof ColdStoreDefault>(key: K) {
+	// TODO: VueのcustomRef使うと良い感じになるかも
+	const valueRef = ref(key);
+	return {
+		get: () => {
+			return valueRef.value;
+		},
+		set: (value: (typeof ColdStoreDefault)[K]) => {
+			const val = value;
+			set(key, val);
+		},
+	};
+}
+
+export default {
+	default: ColdStoreDefault,
+	watchers,
+	get,
+	set,
+	watch,
+	ref,
+	makeGetterSetter,
+};
diff --git a/packages/client/src/store.ts b/packages/client/src/store.ts
index fed9acc035..8443038435 100644
--- a/packages/client/src/store.ts
+++ b/packages/client/src/store.ts
@@ -1,9 +1,12 @@
-import { markRaw, ref } from "vue";
+import { markRaw } from "vue";
 import type { ApiTypes, entities } from "firefish-js";
 import { isSignedIn, me } from "./me";
 import { Storage } from "./pizzax";
 import type { NoteVisibility } from "@/types/note";
 
+// biome-ignore lint/suspicious/noExplicitAny: <explanation>
+type TODO = any;
+
 export const postFormActions: {
 	title: string;
 	handler: (from, update) => void | Promise<void>;
@@ -152,7 +155,7 @@ export const defaultStore = markRaw(
 				type: string;
 				size: "verySmall" | "small" | "medium" | "large" | "veryLarge";
 				black: boolean;
-				props: Record<string, any>;
+				props: Record<string, TODO>;
 			}[],
 		},
 		widgets: {
@@ -161,7 +164,7 @@ export const defaultStore = markRaw(
 				name: string;
 				id: string;
 				place: string | null;
-				data: Record<string, any>;
+				data: Record<string, TODO>;
 			}[],
 		},
 		tl: {
@@ -453,109 +456,6 @@ export const defaultStore = markRaw(
 	}),
 );
 
-// TODO: 他のタブと永続化されたstateを同期
+import ColdStore from "./cold-store";
 
-const PREFIX = "miux:";
-
-interface Plugin {
-	id: string;
-	name: string;
-	active: boolean;
-	configData: Record<string, any>;
-	token: string;
-	ast: any[];
-}
-
-import darkTheme from "@/themes/d-rosepine.json5";
-/**
- * Storage for configuration information that does not need to be constantly loaded into memory (non-reactive)
- */
-import lightTheme from "@/themes/l-rosepinedawn.json5";
-
-export class ColdDeviceStorage {
-	public static default = {
-		lightTheme,
-		darkTheme,
-		syncDeviceDarkMode: true,
-		plugins: [] as Plugin[],
-		mediaVolume: 0.5,
-		vibrate: false,
-		sound_masterVolume: 0.3,
-		sound_note: { type: "none", volume: 0 },
-		sound_noteMy: { type: "syuilo/up", volume: 1 },
-		sound_notification: { type: "syuilo/pope2", volume: 1 },
-		sound_chat: { type: "syuilo/pope1", volume: 1 },
-		sound_chatBg: { type: "syuilo/waon", volume: 1 },
-		sound_antenna: { type: "syuilo/triple", volume: 1 },
-		sound_channel: { type: "syuilo/square-pico", volume: 1 },
-	};
-
-	public static watchers = [];
-
-	public static get<T extends keyof typeof ColdDeviceStorage.default>(
-		key: T,
-	): (typeof ColdDeviceStorage.default)[T] {
-		// TODO: indexedDBにする
-		//       ただしその際はnullチェックではなくキー存在チェックにしないとダメ
-		//       (indexedDBはnullを保存できるため、ユーザーが意図してnullを格納した可能性がある)
-		const value = localStorage.getItem(PREFIX + key);
-		if (value == null) {
-			return ColdDeviceStorage.default[key];
-		} else {
-			return JSON.parse(value);
-		}
-	}
-
-	public static set<T extends keyof typeof ColdDeviceStorage.default>(
-		key: T,
-		value: (typeof ColdDeviceStorage.default)[T],
-	): void {
-		// 呼び出し側のバグ等で undefined が来ることがある
-		// undefined を文字列として localStorage に入れると参照する際の JSON.parse でコケて不具合の元になるため無視
-		if (value === undefined) {
-			console.error(`attempt to store undefined value for key '${key}'`);
-			return;
-		}
-
-		localStorage.setItem(PREFIX + key, JSON.stringify(value));
-
-		for (const watcher of this.watchers) {
-			if (watcher.key === key) watcher.callback(value);
-		}
-	}
-
-	public static watch(key, callback) {
-		this.watchers.push({ key, callback });
-	}
-
-	// TODO: VueのcustomRef使うと良い感じになるかも
-	public static ref<T extends keyof typeof ColdDeviceStorage.default>(key: T) {
-		const v = ColdDeviceStorage.get(key);
-		const r = ref(v);
-		// TODO: このままではwatcherがリークするので開放する方法を考える
-		this.watch(key, (v) => {
-			r.value = v;
-		});
-		return r;
-	}
-
-	/**
-	 * 特定のキーの、簡易的なgetter/setterを作ります
-	 * 主にvue場で設定コントロールのmodelとして使う用
-	 */
-	public static makeGetterSetter<
-		K extends keyof typeof ColdDeviceStorage.default,
-	>(key: K) {
-		// TODO: VueのcustomRef使うと良い感じになるかも
-		const valueRef = ColdDeviceStorage.ref(key);
-		return {
-			get: () => {
-				return valueRef.value;
-			},
-			set: (value: unknown) => {
-				const val = value;
-				ColdDeviceStorage.set(key, val);
-			},
-		};
-	}
-}
+export const ColdDeviceStorage = ColdStore;
diff --git a/packages/client/src/stream.ts b/packages/client/src/stream.ts
index 89dda63f08..b620c60c9d 100644
--- a/packages/client/src/stream.ts
+++ b/packages/client/src/stream.ts
@@ -31,6 +31,7 @@ export function reloadStream() {
 
 	isReloading = true;
 	stream.close();
+	// biome-ignore lint/suspicious/noAssignInExpressions: assign intentionally
 	stream.once("_connected_", () => (isReloading = false));
 	stream.stream.reconnect();
 	isReloading = false;
diff --git a/packages/client/src/theme-store.ts b/packages/client/src/theme-store.ts
index bc344b339d..6f4616878f 100644
--- a/packages/client/src/theme-store.ts
+++ b/packages/client/src/theme-store.ts
@@ -17,7 +17,8 @@ export async function fetchThemes(): Promise<void> {
 			key: "themes",
 		});
 		localStorage.setItem(lsCacheKey, JSON.stringify(themes));
-	} catch (err) {
+		// biome-ignore lint/suspicious/noExplicitAny: Safely any
+	} catch (err: any) {
 		if (err.code === "NO_SUCH_KEY") return;
 		throw err;
 	}
diff --git a/packages/firefish-js/src/api.ts b/packages/firefish-js/src/api.ts
index 4a7e1f0b64..3cfdfedbfe 100644
--- a/packages/firefish-js/src/api.ts
+++ b/packages/firefish-js/src/api.ts
@@ -7,10 +7,12 @@ export type APIError = {
 	code: string;
 	message: string;
 	kind: "client" | "server";
-	info: Record<string, any>;
+	info: Record<string, unknown>;
 };
 
-export function isAPIError(reason: any): reason is APIError {
+// biome-ignore lint/suspicious/noExplicitAny: used it intentially
+type ExplicitlyUsedAny = any;
+export function isAPIError(reason: ExplicitlyUsedAny): reason is APIError {
 	return reason[MK_API_ERROR] === true;
 }
 
@@ -24,7 +26,7 @@ export type FetchLike = (
 	},
 ) => Promise<{
 	status: number;
-	json(): Promise<any>;
+	json(): Promise<ExplicitlyUsedAny>;
 }>;
 
 type IsNeverType<T> = [T] extends [never] ? true : false;
@@ -36,7 +38,10 @@ type IsCaseMatched<
 	P extends Endpoints[E]["req"],
 	C extends number,
 > = IsNeverType<
-	StrictExtract<Endpoints[E]["res"]["$switch"]["$cases"][C], [P, any]>
+	StrictExtract<
+		Endpoints[E]["res"]["$switch"]["$cases"][C],
+		[P, ExplicitlyUsedAny]
+	>
 > extends false
 	? true
 	: false;
@@ -45,7 +50,10 @@ type GetCaseResult<
 	E extends keyof Endpoints,
 	P extends Endpoints[E]["req"],
 	C extends number,
-> = StrictExtract<Endpoints[E]["res"]["$switch"]["$cases"][C], [P, any]>[1];
+> = StrictExtract<
+	Endpoints[E]["res"]["$switch"]["$cases"][C],
+	[P, ExplicitlyUsedAny]
+>[1];
 
 export class APIClient {
 	public origin: string;
@@ -70,7 +78,7 @@ export class APIClient {
 		credential?: string | null | undefined,
 	): Promise<
 		Endpoints[E]["res"] extends {
-			$switch: { $cases: [any, any][]; $default: any };
+			$switch: { $cases: [unknown, unknown][]; $default: unknown };
 		}
 			? IsCaseMatched<E, P, 0> extends true
 				? GetCaseResult<E, P, 0>