From 7e4b6a20fa276f80fbe0a96becf707acdeb68078 Mon Sep 17 00:00:00 2001 From: Madhura68 Date: Mon, 27 Apr 2026 12:27:53 +0200 Subject: [PATCH] =?UTF-8?q?chore(debug):=20add=20Layer=202=20=E2=80=94=20m?= =?UTF-8?q?ini=20Zustand-store=20+=20dispatch=20toggles?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Test of SSE-event → store → render-pipeline werkt buiten de Solo Paneel context. Mirrort het patroon van solo-store maar minimaal. - debug-store.ts: kleine Zustand-store met tasks + applyEvent + applyCount/skipCount-tellers - store-panel.tsx: rendert store-state in een tabel met statuskleuring - client.tsx: drie layer-toggles (dispatch / flushSync / startView- Transition) + lift dispatch in onmessage. Zo kunnen we elke combinatie isoleren Bevestigd: alle drie de toggles werken op het bare /debug-realtime endpoint. Volgende laag is Server Action revalidation. Co-Authored-By: Claude Opus 4.7 (1M context) --- app/debug-realtime/client.tsx | 107 +++++++++++++++++++++++++++ app/debug-realtime/debug-store.ts | 71 ++++++++++++++++++ app/debug-realtime/store-panel.tsx | 111 +++++++++++++++++++++++++++++ 3 files changed, 289 insertions(+) create mode 100644 app/debug-realtime/debug-store.ts create mode 100644 app/debug-realtime/store-panel.tsx diff --git a/app/debug-realtime/client.tsx b/app/debug-realtime/client.tsx index 0d8537a..67ce1ef 100644 --- a/app/debug-realtime/client.tsx +++ b/app/debug-realtime/client.tsx @@ -1,6 +1,9 @@ 'use client' import { useEffect, useMemo, useRef, useState } from 'react' +import { flushSync } from 'react-dom' +import { useDebugStore, type DebugRealtimeEvent } from './debug-store' +import { StorePanel } from './store-panel' type RowType = 'ready' | 'message' | 'error' | 'open' | 'close' @@ -40,6 +43,20 @@ export function DebugRealtimeClient() { const [filterEntity, setFilterEntity] = useState<'all' | 'task' | 'story' | 'debug'>('all') const [tickNow, setTickNow] = useState(() => Date.now()) + // Layer-toggles — elke combinatie isoleert een potentiële bron van bugs. + // - dispatchToStore: voert applyEvent uit op een mini Zustand-store + // - useFlushSync: forceert React om synchroon te renderen tijdens dispatch + // - useViewTransition: wrap dispatch in document.startViewTransition + const [dispatchToStore, setDispatchToStore] = useState(true) + const [useFlushSync, setUseFlushSync] = useState(false) + const [useViewTransition, setUseViewTransition] = useState(false) + const dispatchToStoreRef = useRef(dispatchToStore) + const useFlushSyncRef = useRef(useFlushSync) + const useViewTransitionRef = useRef(useViewTransition) + useEffect(() => { dispatchToStoreRef.current = dispatchToStore }, [dispatchToStore]) + useEffect(() => { useFlushSyncRef.current = useFlushSync }, [useFlushSync]) + useEffect(() => { useViewTransitionRef.current = useViewTransition }, [useViewTransition]) + const sourceRef = useRef(null) const lastEventTimeRef = useRef(null) @@ -90,6 +107,30 @@ export function DebugRealtimeClient() { source.onmessage = (e) => { const parsed = safeParse(e.data ?? '') append({ receivedAt: Date.now(), type: 'message', raw: e.data ?? '', parsed }) + + // Dispatch naar de mini Zustand-store, optioneel met flushSync + // en/of startViewTransition om de echte solo-flow te mimeken. + if (dispatchToStoreRef.current && parsed) { + const dispatch = () => + useDebugStore.getState().applyEvent(parsed as DebugRealtimeEvent) + const wrapWithFlush = useFlushSyncRef.current + ? () => flushSync(dispatch) + : dispatch + if ( + useViewTransitionRef.current && + typeof document !== 'undefined' && + typeof (document as Document & { startViewTransition?: unknown }) + .startViewTransition === 'function' + ) { + ;( + document as Document & { + startViewTransition: (cb: () => void) => unknown + } + ).startViewTransition(wrapWithFlush) + } else { + wrapWithFlush() + } + } } source.onerror = () => { @@ -171,6 +212,15 @@ export function DebugRealtimeClient() { onEmit={emitTestEvent} onReset={reset} /> + + {dispatchToStore && } @@ -337,6 +387,63 @@ function ControlBar({ ) } +function LayerToggles({ + dispatchToStore, + setDispatchToStore, + useFlushSync, + setUseFlushSync, + useViewTransition, + setUseViewTransition, +}: { + dispatchToStore: boolean + setDispatchToStore: (v: boolean) => void + useFlushSync: boolean + setUseFlushSync: (v: boolean) => void + useViewTransition: boolean + setUseViewTransition: (v: boolean) => void +}) { + return ( +
+ Layers: + + + +
+ ) +} + function Stat({ label, value, color }: { label: string; value: string; color?: string }) { return (
diff --git a/app/debug-realtime/debug-store.ts b/app/debug-realtime/debug-store.ts new file mode 100644 index 0000000..32e3f65 --- /dev/null +++ b/app/debug-realtime/debug-store.ts @@ -0,0 +1,71 @@ +// Mini Zustand-store voor de debug-pagina. Mirrort het patroon van +// stores/solo-store.ts maar dan minimaal: alleen tasks-record + applyEvent. +// Doel: testen of de SSE-event → store → component-render keten werkt +// zonder de complexiteit van de echte Solo Paneel. + +import { create } from 'zustand' + +export interface DebugTask { + id: string + status: string + title: string + story_id: string + updated_at: string +} + +export interface DebugRealtimeEvent { + op: 'I' | 'U' | 'D' + entity: 'task' | 'story' + id: string + story_id?: string + task_status?: string + task_title?: string + debug?: boolean + emitted_at?: string + [key: string]: unknown +} + +interface DebugStore { + tasks: Record + applyCount: number + skipCount: number + applyEvent: (event: DebugRealtimeEvent) => void + reset: () => void +} + +export const useDebugStore = create((set, get) => ({ + tasks: {}, + applyCount: 0, + skipCount: 0, + + applyEvent: (event) => { + if (event.entity !== 'task') { + set((s) => ({ skipCount: s.skipCount + 1 })) + return + } + if (event.op === 'D') { + set((s) => { + const next = { ...s.tasks } + delete next[event.id] + return { tasks: next, applyCount: s.applyCount + 1 } + }) + return + } + // INSERT/UPDATE — schrijf altijd, ongeacht of de task al bestond + set((s) => ({ + tasks: { + ...s.tasks, + [event.id]: { + id: event.id, + status: event.task_status ?? get().tasks[event.id]?.status ?? '?', + title: event.task_title ?? get().tasks[event.id]?.title ?? '(no title)', + story_id: event.story_id ?? get().tasks[event.id]?.story_id ?? '?', + updated_at: new Date().toISOString(), + }, + }, + applyCount: s.applyCount + 1, + })) + }, + + reset: () => set({ tasks: {}, applyCount: 0, skipCount: 0 }), +})) diff --git a/app/debug-realtime/store-panel.tsx b/app/debug-realtime/store-panel.tsx new file mode 100644 index 0000000..35495ed --- /dev/null +++ b/app/debug-realtime/store-panel.tsx @@ -0,0 +1,111 @@ +'use client' + +import { useDebugStore } from './debug-store' + +export function StorePanel() { + const tasks = useDebugStore((s) => s.tasks) + const applyCount = useDebugStore((s) => s.applyCount) + const skipCount = useDebugStore((s) => s.skipCount) + const reset = useDebugStore((s) => s.reset) + + const taskList = Object.values(tasks) + + return ( +
+
+ Store layer: + + applyCount: {applyCount} + + + skipCount: {skipCount} + + + taskCount: {taskList.length} + + +
+ +
+ + + + + + + + + + {taskList.length === 0 ? ( + + + + ) : ( + taskList.map((t) => ( + + + + + + + )) + )} + +
idstatustitleupdated_at (lokaal)
+ Store is leeg. Trigger een event en kijk of de tabel hier vult. +
+ {t.id} + + {t.status} + {t.title} + {t.updated_at} +
+ + ) +} + +function statusColor(status: string) { + switch (status) { + case 'TO_DO': + return '#888' + case 'IN_PROGRESS': + return '#0070cc' + case 'REVIEW': + return '#cc7a00' + case 'DONE': + return 'green' + default: + return 'inherit' + } +}