[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"verticals":3,"playground-web-locks-api-tab-coordination":32,"search-suggestions":46},[4,20],{"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,{"slug":33,"title":34,"summary":35,"html":36,"css":37,"js":38,"config":39,"doc":41},"web-locks-api-tab-coordination","navigator.locks — the four-tabs race, live",null,"\u003Cdiv class=\"pg\">\n    \u003Ch1 class=\"pg-title\">🔒 The four-tabs race, live\u003C\u002Fh1>\n    \u003Cp class=\"pg-sub\">\n      Four simulated browser tabs all notice an expired token at once and race to refresh it.\n      Toggle \u003Ccode>navigator.locks\u003C\u002Fcode> on or off and click \u003Cstrong>Expire token\u003C\u002Fstrong> to see\n      whether they trample each other or take turns.\n    \u003C\u002Fp>\n\n    \u003Cdiv class=\"panel\">\n      \u003Cdiv class=\"panel-label\">\u003Cspan class=\"n\">1\u003C\u002Fspan> Controls\u003C\u002Fdiv>\n      \u003Cdiv class=\"btn-row\">\n        \u003Cbutton class=\"btn primary\" id=\"run\">⏱️ Expire token (all 4 tabs notice)\u003C\u002Fbutton>\n        \u003Cbutton class=\"btn ghost\" id=\"reset\">Reset\u003C\u002Fbutton>\n        \u003Clabel class=\"toggle\" style=\"margin-left:6px\">\n          \u003Cinput type=\"checkbox\" id=\"useLock\" checked \u002F>\u003Cspan class=\"switch\">\u003C\u002Fspan>\n          \u003Cspan>use \u003Ccode>navigator.locks\u003C\u002Fcode> (uncheck to see the race)\u003C\u002Fspan>\n        \u003C\u002Flabel>\n      \u003C\u002Fdiv>\n      \u003Cdiv class=\"presets\" style=\"margin-top:14px\">\n        \u003Cbutton class=\"btn sm\" data-preset=\"locked\">Preset: locked (safe)\u003C\u002Fbutton>\n        \u003Cbutton class=\"btn sm\" data-preset=\"unlocked\">Preset: unlocked (racy)\u003C\u002Fbutton>\n      \u003C\u002Fdiv>\n    \u003C\u002Fdiv>\n\n    \u003Cdiv class=\"panel\">\n      \u003Cdiv class=\"panel-label\">\u003Cspan class=\"n\">2\u003C\u002Fspan> Four tabs, one token\u003C\u002Fdiv>\n      \u003Cdiv class=\"tabs\" id=\"tabs\">\u003C\u002Fdiv>\n      \u003Cdiv class=\"status\" id=\"status\" style=\"margin-top:14px\">\n        \u003Cspan class=\"dot info\" id=\"dot\">\u003C\u002Fspan>\u003Cspan id=\"statusText\">Ready — click \"Expire token.\"\u003C\u002Fspan>\n      \u003C\u002Fdiv>\n      \u003Cp class=\"hint\" id=\"hint\">\n        With the lock on, one tab refreshes while the other three wait, then check the token and do\n        nothing. With it off, all four call the refresh endpoint before any of them notices someone\n        beat them to it.\n      \u003C\u002Fp>\n    \u003C\u002Fdiv>\n\n    \u003Cdiv class=\"panel\">\n      \u003Cdiv class=\"panel-label\">\u003Cspan class=\"n\">3\u003C\u002Fspan> Server ledger — refresh calls it actually received\u003C\u002Fdiv>\n      \u003Cdiv class=\"ledger\" id=\"ledger\">\u003C\u002Fdiv>\n      \u003Cdiv style=\"margin-top:10px\">\n        Refresh calls this run: \u003Cspan class=\"count-badge zero\" id=\"badge\">0\u003C\u002Fspan>\n      \u003C\u002Fdiv>\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.tabs { display: grid; grid-template-columns: repeat(4, 1fr); gap: 10px; margin-top: 4px; }\n  .tab {\n    border: 1px solid var(--line); border-radius: var(--radius-sm); background: var(--panel-2);\n    padding: 10px; text-align: center; font-size: 0.85rem; position: relative; overflow: hidden;\n  }\n  .tab .name { font-weight: 600; margin-bottom: 6px; }\n  .tab .state { font-size: 0.78rem; min-height: 1.4em; }\n  .tab.waiting { opacity: 0.6; }\n  .tab.running .state { color: var(--accent, #6ee7ff); }\n  .tab.done .state { color: #6bd97a; }\n  .tab.duplicate .state { color: #ff6b6b; font-weight: 700; }\n  .ledger { margin-top: 10px; }\n  .ledger .row {\n    display: flex; justify-content: space-between; gap: 8px; font-family: var(--mono, monospace);\n    font-size: 0.8rem; padding: 3px 0; border-bottom: 1px dashed var(--line);\n  }\n  .ledger .row.bad { color: #ff6b6b; }\n  .ledger .row.good { color: #6bd97a; }\n  .count-badge {\n    display: inline-block; min-width: 1.6em; text-align: center; border-radius: 999px;\n    padding: 1px 8px; font-weight: 700; font-size: 0.85rem;\n  }\n  .count-badge.zero { background: rgba(107, 217, 122, 0.18); color: #6bd97a; }\n  .count-badge.many { background: rgba(255, 107, 107, 0.18); color: #ff6b6b; }","\"use strict\";\n  const $ = (id) => document.getElementById(id);\n  const TAB_NAMES = [\"Tab A\", \"Tab B\", \"Tab C\", \"Tab D\"];\n  let tokenFresh = true;\n  let ledger = [];\n  let running = false;\n\n  function renderTabs(states) {\n    $(\"tabs\").innerHTML = TAB_NAMES.map((name, i) => {\n      const s = states[i] || { cls: \"\", text: \"idle\" };\n      return `\u003Cdiv class=\"tab ${s.cls}\">\u003Cdiv class=\"name\">${name}\u003C\u002Fdiv>\u003Cdiv class=\"state\">${s.text}\u003C\u002Fdiv>\u003C\u002Fdiv>`;\n    }).join(\"\");\n  }\n\n  function renderLedger() {\n    if (ledger.length === 0) {\n      $(\"ledger\").innerHTML = '\u003Cp class=\"hint\">No refresh calls yet.\u003C\u002Fp>';\n    } else {\n      $(\"ledger\").innerHTML = ledger\n        .map((e) => `\u003Cdiv class=\"row ${e.bad ? \"bad\" : \"good\"}\">\u003Cspan>${e.tab} → POST \u002Frefresh\u003C\u002Fspan>\u003Cspan>${e.note}\u003C\u002Fspan>\u003C\u002Fdiv>`)\n        .join(\"\");\n    }\n    const badge = $(\"badge\");\n    badge.textContent = String(ledger.length);\n    badge.className = \"count-badge \" + (ledger.length > 1 ? \"many\" : \"zero\");\n  }\n\n  function setStatus(text, kind = \"info\") {\n    $(\"statusText\").textContent = text;\n    $(\"dot\").className = \"dot \" + kind;\n  }\n\n  function delay(ms) { return new Promise((res) => setTimeout(res, ms)); }\n\n  \u002F\u002F Simulates the browser's real lock manager: exclusive, FIFO, one holder at a time.\n  function makeFakeLockManager() {\n    let queue = Promise.resolve();\n    return {\n      request(name, cb) {\n        const runNext = queue.then(() => cb());\n        queue = runNext.catch(() => {});\n        return runNext;\n      },\n    };\n  }\n\n  async function refreshViaTab(tabName, useLock, states, idx, lockManager) {\n    const doRefresh = async () => {\n      if (tokenFresh) {\n        states[idx] = { cls: \"done\", text: \"✅ already fresh — skipped\" };\n        renderTabs(states);\n        return;\n      }\n      states[idx] = { cls: \"running\", text: \"🔄 refreshing…\" };\n      renderTabs(states);\n      await delay(220 + Math.random() * 120);\n      const isFirst = ledger.length === 0;\n      ledger.push({\n        tab: tabName,\n        bad: !isFirst,\n        note: isFirst ? \"accepted\" : \"⚠️ reused token — session revoked\",\n      });\n      renderLedger();\n      tokenFresh = true;\n      states[idx] = isFirst\n        ? { cls: \"done\", text: \"✅ refreshed it\" }\n        : { cls: \"duplicate\", text: \"🚨 rejected by server\" };\n      renderTabs(states);\n    };\n\n    if (useLock) {\n      states[idx] = { cls: \"waiting\", text: \"⏳ waiting for lock…\" };\n      renderTabs(states);\n      await lockManager.request(\"refresh-token\", doRefresh);\n    } else {\n      await doRefresh();\n    }\n  }\n\n  async function runRace(useLock) {\n    if (running) return;\n    running = true;\n    tokenFresh = false; \u002F\u002F all four tabs will now see it as stale\n    ledger = [];\n    renderLedger();\n    const states = TAB_NAMES.map(() => ({ cls: \"\", text: \"noticing expiry…\" }));\n    renderTabs(states);\n    setStatus(useLock ? \"Lock ON — tabs will take turns.\" : \"Lock OFF — tabs are racing.\", \"info\");\n\n    const lockManager = makeFakeLockManager();\n    await Promise.all(\n      TAB_NAMES.map((name, i) => refreshViaTab(name, useLock, states, i, lockManager))\n    );\n\n    const bad = ledger.filter((e) => e.bad).length;\n    if (ledger.length \u003C= 1) {\n      setStatus(\"Done — exactly one refresh call reached the server.\", \"good\");\n    } else {\n      setStatus(`Done — ${ledger.length} refresh calls reached the server. The server just revoked this session.`, \"bad\");\n    }\n    running = false;\n  }\n\n  $(\"run\").addEventListener(\"click\", () => runRace($(\"useLock\").checked));\n\n  $(\"reset\").addEventListener(\"click\", () => {\n    tokenFresh = true;\n    ledger = [];\n    renderLedger();\n    renderTabs(TAB_NAMES.map(() => ({ cls: \"\", text: \"idle\" })));\n    setStatus(\"Ready — click \\\"Expire token.\\\"\", \"info\");\n  });\n\n  document.querySelectorAll(\"[data-preset]\").forEach((btn) => {\n    btn.addEventListener(\"click\", () => {\n      const locked = btn.dataset.preset === \"locked\";\n      $(\"useLock\").checked = locked;\n      runRace(locked);\n    });\n  });\n\n  renderTabs(TAB_NAMES.map(() => ({ cls: \"\", text: \"idle\" })));\n  renderLedger();",{"height":40,"autorun":17},520,{"slug":33,"title":42,"excerpt":43,"coverUrl":44,"vertical":45},"Five tabs open, one refresh token — the race nobody noticed","Users with multiple tabs open triggered a silent race: every tab refreshed the same expiring auth token at once, and the server started invalidating its own sessions. navigator.locks fixes it with a real mutex, no polling or localStorage hacks required.","\u002Fmedia\u002Fcovers\u002Fweb-locks-api-tab-coordination.png",{"slug":6},[47,51,55,59,63,67,71,75,79,83,87,90],{"slug":48,"name":49,"articles":50},"webdev","Webdev",54,{"slug":52,"name":53,"articles":54},"javascript","Javascript",49,{"slug":56,"name":57,"articles":58},"frontend","Frontend",48,{"slug":60,"name":61,"articles":62},"css","Css",19,{"slug":64,"name":65,"articles":66},"typescript","Typescript",10,{"slug":68,"name":69,"articles":70},"performance","Performance",8,{"slug":72,"name":73,"articles":74},"grammar","Grammar",6,{"slug":76,"name":77,"articles":78},"react","React",5,{"slug":80,"name":81,"articles":82},"node","Node",4,{"slug":84,"name":85,"articles":86},"ai","Ai",3,{"slug":88,"name":89,"articles":86},"html","Html",{"slug":91,"name":92,"articles":86},"tutorial","Tutorial"]