11<script setup lang="ts">
2- import type { watchPausable } from ' @vueuse/core'
3- import { useVModel } from ' @vueuse/core'
2+ import { useVModel , watchDebounced } from ' @vueuse/core'
43import JsonEditorVue from ' json-editor-vue'
5- import { nextTick , onMounted , shallowRef , watch } from ' vue'
4+ import { structuredClone } from ' structured-clone-es'
5+ import { computed , onMounted , shallowRef } from ' vue'
66import { getColorMode } from ' ~/composables/client'
77
88const props = defineProps <{
@@ -22,56 +22,54 @@ const colorMode = getColorMode()
2222const proxy = shallowRef ()
2323const error = shallowRef ()
2424
25+ // A named editor is collapsible; only the visible ones need a fresh snapshot.
26+ const isVisible = computed (() => ! props .name || !! isOpen .value )
27+
2528function isPrimitive(value : any ): boolean {
2629 return [' number' , ' bigint' , ' string' , ' boolean' ].includes (typeof value )
2730}
2831
32+ // Produce a plain, non-reactive snapshot of the state for the JSON editor.
33+ //
34+ // This must NEVER mutate `props.state`: it is the live reactive Nuxt payload /
35+ // app config / `useState` data. The previous implementation deep-synced the
36+ // state back into itself under a `{ deep: true }` watcher, so any array or
37+ // nested object endlessly re-triggered the watcher and froze the whole page
38+ // (nuxt/devtools#972). Re-cloning into a detached `proxy` avoids that entirely.
39+ //
40+ // `structured-clone-es` is used instead of `JSON.parse(JSON.stringify())`: it
41+ // detaches from Vue reactivity, tolerates circular references (rather than
42+ // throwing), and preserves richer types (Map/Set/Date/…). `lossy: true` drops
43+ // functions/symbols instead of throwing on them.
2944function clone() {
3045 error .value = undefined
46+ if (! isVisible .value )
47+ return
3148 try {
3249 proxy .value = isPrimitive (props .state )
3350 ? props .state
34- : JSON . parse ( JSON . stringify ( props .state || {}) )
51+ : structuredClone ( props .state ?? {}, { lossy: true } )
3552 }
3653 catch (e ) {
3754 console .error (e )
3855 error .value = e
3956 }
4057}
4158
42- let watcher: ReturnType <typeof watchPausable > | undefined
43-
44- onMounted (() => {
45- clone ()
46-
47- watch (
48- () => [props .revision , props .state ],
49- ([_ , state ]) => {
50- if (! isPrimitive (state ))
51- deepSync (state , props .state )
52- else
53- proxy .value = props .state
54- },
55- { deep: true },
56- )
57- })
59+ onMounted (clone )
5860
59- function deepSync(from : any , to : any ) {
60- for (const key in from ) {
61- if (Array .isArray (from [key ]))
62- to [key ] = from [key ].slice ()
63- else if (typeof from [key ] === ' object' && from [key ] !== null )
64- deepSync (from [key ], to [key ])
65- else
66- to [key ] = from [key ]
67- }
68- }
61+ // `revision` is bumped by DevTools whenever host reactivity updates, so it is a
62+ // sufficient change signal — we don't need (and must not use) a deep watcher on
63+ // the potentially huge state object. Debounced to avoid thrashing on rapid
64+ // updates.
65+ watchDebounced (
66+ () => [props .revision , props .state , isVisible .value ],
67+ clone ,
68+ { debounce: 100 , maxWait: 500 },
69+ )
6970
70- async function refresh() {
71- watcher ?.pause ()
71+ function refresh() {
7272 clone ()
73- await nextTick ()
74- watcher ?.resume ()
7573}
7674 </script >
7775
0 commit comments