[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"verticals":3,"playground-vue-weekly-reactivity-batch-watcher-throws":44,"search-suggestions":58},[4,20,32],{"id":5,"slug":6,"name":7,"tagline":8,"description":9,"accentFrom":10,"accentTo":11,"icon":12,"defaultLocale":13,"locales":14,"features":16,"position":19},"019fe637-3d33-714b-b57f-23e163ffca0c","dev","Web Development","Read it. Run it. Prove it.","A post a day on modern web development — most with an editable playground and a quiz that explains every answer. Free, no account needed.","violet-500","cyan-400","◇","en",[13,15],"fa",{"courses":17,"paths":17,"articles":17,"exams":18,"flashcards":18,"packages":17,"community":17,"certificates":17,"teams":17,"commerce":17},true,false,0,{"id":21,"slug":22,"name":23,"tagline":24,"description":25,"accentFrom":26,"accentTo":10,"icon":27,"defaultLocale":13,"locales":28,"features":30,"position":31},"019fe637-3dc2-754c-8657-0f175bfee7c6","lang","Languages","Learn a language the way you learn a codebase.","Grammar explained the way good documentation explains an API — one idea at a time, each with a quiz.","amber-400","⌘",[13,15,29],"es",{"courses":18,"paths":18,"articles":17,"exams":18,"flashcards":17,"packages":18,"community":17,"certificates":17,"teams":18,"commerce":18},2,{"id":33,"slug":34,"name":35,"tagline":36,"description":37,"accentFrom":38,"accentTo":39,"icon":40,"defaultLocale":13,"locales":41,"features":42,"position":43},"7b3c16f2-931d-410e-802e-e1fa4edab7de","soft","Soft Skills","The half of the job nobody wrote documentation for.","Weekly, on the parts of working life that decide more than your code does — first weeks, meetings, interviews, promotions, and the people around you. Written from what actually happens, and recorded as a podcast you can listen to on the walk.","emerald-400","teal-300","◉",[13],{"courses":18,"paths":18,"articles":17,"exams":18,"flashcards":18,"packages":18,"community":17,"certificates":18,"teams":18,"commerce":18},3,{"slug":45,"title":46,"summary":47,"html":48,"css":49,"js":50,"config":51,"doc":53},"vue-weekly-reactivity-batch-watcher-throws","Vue 3.6 notify buffer &amp; flush() — interactive playground",null,"\u003Cdiv class=\"pg\">\n  \u003Cdiv class=\"pg-title\">Vue 3.6 reactivity: the notify buffer &amp; flush() loop\u003C\u002Fdiv>\n  \u003Cp class=\"pg-sub\">\n    This is a \u003Cstrong>model\u003C\u002Fstrong> of the real algorithm in Vue's \u003Ccode>@vue\u002Freactivity\u003C\u002Fcode>\n    package, not a live Vue instance — the notify buffer and \u003Ccode>flush()\u003C\u002Fcode> are internal,\n    not exposed to application code, so there's no public way to show the fixed \u003Cem>and\u003C\u002Fem> the\n    buggy behavior side by side from one real instance. Read the article for the\n    real source: \u003Ca href=\"https:\u002F\u002Fgithub.com\u002Fvuejs\u002Fcore\u002Fissues\u002F15674\" target=\"_blank\" rel=\"noopener\">issue #15674\u003C\u002Fa>\n    → \u003Ca href=\"https:\u002F\u002Fgithub.com\u002Fvuejs\u002Fcore\u002Fpull\u002F15676\" target=\"_blank\" rel=\"noopener\">PR #15676\u003C\u002Fa>.\n  \u003C\u002Fp>\n\n  \u003Cdiv id=\"app\">\n\n    \u003Cdiv class=\"panel\">\n      \u003Cdiv class=\"panel-label\">\u003Cspan class=\"n\">1\u003C\u002Fspan> Configure the batch\u003C\u002Fdiv>\n\n      \u003Cdiv class=\"group-label\">Vue version behavior being modeled\u003C\u002Fdiv>\n      \u003Cdiv class=\"mode-row btn-row\">\n        \u003Cbutton class=\"btn active\" id=\"mode-buggy\" type=\"button\">3.6.0-rc.9 (buggy)\u003C\u002Fbutton>\n        \u003Cbutton class=\"btn\" id=\"mode-fixed\" type=\"button\">3.6.0-rc.10 \u002F 3.5.x (fixed)\u003C\u002Fbutton>\n      \u003C\u002Fdiv>\n\n      \u003Cdiv class=\"group-label\">Three effects, queued in this order every write: Watcher A → Computed getter → Watcher B\u003C\u002Fdiv>\n      \u003Cdiv class=\"btn-row\">\n        \u003Clabel class=\"toggle\">\n          \u003Cinput type=\"checkbox\" id=\"toggle-throw-computed\" checked \u002F>\n          \u003Cspan class=\"switch\">\u003C\u002Fspan>\n          Computed getter throws\n        \u003C\u002Flabel>\n        \u003Clabel class=\"toggle\">\n          \u003Cinput type=\"checkbox\" id=\"toggle-throw-b\" \u002F>\n          \u003Cspan class=\"switch\">\u003C\u002Fspan>\n          Watcher B also throws\n        \u003C\u002Flabel>\n      \u003C\u002Fdiv>\n    \u003C\u002Fdiv>\n\n    \u003Cdiv class=\"panel\">\n      \u003Cdiv class=\"panel-label\">\u003Cspan class=\"n\">2\u003C\u002Fspan> Run it\u003C\u002Fdiv>\n      \u003Cdiv class=\"btn-row\">\n        \u003Cbutton class=\"btn primary\" id=\"btn-write\">① Write to source ref\u003C\u002Fbutton>\n        \u003Cbutton class=\"btn\" id=\"btn-unrelated\">② Write to an unrelated ref\u003C\u002Fbutton>\n        \u003Cbutton class=\"btn ghost\" id=\"btn-reset\">Reset\u003C\u002Fbutton>\n      \u003C\u002Fdiv>\n\n      \u003Cdiv class=\"buffer\" id=\"buffer\">\u003C\u002Fdiv>\n\n      \u003Cdiv class=\"stats\">\n        \u003Cdiv class=\"stat\">\u003Cdiv class=\"label\">notifyIndex\u003C\u002Fdiv>\u003Cdiv class=\"value\" id=\"stat-index\">0\u003C\u002Fdiv>\u003C\u002Fdiv>\n        \u003Cdiv class=\"stat\">\u003Cdiv class=\"label\">notifyBufferLength\u003C\u002Fdiv>\u003Cdiv class=\"value\" id=\"stat-length\">0\u003C\u002Fdiv>\u003C\u002Fdiv>\n        \u003Cdiv class=\"stat\">\u003Cdiv class=\"label\">Rethrown error\u003C\u002Fdiv>\u003Cdiv class=\"value\" id=\"stat-error\">none\u003C\u002Fdiv>\u003C\u002Fdiv>\n      \u003C\u002Fdiv>\n    \u003C\u002Fdiv>\n\n    \u003Cdiv class=\"panel\">\n      \u003Cdiv class=\"panel-label\">\u003Cspan class=\"n\">3\u003C\u002Fspan> Timeline\u003C\u002Fdiv>\n      \u003Cp class=\"hint\" style=\"margin-top:0\">\n        Every call to \u003Ccode>effect.notify()\u003C\u002Fcode> is logged the instant it happens — in the buggy\n        mode, watch the loop stop dead the moment the throwing effect runs, and watch button ②\n        deliver the leftover effect late, attached to a write it has nothing to do with.\n      \u003C\u002Fp>\n      \u003Cdiv class=\"log\" id=\"log\" aria-live=\"polite\">\u003C\u002Fdiv>\n    \u003C\u002Fdiv>\n\n  \u003C\u002Fdiv>\n\u003C\u002Fdiv>","\u002F*\n * Shared playground design system — daily-post\n * ============================================\n * Link it from any post's playground\u002Findex.html (no per-post boilerplate):\n *\n *   \u003Clink rel=\"stylesheet\" href=\"..\u002F..\u002F..\u002Fplayground\u002Fassets\u002Fbase.css\" \u002F>\n *\n * The relative path resolves the same locally (posts\u002F\u003Cbase>\u002Fplayground\u002F) and on\n * the deployed host (public\u002Fposts\u002F\u003Cbase>\u002Fplayground\u002F), because build-site.js\n * mirrors this file to public\u002Fplayground\u002Fassets\u002Fbase.css. For a CodeSandbox\n * --embed, playground.js inlines this file so the single-file sandbox still styles.\n *\n * Then write ONLY the demo-specific CSS in your inline \u003Cstyle>. Everything below\n * — tokens, layout, panels, buttons, toggles, sliders, code blocks, status\u002Flog,\n * banners, presets — comes for free and looks consistent across every post.\n *\n * Theme-aware: dark by default, light via prefers-color-scheme AND a\n * [data-theme] override on \u003Chtml> (so a toggle can force either). Accessible:\n * real focus rings, adequate contrast, reduced-motion honored.\n *\u002F\n\n\u002F* ---------- design tokens ---------- *\u002F\n:root {\n  --bg: #0d1117;\n  --bg-grid: #ffffff0a;\n  --panel: #161b22;\n  --panel-2: #0b0e14;\n  --card: #0b0e14;\n  --ink: #e8ebf2;\n  --muted: #9aa4b2;\n  --faint: #6b7688;\n  --line: #232a35;\n  --line-soft: #ffffff12;\n  --accent: #7c9cff;\n  --accent-ink: #0b0e14;\n  --good: #3fb950;\n  --warn: #f0883e;\n  --bad: #f85149;\n  --info: #58a6ff;\n  --radius: 14px;\n  --radius-sm: 9px;\n  --shadow: 0 8px 30px #0006;\n  --shadow-sm: 0 4px 14px #0005;\n  --mono: ui-monospace, SFMono-Regular, \"SF Mono\", Menlo, Consolas, \"Liberation Mono\", monospace;\n  --sans: ui-sans-serif, system-ui, -apple-system, \"Segoe UI\", Roboto, \"Helvetica Neue\", sans-serif;\n  color-scheme: dark;\n}\n\n\u002F* Light theme — system preference, overridable by [data-theme] on \u003Chtml>. *\u002F\n@media (prefers-color-scheme: light) {\n  :root:not([data-theme=\"dark\"]) {\n    --bg: #f4f6fb;\n    --bg-grid: #00000008;\n    --panel: #ffffff;\n    --panel-2: #f0f3f9;\n    --card: #f7f9fc;\n    --ink: #131722;\n    --muted: #5b6472;\n    --faint: #8a93a3;\n    --line: #e2e7f0;\n    --line-soft: #00000010;\n    --accent: #2f6df0;\n    --accent-ink: #ffffff;\n    --good: #1a7f37;\n    --warn: #bc4c00;\n    --bad: #cf222e;\n    --info: #0969da;\n    --shadow: 0 8px 30px #0000001f;\n    --shadow-sm: 0 4px 14px #00000014;\n    color-scheme: light;\n  }\n}\n:root[data-theme=\"light\"] {\n  --bg: #f4f6fb;\n  --bg-grid: #00000008;\n  --panel: #ffffff;\n  --panel-2: #f0f3f9;\n  --card: #f7f9fc;\n  --ink: #131722;\n  --muted: #5b6472;\n  --faint: #8a93a3;\n  --line: #e2e7f0;\n  --line-soft: #00000010;\n  --accent: #2f6df0;\n  --accent-ink: #ffffff;\n  --good: #1a7f37;\n  --warn: #bc4c00;\n  --bad: #cf222e;\n  --info: #0969da;\n  --shadow: 0 8px 30px #0000001f;\n  --shadow-sm: 0 4px 14px #00000014;\n  color-scheme: light;\n}\n\n\u002F* ---------- reset & base ---------- *\u002F\n*, *::before, *::after { box-sizing: border-box; }\n\nbody {\n  margin: 0;\n  min-height: 100vh;\n  font-family: var(--sans);\n  color: var(--ink);\n  background:\n    radial-gradient(circle at 1px 1px, var(--bg-grid) 1px, transparent 0) 0 0 \u002F 24px 24px,\n    var(--bg);\n  display: flex;\n  justify-content: center;\n  align-items: flex-start;\n  padding: clamp(14px, 3vw, 26px);\n  line-height: 1.55;\n  -webkit-font-smoothing: antialiased;\n  text-rendering: optimizeLegibility;\n}\n\n\u002F* The single content column. *\u002F\n.pg {\n  width: 100%;\n  max-width: 860px;\n}\n\n\u002F* ---------- header ---------- *\u002F\n.pg-title {\n  font-size: clamp(1.15rem, 1rem + 1vw, 1.4rem);\n  font-weight: 800;\n  letter-spacing: -0.02em;\n  margin: 0 0 4px;\n  display: flex;\n  align-items: center;\n  gap: 9px;\n}\n.pg-sub {\n  color: var(--muted);\n  font-size: 0.92rem;\n  line-height: 1.5;\n  margin: 0 0 16px;\n  max-width: 66ch;\n}\n.pg-sub strong, .pg-sub b { color: var(--ink); font-weight: 700; }\n.pg-sub code { font-family: var(--mono); background: var(--line-soft); color: var(--accent);\n  padding: 1px 6px; border-radius: 6px; font-size: 0.85em; }\n\n\u002F* ---------- panels ---------- *\u002F\n.panel {\n  background: var(--panel);\n  border: 1px solid var(--line);\n  border-radius: var(--radius);\n  padding: 16px 18px;\n  margin-bottom: 14px;\n  box-shadow: var(--shadow-sm);\n}\n.panel-label {\n  font-size: 0.7rem;\n  text-transform: uppercase;\n  letter-spacing: 0.08em;\n  color: var(--muted);\n  font-weight: 800;\n  margin: 0 0 13px;\n  display: flex;\n  align-items: center;\n  gap: 8px;\n}\n.panel-label .n {\n  display: inline-grid; place-items: center;\n  width: 18px; height: 18px; border-radius: 50%;\n  background: var(--accent); color: var(--accent-ink);\n  font-size: 0.66rem; font-weight: 800;\n}\n\n\u002F* ---------- layout helpers ---------- *\u002F\n.row { display: flex; flex-wrap: wrap; gap: 12px; align-items: center; }\n.row.between { justify-content: space-between; }\n.col { display: flex; flex-direction: column; gap: 10px; }\n.grow { flex: 1 1 0; min-width: 0; }\n.wrap-controls { display: flex; flex-wrap: wrap; gap: 16px 24px; align-items: flex-end; }\n.field { display: flex; flex-direction: column; gap: 7px; min-width: 0; }\n.field > .label, .ctl-label {\n  font-size: 0.7rem; text-transform: uppercase; letter-spacing: 0.04em;\n  color: var(--muted); font-weight: 800;\n}\n\n\u002F* ---------- buttons ---------- *\u002F\n.btn {\n  font: inherit; font-weight: 650; font-size: 0.88rem;\n  cursor: pointer; user-select: none;\n  border: 1px solid var(--line);\n  background: var(--card); color: var(--ink);\n  padding: 9px 15px; border-radius: var(--radius-sm);\n  display: inline-flex; align-items: center; gap: 8px;\n  transition: background .15s ease, border-color .15s ease, transform .05s ease, color .15s ease;\n}\n.btn:hover { background: var(--line-soft); border-color: var(--muted); }\n.btn:active { transform: translateY(1px); }\n.btn:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }\n.btn[disabled] { opacity: 0.45; cursor: not-allowed; }\n.btn.primary { background: var(--accent); border-color: var(--accent); color: var(--accent-ink); }\n.btn.primary:hover { filter: brightness(1.08); background: var(--accent); }\n.btn.good { background: var(--good); border-color: var(--good); color: #04140a; }\n.btn.bad  { background: var(--bad);  border-color: var(--bad);  color: #fff; }\n.btn.ghost { background: transparent; }\n.btn.sm { padding: 6px 11px; font-size: 0.82rem; }\n\n.btn-row { display: flex; flex-wrap: wrap; gap: 9px; }\n\n\u002F* Segmented control — a row of buttons where one is active (aria-pressed). *\u002F\n.seg { display: inline-flex; background: var(--card); border: 1px solid var(--line);\n  border-radius: var(--radius-sm); padding: 3px; gap: 3px; }\n.seg button {\n  font: inherit; font-weight: 650; font-size: 0.84rem; cursor: pointer;\n  border: none; background: transparent; color: var(--muted);\n  padding: 7px 13px; border-radius: 7px;\n  transition: background .15s ease, color .15s ease;\n}\n.seg button:hover { color: var(--ink); }\n.seg button[aria-pressed=\"true\"] { background: var(--accent); color: var(--accent-ink); }\n.seg button:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }\n\n\u002F* Preset chips — worked-example shortcuts. *\u002F\n.presets { display: flex; flex-wrap: wrap; gap: 8px; }\n.presets .btn { background: var(--panel-2); }\n\n\u002F* ---------- toggle switch ---------- *\u002F\n.toggle { display: inline-flex; align-items: center; gap: 10px; cursor: pointer;\n  font-size: 0.88rem; font-weight: 600; user-select: none; }\n.toggle input { position: absolute; opacity: 0; width: 0; height: 0; }\n.switch { position: relative; width: 44px; height: 25px; border-radius: 999px; flex: none;\n  background: var(--line); border: 1px solid var(--line); transition: background .18s ease; }\n.switch::after { content: \"\"; position: absolute; top: 2px; left: 2px; width: 19px; height: 19px;\n  border-radius: 50%; background: #cfd6e4; transition: transform .18s ease; box-shadow: 0 1px 3px #0006; }\n.toggle input:checked + .switch { background: var(--good); border-color: var(--good); }\n.toggle input:checked + .switch::after { transform: translateX(19px); background: #fff; }\n.toggle input:focus-visible + .switch { outline: 2px solid var(--accent); outline-offset: 2px; }\n\n\u002F* ---------- range slider ---------- *\u002F\ninput[type=\"range\"] {\n  -webkit-appearance: none; appearance: none; width: 100%; height: 6px;\n  background: var(--line); border-radius: 999px; outline: none; cursor: pointer; margin: 8px 0;\n}\ninput[type=\"range\"]::-webkit-slider-thumb {\n  -webkit-appearance: none; appearance: none; width: 19px; height: 19px; border-radius: 50%;\n  background: var(--accent); border: 2px solid var(--panel); box-shadow: var(--shadow-sm); cursor: pointer;\n}\ninput[type=\"range\"]::-moz-range-thumb {\n  width: 19px; height: 19px; border-radius: 50%; background: var(--accent);\n  border: 2px solid var(--panel); box-shadow: var(--shadow-sm); cursor: pointer;\n}\ninput[type=\"range\"]:focus-visible { outline: 2px solid var(--accent); outline-offset: 3px; }\n\n\u002F* ---------- text inputs \u002F selects ---------- *\u002F\ninput[type=\"text\"], input[type=\"number\"], input[type=\"search\"], select, textarea {\n  font: inherit; color: var(--ink); background: var(--card);\n  border: 1px solid var(--line); border-radius: var(--radius-sm); padding: 8px 11px; width: 100%;\n}\ntextarea { font-family: var(--mono); font-size: 0.85rem; line-height: 1.6; resize: vertical; }\ninput:focus-visible, select:focus-visible, textarea:focus-visible {\n  outline: 2px solid var(--accent); outline-offset: 1px; border-color: var(--accent);\n}\n\n\u002F* ---------- status \u002F output \u002F log ---------- *\u002F\n.status {\n  display: flex; align-items: center; gap: 9px; flex-wrap: wrap;\n  font-size: 0.9rem; font-weight: 600;\n  padding: 11px 14px; border-radius: var(--radius-sm);\n  background: var(--panel-2); border: 1px solid var(--line); color: var(--ink);\n}\n.status.good { border-color: color-mix(in srgb, var(--good) 55%, var(--line)); }\n.status.warn { border-color: color-mix(in srgb, var(--warn) 55%, var(--line)); }\n.status.bad  { border-color: color-mix(in srgb, var(--bad) 55%, var(--line)); }\n.dot { width: 9px; height: 9px; border-radius: 50%; background: var(--muted); flex: none; }\n.dot.good { background: var(--good); } .dot.warn { background: var(--warn); }\n.dot.bad { background: var(--bad); } .dot.info { background: var(--info); }\n\n.output { background: var(--panel-2); border: 1px solid var(--line); border-radius: var(--radius-sm);\n  padding: 13px 15px; }\n.log { font-family: var(--mono); font-size: 0.8rem; line-height: 1.7; background: var(--card);\n  border: 1px solid var(--line); border-radius: var(--radius-sm); padding: 11px 14px;\n  max-height: 220px; overflow-y: auto; white-space: pre-wrap; }\n\n\u002F* ---------- code block + syntax tokens ---------- *\u002F\n.code {\n  font-family: var(--mono); font-size: 0.82rem; line-height: 1.7;\n  background: var(--panel-2); border: 1px solid var(--line); border-radius: var(--radius-sm);\n  padding: 13px 15px; overflow-x: auto; white-space: pre; tab-size: 2;\n}\n.code .tok-sel  { color: var(--accent); }\n.code .tok-prop { color: #86e29b; }\n.code .tok-kw   { color: #ff9bcb; }\n.code .tok-str  { color: #f0c674; }\n.code .tok-num  { color: #f0883e; }\n.code .tok-cmt  { color: var(--faint); font-style: italic; }\n.code .hl { background: color-mix(in srgb, var(--good) 22%, transparent); border-radius: 4px; padding: 0 3px; }\n\n\u002F* ---------- hint \u002F caption ---------- *\u002F\n.hint { font-size: 0.83rem; color: var(--muted); line-height: 1.55; margin: 10px 0 0; }\n.hint code { font-family: var(--mono); color: var(--ink); background: var(--line-soft);\n  padding: 1px 5px; border-radius: 5px; font-size: 0.86em; }\n.hint strong { color: var(--ink); }\n\n\u002F* ---------- banner (feature-support \u002F notices) ---------- *\u002F\n.banner {\n  display: none; margin-bottom: 14px; padding: 11px 14px; border-radius: 11px;\n  font-size: 0.85rem; line-height: 1.5;\n  color: var(--ink); background: color-mix(in srgb, var(--warn) 12%, var(--panel));\n  border: 1px solid color-mix(in srgb, var(--warn) 45%, var(--line));\n}\n.banner.show { display: block; }\n.banner.info { background: color-mix(in srgb, var(--info) 12%, var(--panel));\n  border-color: color-mix(in srgb, var(--info) 45%, var(--line)); }\n.banner code { font-family: var(--mono); background: var(--line-soft); padding: 1px 5px; border-radius: 5px; }\n\n\u002F* ---------- misc atoms ---------- *\u002F\n.pill { display: inline-flex; align-items: center; gap: 6px; font-size: 0.75rem; font-weight: 700;\n  padding: 3px 9px; border-radius: 999px; background: var(--line-soft); color: var(--muted); }\n.pill.good { color: var(--good); background: color-mix(in srgb, var(--good) 16%, transparent); }\n.pill.bad  { color: var(--bad);  background: color-mix(in srgb, var(--bad) 16%, transparent); }\n.mono { font-family: var(--mono); }\n.muted { color: var(--muted); }\n.big-num { font-family: var(--mono); font-size: 1.6rem; font-weight: 800; letter-spacing: -0.02em; }\n\n\u002F* Optional theme toggle button (top-right). Wire it in JS if you want it. *\u002F\n.theme-btn { position: fixed; top: 12px; right: 12px; z-index: 20; }\n\n\u002F* ---------- responsive ---------- *\u002F\n@media (max-width: 560px) {\n  .panel { padding: 14px; }\n  .wrap-controls { gap: 14px; }\n  .btn { padding: 9px 13px; }\n}\n\n\u002F* ---------- reduced motion ---------- *\u002F\n@media (prefers-reduced-motion: reduce) {\n  *, *::before, *::after { transition-duration: 0.01ms !important; animation-duration: 0.01ms !important; }\n}\n\n\n\u002F* Demo-specific only — base.css owns tokens, panels, buttons, status, log. *\u002F\n  .buffer { display: flex; gap: 8px; flex-wrap: wrap; margin: 10px 0 6px; min-height: 56px; }\n  .slot {\n    position: relative; min-width: 108px; padding: 10px 10px 8px; border-radius: var(--radius-sm);\n    border: 1px solid var(--line); background: var(--panel-2); font-family: var(--mono); font-size: 0.78rem;\n    transition: border-color .15s, background .15s;\n  }\n  .slot .name { font-weight: 700; color: var(--ink); display: block; margin-bottom: 4px; }\n  .slot .st { font-size: 0.72rem; color: var(--muted); }\n  .slot.pending { opacity: 0.55; }\n  .slot.ran { border-color: var(--good); background: color-mix(in srgb, var(--good) 12%, var(--panel-2)); }\n  .slot.ran .st { color: var(--good); }\n  .slot.err { border-color: var(--bad); background: color-mix(in srgb, var(--bad) 14%, var(--panel-2)); }\n  .slot.err .st { color: var(--bad); }\n  .slot.stuck { border-color: var(--warn); background: color-mix(in srgb, var(--warn) 14%, var(--panel-2)); }\n  .slot.stuck .st { color: var(--warn); }\n  .slot.ptr::before {\n    content: \"↑ notifyIndex\"; position: absolute; top: -20px; left: 0; font-size: 0.68rem;\n    color: var(--accent); white-space: nowrap;\n  }\n  .stats { display: grid; grid-template-columns: repeat(3, 1fr); gap: 10px; margin: 12px 0 4px; }\n  .stat { padding: 10px 12px; border: 1px solid var(--line); border-radius: var(--radius-sm); background: var(--panel-2); text-align: center; }\n  .stat .label { font-size: 0.7rem; text-transform: uppercase; letter-spacing: 0.5px; color: var(--muted); }\n  .stat .value { font-family: var(--mono); font-size: 1.15rem; font-weight: 800; margin-top: 2px; }\n  .group-label { font-size: 0.78rem; color: var(--muted); margin: 12px 0 6px; }\n  .group-label:first-child { margin-top: 0; }\n  .mode-row { display: flex; gap: 8px; flex-wrap: wrap; }\n  .mode-row .btn.active { background: var(--accent); color: var(--accent-ink); border-color: var(--accent); }","const $ = (s, r = document) => r.querySelector(s);\n\nconst log = $(\"#log\");\nfunction line(text, cls = \"\") {\n  const el = document.createElement(\"div\");\n  el.className = cls;\n  el.textContent = text;\n  log.prepend(el);\n  while (log.childElementCount > 60) log.lastElementChild.remove();\n}\n\n\u002F\u002F ---- state mirroring Vue's actual reactivity\u002Fsrc\u002Fsystem.ts shape ----\nlet notifyBuffer = [];\nlet notifyIndex = 0;\nlet notifyBufferLength = 0;\nlet mode = \"buggy\"; \u002F\u002F \"buggy\" = rc.9 shape, \"fixed\" = rc.10+\u002F3.5 shape\nlet writeCount = 0;\n\nfunction makeEffect(label, willThrow) {\n  return {\n    label,\n    notify() {\n      if (willThrow()) {\n        throw new Error(`${label} blew up`);\n      }\n      return true;\n    },\n  };\n}\n\nfunction queue(effect) {\n  notifyBuffer[notifyBufferLength++] = effect;\n}\n\n\u002F\u002F The buggy shape: no protection at all. A throw propagates straight out of\n\u002F\u002F the while loop, abandoning notifyIndex\u002FnotifyBufferLength mid-walk.\nfunction flushBuggy() {\n  while (notifyIndex \u003C notifyBufferLength) {\n    const effect = notifyBuffer[notifyIndex];\n    notifyBuffer[notifyIndex] = undefined;\n    notifyIndex++;\n    effect.notify(); \u002F\u002F uncaught — if this throws, the loop (and the state) stops right here\n    line(`✅ ${effect.label}.notify() ran`, \"good\");\n    renderBuffer();\n  }\n  notifyIndex = 0;\n  notifyBufferLength = 0;\n}\n\n\u002F\u002F The fixed shape: inner try\u002Fcatch per effect. That alone means the while\n\u002F\u002F loop can no longer throw, so the reset right after it is unconditional —\n\u002F\u002F there's no separate outer try\u002Ffinally in the real fix, because none is needed.\nfunction flushFixed() {\n  let error, hasError = false;\n  while (notifyIndex \u003C notifyBufferLength) {\n    const effect = notifyBuffer[notifyIndex];\n    notifyBuffer[notifyIndex] = undefined;\n    notifyIndex++;\n    try {\n      effect.notify();\n      line(`✅ ${effect.label}.notify() ran`, \"good\");\n    } catch (e) {\n      line(`❌ ${effect.label}.notify() threw: ${e.message}`, \"bad\");\n      if (!hasError) { hasError = true; error = e; }\n      else line(`   (a second error in this batch — discarded, only the first rethrows)`, \"warn\");\n    }\n    renderBuffer();\n  }\n  notifyIndex = 0;\n  notifyBufferLength = 0;\n  if (hasError) throw error;\n}\n\nfunction runFlush(label) {\n  writeCount++;\n  line(`✍️ ${label} → flush() starts (notifyIndex=${notifyIndex}, notifyBufferLength=${notifyBufferLength})`, \"\");\n  renderStats();\n  try {\n    if (mode === \"buggy\") flushBuggy();\n    else flushFixed();\n    $(\"#stat-error\").textContent = \"none\";\n  } catch (e) {\n    $(\"#stat-error\").textContent = e.message;\n    if (mode === \"buggy\") {\n      line(`💥 flush() crashed on an uncaught throw — loop abandoned, state left dirty`, \"bad\");\n    } else {\n      line(`⚠️ flush() finished the whole batch, THEN rethrew: ${e.message}`, \"warn\");\n    }\n  }\n  renderStats();\n  renderBuffer();\n}\n\nfunction renderStats() {\n  $(\"#stat-index\").textContent = String(notifyIndex);\n  $(\"#stat-length\").textContent = String(notifyBufferLength);\n}\n\nfunction renderBuffer() {\n  const el = $(\"#buffer\");\n  el.innerHTML = \"\";\n  if (notifyBufferLength === 0) {\n    el.innerHTML = '\u003Cspan class=\"hint\">empty — click \"Write to source ref\"\u003C\u002Fspan>';\n    return;\n  }\n  for (let i = 0; i \u003C notifyBufferLength; i++) {\n    const effect = notifyBuffer[i];\n    const div = document.createElement(\"div\");\n    let cls = \"slot\";\n    let status = \"\";\n    if (i \u003C notifyIndex) {\n      \u002F\u002F already consumed this pass — but was it this write's pass or a stale one?\n      cls += \" ran\";\n      status = \"ran this flush\";\n    } else if (effect) {\n      cls += \" stuck\";\n      status = \"queued — not reached yet\";\n    } else {\n      cls += \" pending\";\n      status = \"pending\";\n    }\n    if (i === notifyIndex && notifyIndex \u003C notifyBufferLength) cls += \" ptr\";\n    div.className = cls;\n    const label = effect ? effect.label : `(slot ${i}, already run)`;\n    div.innerHTML = `\u003Cspan class=\"name\">${label}\u003C\u002Fspan>\u003Cspan class=\"st\">${status}\u003C\u002Fspan>`;\n    el.appendChild(div);\n  }\n}\n\nfunction buildEffects() {\n  const throwComputed = $(\"#toggle-throw-computed\").checked;\n  const throwB = $(\"#toggle-throw-b\").checked;\n  return [\n    makeEffect(\"Watcher A (UI label)\", () => false),\n    makeEffect(\"Computed getter\", () => throwComputed),\n    makeEffect(\"Watcher B (analytics)\", () => throwB),\n  ];\n}\n\n$(\"#btn-write\").addEventListener(\"click\", () => {\n  for (const e of buildEffects()) queue(e);\n  renderBuffer();\n  runFlush(\"Write to source ref\");\n});\n\n$(\"#btn-unrelated\").addEventListener(\"click\", () => {\n  const hadLeftover = notifyIndex \u003C notifyBufferLength;\n  queue(makeEffect(\"Unrelated effect\", () => false));\n  renderBuffer();\n  if (!hadLeftover) {\n    line(`✍️ Write to an unrelated ref → buffer was already empty, nothing was left over`, \"\");\n  } else {\n    line(`✍️ Write to an unrelated ref → flush() resumes a LEFTOVER buffer from an earlier write`, \"warn\");\n  }\n  runFlush(\"Write to an unrelated ref\");\n});\n\n$(\"#btn-reset\").addEventListener(\"click\", () => {\n  notifyBuffer = [];\n  notifyIndex = 0;\n  notifyBufferLength = 0;\n  writeCount = 0;\n  log.innerHTML = \"\";\n  $(\"#stat-error\").textContent = \"none\";\n  renderStats();\n  renderBuffer();\n  line(\"— reset —\");\n});\n\n$(\"#mode-buggy\").addEventListener(\"click\", () => {\n  mode = \"buggy\";\n  $(\"#mode-buggy\").classList.add(\"active\");\n  $(\"#mode-fixed\").classList.remove(\"active\");\n  line(\"→ switched to 3.6.0-rc.9 (buggy) model\", \"\");\n});\n\n$(\"#mode-fixed\").addEventListener(\"click\", () => {\n  mode = \"fixed\";\n  $(\"#mode-fixed\").classList.add(\"active\");\n  $(\"#mode-buggy\").classList.remove(\"active\");\n  line(\"→ switched to 3.6.0-rc.10 \u002F 3.5.x (fixed) model\", \"\");\n});\n\nrenderStats();\nrenderBuffer();\nline(\"Ready. Try it in buggy mode first: write, then write again to an unrelated ref.\");",{"height":52,"autorun":17},520,{"slug":45,"title":54,"excerpt":55,"coverUrl":56,"vertical":57},"Vue 3.6: When a Watcher Throws, Your Other Watchers Stall","A Vue 3.6 reactivity regression let a throwing watcher silently stall its siblings. The exact flush() mechanism, the fix, and a copy-paste cheat sheet.","\u002Fmedia\u002Fcovers\u002Fvue-weekly-reactivity-batch-watcher-throws.png",{"slug":6},[59,63,67,71,75,79,83,87,91,95,99,103],{"slug":60,"name":61,"articles":62},"webdev","Webdev",146,{"slug":64,"name":65,"articles":66},"javascript","Javascript",119,{"slug":68,"name":69,"articles":70},"frontend","Frontend",87,{"slug":72,"name":73,"articles":74},"tutorial","Tutorial",56,{"slug":76,"name":77,"articles":78},"css","Css",44,{"slug":80,"name":81,"articles":82},"performance","Performance",24,{"slug":84,"name":85,"articles":86},"typescript","Typescript",19,{"slug":88,"name":89,"articles":90},"react","React",18,{"slug":92,"name":93,"articles":94},"browser","Browser",14,{"slug":96,"name":97,"articles":98},"node","Node",12,{"slug":100,"name":101,"articles":102},"programming","Programming",11,{"slug":104,"name":105,"articles":106},"accessibility","Accessibility",10]