From 93ea9c20337a652defbd90fe41932a43d252baa3 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Acid=20Chicken=20=28=E7=A1=AB=E9=85=B8=E9=B6=8F=29?=
 <root@acid-chicken.com>
Date: Thu, 9 Mar 2023 14:35:38 +0900
Subject: [PATCH] chore(frontend): add debugger for #6864 (#10270)

---
 .../src/components/MkDateSeparatedList.vue    | 19 ++++++++++++-
 packages/frontend/src/debug.ts                | 27 +++++++++++++++++++
 2 files changed, 45 insertions(+), 1 deletion(-)
 create mode 100644 packages/frontend/src/debug.ts

diff --git a/packages/frontend/src/components/MkDateSeparatedList.vue b/packages/frontend/src/components/MkDateSeparatedList.vue
index 4525d3a009..d6303f9675 100644
--- a/packages/frontend/src/components/MkDateSeparatedList.vue
+++ b/packages/frontend/src/components/MkDateSeparatedList.vue
@@ -1,7 +1,9 @@
 <script lang="ts">
 import { defineComponent, h, PropType, TransitionGroup, useCssModule } from 'vue';
 import MkAd from '@/components/global/MkAd.vue';
+import { isDebuggerEnabled, stackTraceInstances } from '@/debug';
 import { i18n } from '@/i18n';
+import * as os from '@/os';
 import { defaultStore } from '@/store';
 import { MisskeyEntity } from '@/types/date-separated-list';
 
@@ -46,7 +48,7 @@ export default defineComponent({
 
 		if (props.items.length === 0) return;
 
-		const renderChildren = () => props.items.map((item, i) => {
+		const renderChildrenImpl = () => props.items.map((item, i) => {
 			if (!slots || !slots.default) return;
 
 			const el = slots.default({
@@ -95,6 +97,21 @@ export default defineComponent({
 			}
 		});
 
+		const renderChildren = () => {
+			const children = renderChildrenImpl();
+			if (isDebuggerEnabled(6864)) {
+				const nodes = children.flatMap((node) => node ?? []);
+				const keys = new Set(nodes.map((node) => node.key));
+				if (keys.size !== nodes.length) {
+					const id = crypto.randomUUID();
+					const instances = stackTraceInstances();
+					os.toast(instances.reduce((a, c) => `${a} at ${c.type.name}`, `[DEBUG_6864 (${id})]: ${nodes.length - keys.size} duplicated keys found`));
+					console.warn({ id, debugId: 6864, stack: instances });
+				}
+			}
+			return children;
+		};
+
 		function onBeforeLeave(el: HTMLElement) {
 			el.style.top = `${el.offsetTop}px`;
 			el.style.left = `${el.offsetLeft}px`;
diff --git a/packages/frontend/src/debug.ts b/packages/frontend/src/debug.ts
new file mode 100644
index 0000000000..5715acf674
--- /dev/null
+++ b/packages/frontend/src/debug.ts
@@ -0,0 +1,27 @@
+import { type ComponentInternalInstance, getCurrentInstance } from 'vue';
+
+export function isDebuggerEnabled(id: number): boolean {
+	try {
+		return localStorage.getItem(`DEBUG_${id}`) !== null;
+	} catch {
+		return false;
+	}
+}
+
+export function switchDebuggerEnabled(id: number, enabled: boolean): void {
+	if (enabled) {
+		localStorage.setItem(`DEBUG_${id}`, '');
+	} else {
+		localStorage.removeItem(`DEBUG_${id}`);
+	}
+}
+
+export function stackTraceInstances(): ComponentInternalInstance[] {
+	let instance = getCurrentInstance();
+	const stack: ComponentInternalInstance[] = [];
+	while (instance) {
+		stack.push(instance);
+		instance = instance.parent;
+	}
+	return stack;
+}