[{"data":1,"prerenderedAt":36700},["ShallowReactive",2],{"blog-all-posts":3},[4,2053,3843,8634,16488,17768,21455,25722,31217,33111,33307,34095,36003],{"id":5,"title":6,"body":7,"cover":2042,"date":2043,"description":2044,"extension":2045,"meta":2046,"navigation":335,"path":2047,"readingTime":712,"seo":2048,"stem":2049,"tags":2050,"__hash__":2052},"blog/blog/data-structures-names.md","It's just an array",{"type":8,"value":9,"toc":2029},"minimark",[10,36,39,44,47,58,114,126,244,251,254,378,393,404,408,415,477,512,531,552,555,559,566,725,734,749,753,759,828,883,887,989,1076,1083,1086,1090,1096,1103,1132,1135,1271,1289,1324,1348,1352,1355,1531,1534,1541,1545,1559,1562,1645,1658,1665,1669,1672,1683,1698,1702,1705,1788,1802,1814,1818,1821,1982,2000,2003,2006,2025],[11,12,13,14,18,19,22,23,27,28,31,32,35],"p",{},"C++ has a type called ",[15,16,17],"code",{},"std::list",". It's not an array. Python has a type called ",[15,20,21],{},"list",". It ",[24,25,26],"em",{},"is"," an array. Java has ",[15,29,30],{},"Stack",", which extends ",[15,33,34],{},"Vector",", which is an array. A binary heap is stored as a flat array. A float is an integer interpreted differently.",[11,37,38],{},"How much of what computer science calls \"data structures\" is actually a different thing - and how much is just an array with extra rules?",[40,41,43],"h2",{"id":42},"a-float-is-just-an-integer","A float is just an integer",[11,45,46],{},"This sounds wrong, but it's literally true.",[11,48,49,50,57],{},"A 64-bit floating-point number (",[51,52,56],"a",{"href":53,"rel":54},"https://en.wikipedia.org/wiki/Double-precision_floating-point_format",[55],"nofollow","IEEE 754 double-precision",") is stored as three groups of bits:",[59,60,61,77],"table",{},[62,63,64],"thead",{},[65,66,67,71,74],"tr",{},[68,69,70],"th",{},"Part",[68,72,73],{},"Bits",[68,75,76],{},"What it stores",[78,79,80,92,103],"tbody",{},[65,81,82,86,89],{},[83,84,85],"td",{},"Sign",[83,87,88],{},"1 bit",[83,90,91],{},"Positive or negative",[65,93,94,97,100],{},[83,95,96],{},"Exponent",[83,98,99],{},"11 bits",[83,101,102],{},"The power (scale)",[65,104,105,108,111],{},[83,106,107],{},"Mantissa",[83,109,110],{},"52 bits",[83,112,113],{},"The actual digits",[11,115,116,117,121,122,125],{},"All three parts are ",[118,119,120],"strong",{},"integers",". The hardware reads these 64 bits and ",[24,123,124],{},"interprets"," them as a floating-point number using a formula:",[11,127,128],{},[129,130,133],"span",{"className":131},[132],"katex",[134,135,137],"math",{"xmlns":136},"http://www.w3.org/1998/Math/MathML",[138,139,140,239],"semantics",{},[141,142,143,148,151,155,176,179,211,213,215,217,220,223,225,227,229,231,233,235,237],"mrow",{},[144,145,147],"mo",{"stretchy":146},"false","(",[144,149,150],{},"−",[152,153,154],"mn",{},"1",[156,157,158,161],"msup",{},[144,159,160],{"stretchy":146},")",[141,162,163,167,170,173],{},[164,165,166],"mi",{},"s",[164,168,169],{},"i",[164,171,172],{},"g",[164,174,175],{},"n",[144,177,178],{},"×",[156,180,181,184],{},[152,182,183],{},"2",[141,185,186,189,192,194,197,199,201,203,206,208],{},[164,187,188],{},"e",[164,190,191],{},"x",[164,193,11],{},[164,195,196],{},"o",[164,198,175],{},[164,200,188],{},[164,202,175],{},[164,204,205],{},"t",[144,207,150],{},[152,209,210],{},"1023",[144,212,178],{},[144,214,147],{"stretchy":146},[152,216,154],{},[144,218,219],{},"+",[164,221,222],{},"m",[164,224,51],{},[164,226,175],{},[164,228,205],{},[164,230,169],{},[164,232,166],{},[164,234,166],{},[164,236,51],{},[144,238,160],{"stretchy":146},[240,241,243],"annotation",{"encoding":242},"application/x-tex","(-1)^{sign} \\times 2^{exponent - 1023} \\times (1 + mantissa)",[11,245,246,247,250],{},"The bits themselves are indistinguishable from a regular 64-bit integer. The only difference is how the CPU ",[24,248,249],{},"reads"," them.",[11,252,253],{},"You can prove this in JavaScript:",[255,256,261],"pre",{"className":257,"code":258,"language":259,"meta":260,"style":260},"language-javascript shiki shiki-themes material-theme-lighter material-theme material-theme-palenight","const buffer = new ArrayBuffer(8)\nconst float = new Float64Array(buffer)\nconst int = new BigUint64Array(buffer)\n\nfloat[0] = 3.14\nconsole.log(int[0]) // 4614253070214989087n — same bits, read as integer\n","javascript","",[15,262,263,295,313,330,337,354],{"__ignoreMap":260},[129,264,267,271,275,279,282,286,288,292],{"class":265,"line":266},"line",1,[129,268,270],{"class":269},"spNyl","const",[129,272,274],{"class":273},"sTEyZ"," buffer ",[129,276,278],{"class":277},"sMK4o","=",[129,280,281],{"class":277}," new",[129,283,285],{"class":284},"s2Zo4"," ArrayBuffer",[129,287,147],{"class":273},[129,289,291],{"class":290},"sbssI","8",[129,293,294],{"class":273},")\n",[129,296,298,300,303,305,307,310],{"class":265,"line":297},2,[129,299,270],{"class":269},[129,301,302],{"class":273}," float ",[129,304,278],{"class":277},[129,306,281],{"class":277},[129,308,309],{"class":284}," Float64Array",[129,311,312],{"class":273},"(buffer)\n",[129,314,316,318,321,323,325,328],{"class":265,"line":315},3,[129,317,270],{"class":269},[129,319,320],{"class":273}," int ",[129,322,278],{"class":277},[129,324,281],{"class":277},[129,326,327],{"class":284}," BigUint64Array",[129,329,312],{"class":273},[129,331,333],{"class":265,"line":332},4,[129,334,336],{"emptyLinePlaceholder":335},true,"\n",[129,338,340,343,346,349,351],{"class":265,"line":339},5,[129,341,342],{"class":273},"float[",[129,344,345],{"class":290},"0",[129,347,348],{"class":273},"] ",[129,350,278],{"class":277},[129,352,353],{"class":290}," 3.14\n",[129,355,357,360,363,366,369,371,374],{"class":265,"line":356},6,[129,358,359],{"class":273},"console",[129,361,362],{"class":277},".",[129,364,365],{"class":284},"log",[129,367,368],{"class":273},"(int[",[129,370,345],{"class":290},[129,372,373],{"class":273},"]) ",[129,375,377],{"class":376},"sHwdD","// 4614253070214989087n — same bits, read as integer\n",[11,379,380,381,384,385,388,389,392],{},"The same 64 bits. The same memory. One interpretation says ",[15,382,383],{},"3.14",", the other says ",[15,386,387],{},"4614253070214989087",". A float it's a different ",[24,390,391],{},"lens"," on the same bits.",[11,394,395,396,399,400,403],{},"This is also why ",[15,397,398],{},"0.1 + 0.2 !== 0.3"," in every language that uses IEEE 754. The mantissa has 52 bits of precision. The decimal ",[15,401,402],{},"0.1"," is a repeating fraction in binary (like 1/3 is in decimal) - it can't be stored exactly, so the hardware rounds it. The \"float\" abstraction hides this until it bites you.",[40,405,407],{"id":406},"a-string-is-just-an-array-of-numbers","A string is just an array of numbers",[11,409,410,411,414],{},"A string like ",[15,412,413],{},"\"hello\""," is stored as:",[255,416,418],{"className":257,"code":417,"language":259,"meta":260,"style":260},"'hello'.split('').map(c => c.charCodeAt(0))\n// [104, 101, 108, 108, 111]\n",[15,419,420,472],{"__ignoreMap":260},[129,421,422,425,429,431,433,436,438,441,443,445,448,450,454,457,460,462,465,467,469],{"class":265,"line":266},[129,423,424],{"class":277},"'",[129,426,428],{"class":427},"sfazB","hello",[129,430,424],{"class":277},[129,432,362],{"class":277},[129,434,435],{"class":284},"split",[129,437,147],{"class":273},[129,439,440],{"class":277},"''",[129,442,160],{"class":273},[129,444,362],{"class":277},[129,446,447],{"class":284},"map",[129,449,147],{"class":273},[129,451,453],{"class":452},"sHdIc","c",[129,455,456],{"class":269}," =>",[129,458,459],{"class":273}," c",[129,461,362],{"class":277},[129,463,464],{"class":284},"charCodeAt",[129,466,147],{"class":273},[129,468,345],{"class":290},[129,470,471],{"class":273},"))\n",[129,473,474],{"class":265,"line":297},[129,475,476],{"class":376},"// [104, 101, 108, 108, 111]\n",[11,478,479,480,485,486,488,489,492,493,496,497,500,501,500,504,507,508,511],{},"Five numbers. That's it. Each character is a ",[51,481,484],{"href":482,"rel":483},"https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/charCodeAt",[55],"Unicode code point"," - a number between 0 and 65,535 (for the Basic Multilingual Plane). The string ",[15,487,413],{}," and the array ",[15,490,491],{},"[104, 101, 108, 108, 111]"," are the same data. The difference is that ",[15,494,495],{},"String"," gives you ",[15,498,499],{},".toUpperCase()",", ",[15,502,503],{},".slice()",[15,505,506],{},".includes()"," - methods that ",[24,509,510],{},"interpret"," those numbers as text.",[11,513,514,515,518,519,522,523,526,527,530],{},"In C, this isn't even hidden. A string is literally ",[15,516,517],{},"char[]"," - an array of characters. And ",[15,520,521],{},"char"," is literally an 8-bit integer. ",[15,524,525],{},"'A'"," is just the number ",[15,528,529],{},"65",". There's no separate \"character type\" that's fundamentally different from a number - it's a number the compiler agrees to print as a letter.",[255,532,535],{"className":533,"code":534,"language":453,"meta":260,"style":260},"language-c shiki shiki-themes material-theme-lighter material-theme material-theme-palenight","char c = 65;\nprintf(\"%c\", c); // prints 'A'\nprintf(\"%d\", c); // prints 65\n",[15,536,537,542,547],{"__ignoreMap":260},[129,538,539],{"class":265,"line":266},[129,540,541],{},"char c = 65;\n",[129,543,544],{"class":265,"line":297},[129,545,546],{},"printf(\"%c\", c); // prints 'A'\n",[129,548,549],{"class":265,"line":315},[129,550,551],{},"printf(\"%d\", c); // prints 65\n",[11,553,554],{},"Same bits. Different interpretation. Again.",[40,556,558],{"id":557},"a-stack-is-just-an-array-with-rules","A stack is just an array with rules",[11,560,561,562,565],{},"A ",[118,563,564],{},"stack"," is an array where you're only allowed to add and remove from the end. That's it. The entire concept of a \"stack\" is an array with a restriction:",[255,567,569],{"className":257,"code":568,"language":259,"meta":260,"style":260},"// \"Stack\"\nconst stack = []\nstack.push('a')  // add to end\nstack.push('b')\nstack.pop()      // remove from end → 'b'\n\n// Array\nconst arr = []\narr.push('a')    // ...same thing\narr.push('b')\narr.pop()        // ...same thing → 'b'\n",[15,570,571,576,588,611,630,645,649,655,667,691,710],{"__ignoreMap":260},[129,572,573],{"class":265,"line":266},[129,574,575],{"class":376},"// \"Stack\"\n",[129,577,578,580,583,585],{"class":265,"line":297},[129,579,270],{"class":269},[129,581,582],{"class":273}," stack ",[129,584,278],{"class":277},[129,586,587],{"class":273}," []\n",[129,589,590,592,594,597,599,601,603,605,608],{"class":265,"line":315},[129,591,564],{"class":273},[129,593,362],{"class":277},[129,595,596],{"class":284},"push",[129,598,147],{"class":273},[129,600,424],{"class":277},[129,602,51],{"class":427},[129,604,424],{"class":277},[129,606,607],{"class":273},")  ",[129,609,610],{"class":376},"// add to end\n",[129,612,613,615,617,619,621,623,626,628],{"class":265,"line":332},[129,614,564],{"class":273},[129,616,362],{"class":277},[129,618,596],{"class":284},[129,620,147],{"class":273},[129,622,424],{"class":277},[129,624,625],{"class":427},"b",[129,627,424],{"class":277},[129,629,294],{"class":273},[129,631,632,634,636,639,642],{"class":265,"line":339},[129,633,564],{"class":273},[129,635,362],{"class":277},[129,637,638],{"class":284},"pop",[129,640,641],{"class":273},"()      ",[129,643,644],{"class":376},"// remove from end → 'b'\n",[129,646,647],{"class":265,"line":356},[129,648,336],{"emptyLinePlaceholder":335},[129,650,652],{"class":265,"line":651},7,[129,653,654],{"class":376},"// Array\n",[129,656,658,660,663,665],{"class":265,"line":657},8,[129,659,270],{"class":269},[129,661,662],{"class":273}," arr ",[129,664,278],{"class":277},[129,666,587],{"class":273},[129,668,670,673,675,677,679,681,683,685,688],{"class":265,"line":669},9,[129,671,672],{"class":273},"arr",[129,674,362],{"class":277},[129,676,596],{"class":284},[129,678,147],{"class":273},[129,680,424],{"class":277},[129,682,51],{"class":427},[129,684,424],{"class":277},[129,686,687],{"class":273},")    ",[129,689,690],{"class":376},"// ...same thing\n",[129,692,694,696,698,700,702,704,706,708],{"class":265,"line":693},10,[129,695,672],{"class":273},[129,697,362],{"class":277},[129,699,596],{"class":284},[129,701,147],{"class":273},[129,703,424],{"class":277},[129,705,625],{"class":427},[129,707,424],{"class":277},[129,709,294],{"class":273},[129,711,713,715,717,719,722],{"class":265,"line":712},11,[129,714,672],{"class":273},[129,716,362],{"class":277},[129,718,638],{"class":284},[129,720,721],{"class":273},"()        ",[129,723,724],{"class":376},"// ...same thing → 'b'\n",[11,726,727,728,733],{},"There is no difference. None. Zero. In JavaScript, Python, Go, and most languages - a stack IS an array. ",[51,729,732],{"href":730,"rel":731},"https://docs.python.org/3/tutorial/datastructures.html#using-lists-as-stacks",[55],"Python's documentation"," literally has a section titled \"Using Lists as Stacks.\" It doesn't even pretend these are different things.",[11,735,736,737,739,740,742,743,748],{},"Java created a dedicated ",[15,738,30],{}," class. It extends ",[15,741,34],{}," (which is just a synchronized array). The entire class is ",[51,744,747],{"href":745,"rel":746},"https://docs.oracle.com/javase/8/docs/api/java/util/Stack.html",[55],"six methods"," that wrap array operations.",[40,750,752],{"id":751},"a-queue-is-just-an-array-with-different-rules","A queue is just an array with different rules",[11,754,561,755,758],{},[118,756,757],{},"queue"," is an array where you add to the end and remove from the front. In JavaScript:",[255,760,762],{"className":257,"code":761,"language":259,"meta":260,"style":260},"const queue = []\nqueue.push('first')\nqueue.push('second')\nqueue.shift() // 'first'\n",[15,763,764,775,794,813],{"__ignoreMap":260},[129,765,766,768,771,773],{"class":265,"line":266},[129,767,270],{"class":269},[129,769,770],{"class":273}," queue ",[129,772,278],{"class":277},[129,774,587],{"class":273},[129,776,777,779,781,783,785,787,790,792],{"class":265,"line":297},[129,778,757],{"class":273},[129,780,362],{"class":277},[129,782,596],{"class":284},[129,784,147],{"class":273},[129,786,424],{"class":277},[129,788,789],{"class":427},"first",[129,791,424],{"class":277},[129,793,294],{"class":273},[129,795,796,798,800,802,804,806,809,811],{"class":265,"line":315},[129,797,757],{"class":273},[129,799,362],{"class":277},[129,801,596],{"class":284},[129,803,147],{"class":273},[129,805,424],{"class":277},[129,807,808],{"class":427},"second",[129,810,424],{"class":277},[129,812,294],{"class":273},[129,814,815,817,819,822,825],{"class":265,"line":332},[129,816,757],{"class":273},[129,818,362],{"class":277},[129,820,821],{"class":284},"shift",[129,823,824],{"class":273},"() ",[129,826,827],{"class":376},"// 'first'\n",[11,829,830,833,834,855,856,861,862,882],{},[15,831,832],{},"shift()"," is theoretically ",[129,835,837],{"className":836},[132],[134,838,839],{"xmlns":136},[138,840,841,852],{},[141,842,843,846,848,850],{},[164,844,845],{},"O",[144,847,147],{"stretchy":146},[164,849,175],{},[144,851,160],{"stretchy":146},[240,853,854],{"encoding":242},"O(n)"," because the engine has to reindex the remaining elements. In practice, ",[51,857,860],{"href":858,"rel":859},"https://lannonbr.com/blog/2020-01-27-shift-optimizations/",[55],"V8 and SpiderMonkey have optimized this"," so aggressively that for arrays under ~50,000 elements it's effectively ",[129,863,865],{"className":864},[132],[134,866,867],{"xmlns":136},[138,868,869,879],{},[141,870,871,873,875,877],{},[164,872,845],{},[144,874,147],{"stretchy":146},[152,876,154],{},[144,878,160],{"stretchy":146},[240,880,881],{"encoding":242},"O(1)",". SpiderMonkey doesn't even move the data - it just increments an internal pointer.",[40,884,886],{"id":885},"a-heap-is-an-array-that-pretends-to-be-a-tree","A heap is an array that pretends to be a tree",[11,888,561,889,892,893,898,899,912,913,947,948,968,969,362],{},[118,890,891],{},"binary heap"," - the structure behind priority queues, used in Dijkstra's algorithm and task schedulers - is ",[51,894,897],{"href":895,"rel":896},"https://en.wikipedia.org/wiki/Binary_heap#Heap_implementation",[55],"stored as a flat array",". The parent of element at index ",[129,900,902],{"className":901},[132],[134,903,904],{"xmlns":136},[138,905,906,910],{},[141,907,908],{},[164,909,169],{},[240,911,169],{"encoding":242}," is at ",[129,914,916],{"className":915},[132],[134,917,918],{"xmlns":136},[138,919,920,944],{},[141,921,922,925,927,929,931,933,935,939,941],{},[144,923,924],{"stretchy":146},"⌊",[144,926,147],{"stretchy":146},[164,928,169],{},[144,930,150],{},[152,932,154],{},[144,934,160],{"stretchy":146},[164,936,938],{"mathvariant":937},"normal","/",[152,940,183],{},[144,942,943],{"stretchy":146},"⌋",[240,945,946],{"encoding":242},"\\lfloor(i - 1) / 2\\rfloor",". The children are at ",[129,949,951],{"className":950},[132],[134,952,953],{"xmlns":136},[138,954,955,965],{},[141,956,957,959,961,963],{},[152,958,183],{},[164,960,169],{},[144,962,219],{},[152,964,154],{},[240,966,967],{"encoding":242},"2i + 1"," and ",[129,970,972],{"className":971},[132],[134,973,974],{"xmlns":136},[138,975,976,986],{},[141,977,978,980,982,984],{},[152,979,183],{},[164,981,169],{},[144,983,219],{},[152,985,183],{},[240,987,988],{"encoding":242},"2i + 2",[255,990,992],{"className":257,"code":991,"language":259,"meta":260,"style":260},"// This array IS a valid min-heap:\nconst heap = [1, 3, 2, 7, 6, 4, 5]\n\n//          1          ← index 0\n//        /   \\\n//       3     2       ← indices 1, 2\n//      / \\   / \\\n//     7   6 4   5     ← indices 3, 4, 5, 6\n",[15,993,994,999,1047,1051,1056,1061,1066,1071],{"__ignoreMap":260},[129,995,996],{"class":265,"line":266},[129,997,998],{"class":376},"// This array IS a valid min-heap:\n",[129,1000,1001,1003,1006,1008,1011,1013,1016,1019,1021,1024,1026,1029,1031,1034,1036,1039,1041,1044],{"class":265,"line":297},[129,1002,270],{"class":269},[129,1004,1005],{"class":273}," heap ",[129,1007,278],{"class":277},[129,1009,1010],{"class":273}," [",[129,1012,154],{"class":290},[129,1014,1015],{"class":277},",",[129,1017,1018],{"class":290}," 3",[129,1020,1015],{"class":277},[129,1022,1023],{"class":290}," 2",[129,1025,1015],{"class":277},[129,1027,1028],{"class":290}," 7",[129,1030,1015],{"class":277},[129,1032,1033],{"class":290}," 6",[129,1035,1015],{"class":277},[129,1037,1038],{"class":290}," 4",[129,1040,1015],{"class":277},[129,1042,1043],{"class":290}," 5",[129,1045,1046],{"class":273},"]\n",[129,1048,1049],{"class":265,"line":315},[129,1050,336],{"emptyLinePlaceholder":335},[129,1052,1053],{"class":265,"line":332},[129,1054,1055],{"class":376},"//          1          ← index 0\n",[129,1057,1058],{"class":265,"line":339},[129,1059,1060],{"class":376},"//        /   \\\n",[129,1062,1063],{"class":265,"line":356},[129,1064,1065],{"class":376},"//       3     2       ← indices 1, 2\n",[129,1067,1068],{"class":265,"line":651},[129,1069,1070],{"class":376},"//      / \\   / \\\n",[129,1072,1073],{"class":265,"line":657},[129,1074,1075],{"class":376},"//     7   6 4   5     ← indices 3, 4, 5, 6\n",[11,1077,1078,1079,1082],{},"No nodes. No pointers. No ",[15,1080,1081],{},"TreeNode"," class. Just an array with a formula for navigating it. The \"tree\" exists only in our imagination and in the index math. The memory layout is a contiguous array - identical to any other.",[11,1084,1085],{},"A float is integers with a formula. A heap is an array with a formula. The pattern keeps repeating.",[40,1087,1089],{"id":1088},"what-about-linked-lists","What about linked lists?",[11,1091,1092,1093,362],{},"This is where the pattern breaks. ",[118,1094,1095],{},"Linked lists are genuinely different",[11,1097,1098,1099,1102],{},"An array stores elements next to each other in memory. A linked list stores each element in a separate ",[118,1100,1101],{},"node"," scattered across the heap, with each node holding a pointer to the next one:",[255,1104,1106],{"className":257,"code":1105,"language":259,"meta":260,"style":260},"// Array: [10, 20, 30] → contiguous in memory\n// Memory: | 10 | 20 | 30 |\n\n// Linked list: 10 → 20 → 30 → null\n// Memory: | 10 | ptr | ... garbage ... | 20 | ptr | ... | 30 | null |\n",[15,1107,1108,1113,1118,1122,1127],{"__ignoreMap":260},[129,1109,1110],{"class":265,"line":266},[129,1111,1112],{"class":376},"// Array: [10, 20, 30] → contiguous in memory\n",[129,1114,1115],{"class":265,"line":297},[129,1116,1117],{"class":376},"// Memory: | 10 | 20 | 30 |\n",[129,1119,1120],{"class":265,"line":315},[129,1121,336],{"emptyLinePlaceholder":335},[129,1123,1124],{"class":265,"line":332},[129,1125,1126],{"class":376},"// Linked list: 10 → 20 → 30 → null\n",[129,1128,1129],{"class":265,"line":339},[129,1130,1131],{"class":376},"// Memory: | 10 | ptr | ... garbage ... | 20 | ptr | ... | 30 | null |\n",[11,1133,1134],{},"This isn't just a naming difference. The memory layout is fundamentally different, and it has real consequences:",[59,1136,1137,1149],{},[62,1138,1139],{},[65,1140,1141,1143,1146],{},[68,1142],{},[68,1144,1145],{},"Array",[68,1147,1148],{},"Linked list",[78,1150,1151,1200,1249,1260],{},[65,1152,1153,1156,1178],{},[83,1154,1155],{},"Access element by index",[83,1157,1158,1177],{},[129,1159,1161],{"className":1160},[132],[134,1162,1163],{"xmlns":136},[138,1164,1165,1175],{},[141,1166,1167,1169,1171,1173],{},[164,1168,845],{},[144,1170,147],{"stretchy":146},[152,1172,154],{},[144,1174,160],{"stretchy":146},[240,1176,881],{"encoding":242}," - direct offset",[83,1179,1180,1199],{},[129,1181,1183],{"className":1182},[132],[134,1184,1185],{"xmlns":136},[138,1186,1187,1197],{},[141,1188,1189,1191,1193,1195],{},[164,1190,845],{},[144,1192,147],{"stretchy":146},[164,1194,175],{},[144,1196,160],{"stretchy":146},[240,1198,854],{"encoding":242}," - walk from head",[65,1201,1202,1205,1227],{},[83,1203,1204],{},"Insert at known position",[83,1206,1207,1226],{},[129,1208,1210],{"className":1209},[132],[134,1211,1212],{"xmlns":136},[138,1213,1214,1224],{},[141,1215,1216,1218,1220,1222],{},[164,1217,845],{},[144,1219,147],{"stretchy":146},[164,1221,175],{},[144,1223,160],{"stretchy":146},[240,1225,854],{"encoding":242}," - shift everything after",[83,1228,1229,1248],{},[129,1230,1232],{"className":1231},[132],[134,1233,1234],{"xmlns":136},[138,1235,1236,1246],{},[141,1237,1238,1240,1242,1244],{},[164,1239,845],{},[144,1241,147],{"stretchy":146},[152,1243,154],{},[144,1245,160],{"stretchy":146},[240,1247,881],{"encoding":242}," - repoint two pointers",[65,1250,1251,1254,1257],{},[83,1252,1253],{},"Memory layout",[83,1255,1256],{},"Contiguous, cache-friendly",[83,1258,1259],{},"Scattered, cache-hostile",[65,1261,1262,1265,1268],{},[83,1263,1264],{},"Memory overhead",[83,1266,1267],{},"Just the data",[83,1269,1270],{},"Data + pointer per node",[11,1272,1273,1274,1280,1281,1288],{},"This is also where naming gets confusing. In C++, ",[51,1275,1278],{"href":1276,"rel":1277},"https://en.cppreference.com/w/cpp/container/list",[55],[15,1279,17],{}," is specifically a doubly-linked list, and ",[51,1282,1285],{"href":1283,"rel":1284},"https://en.cppreference.com/w/cpp/container/vector",[55],[15,1286,1287],{},"std::vector"," is a dynamic array. \"Reverse a list\" and \"reverse a vector\" sound like the same task, but the memory layout is completely different.",[11,1290,1291,1292,1297,1298,1300,1301,1303,1304,1323],{},"That said - ",[51,1293,1296],{"href":1294,"rel":1295},"https://isocpp.org/blog/2014/06/stroustrup-lists",[55],"Bjarne Stroustrup"," himself (the creator of C++) demonstrated that ",[15,1299,1287],{}," beats ",[15,1302,17],{}," in almost every benchmark, including insertion-heavy workloads where linked lists theoretically win. The reason: CPU cache. Contiguous memory lets the prefetcher do its job. Scattered nodes cause cache misses on every access. The theoretical ",[129,1305,1307],{"className":1306},[132],[134,1308,1309],{"xmlns":136},[138,1310,1311,1321],{},[141,1312,1313,1315,1317,1319],{},[164,1314,845],{},[144,1316,147],{"stretchy":146},[152,1318,154],{},[144,1320,160],{"stretchy":146},[240,1322,881],{"encoding":242}," insertion of linked lists loses to the practical cache efficiency of arrays by factors of 10x-80x.",[11,1325,1326,1331,1332,1335,1336,1339,1340,1343,1344,1347],{},[51,1327,1330],{"href":1328,"rel":1329},"https://doc.rust-lang.org/std/collections/index.html",[55],"Rust's standard library"," goes even further: \"It is almost always better to use ",[15,1333,1334],{},"Vec"," or ",[15,1337,1338],{},"VecDeque"," instead of ",[15,1341,1342],{},"LinkedList",".\" When the language that ",[24,1345,1346],{},"ships"," a linked list tells you not to use it.",[40,1349,1351],{"id":1350},"trees-are-objects-pointing-to-objects","Trees are objects pointing to objects",[11,1353,1354],{},"A tree is a node with a value and references to child nodes. In JavaScript:",[255,1356,1358],{"className":257,"code":1357,"language":259,"meta":260,"style":260},"const tree = {\n  value: 1,\n  left: {\n    value: 2,\n    left: { value: 4, left: null, right: null },\n    right: { value: 5, left: null, right: null }\n  },\n  right: {\n    value: 3,\n    left: null,\n    right: null\n  }\n}\n",[15,1359,1360,1372,1387,1396,1407,1445,1477,1482,1491,1501,1510,1519,1525],{"__ignoreMap":260},[129,1361,1362,1364,1367,1369],{"class":265,"line":266},[129,1363,270],{"class":269},[129,1365,1366],{"class":273}," tree ",[129,1368,278],{"class":277},[129,1370,1371],{"class":277}," {\n",[129,1373,1374,1378,1381,1384],{"class":265,"line":297},[129,1375,1377],{"class":1376},"swJcz","  value",[129,1379,1380],{"class":277},":",[129,1382,1383],{"class":290}," 1",[129,1385,1386],{"class":277},",\n",[129,1388,1389,1392,1394],{"class":265,"line":315},[129,1390,1391],{"class":1376},"  left",[129,1393,1380],{"class":277},[129,1395,1371],{"class":277},[129,1397,1398,1401,1403,1405],{"class":265,"line":332},[129,1399,1400],{"class":1376},"    value",[129,1402,1380],{"class":277},[129,1404,1023],{"class":290},[129,1406,1386],{"class":277},[129,1408,1409,1412,1414,1417,1420,1422,1424,1426,1429,1431,1434,1437,1439,1442],{"class":265,"line":339},[129,1410,1411],{"class":1376},"    left",[129,1413,1380],{"class":277},[129,1415,1416],{"class":277}," {",[129,1418,1419],{"class":1376}," value",[129,1421,1380],{"class":277},[129,1423,1038],{"class":290},[129,1425,1015],{"class":277},[129,1427,1428],{"class":1376}," left",[129,1430,1380],{"class":277},[129,1432,1433],{"class":277}," null,",[129,1435,1436],{"class":1376}," right",[129,1438,1380],{"class":277},[129,1440,1441],{"class":277}," null",[129,1443,1444],{"class":277}," },\n",[129,1446,1447,1450,1452,1454,1456,1458,1460,1462,1464,1466,1468,1470,1472,1474],{"class":265,"line":356},[129,1448,1449],{"class":1376},"    right",[129,1451,1380],{"class":277},[129,1453,1416],{"class":277},[129,1455,1419],{"class":1376},[129,1457,1380],{"class":277},[129,1459,1043],{"class":290},[129,1461,1015],{"class":277},[129,1463,1428],{"class":1376},[129,1465,1380],{"class":277},[129,1467,1433],{"class":277},[129,1469,1436],{"class":1376},[129,1471,1380],{"class":277},[129,1473,1441],{"class":277},[129,1475,1476],{"class":277}," }\n",[129,1478,1479],{"class":265,"line":651},[129,1480,1481],{"class":277},"  },\n",[129,1483,1484,1487,1489],{"class":265,"line":657},[129,1485,1486],{"class":1376},"  right",[129,1488,1380],{"class":277},[129,1490,1371],{"class":277},[129,1492,1493,1495,1497,1499],{"class":265,"line":669},[129,1494,1400],{"class":1376},[129,1496,1380],{"class":277},[129,1498,1018],{"class":290},[129,1500,1386],{"class":277},[129,1502,1503,1505,1507],{"class":265,"line":693},[129,1504,1411],{"class":1376},[129,1506,1380],{"class":277},[129,1508,1509],{"class":277}," null,\n",[129,1511,1512,1514,1516],{"class":265,"line":712},[129,1513,1449],{"class":1376},[129,1515,1380],{"class":277},[129,1517,1518],{"class":277}," null\n",[129,1520,1522],{"class":265,"line":1521},12,[129,1523,1524],{"class":277},"  }\n",[129,1526,1528],{"class":265,"line":1527},13,[129,1529,1530],{"class":277},"}\n",[11,1532,1533],{},"It's nested objects. That's all a tree is - objects that reference other objects. The \"tree\" is a pattern of how you organize the references.",[11,1535,1536,1537,1540],{},"And references (pointers) are just ",[118,1538,1539],{},"numbers",". A pointer is a memory address, which is a 32-bit or 64-bit integer. So a tree is objects containing numbers that point to other objects containing numbers. Which are stored as... bits in memory. Which are interpreted as... etc. etc.",[40,1542,1544],{"id":1543},"the-abstraction-stack","The abstraction stack",[1546,1547,1548,1551],"blockquote",{},[11,1549,1550],{},"\"All problems in computer science can be solved by another level of indirection... except for the problem of too many levels of indirection.\"",[11,1552,1553,1558],{},[51,1554,1557],{"href":1555,"rel":1556},"https://en.wikipedia.org/wiki/David_Wheeler_(computer_scientist)",[55],"David Wheeler",", one of the first computer science PhDs in history",[11,1560,1561],{},"Here is every type discussed so far, stripped to what it actually is:",[59,1563,1564,1574],{},[62,1565,1566],{},[65,1567,1568,1571],{},[68,1569,1570],{},"What we call it",[68,1572,1573],{},"What it actually is",[78,1575,1576,1584,1592,1599,1606,1614,1622,1629,1637],{},[65,1577,1578,1581],{},[83,1579,1580],{},"Float",[83,1582,1583],{},"Integer bits, interpreted with a formula",[65,1585,1586,1589],{},[83,1587,1588],{},"Character",[83,1590,1591],{},"A number (Unicode code point)",[65,1593,1594,1596],{},[83,1595,495],{},[83,1597,1598],{},"Array of numbers (characters)",[65,1600,1601,1603],{},[83,1602,30],{},[83,1604,1605],{},"Array, restricted to push/pop",[65,1607,1608,1611],{},[83,1609,1610],{},"Queue",[83,1612,1613],{},"Array, restricted to push/shift",[65,1615,1616,1619],{},[83,1617,1618],{},"Heap",[83,1620,1621],{},"Array, navigated with index math",[65,1623,1624,1626],{},[83,1625,1148],{},[83,1627,1628],{},"Objects (nodes) containing numbers (pointers)",[65,1630,1631,1634],{},[83,1632,1633],{},"Tree",[83,1635,1636],{},"Objects containing references to other objects",[65,1638,1639,1642],{},[83,1640,1641],{},"Hash map",[83,1643,1644],{},"Array of buckets, indexed by a hash function",[11,1646,1647,1648,500,1650,1653,1654,1657],{},"Everything reduces to three primitives: ",[118,1649,1539],{},[118,1651,1652],{},"arrays of numbers",", and ",[118,1655,1656],{},"pointers"," (which are numbers). The rest is interpretation. Rules. Agreements between the programmer and the compiler about how to read the same bits.",[11,1659,1660,1661,1664],{},"This is, in a very real sense, the entire field of computer science: inventing useful ",[24,1662,1663],{},"interpretations"," of numbers stored in arrays.",[40,1666,1668],{"id":1667},"so-why-the-separate-names","So why the separate names?",[11,1670,1671],{},"Naming creates a shared vocabulary. When someone says \"use a stack,\" every developer knows: LIFO, push, pop. When someone says \"use a priority queue\", the interface is clear: elements come out sorted by priority, probably backed by a heap.",[11,1673,1674,1675,1678,1679,1682],{},"The names are ",[118,1676,1677],{},"communication shortcuts",". They work the same way design patterns work - not because ",[15,1680,1681],{},"Observer"," is fundamentally different from \"a list of callbacks you loop through and call,\" but because the name lets two developers skip 10 minutes of explanation.",[11,1684,1685,1686,1691,1692,1697],{},"The question is whether the naming needs to come with separate types, dedicated implementations, and ",[51,1687,1690],{"href":1688,"rel":1689},"https://en.wikipedia.org/wiki/Introduction_to_Algorithms",[55],"entire textbook chapters"," - or whether the names alone are enough. ",[51,1693,1696],{"href":1694,"rel":1695},"https://x.com/mxcl/status/608682016205344768",[55],"Max Howell",", the creator of Homebrew, was rejected by Google because he couldn't invert a binary tree on a whiteboard - while 90% of Google's engineers were using his software daily.",[40,1699,1701],{"id":1700},"half-the-languages-skipped-the-ceremony","Half the languages skipped the ceremony",[11,1703,1704],{},"Look at what the most popular languages actually ship:",[59,1706,1707,1723],{},[62,1708,1709],{},[65,1710,1711,1714,1717,1720],{},[68,1712,1713],{},"Structure",[68,1715,1716],{},"JavaScript",[68,1718,1719],{},"Python",[68,1721,1722],{},"Go",[78,1724,1725,1741,1751,1764,1774],{},[65,1726,1727,1730,1733,1735],{},[83,1728,1729],{},"Linked List",[83,1731,1732],{},"No",[83,1734,1732],{},[83,1736,1737,1740],{},[15,1738,1739],{},"container/list"," (rarely used)",[65,1742,1743,1745,1747,1749],{},[83,1744,30],{},[83,1746,1732],{},[83,1748,1732],{},[83,1750,1732],{},[65,1752,1753,1755,1757,1762],{},[83,1754,1610],{},[83,1756,1732],{},[83,1758,1759],{},[15,1760,1761],{},"collections.deque",[83,1763,1732],{},[65,1765,1766,1768,1770,1772],{},[83,1767,1633],{},[83,1769,1732],{},[83,1771,1732],{},[83,1773,1732],{},[65,1775,1776,1778,1780,1785],{},[83,1777,1618],{},[83,1779,1732],{},[83,1781,1782],{},[15,1783,1784],{},"heapq",[83,1786,1787],{},"Interface only",[11,1789,1790,1791,1793,1794,1793,1796,1798,1799,1801],{},"JavaScript doesn't have a ",[15,1792,30],{},", a ",[15,1795,1610],{},[15,1797,1342],{},", or a ",[15,1800,1633],{},". Python calls its dynamic array a \"list\" (which, ironically, is NOT a linked list - it's a contiguous array). Go uses slices for everything.",[11,1803,1804,1805,1807,1808,968,1811,362],{},"Millions of developers build production software every day in these languages without dedicated stack or queue types. Because you don't need a type called ",[15,1806,30],{}," to use an array as a stack. You just need ",[15,1809,1810],{},"push()",[15,1812,1813],{},"pop()",[40,1815,1817],{"id":1816},"when-the-names-actually-matter","When the names actually matter",[11,1819,1820],{},"There are cases where the distinction is genuinely important:",[1822,1823,1824,1857,1889,1915],"ul",{},[1825,1826,1827,1830,1831,1856],"li",{},[118,1828,1829],{},"Database internals"," use B-trees and B+ trees for indexing - the tree structure enables ",[129,1832,1834],{"className":1833},[132],[134,1835,1836],{"xmlns":136},[138,1837,1838,1853],{},[141,1839,1840,1842,1844,1846,1849,1851],{},[164,1841,845],{},[144,1843,147],{"stretchy":146},[164,1845,365],{},[144,1847,1848],{},"⁡",[164,1850,175],{},[144,1852,160],{"stretchy":146},[240,1854,1855],{"encoding":242},"O(\\log n)"," lookups across billions of rows. An array can't do that.",[1825,1858,1859,1862,1863,1868,1869,1888],{},[118,1860,1861],{},"Your framework's router"," ",[51,1864,1867],{"href":1865,"rel":1866},"https://github.com/delvedor/find-my-way",[55],"probably uses a trie"," (a specialized tree) for URL matching. Flat arrays would make routing ",[129,1870,1872],{"className":1871},[132],[134,1873,1874],{"xmlns":136},[138,1875,1876,1886],{},[141,1877,1878,1880,1882,1884],{},[164,1879,845],{},[144,1881,147],{"stretchy":146},[164,1883,175],{},[144,1885,160],{"stretchy":146},[240,1887,854],{"encoding":242}," per request.",[1825,1890,1891,1894,1895,1914],{},[118,1892,1893],{},"Operating system schedulers"," use priority queues (heaps) for process scheduling. The heap property - ",[129,1896,1898],{"className":1897},[132],[134,1899,1900],{"xmlns":136},[138,1901,1902,1912],{},[141,1903,1904,1906,1908,1910],{},[164,1905,845],{},[144,1907,147],{"stretchy":146},[152,1909,154],{},[144,1911,160],{"stretchy":146},[240,1913,881],{"encoding":242}," access to the highest priority element - is genuinely useful here.",[1825,1916,1917,1920,1921,1339,1946,1981],{},[118,1918,1919],{},"Dijkstra's algorithm"," uses a priority queue to always process the closest unvisited node first. You could use an unsorted array and scan for the minimum each time, but that makes the algorithm ",[129,1922,1924],{"className":1923},[132],[134,1925,1926],{"xmlns":136},[138,1927,1928,1943],{},[141,1929,1930,1932,1934,1941],{},[164,1931,845],{},[144,1933,147],{"stretchy":146},[156,1935,1936,1939],{},[164,1937,1938],{},"V",[152,1940,183],{},[144,1942,160],{"stretchy":146},[240,1944,1945],{"encoding":242},"O(V^2)",[129,1947,1949],{"className":1948},[132],[134,1950,1951],{"xmlns":136},[138,1952,1953,1978],{},[141,1954,1955,1957,1959,1961,1963,1965,1968,1970,1972,1974,1976],{},[164,1956,845],{},[144,1958,147],{"stretchy":146},[144,1960,147],{"stretchy":146},[164,1962,1938],{},[144,1964,219],{},[164,1966,1967],{},"E",[144,1969,160],{"stretchy":146},[164,1971,365],{},[144,1973,1848],{},[164,1975,1938],{},[144,1977,160],{"stretchy":146},[240,1979,1980],{"encoding":242},"O((V + E) \\log V)",". The named structure carries a real performance guarantee.",[11,1983,1984,1985,1988,1989,1992,1993,1996,1997,362],{},"These are all ",[118,1986,1987],{},"infrastructure-level"," concerns. Databases, routers, schedulers, graph algorithms. If you're building a web app, a REST API, a dashboard - you interact with the ",[24,1990,1991],{},"results"," of these structures, not the structures themselves. The DOM is a tree, but you use ",[15,1994,1995],{},"querySelector()",". The router uses a trie, but you write ",[15,1998,1999],{},"app.get('/users/:id', handler)",[2001,2002],"hr",{},[11,2004,2005],{},"Everything is just arrays, numbers, and interpretation. The names are useful when they communicate intent. They're less useful when they become the point.",[11,2007,2008,2009,1793,2014,2017,2018,2021,2022,362],{},"Maybe it's because I'm a web developer, or maybe it's because I'm a JavaScript developer and in my world, ",[51,2010,2013],{"href":2011,"rel":2012},"https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Working_with_objects",[55],"almost everything is an object",[15,2015,2016],{},"Map"," is an object with better manners, an array is a stack, queue, list, and ",[15,2019,2020],{},"typeof null === 'object'"," is not a bug but a ",[24,2023,2024],{},"feature",[2026,2027,2028],"style",{},"html pre.shiki code .spNyl, html code.shiki .spNyl{--shiki-light:#9C3EDA;--shiki-default:#C792EA;--shiki-dark:#C792EA}html pre.shiki code .sTEyZ, html code.shiki .sTEyZ{--shiki-light:#90A4AE;--shiki-default:#EEFFFF;--shiki-dark:#BABED8}html pre.shiki code .sMK4o, html code.shiki .sMK4o{--shiki-light:#39ADB5;--shiki-default:#89DDFF;--shiki-dark:#89DDFF}html pre.shiki code .s2Zo4, html code.shiki .s2Zo4{--shiki-light:#6182B8;--shiki-default:#82AAFF;--shiki-dark:#82AAFF}html pre.shiki code .sbssI, html code.shiki .sbssI{--shiki-light:#F76D47;--shiki-default:#F78C6C;--shiki-dark:#F78C6C}html pre.shiki code .sHwdD, html code.shiki .sHwdD{--shiki-light:#90A4AE;--shiki-light-font-style:italic;--shiki-default:#546E7A;--shiki-default-font-style:italic;--shiki-dark:#676E95;--shiki-dark-font-style:italic}html .light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html.light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .sfazB, html code.shiki .sfazB{--shiki-light:#91B859;--shiki-default:#C3E88D;--shiki-dark:#C3E88D}html pre.shiki code .sHdIc, html code.shiki .sHdIc{--shiki-light:#90A4AE;--shiki-light-font-style:italic;--shiki-default:#EEFFFF;--shiki-default-font-style:italic;--shiki-dark:#BABED8;--shiki-dark-font-style:italic}html pre.shiki code .swJcz, html code.shiki .swJcz{--shiki-light:#E53935;--shiki-default:#F07178;--shiki-dark:#F07178}",{"title":260,"searchDepth":297,"depth":297,"links":2030},[2031,2032,2033,2034,2035,2036,2037,2038,2039,2040,2041],{"id":42,"depth":297,"text":43},{"id":406,"depth":297,"text":407},{"id":557,"depth":297,"text":558},{"id":751,"depth":297,"text":752},{"id":885,"depth":297,"text":886},{"id":1088,"depth":297,"text":1089},{"id":1350,"depth":297,"text":1351},{"id":1543,"depth":297,"text":1544},{"id":1667,"depth":297,"text":1668},{"id":1700,"depth":297,"text":1701},{"id":1816,"depth":297,"text":1817},null,"2026-03-04","Stacks, queues, heaps, trees - most reduce to arrays, numbers, and pointers. Here's what actually happens in memory.","md",{},"/blog/data-structures-names",{"title":6,"description":2044},"blog/data-structures-names",[1716,2051],"Architecture","ffGL5IY11NMEc_jR1P8yQL9A7fTiMnnnFiHO1HyZPmE",{"id":2054,"title":2055,"body":2056,"cover":2042,"date":3832,"description":3833,"extension":2045,"meta":3834,"navigation":335,"path":3835,"readingTime":1521,"seo":3836,"stem":3837,"tags":3838,"__hash__":3842},"blog/blog/pathway-framework.md","Pathway: Real-time AI pipelines and a post-transformer future",{"type":8,"value":2057,"toc":3800},[2058,2062,2071,2078,2082,2085,2100,2103,2107,2124,2127,2452,2455,2460,2514,2518,2539,2542,2546,2553,2556,2560,2563,2569,2921,2928,2932,2941,3272,3275,3279,3282,3292,3296,3299,3303,3315,3324,3330,3334,3342,3346,3357,3475,3479,3482,3486,3520,3524,3532,3536,3544,3548,3575,3588,3592,3599,3603,3613,3623,3628,3632,3635,3641,3667,3674,3678,3681,3693,3699,3705,3709,3717,3732,3738,3742,3745,3772,3775,3779,3785,3791,3797],[40,2059,2061],{"id":2060},"_60000-messages-per-second-from-a-pip-install","60,000 messages per second from a pip install",[11,2063,2064,2065,2070],{},"Most Python developers have a complicated relationship with real-time data. You want streaming pipelines, live search, instant updates - but the moment someone mentions Apache Flink or Kafka Streams, you're suddenly reading Java docs and configuring JVM clusters. ",[51,2066,2069],{"href":2067,"rel":2068},"https://pathway.com/",[55],"Pathway"," looked at that situation and said: what if real-time data processing was just... a Python library?",[11,2072,2073,2074,2077],{},"And it works. But that's only half the story. The same company is also building ",[118,2075,2076],{},"BDH"," - a brain-inspired neural architecture that might genuinely challenge the Transformer. Two very different products, one company, and both worth understanding.",[40,2079,2081],{"id":2080},"what-is-pathway","What is Pathway?",[11,2083,2084],{},"Pathway is actually two separate things under one roof:",[2086,2087,2088,2094],"ol",{},[1825,2089,2090,2093],{},[118,2091,2092],{},"Pathway Framework"," - an open-source Python ETL framework for stream processing, real-time analytics, and RAG pipelines. Think of it as what you'd get if pandas and Apache Flink had a child that was actually pleasant to use.",[1825,2095,2096,2099],{},[118,2097,2098],{},"Baby Dragon Hatchling (BDH)"," - a post-Transformer neural architecture that takes inspiration from how biological brains work. This is the research side.",[11,2101,2102],{},"The framework is the mature, production-ready product. BDH is the ambitious research bet. Let's start with what you can use today.",[40,2104,2106],{"id":2105},"the-framework","The framework",[11,2108,2109,2110,2113,2114,2117,2118,2123],{},"Pathway's core idea is simple: ",[118,2111,2112],{},"write Python, run Rust",". Your pipeline code looks like regular Python - familiar syntax, ",[15,2115,2116],{},"pip install",", notebook-friendly - but under the hood it compiles to a ",[51,2119,2122],{"href":2120,"rel":2121},"https://github.com/pathwaycom/pathway",[55],"Rust engine based on Differential Dataflow"," that handles multithreading, incremental computation, and distributed processing.",[11,2125,2126],{},"The same code handles both batch and streaming data. No separate pipelines, no mode switching. When new data arrives, Pathway computes only the minimum delta needed - not the entire dataset from scratch.",[255,2128,2133],{"className":2129,"code":2130,"filename":2131,"language":2132,"meta":260,"style":260},"language-python shiki shiki-themes material-theme-lighter material-theme material-theme-palenight","import pathway as pw\n\nclass SensorSchema(pw.Schema):\n    device_id: str\n    temperature: float\n    timestamp: str\n\n# Read from Kafka (streaming) or CSV (batch) - same API\nreadings = pw.io.kafka.read(\n    rdkafka_settings,\n    topic=\"sensors\",\n    schema=SensorSchema,\n    format=\"json\"\n)\n\n# Filter anomalies\nalerts = readings.filter(\n    pw.this.temperature > 85.0\n).select(pw.this.device_id, pw.this.temperature)\n\n# Push results to another Kafka topic\npw.io.kafka.write(alerts, rdkafka_settings, topic_name=\"alerts\")\npw.run()\n","pipeline.py","python",[15,2134,2135,2150,2154,2176,2186,2196,2205,2209,2214,2242,2249,2266,2278,2293,2298,2303,2309,2327,2349,2385,2390,2396,2439],{"__ignoreMap":260},[129,2136,2137,2141,2144,2147],{"class":265,"line":266},[129,2138,2140],{"class":2139},"s7zQu","import",[129,2142,2143],{"class":273}," pathway ",[129,2145,2146],{"class":2139},"as",[129,2148,2149],{"class":273}," pw\n",[129,2151,2152],{"class":265,"line":297},[129,2153,336],{"emptyLinePlaceholder":335},[129,2155,2156,2159,2163,2165,2168,2170,2173],{"class":265,"line":315},[129,2157,2158],{"class":269},"class",[129,2160,2162],{"class":2161},"sBMFI"," SensorSchema",[129,2164,147],{"class":277},[129,2166,2167],{"class":2161},"pw",[129,2169,362],{"class":277},[129,2171,2172],{"class":2161},"Schema",[129,2174,2175],{"class":277},"):\n",[129,2177,2178,2181,2183],{"class":265,"line":332},[129,2179,2180],{"class":273},"    device_id",[129,2182,1380],{"class":277},[129,2184,2185],{"class":2161}," str\n",[129,2187,2188,2191,2193],{"class":265,"line":339},[129,2189,2190],{"class":273},"    temperature",[129,2192,1380],{"class":277},[129,2194,2195],{"class":2161}," float\n",[129,2197,2198,2201,2203],{"class":265,"line":356},[129,2199,2200],{"class":273},"    timestamp",[129,2202,1380],{"class":277},[129,2204,2185],{"class":2161},[129,2206,2207],{"class":265,"line":651},[129,2208,336],{"emptyLinePlaceholder":335},[129,2210,2211],{"class":265,"line":657},[129,2212,2213],{"class":376},"# Read from Kafka (streaming) or CSV (batch) - same API\n",[129,2215,2216,2219,2221,2224,2226,2229,2231,2234,2236,2239],{"class":265,"line":669},[129,2217,2218],{"class":273},"readings ",[129,2220,278],{"class":277},[129,2222,2223],{"class":273}," pw",[129,2225,362],{"class":277},[129,2227,2228],{"class":1376},"io",[129,2230,362],{"class":277},[129,2232,2233],{"class":1376},"kafka",[129,2235,362],{"class":277},[129,2237,2238],{"class":284},"read",[129,2240,2241],{"class":277},"(\n",[129,2243,2244,2247],{"class":265,"line":693},[129,2245,2246],{"class":284},"    rdkafka_settings",[129,2248,1386],{"class":277},[129,2250,2251,2254,2256,2259,2262,2264],{"class":265,"line":712},[129,2252,2253],{"class":452},"    topic",[129,2255,278],{"class":277},[129,2257,2258],{"class":277},"\"",[129,2260,2261],{"class":427},"sensors",[129,2263,2258],{"class":277},[129,2265,1386],{"class":277},[129,2267,2268,2271,2273,2276],{"class":265,"line":1521},[129,2269,2270],{"class":452},"    schema",[129,2272,278],{"class":277},[129,2274,2275],{"class":284},"SensorSchema",[129,2277,1386],{"class":277},[129,2279,2280,2283,2285,2287,2290],{"class":265,"line":1527},[129,2281,2282],{"class":452},"    format",[129,2284,278],{"class":277},[129,2286,2258],{"class":277},[129,2288,2289],{"class":427},"json",[129,2291,2292],{"class":277},"\"\n",[129,2294,2296],{"class":265,"line":2295},14,[129,2297,294],{"class":277},[129,2299,2301],{"class":265,"line":2300},15,[129,2302,336],{"emptyLinePlaceholder":335},[129,2304,2306],{"class":265,"line":2305},16,[129,2307,2308],{"class":376},"# Filter anomalies\n",[129,2310,2312,2315,2317,2320,2322,2325],{"class":265,"line":2311},17,[129,2313,2314],{"class":273},"alerts ",[129,2316,278],{"class":277},[129,2318,2319],{"class":273}," readings",[129,2321,362],{"class":277},[129,2323,2324],{"class":284},"filter",[129,2326,2241],{"class":277},[129,2328,2330,2333,2335,2338,2340,2343,2346],{"class":265,"line":2329},18,[129,2331,2332],{"class":284},"    pw",[129,2334,362],{"class":277},[129,2336,2337],{"class":1376},"this",[129,2339,362],{"class":277},[129,2341,2342],{"class":1376},"temperature",[129,2344,2345],{"class":277}," >",[129,2347,2348],{"class":290}," 85.0\n",[129,2350,2352,2355,2358,2360,2362,2364,2366,2368,2371,2373,2375,2377,2379,2381,2383],{"class":265,"line":2351},19,[129,2353,2354],{"class":277},").",[129,2356,2357],{"class":284},"select",[129,2359,147],{"class":277},[129,2361,2167],{"class":284},[129,2363,362],{"class":277},[129,2365,2337],{"class":1376},[129,2367,362],{"class":277},[129,2369,2370],{"class":1376},"device_id",[129,2372,1015],{"class":277},[129,2374,2223],{"class":284},[129,2376,362],{"class":277},[129,2378,2337],{"class":1376},[129,2380,362],{"class":277},[129,2382,2342],{"class":1376},[129,2384,294],{"class":277},[129,2386,2388],{"class":265,"line":2387},20,[129,2389,336],{"emptyLinePlaceholder":335},[129,2391,2393],{"class":265,"line":2392},21,[129,2394,2395],{"class":376},"# Push results to another Kafka topic\n",[129,2397,2399,2401,2403,2405,2407,2409,2411,2414,2416,2419,2421,2424,2426,2429,2431,2433,2435,2437],{"class":265,"line":2398},22,[129,2400,2167],{"class":273},[129,2402,362],{"class":277},[129,2404,2228],{"class":1376},[129,2406,362],{"class":277},[129,2408,2233],{"class":1376},[129,2410,362],{"class":277},[129,2412,2413],{"class":284},"write",[129,2415,147],{"class":277},[129,2417,2418],{"class":284},"alerts",[129,2420,1015],{"class":277},[129,2422,2423],{"class":284}," rdkafka_settings",[129,2425,1015],{"class":277},[129,2427,2428],{"class":452}," topic_name",[129,2430,278],{"class":277},[129,2432,2258],{"class":277},[129,2434,2418],{"class":427},[129,2436,2258],{"class":277},[129,2438,294],{"class":277},[129,2440,2442,2444,2446,2449],{"class":265,"line":2441},23,[129,2443,2167],{"class":273},[129,2445,362],{"class":277},[129,2447,2448],{"class":284},"run",[129,2450,2451],{"class":277},"()\n",[11,2453,2454],{},"That's a complete streaming pipeline. No cluster setup, no YAML configs, no Docker orchestration. Just Python.",[2456,2457,2459],"h3",{"id":2458},"key-numbers","Key numbers",[59,2461,2462,2472],{},[62,2463,2464],{},[65,2465,2466,2469],{},[68,2467,2468],{},"Metric",[68,2470,2471],{},"Value",[78,2473,2474,2482,2490,2498,2506],{},[65,2475,2476,2479],{},[83,2477,2478],{},"Data source connectors",[83,2480,2481],{},"300+ (Kafka, PostgreSQL, S3, Google Drive, SharePoint...)",[65,2483,2484,2487],{},[83,2485,2486],{},"Throughput",[83,2488,2489],{},"60,000 msg/s with sub-second latency",[65,2491,2492,2495],{},[83,2493,2494],{},"Language",[83,2496,2497],{},"Python API, Rust engine",[65,2499,2500,2503],{},[83,2501,2502],{},"License",[83,2504,2505],{},"BSL 1.1 (converts to Apache 2.0 after 4 years)",[65,2507,2508,2511],{},[83,2509,2510],{},"Requirements",[83,2512,2513],{},"Python 3.10+, macOS or Linux (Windows needs WSL)",[2456,2515,2517],{"id":2516},"installation","Installation",[255,2519,2523],{"className":2520,"code":2521,"language":2522,"meta":260,"style":260},"language-bash shiki shiki-themes material-theme-lighter material-theme material-theme-palenight","pip install -U pathway\n","bash",[15,2524,2525],{"__ignoreMap":260},[129,2526,2527,2530,2533,2536],{"class":265,"line":266},[129,2528,2529],{"class":2161},"pip",[129,2531,2532],{"class":427}," install",[129,2534,2535],{"class":427}," -U",[129,2537,2538],{"class":427}," pathway\n",[11,2540,2541],{},"That's it. No JVM, no cluster manager, no Zookeeper.",[40,2543,2545],{"id":2544},"can-a-web-developer-actually-use-this","Can a web developer actually use this?",[11,2547,2548,2549,2552],{},"Short answer: ",[118,2550,2551],{},"yes",", and there are specific scenarios where it makes a lot of sense.",[11,2554,2555],{},"Pathway isn't a web framework - it won't replace your Nuxt, Next.js, or Django. But it can power the backend intelligence behind your web app. Here's where it fits:",[2456,2557,2559],{"id":2558},"real-time-rag-for-your-app","Real-time RAG for your app",[11,2561,2562],{},"If you're building any kind of AI-powered search, chatbot, or document Q&A system, Pathway handles the entire pipeline: data ingestion, document parsing, embedding generation, vector indexing, and retrieval. All in real-time, all synchronized automatically.",[11,2564,2565,2568],{},[118,2566,2567],{},"No separate vector database needed",". Pathway handles vector search natively. When a document changes in your Google Drive or S3 bucket, Pathway detects it, re-parses the content, updates embeddings, and refreshes the index - without you writing a single line of sync logic.",[255,2570,2573],{"className":2129,"code":2571,"filename":2572,"language":2132,"meta":260,"style":260},"import pathway as pw\nfrom pathway.xpacks.llm import embedders, splitters\nfrom pathway.xpacks.llm.vector_store import VectorStoreServer\n\n# Sync documents from multiple sources\ndocs = pw.io.fs.read(\"./documents/\", format=\"binary\", with_metadata=True)\ngdrive_docs = pw.io.gdrive.read(object_id=\"folder_id\", with_metadata=True)\n\nall_docs = docs.concat(gdrive_docs)\n\n# Split, embed, and serve - all reactive\ntext_splitter = splitters.TokenCountSplitter(max_tokens=400)\nembedder = embedders.OpenAIEmbedder(model=\"text-embedding-3-small\")\n\nvector_server = VectorStoreServer(\n    all_docs,\n    embedder=embedder,\n    splitter=text_splitter,\n)\nvector_server.run_server(host=\"0.0.0.0\", port=8765)\n","rag_pipeline.py",[15,2574,2575,2585,2613,2638,2642,2647,2700,2742,2746,2768,2772,2777,2804,2834,2838,2850,2857,2869,2881,2885],{"__ignoreMap":260},[129,2576,2577,2579,2581,2583],{"class":265,"line":266},[129,2578,2140],{"class":2139},[129,2580,2143],{"class":273},[129,2582,2146],{"class":2139},[129,2584,2149],{"class":273},[129,2586,2587,2590,2593,2595,2598,2600,2603,2605,2608,2610],{"class":265,"line":297},[129,2588,2589],{"class":2139},"from",[129,2591,2592],{"class":273}," pathway",[129,2594,362],{"class":277},[129,2596,2597],{"class":273},"xpacks",[129,2599,362],{"class":277},[129,2601,2602],{"class":273},"llm ",[129,2604,2140],{"class":2139},[129,2606,2607],{"class":273}," embedders",[129,2609,1015],{"class":277},[129,2611,2612],{"class":273}," splitters\n",[129,2614,2615,2617,2619,2621,2623,2625,2628,2630,2633,2635],{"class":265,"line":315},[129,2616,2589],{"class":2139},[129,2618,2592],{"class":273},[129,2620,362],{"class":277},[129,2622,2597],{"class":273},[129,2624,362],{"class":277},[129,2626,2627],{"class":273},"llm",[129,2629,362],{"class":277},[129,2631,2632],{"class":273},"vector_store ",[129,2634,2140],{"class":2139},[129,2636,2637],{"class":273}," VectorStoreServer\n",[129,2639,2640],{"class":265,"line":332},[129,2641,336],{"emptyLinePlaceholder":335},[129,2643,2644],{"class":265,"line":339},[129,2645,2646],{"class":376},"# Sync documents from multiple sources\n",[129,2648,2649,2652,2654,2656,2658,2660,2662,2665,2667,2669,2671,2673,2676,2678,2680,2683,2685,2687,2690,2692,2694,2697],{"class":265,"line":356},[129,2650,2651],{"class":273},"docs ",[129,2653,278],{"class":277},[129,2655,2223],{"class":273},[129,2657,362],{"class":277},[129,2659,2228],{"class":1376},[129,2661,362],{"class":277},[129,2663,2664],{"class":1376},"fs",[129,2666,362],{"class":277},[129,2668,2238],{"class":284},[129,2670,147],{"class":277},[129,2672,2258],{"class":277},[129,2674,2675],{"class":427},"./documents/",[129,2677,2258],{"class":277},[129,2679,1015],{"class":277},[129,2681,2682],{"class":452}," format",[129,2684,278],{"class":277},[129,2686,2258],{"class":277},[129,2688,2689],{"class":427},"binary",[129,2691,2258],{"class":277},[129,2693,1015],{"class":277},[129,2695,2696],{"class":452}," with_metadata",[129,2698,2699],{"class":277},"=True)\n",[129,2701,2702,2705,2707,2709,2711,2713,2715,2718,2720,2722,2724,2727,2729,2731,2734,2736,2738,2740],{"class":265,"line":651},[129,2703,2704],{"class":273},"gdrive_docs ",[129,2706,278],{"class":277},[129,2708,2223],{"class":273},[129,2710,362],{"class":277},[129,2712,2228],{"class":1376},[129,2714,362],{"class":277},[129,2716,2717],{"class":1376},"gdrive",[129,2719,362],{"class":277},[129,2721,2238],{"class":284},[129,2723,147],{"class":277},[129,2725,2726],{"class":452},"object_id",[129,2728,278],{"class":277},[129,2730,2258],{"class":277},[129,2732,2733],{"class":427},"folder_id",[129,2735,2258],{"class":277},[129,2737,1015],{"class":277},[129,2739,2696],{"class":452},[129,2741,2699],{"class":277},[129,2743,2744],{"class":265,"line":657},[129,2745,336],{"emptyLinePlaceholder":335},[129,2747,2748,2751,2753,2756,2758,2761,2763,2766],{"class":265,"line":669},[129,2749,2750],{"class":273},"all_docs ",[129,2752,278],{"class":277},[129,2754,2755],{"class":273}," docs",[129,2757,362],{"class":277},[129,2759,2760],{"class":284},"concat",[129,2762,147],{"class":277},[129,2764,2765],{"class":284},"gdrive_docs",[129,2767,294],{"class":277},[129,2769,2770],{"class":265,"line":693},[129,2771,336],{"emptyLinePlaceholder":335},[129,2773,2774],{"class":265,"line":712},[129,2775,2776],{"class":376},"# Split, embed, and serve - all reactive\n",[129,2778,2779,2782,2784,2787,2789,2792,2794,2797,2799,2802],{"class":265,"line":1521},[129,2780,2781],{"class":273},"text_splitter ",[129,2783,278],{"class":277},[129,2785,2786],{"class":273}," splitters",[129,2788,362],{"class":277},[129,2790,2791],{"class":284},"TokenCountSplitter",[129,2793,147],{"class":277},[129,2795,2796],{"class":452},"max_tokens",[129,2798,278],{"class":277},[129,2800,2801],{"class":290},"400",[129,2803,294],{"class":277},[129,2805,2806,2809,2811,2813,2815,2818,2820,2823,2825,2827,2830,2832],{"class":265,"line":1527},[129,2807,2808],{"class":273},"embedder ",[129,2810,278],{"class":277},[129,2812,2607],{"class":273},[129,2814,362],{"class":277},[129,2816,2817],{"class":284},"OpenAIEmbedder",[129,2819,147],{"class":277},[129,2821,2822],{"class":452},"model",[129,2824,278],{"class":277},[129,2826,2258],{"class":277},[129,2828,2829],{"class":427},"text-embedding-3-small",[129,2831,2258],{"class":277},[129,2833,294],{"class":277},[129,2835,2836],{"class":265,"line":2295},[129,2837,336],{"emptyLinePlaceholder":335},[129,2839,2840,2843,2845,2848],{"class":265,"line":2300},[129,2841,2842],{"class":273},"vector_server ",[129,2844,278],{"class":277},[129,2846,2847],{"class":284}," VectorStoreServer",[129,2849,2241],{"class":277},[129,2851,2852,2855],{"class":265,"line":2305},[129,2853,2854],{"class":284},"    all_docs",[129,2856,1386],{"class":277},[129,2858,2859,2862,2864,2867],{"class":265,"line":2311},[129,2860,2861],{"class":452},"    embedder",[129,2863,278],{"class":277},[129,2865,2866],{"class":284},"embedder",[129,2868,1386],{"class":277},[129,2870,2871,2874,2876,2879],{"class":265,"line":2329},[129,2872,2873],{"class":452},"    splitter",[129,2875,278],{"class":277},[129,2877,2878],{"class":284},"text_splitter",[129,2880,1386],{"class":277},[129,2882,2883],{"class":265,"line":2351},[129,2884,294],{"class":277},[129,2886,2887,2890,2892,2895,2897,2900,2902,2904,2907,2909,2911,2914,2916,2919],{"class":265,"line":2387},[129,2888,2889],{"class":273},"vector_server",[129,2891,362],{"class":277},[129,2893,2894],{"class":284},"run_server",[129,2896,147],{"class":277},[129,2898,2899],{"class":452},"host",[129,2901,278],{"class":277},[129,2903,2258],{"class":277},[129,2905,2906],{"class":427},"0.0.0.0",[129,2908,2258],{"class":277},[129,2910,1015],{"class":277},[129,2912,2913],{"class":452}," port",[129,2915,278],{"class":277},[129,2917,2918],{"class":290},"8765",[129,2920,294],{"class":277},[11,2922,2923,2924,2927],{},"Your frontend just hits ",[15,2925,2926],{},"localhost:8765"," with a query. The index stays live.",[2456,2929,2931],{"id":2930},"rest-api-endpoints","REST API endpoints",[11,2933,2934,2935,2940],{},"Pathway can serve HTTP endpoints directly. You don't need Flask or FastAPI as a separate layer - Pathway has a built-in ",[51,2936,2939],{"href":2937,"rel":2938},"https://pathway.com/developers/api-docs/pathway-io/http/",[55],"HTTP connector"," that accepts requests, processes them through your pipeline, and returns results:",[255,2942,2945],{"className":2129,"code":2943,"filename":2944,"language":2132,"meta":260,"style":260},"import pathway as pw\n\nwebserver = pw.io.http.PathwayWebserver(\n    host=\"0.0.0.0\",\n    port=9999,\n    with_schema_endpoint=True  # auto-generates OpenAPI docs at /_schema\n)\n\nclass QuerySchema(pw.Schema):\n    text: str\n\nqueries, response_writer = pw.io.http.rest_connector(\n    webserver=webserver,\n    route=\"/search\",\n    schema=QuerySchema,\n    methods=('POST',)\n)\n\n# Process queries through your pipeline\nresults = queries.select(\n    query_id=queries.id,\n    result=pw.apply(lambda x: search_index(x), pw.this.text)\n)\n\nresponse_writer(results)\npw.run()\n","api.py",[15,2946,2947,2957,2961,2986,3001,3013,3024,3028,3032,3049,3058,3062,3091,3103,3119,3130,3148,3152,3156,3161,3177,3193,3240,3244,3249,3261],{"__ignoreMap":260},[129,2948,2949,2951,2953,2955],{"class":265,"line":266},[129,2950,2140],{"class":2139},[129,2952,2143],{"class":273},[129,2954,2146],{"class":2139},[129,2956,2149],{"class":273},[129,2958,2959],{"class":265,"line":297},[129,2960,336],{"emptyLinePlaceholder":335},[129,2962,2963,2966,2968,2970,2972,2974,2976,2979,2981,2984],{"class":265,"line":315},[129,2964,2965],{"class":273},"webserver ",[129,2967,278],{"class":277},[129,2969,2223],{"class":273},[129,2971,362],{"class":277},[129,2973,2228],{"class":1376},[129,2975,362],{"class":277},[129,2977,2978],{"class":1376},"http",[129,2980,362],{"class":277},[129,2982,2983],{"class":284},"PathwayWebserver",[129,2985,2241],{"class":277},[129,2987,2988,2991,2993,2995,2997,2999],{"class":265,"line":332},[129,2989,2990],{"class":452},"    host",[129,2992,278],{"class":277},[129,2994,2258],{"class":277},[129,2996,2906],{"class":427},[129,2998,2258],{"class":277},[129,3000,1386],{"class":277},[129,3002,3003,3006,3008,3011],{"class":265,"line":339},[129,3004,3005],{"class":452},"    port",[129,3007,278],{"class":277},[129,3009,3010],{"class":290},"9999",[129,3012,1386],{"class":277},[129,3014,3015,3018,3021],{"class":265,"line":356},[129,3016,3017],{"class":452},"    with_schema_endpoint",[129,3019,3020],{"class":277},"=True",[129,3022,3023],{"class":376},"  # auto-generates OpenAPI docs at /_schema\n",[129,3025,3026],{"class":265,"line":651},[129,3027,294],{"class":277},[129,3029,3030],{"class":265,"line":657},[129,3031,336],{"emptyLinePlaceholder":335},[129,3033,3034,3036,3039,3041,3043,3045,3047],{"class":265,"line":669},[129,3035,2158],{"class":269},[129,3037,3038],{"class":2161}," QuerySchema",[129,3040,147],{"class":277},[129,3042,2167],{"class":2161},[129,3044,362],{"class":277},[129,3046,2172],{"class":2161},[129,3048,2175],{"class":277},[129,3050,3051,3054,3056],{"class":265,"line":693},[129,3052,3053],{"class":273},"    text",[129,3055,1380],{"class":277},[129,3057,2185],{"class":2161},[129,3059,3060],{"class":265,"line":712},[129,3061,336],{"emptyLinePlaceholder":335},[129,3063,3064,3067,3069,3072,3074,3076,3078,3080,3082,3084,3086,3089],{"class":265,"line":1521},[129,3065,3066],{"class":273},"queries",[129,3068,1015],{"class":277},[129,3070,3071],{"class":273}," response_writer ",[129,3073,278],{"class":277},[129,3075,2223],{"class":273},[129,3077,362],{"class":277},[129,3079,2228],{"class":1376},[129,3081,362],{"class":277},[129,3083,2978],{"class":1376},[129,3085,362],{"class":277},[129,3087,3088],{"class":284},"rest_connector",[129,3090,2241],{"class":277},[129,3092,3093,3096,3098,3101],{"class":265,"line":1527},[129,3094,3095],{"class":452},"    webserver",[129,3097,278],{"class":277},[129,3099,3100],{"class":284},"webserver",[129,3102,1386],{"class":277},[129,3104,3105,3108,3110,3112,3115,3117],{"class":265,"line":2295},[129,3106,3107],{"class":452},"    route",[129,3109,278],{"class":277},[129,3111,2258],{"class":277},[129,3113,3114],{"class":427},"/search",[129,3116,2258],{"class":277},[129,3118,1386],{"class":277},[129,3120,3121,3123,3125,3128],{"class":265,"line":2300},[129,3122,2270],{"class":452},[129,3124,278],{"class":277},[129,3126,3127],{"class":284},"QuerySchema",[129,3129,1386],{"class":277},[129,3131,3132,3135,3138,3140,3143,3145],{"class":265,"line":2305},[129,3133,3134],{"class":452},"    methods",[129,3136,3137],{"class":277},"=(",[129,3139,424],{"class":277},[129,3141,3142],{"class":427},"POST",[129,3144,424],{"class":277},[129,3146,3147],{"class":277},",)\n",[129,3149,3150],{"class":265,"line":2311},[129,3151,294],{"class":277},[129,3153,3154],{"class":265,"line":2329},[129,3155,336],{"emptyLinePlaceholder":335},[129,3157,3158],{"class":265,"line":2351},[129,3159,3160],{"class":376},"# Process queries through your pipeline\n",[129,3162,3163,3166,3168,3171,3173,3175],{"class":265,"line":2387},[129,3164,3165],{"class":273},"results ",[129,3167,278],{"class":277},[129,3169,3170],{"class":273}," queries",[129,3172,362],{"class":277},[129,3174,2357],{"class":284},[129,3176,2241],{"class":277},[129,3178,3179,3182,3184,3186,3188,3191],{"class":265,"line":2392},[129,3180,3181],{"class":452},"    query_id",[129,3183,278],{"class":277},[129,3185,3066],{"class":284},[129,3187,362],{"class":277},[129,3189,3190],{"class":1376},"id",[129,3192,1386],{"class":277},[129,3194,3195,3198,3200,3202,3204,3207,3209,3212,3215,3217,3220,3222,3224,3227,3229,3231,3233,3235,3238],{"class":265,"line":2398},[129,3196,3197],{"class":452},"    result",[129,3199,278],{"class":277},[129,3201,2167],{"class":284},[129,3203,362],{"class":277},[129,3205,3206],{"class":284},"apply",[129,3208,147],{"class":277},[129,3210,3211],{"class":269},"lambda",[129,3213,3214],{"class":452}," x",[129,3216,1380],{"class":277},[129,3218,3219],{"class":284}," search_index",[129,3221,147],{"class":277},[129,3223,191],{"class":284},[129,3225,3226],{"class":277},"),",[129,3228,2223],{"class":284},[129,3230,362],{"class":277},[129,3232,2337],{"class":1376},[129,3234,362],{"class":277},[129,3236,3237],{"class":1376},"text",[129,3239,294],{"class":277},[129,3241,3242],{"class":265,"line":2441},[129,3243,294],{"class":277},[129,3245,3247],{"class":265,"line":3246},24,[129,3248,336],{"emptyLinePlaceholder":335},[129,3250,3252,3255,3257,3259],{"class":265,"line":3251},25,[129,3253,3254],{"class":284},"response_writer",[129,3256,147],{"class":277},[129,3258,1991],{"class":284},[129,3260,294],{"class":277},[129,3262,3264,3266,3268,3270],{"class":265,"line":3263},26,[129,3265,2167],{"class":273},[129,3267,362],{"class":277},[129,3269,2448],{"class":284},[129,3271,2451],{"class":277},[11,3273,3274],{},"Multiple endpoints, one server instance, auto-generated OpenAPI schema. Not bad for a data processing framework.",[2456,3276,3278],{"id":3277},"live-dashboards-and-monitoring","Live dashboards and monitoring",[11,3280,3281],{},"If your web app needs real-time analytics dashboards - think IoT monitoring, financial data, log analysis - Pathway processes the streams and exposes results that your frontend can consume via HTTP or WebSocket. Pairs well with tools like Streamlit for quick prototyping.",[3283,3284,3285],"note",{},[11,3286,3287,3288,3291],{},"Pathway runs on ",[118,3289,3290],{},"macOS and Linux natively",". On Windows, you'll need WSL or Docker. It requires Python 3.10+.",[40,3293,3295],{"id":3294},"how-it-compares-to-the-alternatives","How it compares to the alternatives",[11,3297,3298],{},"The obvious comparison targets are Apache Flink, Kafka Streams, and for RAG specifically - LangChain and LlamaIndex.",[2456,3300,3302],{"id":3301},"vs-apache-flink","Vs. Apache Flink",[11,3304,3305,3306,3311,3312,3314],{},"Flink is the industry standard for stream processing, but it's a Java-first framework. Its ",[51,3307,3310],{"href":3308,"rel":3309},"https://pathway.com/flink-alternative",[55],"Python support (PyFlink) is a wrapper"," that lags behind the Java API in features. Setting up Flink means configuring a JVM environment, spinning up a cluster, and dealing with a compile-and-submit workflow. Pathway is ",[15,3313,2116],{}," and you're done.",[11,3316,3317,3318,3323],{},"Performance-wise, ",[51,3319,3322],{"href":3320,"rel":3321},"https://pathway.com/stream-processing-frameworks",[55],"benchmarks published by the Pathway team"," show Pathway outperforming Flink on iterative graph algorithms, with some tests showing 20x speed improvements in streaming scenarios.",[3325,3326,3327],"caution",{},[11,3328,3329],{},"Those benchmarks come from Pathway's own team. Independent benchmarks would carry more weight, but a Rust-backed incremental engine must be fast.",[2456,3331,3333],{"id":3332},"vs-kafka-streams","Vs. Kafka Streams",[11,3335,3336,3337,362],{},"Kafka Streams only supports Java and Scala. If you're a Python developer, it's not even an option. And it's tightly coupled to Kafka - your data has to be in Kafka to use it. Pathway connects to ",[51,3338,3341],{"href":3339,"rel":3340},"https://pathway.com/framework",[55],"300+ sources",[2456,3343,3345],{"id":3344},"vs-langchain-llamaindex-for-rag","Vs. LangChain / LlamaIndex for RAG",[11,3347,3348,3349,3352,3353,3356],{},"LangChain and LlamaIndex are excellent for building RAG prototypes. But they're designed around ",[118,3350,3351],{},"static"," indexes. You build the index, query it, done. If your documents change, you rebuild. Pathway's RAG pipelines are ",[118,3354,3355],{},"reactive"," - changes propagate automatically. For production systems where data freshness matters, that's a significant advantage.",[59,3358,3359,3377],{},[62,3360,3361],{},[65,3362,3363,3366,3368,3371,3374],{},[68,3364,3365],{},"Feature",[68,3367,2069],{},[68,3369,3370],{},"Flink",[68,3372,3373],{},"Kafka Streams",[68,3375,3376],{},"LangChain",[78,3378,3379,3394,3413,3430,3445,3459],{},[65,3380,3381,3383,3386,3389,3392],{},[83,3382,2494],{},[83,3384,3385],{},"Python (Rust engine)",[83,3387,3388],{},"Java/Scala (Python wrapper)",[83,3390,3391],{},"Java/Scala only",[83,3393,1719],{},[65,3395,3396,3399,3403,3406,3409],{},[83,3397,3398],{},"Install",[83,3400,3401],{},[15,3402,2116],{},[83,3404,3405],{},"JVM + cluster setup",[83,3407,3408],{},"JVM + Kafka required",[83,3410,3411],{},[15,3412,2116],{},[65,3414,3415,3418,3421,3424,3427],{},[83,3416,3417],{},"Batch + Streaming",[83,3419,3420],{},"Unified",[83,3422,3423],{},"Separate APIs",[83,3425,3426],{},"Streaming only",[83,3428,3429],{},"N/A",[65,3431,3432,3435,3438,3440,3442],{},[83,3433,3434],{},"Built-in vector search",[83,3436,3437],{},"Yes",[83,3439,1732],{},[83,3441,1732],{},[83,3443,3444],{},"Via integrations",[65,3446,3447,3450,3452,3455,3457],{},[83,3448,3449],{},"Real-time index sync",[83,3451,3437],{},[83,3453,3454],{},"Manual",[83,3456,3454],{},[83,3458,3454],{},[65,3460,3461,3464,3467,3470,3473],{},[83,3462,3463],{},"Data consistency",[83,3465,3466],{},"Internal consistency",[83,3468,3469],{},"Exactly-once",[83,3471,3472],{},"Eventual",[83,3474,3429],{},[40,3476,3478],{"id":3477},"whos-already-using-it","Who's already using it",[11,3480,3481],{},"Pathway isn't just a cool open-source project with GitHub stars. It's running in production at companies you've heard of - and in environments where \"eventually consistent\" isn't good enough.",[2456,3483,3485],{"id":3484},"logistics-and-transport","Logistics and transport",[11,3487,3488,3489,3494,3495,3500,3501,3504,3505,3510,3511,3516,3517,362],{},"This is where Pathway seems to have the deepest foothold. ",[51,3490,3493],{"href":3491,"rel":3492},"https://pathway.com/success-stories/db-schenker",[55],"DB Schenker"," - one of the world's largest logistics providers - built a cloud-based application on Pathway for real-time insights on IoT and status data across their logistics fleet. ",[51,3496,3499],{"href":3497,"rel":3498},"https://pathway.com/success-stories/la-poste",[55],"La Poste"," (French postal service) ",[118,3502,3503],{},"cut IoT deployment costs by 50%"," using Pathway for container operations analytics. ",[51,3506,3509],{"href":3507,"rel":3508},"https://pathway.com/success-stories/cma-cgm",[55],"CMA CGM",", the shipping giant, improved container gate-out ETA precision, directly reducing operational costs and handling times. ",[51,3512,3515],{"href":3513,"rel":3514},"https://pathway.com/success-stories/transdev",[55],"Transdev"," uses it for real-time passenger information - bus deviations, arrival time estimates, the kind of data that needs to be accurate ",[24,3518,3519],{},"right now",[2456,3521,3523],{"id":3522},"formula-1","Formula 1",[11,3525,3526,3531],{},[51,3527,3530],{"href":3528,"rel":3529},"https://pathway.com/success-stories/formula-1-team",[55],"F1 uses Pathway"," to process real-time telemetry streams. The system is flexible enough to allow independent user-defined functions for different business needs - race strategy, broadcast data, performance analysis - all from the same streaming pipeline.",[2456,3533,3535],{"id":3534},"defense","Defense",[11,3537,3538,3543],{},[51,3539,3542],{"href":3540,"rel":3541},"https://pathway.com/success-stories/nato",[55],"NATO/JSEC collaborated with Pathway"," during Steadfast Foxtrot 2024 for data processing and simulation capabilities in military operations across Eastern Europe. When NATO trusts your data processing framework for operational exercises, that's a different level of validation than GitHub stars.",[2456,3545,3547],{"id":3546},"enterprise-ai-solutions","Enterprise AI solutions",[11,3549,3550,3551,3556,3557,3560,3561,3564,3565,3568,3569,3574],{},"Beyond the framework itself, Pathway offers ",[51,3552,3555],{"href":3553,"rel":3554},"https://pathway.com/solutions",[55],"ready-made solutions",": ",[118,3558,3559],{},"document answering"," systems for enterprise knowledge bases, ",[118,3562,3563],{},"RAG-powered slide search"," across SharePoint and Google Drive, and ",[118,3566,3567],{},"AI contract management"," tools. These aren't demos - they're products deployed at companies like ",[51,3570,3573],{"href":3571,"rel":3572},"https://pathway.com/blog/intel-summit",[55],"Intel"," for internal document discovery.",[3576,3577,3578],"tip",{},[11,3579,3580,3581,3584,3585,362],{},"The pattern here is clear: Pathway's sweet spot is organizations that have ",[118,3582,3583],{},"lots of live data"," and need to make decisions based on what's happening ",[24,3586,3587],{},"now",[40,3589,3591],{"id":3590},"baby-dragon-hatchling-the-post-transformer-bet","Baby Dragon Hatchling - the post-Transformer bet",[11,3593,3594,3595,3598],{},"Now for the wild part. Pathway isn't just building data tools - they're trying to ",[118,3596,3597],{},"replace the Transformer architecture"," that powers GPT, Claude, and every major LLM.",[2456,3600,3602],{"id":3601},"what-is-bdh","What is BDH?",[11,3604,3605,3609,3610,362],{},[51,3606,2098],{"href":3607,"rel":3608},"https://arxiv.org/abs/2509.26507",[55]," is a neural architecture inspired by biological brains. The paper's title says it all: ",[24,3611,3612],{},"\"The Missing Link Between the Transformer and Models of the Brain\"",[11,3614,3615,3618,3619,3622],{},[118,3616,3617],{},"BDH is not a language model like GPT",". It's an ",[24,3620,3621],{},"architecture"," - a fundamentally different way of organizing neural computation. GPT-4, Claude, Gemini - they're all products built on the Transformer architecture. BDH is proposing an alternative foundation.",[3576,3624,3625],{},[11,3626,3627],{},"Think of it this way: Transformer is to GPT what BDH could be to future AI models. One is the blueprint, the other is the building.",[2456,3629,3631],{"id":3630},"how-it-works","How it works",[11,3633,3634],{},"In a traditional Transformer, attention is a global operation - every token can attend to every other token. It's powerful but computationally expensive and fundamentally limited in how it handles time.",[11,3636,3637,3638,1380],{},"BDH flips this. Instead of global attention, it uses a ",[118,3639,3640],{},"population of artificial neurons that interact locally",[1822,3642,3643,3649,3655,3661],{},[1825,3644,3645,3648],{},[118,3646,3647],{},"State lives on synapses",", not neurons. The connections between neurons carry and update information dynamically",[1825,3650,3651,3654],{},[118,3652,3653],{},"Local interactions"," instead of global attention - each neuron mostly talks to its neighbors, and understanding builds step by step",[1825,3656,3657,3660],{},[118,3658,3659],{},"Hebbian learning during inference"," - \"neurons that fire together, wire together.\" The network literally rewires itself as it processes data",[1825,3662,3663,3666],{},[118,3664,3665],{},"No fixed context length"," - because context emerges from network dynamics, not from position embeddings",[11,3668,3669,3670,3673],{},"The modular structure of the network isn't engineered - it ",[118,3671,3672],{},"emerges spontaneously during training",". Pathway's researchers argue that this emergence is the key to genuine intelligence.",[2456,3675,3677],{"id":3676},"why-it-matters","Why it matters",[11,3679,3680],{},"Three properties make BDH genuinely interesting:",[11,3682,3683,3686,3687,3692],{},[118,3684,3685],{},"Monosemantic synapses"," - individual connections consistently activate for specific concepts, even across languages. In Transformers, ",[51,3688,3691],{"href":3689,"rel":3690},"https://colinmcnamara.com/blog/understanding-baby-dragon-hatchling-bdh",[55],"interpretability is a massive unsolved problem",". BDH gets it for free.",[11,3694,3695,3698],{},[118,3696,3697],{},"Continual learning"," - Transformers are static after training. BDH's synapses update during inference, meaning the model can theoretically learn from new data without retraining. This is the \"generalization over time\" that Pathway keeps emphasizing.",[11,3700,3701,3704],{},[118,3702,3703],{},"Scale-free architecture"," - the network can grow and reason over extended periods predictably. Unlike Transformers where longer contexts degrade performance, BDH's local interaction model scales differently.",[2456,3706,3708],{"id":3707},"current-status","Current status",[11,3710,3711,3712,3716],{},"Let's be honest about where BDH is. The current benchmarks show ",[51,3713,3715],{"href":3689,"rel":3714},[55],"GPT-2 scale parity"," - competitive loss-versus-parameters scaling at 10 million to 1 billion parameters. That's promising for a new architecture, but it's a long way from competing with GPT-4 or Claude at hundreds of billions of parameters.",[11,3718,3719,3720,3725,3726,3731],{},"BDH is ",[51,3721,3724],{"href":3722,"rel":3723},"https://github.com/pathwaycom/bdh",[55],"available on GitHub",", runs on NVIDIA GPUs, and has a partnership with ",[51,3727,3730],{"href":3728,"rel":3729},"https://www.businesswire.com/news/home/20251201914013/en/Pathway-to-Deliver-New-Class-of-Adaptive-and-Continuously-Learning-AI-Systems-with-AWS-and-NVIDIA-Technologies",[55],"AWS for cloud deployment",". The team is backed by Łukasz Kaiser - one of the co-inventors of the original Transformer architecture (\"Attention Is All You Need\"). When someone who built the Transformer funds the thing trying to replace it, that's worth paying attention to.",[3733,3734,3735],"warning",{},[11,3736,3737],{},"BDH is research-stage technology. It shows exciting properties at small scale, but scaling a new architecture to frontier-model sizes is an enormous engineering challenge. Don't expect BDH-powered products in production anytime soon.",[40,3739,3741],{"id":3740},"the-team-behind-it","The team behind it",[11,3743,3744],{},"Pathway's founding team has serious credentials:",[1822,3746,3747,3753,3759,3765],{},[1825,3748,3749,3752],{},[118,3750,3751],{},"Zuzanna Stamirowska"," (CEO) - complex systems expert, recognized by the US National Academy of Sciences",[1825,3754,3755,3758],{},[118,3756,3757],{},"Jan Chorowski"," (CTO) - applied attention mechanisms to speech processing, collaborated with Geoffrey Hinton",[1825,3760,3761,3764],{},[118,3762,3763],{},"Adrian Kosowski"," (CSO) - theoretical computer scientist with 100+ published papers",[1825,3766,3767,3768,3771],{},"Backed by ",[118,3769,3770],{},"Łukasz Kaiser"," (Transformer co-inventor), TQ Ventures, and Kadmos Capital",[11,3773,3774],{},"This isn't a random startup making bold claims. The scientific bench is deep.",[40,3776,3778],{"id":3777},"what-should-you-actually-do-with-this","What should you actually do with this?",[11,3780,3781,3784],{},[118,3782,3783],{},"If you're a web developer"," who needs real-time data features - live search, synced document indexes, streaming analytics, or RAG pipelines - Pathway's framework is a genuinely useful tool. It's easier than Flink, more capable than LangChain for real-time use cases, and the Python API means you don't need to learn a new language.",[11,3786,3787,3790],{},[118,3788,3789],{},"If you're interested in AI architecture"," - keep an eye on BDH. The interpretability and continual learning properties are exactly what the field needs, even if it takes years to reach scale. The paper is worth reading, and the code is open source.",[11,3792,3793,3796],{},[118,3794,3795],{},"If you're neither"," - file Pathway away as \"interesting company doing important things\" and check back in a year. The framework will probably have even more connectors and better RAG tooling by then. BDH might have scaled to something more competitive. Either way, the trajectory is worth watching.",[2026,3798,3799],{},"html pre.shiki code .s7zQu, html code.shiki .s7zQu{--shiki-light:#39ADB5;--shiki-light-font-style:italic;--shiki-default:#89DDFF;--shiki-default-font-style:italic;--shiki-dark:#89DDFF;--shiki-dark-font-style:italic}html pre.shiki code .sTEyZ, html code.shiki .sTEyZ{--shiki-light:#90A4AE;--shiki-default:#EEFFFF;--shiki-dark:#BABED8}html pre.shiki code .spNyl, html code.shiki .spNyl{--shiki-light:#9C3EDA;--shiki-default:#C792EA;--shiki-dark:#C792EA}html pre.shiki code .sBMFI, html code.shiki .sBMFI{--shiki-light:#E2931D;--shiki-default:#FFCB6B;--shiki-dark:#FFCB6B}html pre.shiki code .sMK4o, html code.shiki .sMK4o{--shiki-light:#39ADB5;--shiki-default:#89DDFF;--shiki-dark:#89DDFF}html pre.shiki code .sHwdD, html code.shiki .sHwdD{--shiki-light:#90A4AE;--shiki-light-font-style:italic;--shiki-default:#546E7A;--shiki-default-font-style:italic;--shiki-dark:#676E95;--shiki-dark-font-style:italic}html pre.shiki code .swJcz, html code.shiki .swJcz{--shiki-light:#E53935;--shiki-default:#F07178;--shiki-dark:#F07178}html pre.shiki code .s2Zo4, html code.shiki .s2Zo4{--shiki-light:#6182B8;--shiki-default:#82AAFF;--shiki-dark:#82AAFF}html pre.shiki code .sHdIc, html code.shiki .sHdIc{--shiki-light:#90A4AE;--shiki-light-font-style:italic;--shiki-default:#EEFFFF;--shiki-default-font-style:italic;--shiki-dark:#BABED8;--shiki-dark-font-style:italic}html pre.shiki code .sfazB, html code.shiki .sfazB{--shiki-light:#91B859;--shiki-default:#C3E88D;--shiki-dark:#C3E88D}html pre.shiki code .sbssI, html code.shiki .sbssI{--shiki-light:#F76D47;--shiki-default:#F78C6C;--shiki-dark:#F78C6C}html .light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html.light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}",{"title":260,"searchDepth":297,"depth":297,"links":3801},[3802,3803,3804,3808,3813,3818,3824,3830,3831],{"id":2060,"depth":297,"text":2061},{"id":2080,"depth":297,"text":2081},{"id":2105,"depth":297,"text":2106,"children":3805},[3806,3807],{"id":2458,"depth":315,"text":2459},{"id":2516,"depth":315,"text":2517},{"id":2544,"depth":297,"text":2545,"children":3809},[3810,3811,3812],{"id":2558,"depth":315,"text":2559},{"id":2930,"depth":315,"text":2931},{"id":3277,"depth":315,"text":3278},{"id":3294,"depth":297,"text":3295,"children":3814},[3815,3816,3817],{"id":3301,"depth":315,"text":3302},{"id":3332,"depth":315,"text":3333},{"id":3344,"depth":315,"text":3345},{"id":3477,"depth":297,"text":3478,"children":3819},[3820,3821,3822,3823],{"id":3484,"depth":315,"text":3485},{"id":3522,"depth":315,"text":3523},{"id":3534,"depth":315,"text":3535},{"id":3546,"depth":315,"text":3547},{"id":3590,"depth":297,"text":3591,"children":3825},[3826,3827,3828,3829],{"id":3601,"depth":315,"text":3602},{"id":3630,"depth":315,"text":3631},{"id":3676,"depth":315,"text":3677},{"id":3707,"depth":315,"text":3708},{"id":3740,"depth":297,"text":3741},{"id":3777,"depth":297,"text":3778},"2026-03-02","Pathway is two things at once - a Python framework that makes real-time data pipelines trivially easy, and a research lab building what might replace the Transformer. Here's what web developers should actually care about.",{},"/blog/pathway-framework",{"title":2055,"description":3833},"blog/pathway-framework",[3839,1719,3840,3841],"AI","Data Engineering","RAG","eU6XYzIuLeMNDnvcfySCvCMSWKEyExiFEX6TTekjRLw",{"id":3844,"title":3845,"body":3846,"cover":2042,"date":8623,"description":8624,"extension":2045,"meta":8625,"navigation":335,"path":8626,"readingTime":1521,"seo":8627,"stem":8628,"tags":8629,"__hash__":8633},"blog/blog/design-patterns-nuxt.md","Design patterns in 2026 vs Nuxt",{"type":8,"value":3847,"toc":8612},[3848,3859,3862,3864,3867,3872,3880,3920,4031,4040,4191,4204,4215,4217,4221,4228,4235,4238,4368,4465,4553,4559,4562,4564,4568,4579,4582,5193,5325,5338,5341,5343,5347,5354,5357,5635,5638,5933,5936,5939,5941,5945,5952,5955,6344,6532,6535,6538,6540,6544,6551,6557,6652,6655,7080,7197,7210,7305,7311,7313,7317,7324,7327,7330,7919,8011,8201,8204,8448,8451,8453,8457,8460,8566,8569,8571,8575,8578,8593,8596,8598,8601,8609],[11,3849,3850,3851,3854,3855,3858],{},"Vue's reactivity system is the Observer pattern. ",[15,3852,3853],{},"defineCachedEventHandler"," is a Decorator. Drizzle ORM's ",[15,3856,3857],{},".where().orderBy().limit()"," is a Builder.",[11,3860,3861],{},"Some of these are worth knowing by name and reaching for deliberately. Others are interview vocabulary - frameworks solved them for you already.",[2001,3863],{},[40,3865,1681],{"id":3866},"observer",[11,3868,3869],{},[118,3870,3871],{},"The most important pattern in frontend development.",[3576,3873,3874],{},[11,3875,3876,3879],{},[118,3877,3878],{},"The idea",": a subject maintains a list of observers. When state changes, it notifies all observers automatically. Observers don't poll - they subscribe.",[11,3881,3882,3883,3901,3902,3905,3906,3909,3910,500,3913,500,3916,3919],{},"Vue's entire reactivity system is Observer under the hood. When you write ",[15,3884,3885,3887,3890,3892,3895,3897,3899],{"className":257,"language":259,"style":260},[129,3886,270],{"class":269},[129,3888,3889],{"class":273}," count ",[129,3891,278],{"class":277},[129,3893,3894],{"class":284}," ref",[129,3896,147],{"class":273},[129,3898,345],{"class":290},[129,3900,160],{"class":273}," and use it in a template, Vue registers the template render function as an observer of ",[15,3903,3904],{},"count",". When ",[15,3907,3908],{},"count.value++"," happens, Vue notifies all registered observers and triggers re-renders. ",[15,3911,3912],{},"watch",[15,3914,3915],{},"watchEffect",[15,3917,3918],{},"computed"," - all of them are Observer implementations.",[255,3921,3925],{"className":3922,"code":3923,"language":3924,"meta":260,"style":260},"language-ts shiki shiki-themes material-theme-lighter material-theme material-theme-palenight","// This is Observer.\nconst user = ref\u003CUser | null>(null)\n\n// Registers \"send analytics\" as an observer of `user`\nwatch(user, (newUser) => {\n  if (newUser) analytics.identify(newUser.id)\n})\n","ts",[15,3926,3927,3932,3964,3968,3973,3994,4024],{"__ignoreMap":260},[129,3928,3929],{"class":265,"line":266},[129,3930,3931],{"class":376},"// This is Observer.\n",[129,3933,3934,3936,3939,3941,3943,3946,3949,3952,3954,3957,3959,3962],{"class":265,"line":297},[129,3935,270],{"class":269},[129,3937,3938],{"class":273}," user ",[129,3940,278],{"class":277},[129,3942,3894],{"class":284},[129,3944,3945],{"class":277},"\u003C",[129,3947,3948],{"class":2161},"User",[129,3950,3951],{"class":277}," |",[129,3953,1441],{"class":2161},[129,3955,3956],{"class":277},">",[129,3958,147],{"class":273},[129,3960,3961],{"class":277},"null",[129,3963,294],{"class":273},[129,3965,3966],{"class":265,"line":315},[129,3967,336],{"emptyLinePlaceholder":335},[129,3969,3970],{"class":265,"line":332},[129,3971,3972],{"class":376},"// Registers \"send analytics\" as an observer of `user`\n",[129,3974,3975,3977,3980,3982,3985,3988,3990,3992],{"class":265,"line":339},[129,3976,3912],{"class":284},[129,3978,3979],{"class":273},"(user",[129,3981,1015],{"class":277},[129,3983,3984],{"class":277}," (",[129,3986,3987],{"class":452},"newUser",[129,3989,160],{"class":277},[129,3991,456],{"class":269},[129,3993,1371],{"class":277},[129,3995,3996,3999,4001,4003,4006,4009,4011,4014,4016,4018,4020,4022],{"class":265,"line":356},[129,3997,3998],{"class":2139},"  if",[129,4000,3984],{"class":1376},[129,4002,3987],{"class":273},[129,4004,4005],{"class":1376},") ",[129,4007,4008],{"class":273},"analytics",[129,4010,362],{"class":277},[129,4012,4013],{"class":284},"identify",[129,4015,147],{"class":1376},[129,4017,3987],{"class":273},[129,4019,362],{"class":277},[129,4021,3190],{"class":273},[129,4023,294],{"class":1376},[129,4025,4026,4029],{"class":265,"line":651},[129,4027,4028],{"class":277},"}",[129,4030,294],{"class":273},[11,4032,4033,4034,4039],{},"Nuxt exposes its own event system via ",[51,4035,4038],{"href":4036,"rel":4037},"https://nitro.build/guide/plugins#nitro-hooks",[55],"Nitro hooks",", which is Observer at the server level:",[255,4041,4044],{"className":3922,"code":4042,"filename":4043,"language":3924,"meta":260,"style":260},"export default defineNitroPlugin((nitro) => {\n  nitro.hooks.hook('request', (event) => {\n    // Observes every incoming request\n    console.log(`[${new Date().toISOString()}] ${event.method} ${event.path}`)\n  })\n})\n","server/plugins/audit.ts",[15,4045,4046,4070,4107,4112,4178,4185],{"__ignoreMap":260},[129,4047,4048,4051,4054,4057,4059,4061,4064,4066,4068],{"class":265,"line":266},[129,4049,4050],{"class":2139},"export",[129,4052,4053],{"class":2139}," default",[129,4055,4056],{"class":284}," defineNitroPlugin",[129,4058,147],{"class":273},[129,4060,147],{"class":277},[129,4062,4063],{"class":452},"nitro",[129,4065,160],{"class":277},[129,4067,456],{"class":269},[129,4069,1371],{"class":277},[129,4071,4072,4075,4077,4080,4082,4085,4087,4089,4092,4094,4096,4098,4101,4103,4105],{"class":265,"line":297},[129,4073,4074],{"class":273},"  nitro",[129,4076,362],{"class":277},[129,4078,4079],{"class":273},"hooks",[129,4081,362],{"class":277},[129,4083,4084],{"class":284},"hook",[129,4086,147],{"class":1376},[129,4088,424],{"class":277},[129,4090,4091],{"class":427},"request",[129,4093,424],{"class":277},[129,4095,1015],{"class":277},[129,4097,3984],{"class":277},[129,4099,4100],{"class":452},"event",[129,4102,160],{"class":277},[129,4104,456],{"class":269},[129,4106,1371],{"class":277},[129,4108,4109],{"class":265,"line":315},[129,4110,4111],{"class":376},"    // Observes every incoming request\n",[129,4113,4114,4117,4119,4121,4123,4126,4129,4132,4135,4138,4141,4143,4146,4148,4150,4152,4154,4156,4158,4161,4163,4166,4168,4170,4173,4176],{"class":265,"line":332},[129,4115,4116],{"class":273},"    console",[129,4118,362],{"class":277},[129,4120,365],{"class":284},[129,4122,147],{"class":1376},[129,4124,4125],{"class":277},"`",[129,4127,4128],{"class":427},"[",[129,4130,4131],{"class":277},"${",[129,4133,4134],{"class":277},"new",[129,4136,4137],{"class":284}," Date",[129,4139,4140],{"class":273},"()",[129,4142,362],{"class":277},[129,4144,4145],{"class":284},"toISOString",[129,4147,4140],{"class":273},[129,4149,4028],{"class":277},[129,4151,348],{"class":427},[129,4153,4131],{"class":277},[129,4155,4100],{"class":273},[129,4157,362],{"class":277},[129,4159,4160],{"class":273},"method",[129,4162,4028],{"class":277},[129,4164,4165],{"class":277}," ${",[129,4167,4100],{"class":273},[129,4169,362],{"class":277},[129,4171,4172],{"class":273},"path",[129,4174,4175],{"class":277},"}`",[129,4177,294],{"class":1376},[129,4179,4180,4183],{"class":265,"line":339},[129,4181,4182],{"class":277},"  }",[129,4184,294],{"class":1376},[129,4186,4187,4189],{"class":265,"line":356},[129,4188,4028],{"class":277},[129,4190,294],{"class":273},[11,4192,4193,4194,4197,4198,4203],{},"Node's ",[15,4195,4196],{},"EventEmitter"," - which backs a massive chunk of the Node ecosystem - is Observer. The pattern predates JavaScript by decades (it's from the original ",[51,4199,4202],{"href":4200,"rel":4201},"https://en.wikipedia.org/wiki/Design_Patterns",[55],"GoF book, 1994",") but became the dominant metaphor for async programming precisely because it maps perfectly to \"something happened, react to it.\"",[11,4205,4206,4207,500,4209,1653,4211,4214],{},"Vue's entire reactive system runs on this. Every ",[15,4208,3912],{},[15,4210,3918],{},[15,4212,4213],{},"ref"," is Observer in practice.",[2001,4216],{},[40,4218,4220],{"id":4219},"singleton","Singleton",[3576,4222,4223],{},[11,4224,4225,4227],{},[118,4226,3878],{},": A class with at most one instance, globally accessible.",[11,4229,4230,4231,4234],{},"Global mutable state is how bugs hide. But Singleton has a completely legitimate use case - ",[118,4232,4233],{},"connections and shared resources"," that are expensive to create and stateful by nature. Database clients, Redis connections, logger instances, etc.",[11,4236,4237],{},"In Nuxt, the correct pattern is to initialize these once in a server plugin and expose them via a utility function:",[255,4239,4242],{"className":3922,"code":4240,"filename":4241,"language":3924,"meta":260,"style":260},"import { drizzle } from 'drizzle-orm/better-sqlite3'\nimport Database from 'better-sqlite3'\n\n// One connection, created once, reused for the lifetime of the server\nconst sqlite = new Database('sqlite.db')\nconst db = drizzle(sqlite)\n\n// Expose via a function so callers don't hold a reference to the module\nexport default defineNitroPlugin(() => {\n  // db is effectively a singleton - initialized once, shared everywhere\n})\n","server/plugins/database.ts",[15,4243,4244,4268,4284,4288,4293,4318,4332,4336,4341,4357,4362],{"__ignoreMap":260},[129,4245,4246,4248,4250,4253,4256,4259,4262,4265],{"class":265,"line":266},[129,4247,2140],{"class":2139},[129,4249,1416],{"class":277},[129,4251,4252],{"class":273}," drizzle",[129,4254,4255],{"class":277}," }",[129,4257,4258],{"class":2139}," from",[129,4260,4261],{"class":277}," '",[129,4263,4264],{"class":427},"drizzle-orm/better-sqlite3",[129,4266,4267],{"class":277},"'\n",[129,4269,4270,4272,4275,4277,4279,4282],{"class":265,"line":297},[129,4271,2140],{"class":2139},[129,4273,4274],{"class":273}," Database ",[129,4276,2589],{"class":2139},[129,4278,4261],{"class":277},[129,4280,4281],{"class":427},"better-sqlite3",[129,4283,4267],{"class":277},[129,4285,4286],{"class":265,"line":315},[129,4287,336],{"emptyLinePlaceholder":335},[129,4289,4290],{"class":265,"line":332},[129,4291,4292],{"class":376},"// One connection, created once, reused for the lifetime of the server\n",[129,4294,4295,4297,4300,4302,4304,4307,4309,4311,4314,4316],{"class":265,"line":339},[129,4296,270],{"class":269},[129,4298,4299],{"class":273}," sqlite ",[129,4301,278],{"class":277},[129,4303,281],{"class":277},[129,4305,4306],{"class":284}," Database",[129,4308,147],{"class":273},[129,4310,424],{"class":277},[129,4312,4313],{"class":427},"sqlite.db",[129,4315,424],{"class":277},[129,4317,294],{"class":273},[129,4319,4320,4322,4325,4327,4329],{"class":265,"line":356},[129,4321,270],{"class":269},[129,4323,4324],{"class":273}," db ",[129,4326,278],{"class":277},[129,4328,4252],{"class":284},[129,4330,4331],{"class":273},"(sqlite)\n",[129,4333,4334],{"class":265,"line":651},[129,4335,336],{"emptyLinePlaceholder":335},[129,4337,4338],{"class":265,"line":657},[129,4339,4340],{"class":376},"// Expose via a function so callers don't hold a reference to the module\n",[129,4342,4343,4345,4347,4349,4351,4353,4355],{"class":265,"line":669},[129,4344,4050],{"class":2139},[129,4346,4053],{"class":2139},[129,4348,4056],{"class":284},[129,4350,147],{"class":273},[129,4352,4140],{"class":277},[129,4354,456],{"class":269},[129,4356,1371],{"class":277},[129,4358,4359],{"class":265,"line":693},[129,4360,4361],{"class":376},"  // db is effectively a singleton - initialized once, shared everywhere\n",[129,4363,4364,4366],{"class":265,"line":712},[129,4365,4028],{"class":277},[129,4367,294],{"class":273},[255,4369,4372],{"className":3922,"code":4370,"filename":4371,"language":3924,"meta":260,"style":260},"import { drizzle } from 'drizzle-orm/better-sqlite3'\nimport Database from 'better-sqlite3'\n\n// In Nitro, top-level module state IS a singleton\n// This runs once per server process\nconst sqlite = new Database(process.env.DB_PATH!)\nexport const db = drizzle(sqlite)\n","server/utils/db.ts",[15,4373,4374,4392,4406,4410,4415,4420,4450],{"__ignoreMap":260},[129,4375,4376,4378,4380,4382,4384,4386,4388,4390],{"class":265,"line":266},[129,4377,2140],{"class":2139},[129,4379,1416],{"class":277},[129,4381,4252],{"class":273},[129,4383,4255],{"class":277},[129,4385,4258],{"class":2139},[129,4387,4261],{"class":277},[129,4389,4264],{"class":427},[129,4391,4267],{"class":277},[129,4393,4394,4396,4398,4400,4402,4404],{"class":265,"line":297},[129,4395,2140],{"class":2139},[129,4397,4274],{"class":273},[129,4399,2589],{"class":2139},[129,4401,4261],{"class":277},[129,4403,4281],{"class":427},[129,4405,4267],{"class":277},[129,4407,4408],{"class":265,"line":315},[129,4409,336],{"emptyLinePlaceholder":335},[129,4411,4412],{"class":265,"line":332},[129,4413,4414],{"class":376},"// In Nitro, top-level module state IS a singleton\n",[129,4416,4417],{"class":265,"line":339},[129,4418,4419],{"class":376},"// This runs once per server process\n",[129,4421,4422,4424,4426,4428,4430,4432,4435,4437,4440,4442,4445,4448],{"class":265,"line":356},[129,4423,270],{"class":269},[129,4425,4299],{"class":273},[129,4427,278],{"class":277},[129,4429,281],{"class":277},[129,4431,4306],{"class":284},[129,4433,4434],{"class":273},"(process",[129,4436,362],{"class":277},[129,4438,4439],{"class":273},"env",[129,4441,362],{"class":277},[129,4443,4444],{"class":273},"DB_PATH",[129,4446,4447],{"class":277},"!",[129,4449,294],{"class":273},[129,4451,4452,4454,4457,4459,4461,4463],{"class":265,"line":651},[129,4453,4050],{"class":2139},[129,4455,4456],{"class":269}," const",[129,4458,4324],{"class":273},[129,4460,278],{"class":277},[129,4462,4252],{"class":284},[129,4464,4331],{"class":273},[255,4466,4469],{"className":3922,"code":4467,"filename":4468,"language":3924,"meta":260,"style":260},"import { db } from '../utils/db'\n\nexport default defineEventHandler(async () => {\n  return db.select().from(users).all()\n})\n","server/api/users.get.ts",[15,4470,4471,4491,4495,4516,4547],{"__ignoreMap":260},[129,4472,4473,4475,4477,4480,4482,4484,4486,4489],{"class":265,"line":266},[129,4474,2140],{"class":2139},[129,4476,1416],{"class":277},[129,4478,4479],{"class":273}," db",[129,4481,4255],{"class":277},[129,4483,4258],{"class":2139},[129,4485,4261],{"class":277},[129,4487,4488],{"class":427},"../utils/db",[129,4490,4267],{"class":277},[129,4492,4493],{"class":265,"line":297},[129,4494,336],{"emptyLinePlaceholder":335},[129,4496,4497,4499,4501,4504,4506,4509,4512,4514],{"class":265,"line":315},[129,4498,4050],{"class":2139},[129,4500,4053],{"class":2139},[129,4502,4503],{"class":284}," defineEventHandler",[129,4505,147],{"class":273},[129,4507,4508],{"class":269},"async",[129,4510,4511],{"class":277}," ()",[129,4513,456],{"class":269},[129,4515,1371],{"class":277},[129,4517,4518,4521,4523,4525,4527,4529,4531,4533,4535,4538,4540,4542,4545],{"class":265,"line":332},[129,4519,4520],{"class":2139},"  return",[129,4522,4479],{"class":273},[129,4524,362],{"class":277},[129,4526,2357],{"class":284},[129,4528,4140],{"class":1376},[129,4530,362],{"class":277},[129,4532,2589],{"class":284},[129,4534,147],{"class":1376},[129,4536,4537],{"class":273},"users",[129,4539,160],{"class":1376},[129,4541,362],{"class":277},[129,4543,4544],{"class":284},"all",[129,4546,2451],{"class":1376},[129,4548,4549,4551],{"class":265,"line":339},[129,4550,4028],{"class":277},[129,4552,294],{"class":273},[11,4554,4555,4558],{},[15,4556,4557],{},"useNuxtApp()"," on the client is also a Singleton - it returns the same Nuxt app instance no matter where you call it.",[11,4560,4561],{},"Good for DB connections, Redis clients, loggers - shared resources that are expensive to initialize. Avoid it for general application state.",[2001,4563],{},[40,4565,4567],{"id":4566},"factory","Factory",[3576,4569,4570],{},[11,4571,4572,4574,4575,4578],{},[118,4573,3878],{},": Instead of calling ",[15,4576,4577],{},"new Thing()"," directly, you call a function that decides what to create and returns it. The caller doesn't need to know the concrete type.",[11,4580,4581],{},"This sounds abstract until you have multiple implementations of the same interface. Payment providers are the classic example:",[255,4583,4586],{"className":3922,"code":4584,"filename":4585,"language":3924,"meta":260,"style":260},"interface PaymentProvider {\n  charge(amount: number, currency: string, token: string): Promise\u003C{ id: string }>\n  refund(chargeId: string): Promise\u003Cvoid>\n}\n\nclass StripeProvider implements PaymentProvider {\n  async charge(amount: number, currency: string, token: string) {\n    const stripe = new Stripe(process.env.STRIPE_KEY!)\n    const charge = await stripe.paymentIntents.create({ amount, currency, payment_method: token, confirm: true })\n    return { id: charge.id }\n  }\n  async refund(chargeId: string) {\n    const stripe = new Stripe(process.env.STRIPE_KEY!)\n    await stripe.refunds.create({ payment_intent: chargeId })\n  }\n}\n\nclass MollieProvider implements PaymentProvider {\n  async charge(amount: number, currency: string, token: string) {\n    // mollie implementation\n    return { id: 'mollie_123' }\n  }\n  async refund(chargeId: string) { /* ... */ }\n}\n\n// Factory - decides which provider based on config\nexport function createPaymentProvider(): PaymentProvider {\n  const provider = process.env.PAYMENT_PROVIDER ?? 'stripe'\n  if (provider === 'stripe') return new StripeProvider()\n  if (provider === 'mollie') return new MollieProvider()\n  throw new Error(`Unknown payment provider: ${provider}`)\n}\n","server/lib/payment.ts",[15,4587,4588,4598,4651,4677,4681,4685,4699,4735,4769,4828,4847,4851,4870,4898,4930,4934,4938,4942,4955,4989,4994,5013,5017,5040,5044,5048,5053,5071,5104,5134,5162,5188],{"__ignoreMap":260},[129,4589,4590,4593,4596],{"class":265,"line":266},[129,4591,4592],{"class":269},"interface",[129,4594,4595],{"class":2161}," PaymentProvider",[129,4597,1371],{"class":277},[129,4599,4600,4603,4605,4608,4610,4613,4615,4618,4620,4623,4625,4628,4630,4632,4635,4638,4641,4644,4646,4648],{"class":265,"line":297},[129,4601,4602],{"class":1376},"  charge",[129,4604,147],{"class":277},[129,4606,4607],{"class":452},"amount",[129,4609,1380],{"class":277},[129,4611,4612],{"class":2161}," number",[129,4614,1015],{"class":277},[129,4616,4617],{"class":452}," currency",[129,4619,1380],{"class":277},[129,4621,4622],{"class":2161}," string",[129,4624,1015],{"class":277},[129,4626,4627],{"class":452}," token",[129,4629,1380],{"class":277},[129,4631,4622],{"class":2161},[129,4633,4634],{"class":277},"):",[129,4636,4637],{"class":2161}," Promise",[129,4639,4640],{"class":277},"\u003C{",[129,4642,4643],{"class":1376}," id",[129,4645,1380],{"class":277},[129,4647,4622],{"class":2161},[129,4649,4650],{"class":277}," }>\n",[129,4652,4653,4656,4658,4661,4663,4665,4667,4669,4671,4674],{"class":265,"line":315},[129,4654,4655],{"class":1376},"  refund",[129,4657,147],{"class":277},[129,4659,4660],{"class":452},"chargeId",[129,4662,1380],{"class":277},[129,4664,4622],{"class":2161},[129,4666,4634],{"class":277},[129,4668,4637],{"class":2161},[129,4670,3945],{"class":277},[129,4672,4673],{"class":2161},"void",[129,4675,4676],{"class":277},">\n",[129,4678,4679],{"class":265,"line":332},[129,4680,1530],{"class":277},[129,4682,4683],{"class":265,"line":339},[129,4684,336],{"emptyLinePlaceholder":335},[129,4686,4687,4689,4692,4695,4697],{"class":265,"line":356},[129,4688,2158],{"class":269},[129,4690,4691],{"class":2161}," StripeProvider",[129,4693,4694],{"class":269}," implements",[129,4696,4595],{"class":2161},[129,4698,1371],{"class":277},[129,4700,4701,4704,4707,4709,4711,4713,4715,4717,4719,4721,4723,4725,4727,4729,4731,4733],{"class":265,"line":651},[129,4702,4703],{"class":269},"  async",[129,4705,4706],{"class":1376}," charge",[129,4708,147],{"class":277},[129,4710,4607],{"class":452},[129,4712,1380],{"class":277},[129,4714,4612],{"class":2161},[129,4716,1015],{"class":277},[129,4718,4617],{"class":452},[129,4720,1380],{"class":277},[129,4722,4622],{"class":2161},[129,4724,1015],{"class":277},[129,4726,4627],{"class":452},[129,4728,1380],{"class":277},[129,4730,4622],{"class":2161},[129,4732,160],{"class":277},[129,4734,1371],{"class":277},[129,4736,4737,4740,4743,4746,4748,4751,4753,4756,4758,4760,4762,4765,4767],{"class":265,"line":657},[129,4738,4739],{"class":269},"    const",[129,4741,4742],{"class":273}," stripe",[129,4744,4745],{"class":277}," =",[129,4747,281],{"class":277},[129,4749,4750],{"class":284}," Stripe",[129,4752,147],{"class":1376},[129,4754,4755],{"class":273},"process",[129,4757,362],{"class":277},[129,4759,4439],{"class":273},[129,4761,362],{"class":277},[129,4763,4764],{"class":273},"STRIPE_KEY",[129,4766,4447],{"class":277},[129,4768,294],{"class":1376},[129,4770,4771,4773,4775,4777,4780,4782,4784,4787,4789,4792,4794,4797,4800,4802,4804,4806,4809,4811,4813,4815,4818,4820,4824,4826],{"class":265,"line":669},[129,4772,4739],{"class":269},[129,4774,4706],{"class":273},[129,4776,4745],{"class":277},[129,4778,4779],{"class":2139}," await",[129,4781,4742],{"class":273},[129,4783,362],{"class":277},[129,4785,4786],{"class":273},"paymentIntents",[129,4788,362],{"class":277},[129,4790,4791],{"class":284},"create",[129,4793,147],{"class":1376},[129,4795,4796],{"class":277},"{",[129,4798,4799],{"class":273}," amount",[129,4801,1015],{"class":277},[129,4803,4617],{"class":273},[129,4805,1015],{"class":277},[129,4807,4808],{"class":1376}," payment_method",[129,4810,1380],{"class":277},[129,4812,4627],{"class":273},[129,4814,1015],{"class":277},[129,4816,4817],{"class":1376}," confirm",[129,4819,1380],{"class":277},[129,4821,4823],{"class":4822},"sfNiH"," true",[129,4825,4255],{"class":277},[129,4827,294],{"class":1376},[129,4829,4830,4833,4835,4837,4839,4841,4843,4845],{"class":265,"line":693},[129,4831,4832],{"class":2139},"    return",[129,4834,1416],{"class":277},[129,4836,4643],{"class":1376},[129,4838,1380],{"class":277},[129,4840,4706],{"class":273},[129,4842,362],{"class":277},[129,4844,3190],{"class":273},[129,4846,1476],{"class":277},[129,4848,4849],{"class":265,"line":712},[129,4850,1524],{"class":277},[129,4852,4853,4855,4858,4860,4862,4864,4866,4868],{"class":265,"line":1521},[129,4854,4703],{"class":269},[129,4856,4857],{"class":1376}," refund",[129,4859,147],{"class":277},[129,4861,4660],{"class":452},[129,4863,1380],{"class":277},[129,4865,4622],{"class":2161},[129,4867,160],{"class":277},[129,4869,1371],{"class":277},[129,4871,4872,4874,4876,4878,4880,4882,4884,4886,4888,4890,4892,4894,4896],{"class":265,"line":1527},[129,4873,4739],{"class":269},[129,4875,4742],{"class":273},[129,4877,4745],{"class":277},[129,4879,281],{"class":277},[129,4881,4750],{"class":284},[129,4883,147],{"class":1376},[129,4885,4755],{"class":273},[129,4887,362],{"class":277},[129,4889,4439],{"class":273},[129,4891,362],{"class":277},[129,4893,4764],{"class":273},[129,4895,4447],{"class":277},[129,4897,294],{"class":1376},[129,4899,4900,4903,4905,4907,4910,4912,4914,4916,4918,4921,4923,4926,4928],{"class":265,"line":2295},[129,4901,4902],{"class":2139},"    await",[129,4904,4742],{"class":273},[129,4906,362],{"class":277},[129,4908,4909],{"class":273},"refunds",[129,4911,362],{"class":277},[129,4913,4791],{"class":284},[129,4915,147],{"class":1376},[129,4917,4796],{"class":277},[129,4919,4920],{"class":1376}," payment_intent",[129,4922,1380],{"class":277},[129,4924,4925],{"class":273}," chargeId",[129,4927,4255],{"class":277},[129,4929,294],{"class":1376},[129,4931,4932],{"class":265,"line":2300},[129,4933,1524],{"class":277},[129,4935,4936],{"class":265,"line":2305},[129,4937,1530],{"class":277},[129,4939,4940],{"class":265,"line":2311},[129,4941,336],{"emptyLinePlaceholder":335},[129,4943,4944,4946,4949,4951,4953],{"class":265,"line":2329},[129,4945,2158],{"class":269},[129,4947,4948],{"class":2161}," MollieProvider",[129,4950,4694],{"class":269},[129,4952,4595],{"class":2161},[129,4954,1371],{"class":277},[129,4956,4957,4959,4961,4963,4965,4967,4969,4971,4973,4975,4977,4979,4981,4983,4985,4987],{"class":265,"line":2351},[129,4958,4703],{"class":269},[129,4960,4706],{"class":1376},[129,4962,147],{"class":277},[129,4964,4607],{"class":452},[129,4966,1380],{"class":277},[129,4968,4612],{"class":2161},[129,4970,1015],{"class":277},[129,4972,4617],{"class":452},[129,4974,1380],{"class":277},[129,4976,4622],{"class":2161},[129,4978,1015],{"class":277},[129,4980,4627],{"class":452},[129,4982,1380],{"class":277},[129,4984,4622],{"class":2161},[129,4986,160],{"class":277},[129,4988,1371],{"class":277},[129,4990,4991],{"class":265,"line":2387},[129,4992,4993],{"class":376},"    // mollie implementation\n",[129,4995,4996,4998,5000,5002,5004,5006,5009,5011],{"class":265,"line":2392},[129,4997,4832],{"class":2139},[129,4999,1416],{"class":277},[129,5001,4643],{"class":1376},[129,5003,1380],{"class":277},[129,5005,4261],{"class":277},[129,5007,5008],{"class":427},"mollie_123",[129,5010,424],{"class":277},[129,5012,1476],{"class":277},[129,5014,5015],{"class":265,"line":2398},[129,5016,1524],{"class":277},[129,5018,5019,5021,5023,5025,5027,5029,5031,5033,5035,5038],{"class":265,"line":2441},[129,5020,4703],{"class":269},[129,5022,4857],{"class":1376},[129,5024,147],{"class":277},[129,5026,4660],{"class":452},[129,5028,1380],{"class":277},[129,5030,4622],{"class":2161},[129,5032,160],{"class":277},[129,5034,1416],{"class":277},[129,5036,5037],{"class":376}," /* ... */",[129,5039,1476],{"class":277},[129,5041,5042],{"class":265,"line":3246},[129,5043,1530],{"class":277},[129,5045,5046],{"class":265,"line":3251},[129,5047,336],{"emptyLinePlaceholder":335},[129,5049,5050],{"class":265,"line":3263},[129,5051,5052],{"class":376},"// Factory - decides which provider based on config\n",[129,5054,5056,5058,5061,5064,5067,5069],{"class":265,"line":5055},27,[129,5057,4050],{"class":2139},[129,5059,5060],{"class":269}," function",[129,5062,5063],{"class":284}," createPaymentProvider",[129,5065,5066],{"class":277},"():",[129,5068,4595],{"class":2161},[129,5070,1371],{"class":277},[129,5072,5074,5077,5080,5082,5085,5087,5089,5091,5094,5097,5099,5102],{"class":265,"line":5073},28,[129,5075,5076],{"class":269},"  const",[129,5078,5079],{"class":273}," provider",[129,5081,4745],{"class":277},[129,5083,5084],{"class":273}," process",[129,5086,362],{"class":277},[129,5088,4439],{"class":273},[129,5090,362],{"class":277},[129,5092,5093],{"class":273},"PAYMENT_PROVIDER",[129,5095,5096],{"class":277}," ??",[129,5098,4261],{"class":277},[129,5100,5101],{"class":427},"stripe",[129,5103,4267],{"class":277},[129,5105,5107,5109,5111,5114,5117,5119,5121,5123,5125,5128,5130,5132],{"class":265,"line":5106},29,[129,5108,3998],{"class":2139},[129,5110,3984],{"class":1376},[129,5112,5113],{"class":273},"provider",[129,5115,5116],{"class":277}," ===",[129,5118,4261],{"class":277},[129,5120,5101],{"class":427},[129,5122,424],{"class":277},[129,5124,4005],{"class":1376},[129,5126,5127],{"class":2139},"return",[129,5129,281],{"class":277},[129,5131,4691],{"class":284},[129,5133,2451],{"class":1376},[129,5135,5137,5139,5141,5143,5145,5147,5150,5152,5154,5156,5158,5160],{"class":265,"line":5136},30,[129,5138,3998],{"class":2139},[129,5140,3984],{"class":1376},[129,5142,5113],{"class":273},[129,5144,5116],{"class":277},[129,5146,4261],{"class":277},[129,5148,5149],{"class":427},"mollie",[129,5151,424],{"class":277},[129,5153,4005],{"class":1376},[129,5155,5127],{"class":2139},[129,5157,281],{"class":277},[129,5159,4948],{"class":284},[129,5161,2451],{"class":1376},[129,5163,5165,5168,5170,5173,5175,5177,5180,5182,5184,5186],{"class":265,"line":5164},31,[129,5166,5167],{"class":2139},"  throw",[129,5169,281],{"class":277},[129,5171,5172],{"class":284}," Error",[129,5174,147],{"class":1376},[129,5176,4125],{"class":277},[129,5178,5179],{"class":427},"Unknown payment provider: ",[129,5181,4131],{"class":277},[129,5183,5113],{"class":273},[129,5185,4175],{"class":277},[129,5187,294],{"class":1376},[129,5189,5191],{"class":265,"line":5190},32,[129,5192,1530],{"class":277},[255,5194,5197],{"className":3922,"code":5195,"filename":5196,"language":3924,"meta":260,"style":260},"import { createPaymentProvider } from '../lib/payment'\n\nexport default defineEventHandler(async (event) => {\n  const { amount, token } = await readBody(event)\n  const payment = createPaymentProvider() // doesn't know or care which one\n  return await payment.charge(amount, 'eur', token)\n})\n","server/api/checkout.post.ts",[15,5198,5199,5218,5222,5244,5271,5287,5319],{"__ignoreMap":260},[129,5200,5201,5203,5205,5207,5209,5211,5213,5216],{"class":265,"line":266},[129,5202,2140],{"class":2139},[129,5204,1416],{"class":277},[129,5206,5063],{"class":273},[129,5208,4255],{"class":277},[129,5210,4258],{"class":2139},[129,5212,4261],{"class":277},[129,5214,5215],{"class":427},"../lib/payment",[129,5217,4267],{"class":277},[129,5219,5220],{"class":265,"line":297},[129,5221,336],{"emptyLinePlaceholder":335},[129,5223,5224,5226,5228,5230,5232,5234,5236,5238,5240,5242],{"class":265,"line":315},[129,5225,4050],{"class":2139},[129,5227,4053],{"class":2139},[129,5229,4503],{"class":284},[129,5231,147],{"class":273},[129,5233,4508],{"class":269},[129,5235,3984],{"class":277},[129,5237,4100],{"class":452},[129,5239,160],{"class":277},[129,5241,456],{"class":269},[129,5243,1371],{"class":277},[129,5245,5246,5248,5250,5252,5254,5256,5258,5260,5262,5265,5267,5269],{"class":265,"line":332},[129,5247,5076],{"class":269},[129,5249,1416],{"class":277},[129,5251,4799],{"class":273},[129,5253,1015],{"class":277},[129,5255,4627],{"class":273},[129,5257,4255],{"class":277},[129,5259,4745],{"class":277},[129,5261,4779],{"class":2139},[129,5263,5264],{"class":284}," readBody",[129,5266,147],{"class":1376},[129,5268,4100],{"class":273},[129,5270,294],{"class":1376},[129,5272,5273,5275,5278,5280,5282,5284],{"class":265,"line":339},[129,5274,5076],{"class":269},[129,5276,5277],{"class":273}," payment",[129,5279,4745],{"class":277},[129,5281,5063],{"class":284},[129,5283,824],{"class":1376},[129,5285,5286],{"class":376},"// doesn't know or care which one\n",[129,5288,5289,5291,5293,5295,5297,5300,5302,5304,5306,5308,5311,5313,5315,5317],{"class":265,"line":356},[129,5290,4520],{"class":2139},[129,5292,4779],{"class":2139},[129,5294,5277],{"class":273},[129,5296,362],{"class":277},[129,5298,5299],{"class":284},"charge",[129,5301,147],{"class":1376},[129,5303,4607],{"class":273},[129,5305,1015],{"class":277},[129,5307,4261],{"class":277},[129,5309,5310],{"class":427},"eur",[129,5312,424],{"class":277},[129,5314,1015],{"class":277},[129,5316,4627],{"class":273},[129,5318,294],{"class":1376},[129,5320,5321,5323],{"class":265,"line":651},[129,5322,4028],{"class":277},[129,5324,294],{"class":273},[11,5326,5327,5328,500,5331,500,5334,5337],{},"You see this pattern in frameworks constantly. ",[15,5329,5330],{},"defineEventHandler",[15,5332,5333],{},"defineNuxtPlugin",[15,5335,5336],{},"defineNuxtRouteMiddleware"," - all of these are factory functions. They configure the instance based on context.",[11,5339,5340],{},"Worth reaching for when you genuinely have multiple implementations behind the same interface. Make this decision once when you set up the module - the rest of the code stays ignorant of which implementation is running.",[2001,5342],{},[40,5344,5346],{"id":5345},"builder","Builder",[3576,5348,5349],{},[11,5350,5351,5353],{},[118,5352,3878],{},": Constructs complex objects step by step via a fluent API. Each method returns the builder itself so you can chain. The actual object is built at the end.",[11,5355,5356],{},"You use this constantly and probably don't think about it as a pattern:",[255,5358,5360],{"className":3922,"code":5359,"language":3924,"meta":260,"style":260},"// Drizzle ORM is a Builder\nconst results = await db\n  .select({ id: users.id, name: users.name })\n  .from(users)\n  .where(eq(users.active, true))\n  .orderBy(desc(users.createdAt))\n  .limit(20)\n  .offset(page * 20)\n\n// @nuxt/content queryCollection is a Builder\nconst posts = await queryCollection('blog')\n  .where('tags', 'LIKE', `%${tag}%`)\n  .order('date', 'DESC')\n  .limit(10)\n  .find()\n",[15,5361,5362,5367,5381,5421,5430,5456,5475,5489,5507,5511,5516,5541,5586,5613,5626],{"__ignoreMap":260},[129,5363,5364],{"class":265,"line":266},[129,5365,5366],{"class":376},"// Drizzle ORM is a Builder\n",[129,5368,5369,5371,5374,5376,5378],{"class":265,"line":297},[129,5370,270],{"class":269},[129,5372,5373],{"class":273}," results ",[129,5375,278],{"class":277},[129,5377,4779],{"class":2139},[129,5379,5380],{"class":273}," db\n",[129,5382,5383,5386,5388,5390,5392,5394,5396,5399,5401,5403,5405,5408,5410,5412,5414,5417,5419],{"class":265,"line":315},[129,5384,5385],{"class":277},"  .",[129,5387,2357],{"class":284},[129,5389,147],{"class":273},[129,5391,4796],{"class":277},[129,5393,4643],{"class":1376},[129,5395,1380],{"class":277},[129,5397,5398],{"class":273}," users",[129,5400,362],{"class":277},[129,5402,3190],{"class":273},[129,5404,1015],{"class":277},[129,5406,5407],{"class":1376}," name",[129,5409,1380],{"class":277},[129,5411,5398],{"class":273},[129,5413,362],{"class":277},[129,5415,5416],{"class":273},"name ",[129,5418,4028],{"class":277},[129,5420,294],{"class":273},[129,5422,5423,5425,5427],{"class":265,"line":332},[129,5424,5385],{"class":277},[129,5426,2589],{"class":284},[129,5428,5429],{"class":273},"(users)\n",[129,5431,5432,5434,5437,5439,5442,5445,5447,5450,5452,5454],{"class":265,"line":339},[129,5433,5385],{"class":277},[129,5435,5436],{"class":284},"where",[129,5438,147],{"class":273},[129,5440,5441],{"class":284},"eq",[129,5443,5444],{"class":273},"(users",[129,5446,362],{"class":277},[129,5448,5449],{"class":273},"active",[129,5451,1015],{"class":277},[129,5453,4823],{"class":4822},[129,5455,471],{"class":273},[129,5457,5458,5460,5463,5465,5468,5470,5472],{"class":265,"line":356},[129,5459,5385],{"class":277},[129,5461,5462],{"class":284},"orderBy",[129,5464,147],{"class":273},[129,5466,5467],{"class":284},"desc",[129,5469,5444],{"class":273},[129,5471,362],{"class":277},[129,5473,5474],{"class":273},"createdAt))\n",[129,5476,5477,5479,5482,5484,5487],{"class":265,"line":651},[129,5478,5385],{"class":277},[129,5480,5481],{"class":284},"limit",[129,5483,147],{"class":273},[129,5485,5486],{"class":290},"20",[129,5488,294],{"class":273},[129,5490,5491,5493,5496,5499,5502,5505],{"class":265,"line":657},[129,5492,5385],{"class":277},[129,5494,5495],{"class":284},"offset",[129,5497,5498],{"class":273},"(page ",[129,5500,5501],{"class":277},"*",[129,5503,5504],{"class":290}," 20",[129,5506,294],{"class":273},[129,5508,5509],{"class":265,"line":669},[129,5510,336],{"emptyLinePlaceholder":335},[129,5512,5513],{"class":265,"line":693},[129,5514,5515],{"class":376},"// @nuxt/content queryCollection is a Builder\n",[129,5517,5518,5520,5523,5525,5527,5530,5532,5534,5537,5539],{"class":265,"line":712},[129,5519,270],{"class":269},[129,5521,5522],{"class":273}," posts ",[129,5524,278],{"class":277},[129,5526,4779],{"class":2139},[129,5528,5529],{"class":284}," queryCollection",[129,5531,147],{"class":273},[129,5533,424],{"class":277},[129,5535,5536],{"class":427},"blog",[129,5538,424],{"class":277},[129,5540,294],{"class":273},[129,5542,5543,5545,5547,5549,5551,5554,5556,5558,5560,5563,5565,5567,5570,5573,5575,5578,5580,5582,5584],{"class":265,"line":1521},[129,5544,5385],{"class":277},[129,5546,5436],{"class":284},[129,5548,147],{"class":273},[129,5550,424],{"class":277},[129,5552,5553],{"class":427},"tags",[129,5555,424],{"class":277},[129,5557,1015],{"class":277},[129,5559,4261],{"class":277},[129,5561,5562],{"class":427},"LIKE",[129,5564,424],{"class":277},[129,5566,1015],{"class":277},[129,5568,5569],{"class":277}," `",[129,5571,5572],{"class":427},"%",[129,5574,4131],{"class":277},[129,5576,5577],{"class":273},"tag",[129,5579,4028],{"class":277},[129,5581,5572],{"class":427},[129,5583,4125],{"class":277},[129,5585,294],{"class":273},[129,5587,5588,5590,5593,5595,5597,5600,5602,5604,5606,5609,5611],{"class":265,"line":1527},[129,5589,5385],{"class":277},[129,5591,5592],{"class":284},"order",[129,5594,147],{"class":273},[129,5596,424],{"class":277},[129,5598,5599],{"class":427},"date",[129,5601,424],{"class":277},[129,5603,1015],{"class":277},[129,5605,4261],{"class":277},[129,5607,5608],{"class":427},"DESC",[129,5610,424],{"class":277},[129,5612,294],{"class":273},[129,5614,5615,5617,5619,5621,5624],{"class":265,"line":2295},[129,5616,5385],{"class":277},[129,5618,5481],{"class":284},[129,5620,147],{"class":273},[129,5622,5623],{"class":290},"10",[129,5625,294],{"class":273},[129,5627,5628,5630,5633],{"class":265,"line":2300},[129,5629,5385],{"class":277},[129,5631,5632],{"class":284},"find",[129,5634,2451],{"class":273},[11,5636,5637],{},"The value is that you can conditionally add steps without nested ternaries:",[255,5639,5642],{"className":3922,"code":5640,"filename":5641,"language":3924,"meta":260,"style":260},"export default defineEventHandler(async (event) => {\n  const { tag, author, limit = 20, page = 0 } = getQuery(event)\n\n  let query = queryCollection('blog').order('date', 'DESC')\n\n  // Conditionally add filters - much cleaner than building SQL strings\n  if (tag) query = query.where('tags', 'LIKE', `%${tag}%`)\n  if (author) query = query.where('author', '==', author)\n\n  return query.limit(Number(limit)).skip(Number(page) * Number(limit)).find()\n})\n","server/api/posts.get.ts",[15,5643,5644,5666,5712,5716,5760,5764,5769,5824,5868,5872,5927],{"__ignoreMap":260},[129,5645,5646,5648,5650,5652,5654,5656,5658,5660,5662,5664],{"class":265,"line":266},[129,5647,4050],{"class":2139},[129,5649,4053],{"class":2139},[129,5651,4503],{"class":284},[129,5653,147],{"class":273},[129,5655,4508],{"class":269},[129,5657,3984],{"class":277},[129,5659,4100],{"class":452},[129,5661,160],{"class":277},[129,5663,456],{"class":269},[129,5665,1371],{"class":277},[129,5667,5668,5670,5672,5675,5677,5680,5682,5685,5687,5689,5691,5694,5696,5699,5701,5703,5706,5708,5710],{"class":265,"line":297},[129,5669,5076],{"class":269},[129,5671,1416],{"class":277},[129,5673,5674],{"class":273}," tag",[129,5676,1015],{"class":277},[129,5678,5679],{"class":273}," author",[129,5681,1015],{"class":277},[129,5683,5684],{"class":273}," limit",[129,5686,4745],{"class":277},[129,5688,5504],{"class":290},[129,5690,1015],{"class":277},[129,5692,5693],{"class":273}," page",[129,5695,4745],{"class":277},[129,5697,5698],{"class":290}," 0",[129,5700,4255],{"class":277},[129,5702,4745],{"class":277},[129,5704,5705],{"class":284}," getQuery",[129,5707,147],{"class":1376},[129,5709,4100],{"class":273},[129,5711,294],{"class":1376},[129,5713,5714],{"class":265,"line":315},[129,5715,336],{"emptyLinePlaceholder":335},[129,5717,5718,5721,5724,5726,5728,5730,5732,5734,5736,5738,5740,5742,5744,5746,5748,5750,5752,5754,5756,5758],{"class":265,"line":332},[129,5719,5720],{"class":269},"  let",[129,5722,5723],{"class":273}," query",[129,5725,4745],{"class":277},[129,5727,5529],{"class":284},[129,5729,147],{"class":1376},[129,5731,424],{"class":277},[129,5733,5536],{"class":427},[129,5735,424],{"class":277},[129,5737,160],{"class":1376},[129,5739,362],{"class":277},[129,5741,5592],{"class":284},[129,5743,147],{"class":1376},[129,5745,424],{"class":277},[129,5747,5599],{"class":427},[129,5749,424],{"class":277},[129,5751,1015],{"class":277},[129,5753,4261],{"class":277},[129,5755,5608],{"class":427},[129,5757,424],{"class":277},[129,5759,294],{"class":1376},[129,5761,5762],{"class":265,"line":339},[129,5763,336],{"emptyLinePlaceholder":335},[129,5765,5766],{"class":265,"line":356},[129,5767,5768],{"class":376},"  // Conditionally add filters - much cleaner than building SQL strings\n",[129,5770,5771,5773,5775,5777,5779,5782,5784,5786,5788,5790,5792,5794,5796,5798,5800,5802,5804,5806,5808,5810,5812,5814,5816,5818,5820,5822],{"class":265,"line":651},[129,5772,3998],{"class":2139},[129,5774,3984],{"class":1376},[129,5776,5577],{"class":273},[129,5778,4005],{"class":1376},[129,5780,5781],{"class":273},"query",[129,5783,4745],{"class":277},[129,5785,5723],{"class":273},[129,5787,362],{"class":277},[129,5789,5436],{"class":284},[129,5791,147],{"class":1376},[129,5793,424],{"class":277},[129,5795,5553],{"class":427},[129,5797,424],{"class":277},[129,5799,1015],{"class":277},[129,5801,4261],{"class":277},[129,5803,5562],{"class":427},[129,5805,424],{"class":277},[129,5807,1015],{"class":277},[129,5809,5569],{"class":277},[129,5811,5572],{"class":427},[129,5813,4131],{"class":277},[129,5815,5577],{"class":273},[129,5817,4028],{"class":277},[129,5819,5572],{"class":427},[129,5821,4125],{"class":277},[129,5823,294],{"class":1376},[129,5825,5826,5828,5830,5833,5835,5837,5839,5841,5843,5845,5847,5849,5851,5853,5855,5857,5860,5862,5864,5866],{"class":265,"line":657},[129,5827,3998],{"class":2139},[129,5829,3984],{"class":1376},[129,5831,5832],{"class":273},"author",[129,5834,4005],{"class":1376},[129,5836,5781],{"class":273},[129,5838,4745],{"class":277},[129,5840,5723],{"class":273},[129,5842,362],{"class":277},[129,5844,5436],{"class":284},[129,5846,147],{"class":1376},[129,5848,424],{"class":277},[129,5850,5832],{"class":427},[129,5852,424],{"class":277},[129,5854,1015],{"class":277},[129,5856,4261],{"class":277},[129,5858,5859],{"class":427},"==",[129,5861,424],{"class":277},[129,5863,1015],{"class":277},[129,5865,5679],{"class":273},[129,5867,294],{"class":1376},[129,5869,5870],{"class":265,"line":669},[129,5871,336],{"emptyLinePlaceholder":335},[129,5873,5874,5876,5878,5880,5882,5884,5887,5889,5891,5894,5896,5899,5901,5903,5905,5908,5910,5912,5915,5917,5919,5921,5923,5925],{"class":265,"line":693},[129,5875,4520],{"class":2139},[129,5877,5723],{"class":273},[129,5879,362],{"class":277},[129,5881,5481],{"class":284},[129,5883,147],{"class":1376},[129,5885,5886],{"class":284},"Number",[129,5888,147],{"class":1376},[129,5890,5481],{"class":273},[129,5892,5893],{"class":1376},"))",[129,5895,362],{"class":277},[129,5897,5898],{"class":284},"skip",[129,5900,147],{"class":1376},[129,5902,5886],{"class":284},[129,5904,147],{"class":1376},[129,5906,5907],{"class":273},"page",[129,5909,4005],{"class":1376},[129,5911,5501],{"class":277},[129,5913,5914],{"class":284}," Number",[129,5916,147],{"class":1376},[129,5918,5481],{"class":273},[129,5920,5893],{"class":1376},[129,5922,362],{"class":277},[129,5924,5632],{"class":284},[129,5926,2451],{"class":1376},[129,5928,5929,5931],{"class":265,"line":712},[129,5930,4028],{"class":277},[129,5932,294],{"class":273},[11,5934,5935],{},"Writing your own Builder makes sense when you're constructing something complex - an email template, a PDF, a search query with many optional filters. Writing it from scratch just for the pattern is overkill.",[11,5937,5938],{},"We are already using this through ORMs and query libraries. Rolling own makes sense for a genuinely complex object with many optional parts - overkill for two optional parameters.",[2001,5940],{},[40,5942,5944],{"id":5943},"strategy","Strategy",[3576,5946,5947],{},[11,5948,5949,5951],{},[118,5950,3878],{},": Defines a family of algorithms, encapsulates each one, and makes them interchangeable at runtime. The code that uses the algorithm doesn't need to know which one it's using.",[11,5953,5954],{},"In JavaScript, this pattern collapses to \"pass a function.\" Which is exactly how it should be:",[255,5956,5959],{"className":3922,"code":5957,"filename":5958,"language":3924,"meta":260,"style":260},"// Instead of a Strategy class hierarchy, just type the function\ntype AuthStrategy = (event: H3Event) => Promise\u003CUser | null>\n\nconst jwtStrategy: AuthStrategy = async (event) => {\n  const token = getHeader(event, 'authorization')?.replace('Bearer ', '')\n  if (!token) return null\n  return verifyJwt(token)\n}\n\nconst sessionStrategy: AuthStrategy = async (event) => {\n  const sessionId = getCookie(event, 'session_id')\n  if (!sessionId) return null\n  return sessionStore.get(sessionId)\n}\n\nconst apiKeyStrategy: AuthStrategy = async (event) => {\n  const key = getHeader(event, 'x-api-key')\n  if (!key) return null\n  return db.select().from(apiKeys).where(eq(apiKeys.key, key)).get()?.user ?? null\n}\n","server/lib/auth.ts",[15,5960,5961,5966,6001,6005,6031,6079,6096,6109,6113,6117,6142,6169,6186,6204,6208,6212,6237,6263,6280,6340],{"__ignoreMap":260},[129,5962,5963],{"class":265,"line":266},[129,5964,5965],{"class":376},"// Instead of a Strategy class hierarchy, just type the function\n",[129,5967,5968,5971,5974,5976,5978,5980,5982,5985,5987,5989,5991,5993,5995,5997,5999],{"class":265,"line":297},[129,5969,5970],{"class":269},"type",[129,5972,5973],{"class":2161}," AuthStrategy",[129,5975,4745],{"class":277},[129,5977,3984],{"class":277},[129,5979,4100],{"class":452},[129,5981,1380],{"class":277},[129,5983,5984],{"class":2161}," H3Event",[129,5986,160],{"class":277},[129,5988,456],{"class":269},[129,5990,4637],{"class":2161},[129,5992,3945],{"class":277},[129,5994,3948],{"class":2161},[129,5996,3951],{"class":277},[129,5998,1441],{"class":2161},[129,6000,4676],{"class":277},[129,6002,6003],{"class":265,"line":315},[129,6004,336],{"emptyLinePlaceholder":335},[129,6006,6007,6009,6012,6014,6016,6018,6021,6023,6025,6027,6029],{"class":265,"line":332},[129,6008,270],{"class":269},[129,6010,6011],{"class":273}," jwtStrategy",[129,6013,1380],{"class":277},[129,6015,5973],{"class":2161},[129,6017,4745],{"class":277},[129,6019,6020],{"class":269}," async",[129,6022,3984],{"class":277},[129,6024,4100],{"class":452},[129,6026,160],{"class":277},[129,6028,456],{"class":269},[129,6030,1371],{"class":277},[129,6032,6033,6035,6037,6039,6042,6044,6046,6048,6050,6053,6055,6057,6060,6063,6065,6067,6070,6072,6074,6077],{"class":265,"line":339},[129,6034,5076],{"class":269},[129,6036,4627],{"class":273},[129,6038,4745],{"class":277},[129,6040,6041],{"class":284}," getHeader",[129,6043,147],{"class":1376},[129,6045,4100],{"class":273},[129,6047,1015],{"class":277},[129,6049,4261],{"class":277},[129,6051,6052],{"class":427},"authorization",[129,6054,424],{"class":277},[129,6056,160],{"class":1376},[129,6058,6059],{"class":277},"?.",[129,6061,6062],{"class":284},"replace",[129,6064,147],{"class":1376},[129,6066,424],{"class":277},[129,6068,6069],{"class":427},"Bearer ",[129,6071,424],{"class":277},[129,6073,1015],{"class":277},[129,6075,6076],{"class":277}," ''",[129,6078,294],{"class":1376},[129,6080,6081,6083,6085,6087,6090,6092,6094],{"class":265,"line":356},[129,6082,3998],{"class":2139},[129,6084,3984],{"class":1376},[129,6086,4447],{"class":277},[129,6088,6089],{"class":273},"token",[129,6091,4005],{"class":1376},[129,6093,5127],{"class":2139},[129,6095,1518],{"class":277},[129,6097,6098,6100,6103,6105,6107],{"class":265,"line":651},[129,6099,4520],{"class":2139},[129,6101,6102],{"class":284}," verifyJwt",[129,6104,147],{"class":1376},[129,6106,6089],{"class":273},[129,6108,294],{"class":1376},[129,6110,6111],{"class":265,"line":657},[129,6112,1530],{"class":277},[129,6114,6115],{"class":265,"line":669},[129,6116,336],{"emptyLinePlaceholder":335},[129,6118,6119,6121,6124,6126,6128,6130,6132,6134,6136,6138,6140],{"class":265,"line":693},[129,6120,270],{"class":269},[129,6122,6123],{"class":273}," sessionStrategy",[129,6125,1380],{"class":277},[129,6127,5973],{"class":2161},[129,6129,4745],{"class":277},[129,6131,6020],{"class":269},[129,6133,3984],{"class":277},[129,6135,4100],{"class":452},[129,6137,160],{"class":277},[129,6139,456],{"class":269},[129,6141,1371],{"class":277},[129,6143,6144,6146,6149,6151,6154,6156,6158,6160,6162,6165,6167],{"class":265,"line":712},[129,6145,5076],{"class":269},[129,6147,6148],{"class":273}," sessionId",[129,6150,4745],{"class":277},[129,6152,6153],{"class":284}," getCookie",[129,6155,147],{"class":1376},[129,6157,4100],{"class":273},[129,6159,1015],{"class":277},[129,6161,4261],{"class":277},[129,6163,6164],{"class":427},"session_id",[129,6166,424],{"class":277},[129,6168,294],{"class":1376},[129,6170,6171,6173,6175,6177,6180,6182,6184],{"class":265,"line":1521},[129,6172,3998],{"class":2139},[129,6174,3984],{"class":1376},[129,6176,4447],{"class":277},[129,6178,6179],{"class":273},"sessionId",[129,6181,4005],{"class":1376},[129,6183,5127],{"class":2139},[129,6185,1518],{"class":277},[129,6187,6188,6190,6193,6195,6198,6200,6202],{"class":265,"line":1527},[129,6189,4520],{"class":2139},[129,6191,6192],{"class":273}," sessionStore",[129,6194,362],{"class":277},[129,6196,6197],{"class":284},"get",[129,6199,147],{"class":1376},[129,6201,6179],{"class":273},[129,6203,294],{"class":1376},[129,6205,6206],{"class":265,"line":2295},[129,6207,1530],{"class":277},[129,6209,6210],{"class":265,"line":2300},[129,6211,336],{"emptyLinePlaceholder":335},[129,6213,6214,6216,6219,6221,6223,6225,6227,6229,6231,6233,6235],{"class":265,"line":2305},[129,6215,270],{"class":269},[129,6217,6218],{"class":273}," apiKeyStrategy",[129,6220,1380],{"class":277},[129,6222,5973],{"class":2161},[129,6224,4745],{"class":277},[129,6226,6020],{"class":269},[129,6228,3984],{"class":277},[129,6230,4100],{"class":452},[129,6232,160],{"class":277},[129,6234,456],{"class":269},[129,6236,1371],{"class":277},[129,6238,6239,6241,6244,6246,6248,6250,6252,6254,6256,6259,6261],{"class":265,"line":2311},[129,6240,5076],{"class":269},[129,6242,6243],{"class":273}," key",[129,6245,4745],{"class":277},[129,6247,6041],{"class":284},[129,6249,147],{"class":1376},[129,6251,4100],{"class":273},[129,6253,1015],{"class":277},[129,6255,4261],{"class":277},[129,6257,6258],{"class":427},"x-api-key",[129,6260,424],{"class":277},[129,6262,294],{"class":1376},[129,6264,6265,6267,6269,6271,6274,6276,6278],{"class":265,"line":2329},[129,6266,3998],{"class":2139},[129,6268,3984],{"class":1376},[129,6270,4447],{"class":277},[129,6272,6273],{"class":273},"key",[129,6275,4005],{"class":1376},[129,6277,5127],{"class":2139},[129,6279,1518],{"class":277},[129,6281,6282,6284,6286,6288,6290,6292,6294,6296,6298,6301,6303,6305,6307,6309,6311,6313,6315,6317,6319,6321,6323,6325,6327,6329,6331,6333,6336,6338],{"class":265,"line":2351},[129,6283,4520],{"class":2139},[129,6285,4479],{"class":273},[129,6287,362],{"class":277},[129,6289,2357],{"class":284},[129,6291,4140],{"class":1376},[129,6293,362],{"class":277},[129,6295,2589],{"class":284},[129,6297,147],{"class":1376},[129,6299,6300],{"class":273},"apiKeys",[129,6302,160],{"class":1376},[129,6304,362],{"class":277},[129,6306,5436],{"class":284},[129,6308,147],{"class":1376},[129,6310,5441],{"class":284},[129,6312,147],{"class":1376},[129,6314,6300],{"class":273},[129,6316,362],{"class":277},[129,6318,6273],{"class":273},[129,6320,1015],{"class":277},[129,6322,6243],{"class":273},[129,6324,5893],{"class":1376},[129,6326,362],{"class":277},[129,6328,6197],{"class":284},[129,6330,4140],{"class":1376},[129,6332,6059],{"class":277},[129,6334,6335],{"class":273},"user",[129,6337,5096],{"class":277},[129,6339,1518],{"class":277},[129,6341,6342],{"class":265,"line":2387},[129,6343,1530],{"class":277},[255,6345,6348],{"className":3922,"code":6346,"filename":6347,"language":3924,"meta":260,"style":260},"import { jwtStrategy, sessionStrategy, apiKeyStrategy } from '../lib/auth'\n\n// Strategies tried in order - first one that returns a user wins\nconst strategies = [jwtStrategy, sessionStrategy, apiKeyStrategy]\n\nexport default defineEventHandler(async (event) => {\n  for (const strategy of strategies) {\n    const user = await strategy(event)\n    if (user) {\n      event.context.user = user\n      return\n    }\n  }\n  // No strategy matched - user is unauthenticated\n})\n","server/middleware/auth.ts",[15,6349,6350,6377,6381,6386,6407,6411,6433,6456,6475,6488,6507,6512,6517,6521,6526],{"__ignoreMap":260},[129,6351,6352,6354,6356,6358,6360,6362,6364,6366,6368,6370,6372,6375],{"class":265,"line":266},[129,6353,2140],{"class":2139},[129,6355,1416],{"class":277},[129,6357,6011],{"class":273},[129,6359,1015],{"class":277},[129,6361,6123],{"class":273},[129,6363,1015],{"class":277},[129,6365,6218],{"class":273},[129,6367,4255],{"class":277},[129,6369,4258],{"class":2139},[129,6371,4261],{"class":277},[129,6373,6374],{"class":427},"../lib/auth",[129,6376,4267],{"class":277},[129,6378,6379],{"class":265,"line":297},[129,6380,336],{"emptyLinePlaceholder":335},[129,6382,6383],{"class":265,"line":315},[129,6384,6385],{"class":376},"// Strategies tried in order - first one that returns a user wins\n",[129,6387,6388,6390,6393,6395,6398,6400,6402,6404],{"class":265,"line":332},[129,6389,270],{"class":269},[129,6391,6392],{"class":273}," strategies ",[129,6394,278],{"class":277},[129,6396,6397],{"class":273}," [jwtStrategy",[129,6399,1015],{"class":277},[129,6401,6123],{"class":273},[129,6403,1015],{"class":277},[129,6405,6406],{"class":273}," apiKeyStrategy]\n",[129,6408,6409],{"class":265,"line":339},[129,6410,336],{"emptyLinePlaceholder":335},[129,6412,6413,6415,6417,6419,6421,6423,6425,6427,6429,6431],{"class":265,"line":356},[129,6414,4050],{"class":2139},[129,6416,4053],{"class":2139},[129,6418,4503],{"class":284},[129,6420,147],{"class":273},[129,6422,4508],{"class":269},[129,6424,3984],{"class":277},[129,6426,4100],{"class":452},[129,6428,160],{"class":277},[129,6430,456],{"class":269},[129,6432,1371],{"class":277},[129,6434,6435,6438,6440,6442,6445,6448,6451,6453],{"class":265,"line":651},[129,6436,6437],{"class":2139},"  for",[129,6439,3984],{"class":1376},[129,6441,270],{"class":269},[129,6443,6444],{"class":273}," strategy",[129,6446,6447],{"class":277}," of",[129,6449,6450],{"class":273}," strategies",[129,6452,4005],{"class":1376},[129,6454,6455],{"class":277},"{\n",[129,6457,6458,6460,6463,6465,6467,6469,6471,6473],{"class":265,"line":657},[129,6459,4739],{"class":269},[129,6461,6462],{"class":273}," user",[129,6464,4745],{"class":277},[129,6466,4779],{"class":2139},[129,6468,6444],{"class":284},[129,6470,147],{"class":1376},[129,6472,4100],{"class":273},[129,6474,294],{"class":1376},[129,6476,6477,6480,6482,6484,6486],{"class":265,"line":669},[129,6478,6479],{"class":2139},"    if",[129,6481,3984],{"class":1376},[129,6483,6335],{"class":273},[129,6485,4005],{"class":1376},[129,6487,6455],{"class":277},[129,6489,6490,6493,6495,6498,6500,6502,6504],{"class":265,"line":693},[129,6491,6492],{"class":273},"      event",[129,6494,362],{"class":277},[129,6496,6497],{"class":273},"context",[129,6499,362],{"class":277},[129,6501,6335],{"class":273},[129,6503,4745],{"class":277},[129,6505,6506],{"class":273}," user\n",[129,6508,6509],{"class":265,"line":712},[129,6510,6511],{"class":2139},"      return\n",[129,6513,6514],{"class":265,"line":1521},[129,6515,6516],{"class":277},"    }\n",[129,6518,6519],{"class":265,"line":1527},[129,6520,1524],{"class":277},[129,6522,6523],{"class":265,"line":2295},[129,6524,6525],{"class":376},"  // No strategy matched - user is unauthenticated\n",[129,6527,6528,6530],{"class":265,"line":2300},[129,6529,4028],{"class":277},[129,6531,294],{"class":273},[11,6533,6534],{},"This is Strategy, just an array of functions with the same signature. You can add a new auth method by adding a function to the array. You can test each strategy in isolation. The middleware doesn't care which one works.",[11,6536,6537],{},"Genuinely useful - especially for auth and anything where you want swappable behavior.",[2001,6539],{},[40,6541,6543],{"id":6542},"decorator","Decorator",[3576,6545,6546],{},[11,6547,6548,6550],{},[118,6549,3878],{},": Wraps a function or object to add behavior without modifying the original. Adds responsibility dynamically.",[11,6552,6553,6554,6556],{},"This one is everywhere in backend code. Nitro's ",[15,6555,3853],{}," is a Decorator - it takes a handler and returns a new handler that caches the result:",[255,6558,6560],{"className":3922,"code":6559,"language":3924,"meta":260,"style":260},"// defineCachedEventHandler is a decorator built into Nitro\nexport default defineCachedEventHandler(async (event) => {\n  return await db.select().from(products).all()\n}, { maxAge: 300, name: 'products' })\n",[15,6561,6562,6567,6590,6621],{"__ignoreMap":260},[129,6563,6564],{"class":265,"line":266},[129,6565,6566],{"class":376},"// defineCachedEventHandler is a decorator built into Nitro\n",[129,6568,6569,6571,6573,6576,6578,6580,6582,6584,6586,6588],{"class":265,"line":297},[129,6570,4050],{"class":2139},[129,6572,4053],{"class":2139},[129,6574,6575],{"class":284}," defineCachedEventHandler",[129,6577,147],{"class":273},[129,6579,4508],{"class":269},[129,6581,3984],{"class":277},[129,6583,4100],{"class":452},[129,6585,160],{"class":277},[129,6587,456],{"class":269},[129,6589,1371],{"class":277},[129,6591,6592,6594,6596,6598,6600,6602,6604,6606,6608,6610,6613,6615,6617,6619],{"class":265,"line":315},[129,6593,4520],{"class":2139},[129,6595,4779],{"class":2139},[129,6597,4479],{"class":273},[129,6599,362],{"class":277},[129,6601,2357],{"class":284},[129,6603,4140],{"class":1376},[129,6605,362],{"class":277},[129,6607,2589],{"class":284},[129,6609,147],{"class":1376},[129,6611,6612],{"class":273},"products",[129,6614,160],{"class":1376},[129,6616,362],{"class":277},[129,6618,4544],{"class":284},[129,6620,2451],{"class":1376},[129,6622,6623,6626,6628,6631,6633,6636,6638,6640,6642,6644,6646,6648,6650],{"class":265,"line":332},[129,6624,6625],{"class":277},"},",[129,6627,1416],{"class":277},[129,6629,6630],{"class":1376}," maxAge",[129,6632,1380],{"class":277},[129,6634,6635],{"class":290}," 300",[129,6637,1015],{"class":277},[129,6639,5407],{"class":1376},[129,6641,1380],{"class":277},[129,6643,4261],{"class":277},[129,6645,6612],{"class":427},[129,6647,424],{"class":277},[129,6649,4255],{"class":277},[129,6651,294],{"class":273},[11,6653,6654],{},"You can write your own for cross-cutting concerns - logging, timing, auth checks:",[255,6656,6659],{"className":3922,"code":6657,"filename":6658,"language":3924,"meta":260,"style":260},"// Adds timing to any handler\nexport function withTiming(handler: EventHandler): EventHandler {\n  return defineEventHandler(async (event) => {\n    const start = performance.now()\n    const result = await handler(event)\n    const duration = performance.now() - start\n    setResponseHeader(event, 'X-Response-Time', `${duration.toFixed(2)}ms`)\n    return result\n  })\n}\n\n// Requires authentication\nexport function withAuth(handler: EventHandler): EventHandler {\n  return defineEventHandler(async (event) => {\n    if (!event.context.user) {\n      throw createError({ statusCode: 401, message: 'Unauthorized' })\n    }\n    return handler(event)\n  })\n}\n\n// Combine multiple decorators\nexport function pipe(...decorators: Array\u003C(h: EventHandler) => EventHandler>) {\n  return (handler: EventHandler) => decorators.reduceRight((h, d) => d(h), handler)\n}\n","server/utils/decorators.ts",[15,6660,6661,6666,6691,6711,6729,6749,6772,6818,6825,6831,6835,6839,6844,6867,6887,6909,6947,6951,6963,6969,6973,6977,6982,7023,7076],{"__ignoreMap":260},[129,6662,6663],{"class":265,"line":266},[129,6664,6665],{"class":376},"// Adds timing to any handler\n",[129,6667,6668,6670,6672,6675,6677,6680,6682,6685,6687,6689],{"class":265,"line":297},[129,6669,4050],{"class":2139},[129,6671,5060],{"class":269},[129,6673,6674],{"class":284}," withTiming",[129,6676,147],{"class":277},[129,6678,6679],{"class":452},"handler",[129,6681,1380],{"class":277},[129,6683,6684],{"class":2161}," EventHandler",[129,6686,4634],{"class":277},[129,6688,6684],{"class":2161},[129,6690,1371],{"class":277},[129,6692,6693,6695,6697,6699,6701,6703,6705,6707,6709],{"class":265,"line":315},[129,6694,4520],{"class":2139},[129,6696,4503],{"class":284},[129,6698,147],{"class":1376},[129,6700,4508],{"class":269},[129,6702,3984],{"class":277},[129,6704,4100],{"class":452},[129,6706,160],{"class":277},[129,6708,456],{"class":269},[129,6710,1371],{"class":277},[129,6712,6713,6715,6718,6720,6723,6725,6727],{"class":265,"line":332},[129,6714,4739],{"class":269},[129,6716,6717],{"class":273}," start",[129,6719,4745],{"class":277},[129,6721,6722],{"class":273}," performance",[129,6724,362],{"class":277},[129,6726,3587],{"class":284},[129,6728,2451],{"class":1376},[129,6730,6731,6733,6736,6738,6740,6743,6745,6747],{"class":265,"line":339},[129,6732,4739],{"class":269},[129,6734,6735],{"class":273}," result",[129,6737,4745],{"class":277},[129,6739,4779],{"class":2139},[129,6741,6742],{"class":284}," handler",[129,6744,147],{"class":1376},[129,6746,4100],{"class":273},[129,6748,294],{"class":1376},[129,6750,6751,6753,6756,6758,6760,6762,6764,6766,6769],{"class":265,"line":356},[129,6752,4739],{"class":269},[129,6754,6755],{"class":273}," duration",[129,6757,4745],{"class":277},[129,6759,6722],{"class":273},[129,6761,362],{"class":277},[129,6763,3587],{"class":284},[129,6765,824],{"class":1376},[129,6767,6768],{"class":277},"-",[129,6770,6771],{"class":273}," start\n",[129,6773,6774,6777,6779,6781,6783,6785,6788,6790,6792,6795,6798,6800,6803,6805,6807,6809,6811,6814,6816],{"class":265,"line":651},[129,6775,6776],{"class":284},"    setResponseHeader",[129,6778,147],{"class":1376},[129,6780,4100],{"class":273},[129,6782,1015],{"class":277},[129,6784,4261],{"class":277},[129,6786,6787],{"class":427},"X-Response-Time",[129,6789,424],{"class":277},[129,6791,1015],{"class":277},[129,6793,6794],{"class":277}," `${",[129,6796,6797],{"class":273},"duration",[129,6799,362],{"class":277},[129,6801,6802],{"class":284},"toFixed",[129,6804,147],{"class":273},[129,6806,183],{"class":290},[129,6808,160],{"class":273},[129,6810,4028],{"class":277},[129,6812,6813],{"class":427},"ms",[129,6815,4125],{"class":277},[129,6817,294],{"class":1376},[129,6819,6820,6822],{"class":265,"line":657},[129,6821,4832],{"class":2139},[129,6823,6824],{"class":273}," result\n",[129,6826,6827,6829],{"class":265,"line":669},[129,6828,4182],{"class":277},[129,6830,294],{"class":1376},[129,6832,6833],{"class":265,"line":693},[129,6834,1530],{"class":277},[129,6836,6837],{"class":265,"line":712},[129,6838,336],{"emptyLinePlaceholder":335},[129,6840,6841],{"class":265,"line":1521},[129,6842,6843],{"class":376},"// Requires authentication\n",[129,6845,6846,6848,6850,6853,6855,6857,6859,6861,6863,6865],{"class":265,"line":1527},[129,6847,4050],{"class":2139},[129,6849,5060],{"class":269},[129,6851,6852],{"class":284}," withAuth",[129,6854,147],{"class":277},[129,6856,6679],{"class":452},[129,6858,1380],{"class":277},[129,6860,6684],{"class":2161},[129,6862,4634],{"class":277},[129,6864,6684],{"class":2161},[129,6866,1371],{"class":277},[129,6868,6869,6871,6873,6875,6877,6879,6881,6883,6885],{"class":265,"line":2295},[129,6870,4520],{"class":2139},[129,6872,4503],{"class":284},[129,6874,147],{"class":1376},[129,6876,4508],{"class":269},[129,6878,3984],{"class":277},[129,6880,4100],{"class":452},[129,6882,160],{"class":277},[129,6884,456],{"class":269},[129,6886,1371],{"class":277},[129,6888,6889,6891,6893,6895,6897,6899,6901,6903,6905,6907],{"class":265,"line":2300},[129,6890,6479],{"class":2139},[129,6892,3984],{"class":1376},[129,6894,4447],{"class":277},[129,6896,4100],{"class":273},[129,6898,362],{"class":277},[129,6900,6497],{"class":273},[129,6902,362],{"class":277},[129,6904,6335],{"class":273},[129,6906,4005],{"class":1376},[129,6908,6455],{"class":277},[129,6910,6911,6914,6917,6919,6921,6924,6926,6929,6931,6934,6936,6938,6941,6943,6945],{"class":265,"line":2305},[129,6912,6913],{"class":2139},"      throw",[129,6915,6916],{"class":284}," createError",[129,6918,147],{"class":1376},[129,6920,4796],{"class":277},[129,6922,6923],{"class":1376}," statusCode",[129,6925,1380],{"class":277},[129,6927,6928],{"class":290}," 401",[129,6930,1015],{"class":277},[129,6932,6933],{"class":1376}," message",[129,6935,1380],{"class":277},[129,6937,4261],{"class":277},[129,6939,6940],{"class":427},"Unauthorized",[129,6942,424],{"class":277},[129,6944,4255],{"class":277},[129,6946,294],{"class":1376},[129,6948,6949],{"class":265,"line":2311},[129,6950,6516],{"class":277},[129,6952,6953,6955,6957,6959,6961],{"class":265,"line":2329},[129,6954,4832],{"class":2139},[129,6956,6742],{"class":284},[129,6958,147],{"class":1376},[129,6960,4100],{"class":273},[129,6962,294],{"class":1376},[129,6964,6965,6967],{"class":265,"line":2351},[129,6966,4182],{"class":277},[129,6968,294],{"class":1376},[129,6970,6971],{"class":265,"line":2387},[129,6972,1530],{"class":277},[129,6974,6975],{"class":265,"line":2392},[129,6976,336],{"emptyLinePlaceholder":335},[129,6978,6979],{"class":265,"line":2398},[129,6980,6981],{"class":376},"// Combine multiple decorators\n",[129,6983,6984,6986,6988,6991,6994,6997,6999,7002,7005,7008,7010,7012,7014,7016,7018,7021],{"class":265,"line":2441},[129,6985,4050],{"class":2139},[129,6987,5060],{"class":269},[129,6989,6990],{"class":284}," pipe",[129,6992,6993],{"class":277},"(...",[129,6995,6996],{"class":452},"decorators",[129,6998,1380],{"class":277},[129,7000,7001],{"class":2161}," Array",[129,7003,7004],{"class":277},"\u003C(",[129,7006,7007],{"class":452},"h",[129,7009,1380],{"class":277},[129,7011,6684],{"class":2161},[129,7013,160],{"class":277},[129,7015,456],{"class":269},[129,7017,6684],{"class":2161},[129,7019,7020],{"class":277},">)",[129,7022,1371],{"class":277},[129,7024,7025,7027,7029,7031,7033,7035,7037,7039,7042,7044,7047,7049,7051,7053,7055,7058,7060,7062,7064,7066,7068,7070,7072,7074],{"class":265,"line":3246},[129,7026,4520],{"class":2139},[129,7028,3984],{"class":277},[129,7030,6679],{"class":452},[129,7032,1380],{"class":277},[129,7034,6684],{"class":2161},[129,7036,160],{"class":277},[129,7038,456],{"class":269},[129,7040,7041],{"class":273}," decorators",[129,7043,362],{"class":277},[129,7045,7046],{"class":284},"reduceRight",[129,7048,147],{"class":1376},[129,7050,147],{"class":277},[129,7052,7007],{"class":452},[129,7054,1015],{"class":277},[129,7056,7057],{"class":452}," d",[129,7059,160],{"class":277},[129,7061,456],{"class":269},[129,7063,7057],{"class":284},[129,7065,147],{"class":1376},[129,7067,7007],{"class":273},[129,7069,160],{"class":1376},[129,7071,1015],{"class":277},[129,7073,6742],{"class":273},[129,7075,294],{"class":1376},[129,7077,7078],{"class":265,"line":3251},[129,7079,1530],{"class":277},[255,7081,7084],{"className":3922,"code":7082,"filename":7083,"language":3924,"meta":260,"style":260},"import { withAuth, withTiming, pipe } from '../utils/decorators'\n\nconst handler = defineEventHandler(async () => {\n  return db.select().from(users).all()\n})\n\n// Apply both decorators - clean, composable, no modification to handler\nexport default pipe(withAuth, withTiming)(handler)\n","server/api/admin/users.get.ts",[15,7085,7086,7113,7117,7138,7166,7172,7176,7181],{"__ignoreMap":260},[129,7087,7088,7090,7092,7094,7096,7098,7100,7102,7104,7106,7108,7111],{"class":265,"line":266},[129,7089,2140],{"class":2139},[129,7091,1416],{"class":277},[129,7093,6852],{"class":273},[129,7095,1015],{"class":277},[129,7097,6674],{"class":273},[129,7099,1015],{"class":277},[129,7101,6990],{"class":273},[129,7103,4255],{"class":277},[129,7105,4258],{"class":2139},[129,7107,4261],{"class":277},[129,7109,7110],{"class":427},"../utils/decorators",[129,7112,4267],{"class":277},[129,7114,7115],{"class":265,"line":297},[129,7116,336],{"emptyLinePlaceholder":335},[129,7118,7119,7121,7124,7126,7128,7130,7132,7134,7136],{"class":265,"line":315},[129,7120,270],{"class":269},[129,7122,7123],{"class":273}," handler ",[129,7125,278],{"class":277},[129,7127,4503],{"class":284},[129,7129,147],{"class":273},[129,7131,4508],{"class":269},[129,7133,4511],{"class":277},[129,7135,456],{"class":269},[129,7137,1371],{"class":277},[129,7139,7140,7142,7144,7146,7148,7150,7152,7154,7156,7158,7160,7162,7164],{"class":265,"line":332},[129,7141,4520],{"class":2139},[129,7143,4479],{"class":273},[129,7145,362],{"class":277},[129,7147,2357],{"class":284},[129,7149,4140],{"class":1376},[129,7151,362],{"class":277},[129,7153,2589],{"class":284},[129,7155,147],{"class":1376},[129,7157,4537],{"class":273},[129,7159,160],{"class":1376},[129,7161,362],{"class":277},[129,7163,4544],{"class":284},[129,7165,2451],{"class":1376},[129,7167,7168,7170],{"class":265,"line":339},[129,7169,4028],{"class":277},[129,7171,294],{"class":273},[129,7173,7174],{"class":265,"line":356},[129,7175,336],{"emptyLinePlaceholder":335},[129,7177,7178],{"class":265,"line":651},[129,7179,7180],{"class":376},"// Apply both decorators - clean, composable, no modification to handler\n",[129,7182,7183,7185,7187,7189,7192,7194],{"class":265,"line":657},[129,7184,4050],{"class":2139},[129,7186,4053],{"class":2139},[129,7188,6990],{"class":284},[129,7190,7191],{"class":273},"(withAuth",[129,7193,1015],{"class":277},[129,7195,7196],{"class":273}," withTiming)(handler)\n",[11,7198,7199,7200,7203,7204,7209],{},"TypeScript also has a first-class ",[15,7201,7202],{},"@decorator"," syntax (now ",[51,7205,7208],{"href":7206,"rel":7207},"https://github.com/tc39/proposal-decorators",[55],"stage 3","), though it's more common in NestJS than Nuxt:",[255,7211,7213],{"className":3922,"code":7212,"language":3924,"meta":260,"style":260},"// NestJS style - you'll see this in enterprise TS backends\n@Controller('users')\n@UseGuards(AuthGuard)\nexport class UsersController {\n  @Get()\n  @CacheKey('all-users')\n  findAll() { /* ... */ }\n}\n",[15,7214,7215,7220,7238,7248,7260,7270,7288,7301],{"__ignoreMap":260},[129,7216,7217],{"class":265,"line":266},[129,7218,7219],{"class":376},"// NestJS style - you'll see this in enterprise TS backends\n",[129,7221,7222,7225,7228,7230,7232,7234,7236],{"class":265,"line":297},[129,7223,7224],{"class":277},"@",[129,7226,7227],{"class":284},"Controller",[129,7229,147],{"class":273},[129,7231,424],{"class":277},[129,7233,4537],{"class":427},[129,7235,424],{"class":277},[129,7237,294],{"class":273},[129,7239,7240,7242,7245],{"class":265,"line":315},[129,7241,7224],{"class":277},[129,7243,7244],{"class":284},"UseGuards",[129,7246,7247],{"class":273},"(AuthGuard)\n",[129,7249,7250,7252,7255,7258],{"class":265,"line":332},[129,7251,4050],{"class":2139},[129,7253,7254],{"class":269}," class",[129,7256,7257],{"class":2161}," UsersController",[129,7259,1371],{"class":277},[129,7261,7262,7265,7268],{"class":265,"line":339},[129,7263,7264],{"class":277},"  @",[129,7266,7267],{"class":284},"Get",[129,7269,2451],{"class":273},[129,7271,7272,7274,7277,7279,7281,7284,7286],{"class":265,"line":356},[129,7273,7264],{"class":277},[129,7275,7276],{"class":284},"CacheKey",[129,7278,147],{"class":273},[129,7280,424],{"class":277},[129,7282,7283],{"class":427},"all-users",[129,7285,424],{"class":277},[129,7287,294],{"class":273},[129,7289,7290,7293,7295,7297,7299],{"class":265,"line":651},[129,7291,7292],{"class":1376},"  findAll",[129,7294,4140],{"class":277},[129,7296,1416],{"class":277},[129,7298,5037],{"class":376},[129,7300,1476],{"class":277},[129,7302,7303],{"class":265,"line":657},[129,7304,1530],{"class":277},[11,7306,7307,7308,7310],{},"Very useful for Nitro middleware. ",[15,7309,3853],{}," is already this pattern. Good for example for stop copy-pasting auth checks into every route.",[2001,7312],{},[40,7314,7316],{"id":7315},"adapter","Adapter",[3576,7318,7319],{},[11,7320,7321,7323],{},[118,7322,3878],{},": Wraps an incompatible interface so it looks like the one your code expects. The most immediately practical pattern when working with third-party APIs.",[11,7325,7326],{},"The problem it solves: you integrate Stripe, your code now depends on Stripe's interface everywhere. When Stripe changes their API (or you want to add a second provider, or you want to test without hitting the real API), you have to touch every file that calls Stripe directly.",[11,7328,7329],{},"An Adapter puts a layer between your code and theirs:",[255,7331,7334],{"className":3922,"code":7332,"filename":7333,"language":3924,"meta":260,"style":260},"// Your interface - stable, controlled by you\nexport interface EmailService {\n  send(opts: { to: string; subject: string; html: string }): Promise\u003Cvoid>\n}\n\n// Adapter for Resend\nexport class ResendAdapter implements EmailService {\n  private client: Resend\n\n  constructor() {\n    this.client = new Resend(process.env.RESEND_API_KEY!)\n  }\n\n  async send({ to, subject, html }: { to: string; subject: string; html: string }) {\n    await this.client.emails.send({\n      from: 'noreply@yourdomain.com',\n      to,\n      subject,\n      html,\n      // Resend has different field names, extra options - adapter handles that\n    })\n  }\n}\n\n// Adapter for Nodemailer (SMTP - legacy systems)\nexport class SmtpAdapter implements EmailService {\n  private transporter: nodemailer.Transporter\n\n  constructor() {\n    this.transporter = nodemailer.createTransport({\n      host: process.env.SMTP_HOST,\n      port: Number(process.env.SMTP_PORT),\n      auth: { user: process.env.SMTP_USER, pass: process.env.SMTP_PASS },\n    })\n  }\n\n  async send({ to, subject, html }: { to: string; subject: string; html: string }) {\n    await this.transporter.sendMail({ from: process.env.SMTP_FROM, to, subject, html })\n  }\n}\n","server/lib/adapters/email.ts",[15,7335,7336,7341,7353,7404,7408,7412,7417,7432,7445,7449,7458,7490,7494,7498,7550,7573,7589,7596,7603,7610,7615,7622,7626,7630,7634,7639,7654,7671,7675,7683,7703,7723,7749,7794,7801,7806,7811,7860,7909,7914],{"__ignoreMap":260},[129,7337,7338],{"class":265,"line":266},[129,7339,7340],{"class":376},"// Your interface - stable, controlled by you\n",[129,7342,7343,7345,7348,7351],{"class":265,"line":297},[129,7344,4050],{"class":2139},[129,7346,7347],{"class":269}," interface",[129,7349,7350],{"class":2161}," EmailService",[129,7352,1371],{"class":277},[129,7354,7355,7358,7360,7363,7365,7367,7370,7372,7374,7377,7380,7382,7384,7386,7389,7391,7393,7396,7398,7400,7402],{"class":265,"line":315},[129,7356,7357],{"class":1376},"  send",[129,7359,147],{"class":277},[129,7361,7362],{"class":452},"opts",[129,7364,1380],{"class":277},[129,7366,1416],{"class":277},[129,7368,7369],{"class":1376}," to",[129,7371,1380],{"class":277},[129,7373,4622],{"class":2161},[129,7375,7376],{"class":277},";",[129,7378,7379],{"class":1376}," subject",[129,7381,1380],{"class":277},[129,7383,4622],{"class":2161},[129,7385,7376],{"class":277},[129,7387,7388],{"class":1376}," html",[129,7390,1380],{"class":277},[129,7392,4622],{"class":2161},[129,7394,7395],{"class":277}," }):",[129,7397,4637],{"class":2161},[129,7399,3945],{"class":277},[129,7401,4673],{"class":2161},[129,7403,4676],{"class":277},[129,7405,7406],{"class":265,"line":332},[129,7407,1530],{"class":277},[129,7409,7410],{"class":265,"line":339},[129,7411,336],{"emptyLinePlaceholder":335},[129,7413,7414],{"class":265,"line":356},[129,7415,7416],{"class":376},"// Adapter for Resend\n",[129,7418,7419,7421,7423,7426,7428,7430],{"class":265,"line":651},[129,7420,4050],{"class":2139},[129,7422,7254],{"class":269},[129,7424,7425],{"class":2161}," ResendAdapter",[129,7427,4694],{"class":269},[129,7429,7350],{"class":2161},[129,7431,1371],{"class":277},[129,7433,7434,7437,7440,7442],{"class":265,"line":657},[129,7435,7436],{"class":269},"  private",[129,7438,7439],{"class":1376}," client",[129,7441,1380],{"class":277},[129,7443,7444],{"class":2161}," Resend\n",[129,7446,7447],{"class":265,"line":669},[129,7448,336],{"emptyLinePlaceholder":335},[129,7450,7451,7454,7456],{"class":265,"line":693},[129,7452,7453],{"class":269},"  constructor",[129,7455,4140],{"class":277},[129,7457,1371],{"class":277},[129,7459,7460,7463,7466,7468,7470,7473,7475,7477,7479,7481,7483,7486,7488],{"class":265,"line":712},[129,7461,7462],{"class":277},"    this.",[129,7464,7465],{"class":273},"client",[129,7467,4745],{"class":277},[129,7469,281],{"class":277},[129,7471,7472],{"class":284}," Resend",[129,7474,147],{"class":1376},[129,7476,4755],{"class":273},[129,7478,362],{"class":277},[129,7480,4439],{"class":273},[129,7482,362],{"class":277},[129,7484,7485],{"class":273},"RESEND_API_KEY",[129,7487,4447],{"class":277},[129,7489,294],{"class":1376},[129,7491,7492],{"class":265,"line":1521},[129,7493,1524],{"class":277},[129,7495,7496],{"class":265,"line":1527},[129,7497,336],{"emptyLinePlaceholder":335},[129,7499,7500,7502,7505,7508,7510,7512,7514,7516,7518,7521,7523,7525,7527,7529,7531,7533,7535,7537,7539,7541,7543,7545,7548],{"class":265,"line":2295},[129,7501,4703],{"class":269},[129,7503,7504],{"class":1376}," send",[129,7506,7507],{"class":277},"({",[129,7509,7369],{"class":452},[129,7511,1015],{"class":277},[129,7513,7379],{"class":452},[129,7515,1015],{"class":277},[129,7517,7388],{"class":452},[129,7519,7520],{"class":277}," }:",[129,7522,1416],{"class":277},[129,7524,7369],{"class":1376},[129,7526,1380],{"class":277},[129,7528,4622],{"class":2161},[129,7530,7376],{"class":277},[129,7532,7379],{"class":1376},[129,7534,1380],{"class":277},[129,7536,4622],{"class":2161},[129,7538,7376],{"class":277},[129,7540,7388],{"class":1376},[129,7542,1380],{"class":277},[129,7544,4622],{"class":2161},[129,7546,7547],{"class":277}," })",[129,7549,1371],{"class":277},[129,7551,7552,7554,7557,7559,7561,7564,7566,7569,7571],{"class":265,"line":2300},[129,7553,4902],{"class":2139},[129,7555,7556],{"class":277}," this.",[129,7558,7465],{"class":273},[129,7560,362],{"class":277},[129,7562,7563],{"class":273},"emails",[129,7565,362],{"class":277},[129,7567,7568],{"class":284},"send",[129,7570,147],{"class":1376},[129,7572,6455],{"class":277},[129,7574,7575,7578,7580,7582,7585,7587],{"class":265,"line":2305},[129,7576,7577],{"class":1376},"      from",[129,7579,1380],{"class":277},[129,7581,4261],{"class":277},[129,7583,7584],{"class":427},"noreply@yourdomain.com",[129,7586,424],{"class":277},[129,7588,1386],{"class":277},[129,7590,7591,7594],{"class":265,"line":2311},[129,7592,7593],{"class":273},"      to",[129,7595,1386],{"class":277},[129,7597,7598,7601],{"class":265,"line":2329},[129,7599,7600],{"class":273},"      subject",[129,7602,1386],{"class":277},[129,7604,7605,7608],{"class":265,"line":2351},[129,7606,7607],{"class":273},"      html",[129,7609,1386],{"class":277},[129,7611,7612],{"class":265,"line":2387},[129,7613,7614],{"class":376},"      // Resend has different field names, extra options - adapter handles that\n",[129,7616,7617,7620],{"class":265,"line":2392},[129,7618,7619],{"class":277},"    }",[129,7621,294],{"class":1376},[129,7623,7624],{"class":265,"line":2398},[129,7625,1524],{"class":277},[129,7627,7628],{"class":265,"line":2441},[129,7629,1530],{"class":277},[129,7631,7632],{"class":265,"line":3246},[129,7633,336],{"emptyLinePlaceholder":335},[129,7635,7636],{"class":265,"line":3251},[129,7637,7638],{"class":376},"// Adapter for Nodemailer (SMTP - legacy systems)\n",[129,7640,7641,7643,7645,7648,7650,7652],{"class":265,"line":3263},[129,7642,4050],{"class":2139},[129,7644,7254],{"class":269},[129,7646,7647],{"class":2161}," SmtpAdapter",[129,7649,4694],{"class":269},[129,7651,7350],{"class":2161},[129,7653,1371],{"class":277},[129,7655,7656,7658,7661,7663,7666,7668],{"class":265,"line":5055},[129,7657,7436],{"class":269},[129,7659,7660],{"class":1376}," transporter",[129,7662,1380],{"class":277},[129,7664,7665],{"class":2161}," nodemailer",[129,7667,362],{"class":277},[129,7669,7670],{"class":2161},"Transporter\n",[129,7672,7673],{"class":265,"line":5073},[129,7674,336],{"emptyLinePlaceholder":335},[129,7676,7677,7679,7681],{"class":265,"line":5106},[129,7678,7453],{"class":269},[129,7680,4140],{"class":277},[129,7682,1371],{"class":277},[129,7684,7685,7687,7690,7692,7694,7696,7699,7701],{"class":265,"line":5136},[129,7686,7462],{"class":277},[129,7688,7689],{"class":273},"transporter",[129,7691,4745],{"class":277},[129,7693,7665],{"class":273},[129,7695,362],{"class":277},[129,7697,7698],{"class":284},"createTransport",[129,7700,147],{"class":1376},[129,7702,6455],{"class":277},[129,7704,7705,7708,7710,7712,7714,7716,7718,7721],{"class":265,"line":5164},[129,7706,7707],{"class":1376},"      host",[129,7709,1380],{"class":277},[129,7711,5084],{"class":273},[129,7713,362],{"class":277},[129,7715,4439],{"class":273},[129,7717,362],{"class":277},[129,7719,7720],{"class":273},"SMTP_HOST",[129,7722,1386],{"class":277},[129,7724,7725,7728,7730,7732,7734,7736,7738,7740,7742,7745,7747],{"class":265,"line":5190},[129,7726,7727],{"class":1376},"      port",[129,7729,1380],{"class":277},[129,7731,5914],{"class":284},[129,7733,147],{"class":1376},[129,7735,4755],{"class":273},[129,7737,362],{"class":277},[129,7739,4439],{"class":273},[129,7741,362],{"class":277},[129,7743,7744],{"class":273},"SMTP_PORT",[129,7746,160],{"class":1376},[129,7748,1386],{"class":277},[129,7750,7752,7755,7757,7759,7761,7763,7765,7767,7769,7771,7774,7776,7779,7781,7783,7785,7787,7789,7792],{"class":265,"line":7751},33,[129,7753,7754],{"class":1376},"      auth",[129,7756,1380],{"class":277},[129,7758,1416],{"class":277},[129,7760,6462],{"class":1376},[129,7762,1380],{"class":277},[129,7764,5084],{"class":273},[129,7766,362],{"class":277},[129,7768,4439],{"class":273},[129,7770,362],{"class":277},[129,7772,7773],{"class":273},"SMTP_USER",[129,7775,1015],{"class":277},[129,7777,7778],{"class":1376}," pass",[129,7780,1380],{"class":277},[129,7782,5084],{"class":273},[129,7784,362],{"class":277},[129,7786,4439],{"class":273},[129,7788,362],{"class":277},[129,7790,7791],{"class":273},"SMTP_PASS",[129,7793,1444],{"class":277},[129,7795,7797,7799],{"class":265,"line":7796},34,[129,7798,7619],{"class":277},[129,7800,294],{"class":1376},[129,7802,7804],{"class":265,"line":7803},35,[129,7805,1524],{"class":277},[129,7807,7809],{"class":265,"line":7808},36,[129,7810,336],{"emptyLinePlaceholder":335},[129,7812,7814,7816,7818,7820,7822,7824,7826,7828,7830,7832,7834,7836,7838,7840,7842,7844,7846,7848,7850,7852,7854,7856,7858],{"class":265,"line":7813},37,[129,7815,4703],{"class":269},[129,7817,7504],{"class":1376},[129,7819,7507],{"class":277},[129,7821,7369],{"class":452},[129,7823,1015],{"class":277},[129,7825,7379],{"class":452},[129,7827,1015],{"class":277},[129,7829,7388],{"class":452},[129,7831,7520],{"class":277},[129,7833,1416],{"class":277},[129,7835,7369],{"class":1376},[129,7837,1380],{"class":277},[129,7839,4622],{"class":2161},[129,7841,7376],{"class":277},[129,7843,7379],{"class":1376},[129,7845,1380],{"class":277},[129,7847,4622],{"class":2161},[129,7849,7376],{"class":277},[129,7851,7388],{"class":1376},[129,7853,1380],{"class":277},[129,7855,4622],{"class":2161},[129,7857,7547],{"class":277},[129,7859,1371],{"class":277},[129,7861,7863,7865,7867,7869,7871,7874,7876,7878,7880,7882,7884,7886,7888,7890,7893,7895,7897,7899,7901,7903,7905,7907],{"class":265,"line":7862},38,[129,7864,4902],{"class":2139},[129,7866,7556],{"class":277},[129,7868,7689],{"class":273},[129,7870,362],{"class":277},[129,7872,7873],{"class":284},"sendMail",[129,7875,147],{"class":1376},[129,7877,4796],{"class":277},[129,7879,4258],{"class":1376},[129,7881,1380],{"class":277},[129,7883,5084],{"class":273},[129,7885,362],{"class":277},[129,7887,4439],{"class":273},[129,7889,362],{"class":277},[129,7891,7892],{"class":273},"SMTP_FROM",[129,7894,1015],{"class":277},[129,7896,7369],{"class":273},[129,7898,1015],{"class":277},[129,7900,7379],{"class":273},[129,7902,1015],{"class":277},[129,7904,7388],{"class":273},[129,7906,4255],{"class":277},[129,7908,294],{"class":1376},[129,7910,7912],{"class":265,"line":7911},39,[129,7913,1524],{"class":277},[129,7915,7917],{"class":265,"line":7916},40,[129,7918,1530],{"class":277},[255,7920,7923],{"className":3922,"code":7921,"filename":7922,"language":3924,"meta":260,"style":260},"import { ResendAdapter, SmtpAdapter } from './adapters/email'\n\n// Swap providers with one config change\nexport const email = process.env.EMAIL_PROVIDER === 'smtp'\n  ? new SmtpAdapter()\n  : new ResendAdapter()\n","server/lib/email.ts",[15,7924,7925,7948,7952,7957,7989,8000],{"__ignoreMap":260},[129,7926,7927,7929,7931,7933,7935,7937,7939,7941,7943,7946],{"class":265,"line":266},[129,7928,2140],{"class":2139},[129,7930,1416],{"class":277},[129,7932,7425],{"class":273},[129,7934,1015],{"class":277},[129,7936,7647],{"class":273},[129,7938,4255],{"class":277},[129,7940,4258],{"class":2139},[129,7942,4261],{"class":277},[129,7944,7945],{"class":427},"./adapters/email",[129,7947,4267],{"class":277},[129,7949,7950],{"class":265,"line":297},[129,7951,336],{"emptyLinePlaceholder":335},[129,7953,7954],{"class":265,"line":315},[129,7955,7956],{"class":376},"// Swap providers with one config change\n",[129,7958,7959,7961,7963,7966,7968,7970,7972,7974,7976,7979,7982,7984,7987],{"class":265,"line":332},[129,7960,4050],{"class":2139},[129,7962,4456],{"class":269},[129,7964,7965],{"class":273}," email ",[129,7967,278],{"class":277},[129,7969,5084],{"class":273},[129,7971,362],{"class":277},[129,7973,4439],{"class":273},[129,7975,362],{"class":277},[129,7977,7978],{"class":273},"EMAIL_PROVIDER ",[129,7980,7981],{"class":277},"===",[129,7983,4261],{"class":277},[129,7985,7986],{"class":427},"smtp",[129,7988,4267],{"class":277},[129,7990,7991,7994,7996,7998],{"class":265,"line":339},[129,7992,7993],{"class":277},"  ?",[129,7995,281],{"class":277},[129,7997,7647],{"class":284},[129,7999,2451],{"class":273},[129,8001,8002,8005,8007,8009],{"class":265,"line":356},[129,8003,8004],{"class":277},"  :",[129,8006,281],{"class":277},[129,8008,7425],{"class":284},[129,8010,2451],{"class":273},[255,8012,8015],{"className":3922,"code":8013,"filename":8014,"language":3924,"meta":260,"style":260},"import { email } from '../../lib/email' // doesn't know or care which provider\n\nexport default defineEventHandler(async (event) => {\n  const user = await createUser(await readBody(event))\n  // Works with any EmailService implementation\n  await email.send({\n    to: user.email,\n    subject: 'Welcome',\n    html: `\u003Cp>Hi ${user.name}\u003C/p>`,\n  })\n  return { success: true }\n})\n","server/api/auth/register.post.ts",[15,8016,8017,8040,8044,8066,8092,8097,8112,8128,8144,8174,8180,8195],{"__ignoreMap":260},[129,8018,8019,8021,8023,8026,8028,8030,8032,8035,8037],{"class":265,"line":266},[129,8020,2140],{"class":2139},[129,8022,1416],{"class":277},[129,8024,8025],{"class":273}," email",[129,8027,4255],{"class":277},[129,8029,4258],{"class":2139},[129,8031,4261],{"class":277},[129,8033,8034],{"class":427},"../../lib/email",[129,8036,424],{"class":277},[129,8038,8039],{"class":376}," // doesn't know or care which provider\n",[129,8041,8042],{"class":265,"line":297},[129,8043,336],{"emptyLinePlaceholder":335},[129,8045,8046,8048,8050,8052,8054,8056,8058,8060,8062,8064],{"class":265,"line":315},[129,8047,4050],{"class":2139},[129,8049,4053],{"class":2139},[129,8051,4503],{"class":284},[129,8053,147],{"class":273},[129,8055,4508],{"class":269},[129,8057,3984],{"class":277},[129,8059,4100],{"class":452},[129,8061,160],{"class":277},[129,8063,456],{"class":269},[129,8065,1371],{"class":277},[129,8067,8068,8070,8072,8074,8076,8079,8081,8084,8086,8088,8090],{"class":265,"line":332},[129,8069,5076],{"class":269},[129,8071,6462],{"class":273},[129,8073,4745],{"class":277},[129,8075,4779],{"class":2139},[129,8077,8078],{"class":284}," createUser",[129,8080,147],{"class":1376},[129,8082,8083],{"class":2139},"await",[129,8085,5264],{"class":284},[129,8087,147],{"class":1376},[129,8089,4100],{"class":273},[129,8091,471],{"class":1376},[129,8093,8094],{"class":265,"line":339},[129,8095,8096],{"class":376},"  // Works with any EmailService implementation\n",[129,8098,8099,8102,8104,8106,8108,8110],{"class":265,"line":356},[129,8100,8101],{"class":2139},"  await",[129,8103,8025],{"class":273},[129,8105,362],{"class":277},[129,8107,7568],{"class":284},[129,8109,147],{"class":1376},[129,8111,6455],{"class":277},[129,8113,8114,8117,8119,8121,8123,8126],{"class":265,"line":651},[129,8115,8116],{"class":1376},"    to",[129,8118,1380],{"class":277},[129,8120,6462],{"class":273},[129,8122,362],{"class":277},[129,8124,8125],{"class":273},"email",[129,8127,1386],{"class":277},[129,8129,8130,8133,8135,8137,8140,8142],{"class":265,"line":657},[129,8131,8132],{"class":1376},"    subject",[129,8134,1380],{"class":277},[129,8136,4261],{"class":277},[129,8138,8139],{"class":427},"Welcome",[129,8141,424],{"class":277},[129,8143,1386],{"class":277},[129,8145,8146,8149,8151,8153,8156,8158,8160,8162,8165,8167,8170,8172],{"class":265,"line":669},[129,8147,8148],{"class":1376},"    html",[129,8150,1380],{"class":277},[129,8152,5569],{"class":277},[129,8154,8155],{"class":427},"\u003Cp>Hi ",[129,8157,4131],{"class":277},[129,8159,6335],{"class":273},[129,8161,362],{"class":277},[129,8163,8164],{"class":273},"name",[129,8166,4028],{"class":277},[129,8168,8169],{"class":427},"\u003C/p>",[129,8171,4125],{"class":277},[129,8173,1386],{"class":277},[129,8175,8176,8178],{"class":265,"line":693},[129,8177,4182],{"class":277},[129,8179,294],{"class":1376},[129,8181,8182,8184,8186,8189,8191,8193],{"class":265,"line":712},[129,8183,4520],{"class":2139},[129,8185,1416],{"class":277},[129,8187,8188],{"class":1376}," success",[129,8190,1380],{"class":277},[129,8192,4823],{"class":4822},[129,8194,1476],{"class":277},[129,8196,8197,8199],{"class":265,"line":1521},[129,8198,4028],{"class":277},[129,8200,294],{"class":273},[11,8202,8203],{},"The same pattern for legacy REST APIs - when you're integrating a third-party service with an awkward or verbose API surface:",[255,8205,8208],{"className":3922,"code":8206,"filename":8207,"language":3924,"meta":260,"style":260},"// The CRM API returns objects like { ContactData: { first_name_field: '...' } }\n// Your code wants { name: string, email: string }\n\nexport class HubSpotAdapter {\n  async getContact(id: string): Promise\u003C{ name: string; email: string }> {\n    const raw = await $fetch(`https://api.hubapi.com/contacts/v1/contact/vid/${id}/profile`, {\n      headers: { Authorization: `Bearer ${process.env.HUBSPOT_TOKEN}` },\n    })\n    // Translate their weird shape to yours\n    return {\n      name: `${raw.properties.firstname.value} ${raw.properties.lastname.value}`,\n      email: raw.properties.email.value,\n    }\n  }\n}\n","server/lib/adapters/crm.ts",[15,8209,8210,8215,8220,8224,8235,8275,8311,8346,8352,8357,8363,8413,8436,8440,8444],{"__ignoreMap":260},[129,8211,8212],{"class":265,"line":266},[129,8213,8214],{"class":376},"// The CRM API returns objects like { ContactData: { first_name_field: '...' } }\n",[129,8216,8217],{"class":265,"line":297},[129,8218,8219],{"class":376},"// Your code wants { name: string, email: string }\n",[129,8221,8222],{"class":265,"line":315},[129,8223,336],{"emptyLinePlaceholder":335},[129,8225,8226,8228,8230,8233],{"class":265,"line":332},[129,8227,4050],{"class":2139},[129,8229,7254],{"class":269},[129,8231,8232],{"class":2161}," HubSpotAdapter",[129,8234,1371],{"class":277},[129,8236,8237,8239,8242,8244,8246,8248,8250,8252,8254,8256,8258,8260,8262,8264,8266,8268,8270,8273],{"class":265,"line":339},[129,8238,4703],{"class":269},[129,8240,8241],{"class":1376}," getContact",[129,8243,147],{"class":277},[129,8245,3190],{"class":452},[129,8247,1380],{"class":277},[129,8249,4622],{"class":2161},[129,8251,4634],{"class":277},[129,8253,4637],{"class":2161},[129,8255,4640],{"class":277},[129,8257,5407],{"class":1376},[129,8259,1380],{"class":277},[129,8261,4622],{"class":2161},[129,8263,7376],{"class":277},[129,8265,8025],{"class":1376},[129,8267,1380],{"class":277},[129,8269,4622],{"class":2161},[129,8271,8272],{"class":277}," }>",[129,8274,1371],{"class":277},[129,8276,8277,8279,8282,8284,8286,8289,8291,8293,8296,8298,8300,8302,8305,8307,8309],{"class":265,"line":356},[129,8278,4739],{"class":269},[129,8280,8281],{"class":273}," raw",[129,8283,4745],{"class":277},[129,8285,4779],{"class":2139},[129,8287,8288],{"class":284}," $fetch",[129,8290,147],{"class":1376},[129,8292,4125],{"class":277},[129,8294,8295],{"class":427},"https://api.hubapi.com/contacts/v1/contact/vid/",[129,8297,4131],{"class":277},[129,8299,3190],{"class":273},[129,8301,4028],{"class":277},[129,8303,8304],{"class":427},"/profile",[129,8306,4125],{"class":277},[129,8308,1015],{"class":277},[129,8310,1371],{"class":277},[129,8312,8313,8316,8318,8320,8323,8325,8327,8329,8331,8333,8335,8337,8339,8342,8344],{"class":265,"line":651},[129,8314,8315],{"class":1376},"      headers",[129,8317,1380],{"class":277},[129,8319,1416],{"class":277},[129,8321,8322],{"class":1376}," Authorization",[129,8324,1380],{"class":277},[129,8326,5569],{"class":277},[129,8328,6069],{"class":427},[129,8330,4131],{"class":277},[129,8332,4755],{"class":273},[129,8334,362],{"class":277},[129,8336,4439],{"class":273},[129,8338,362],{"class":277},[129,8340,8341],{"class":273},"HUBSPOT_TOKEN",[129,8343,4175],{"class":277},[129,8345,1444],{"class":277},[129,8347,8348,8350],{"class":265,"line":657},[129,8349,7619],{"class":277},[129,8351,294],{"class":1376},[129,8353,8354],{"class":265,"line":669},[129,8355,8356],{"class":376},"    // Translate their weird shape to yours\n",[129,8358,8359,8361],{"class":265,"line":693},[129,8360,4832],{"class":2139},[129,8362,1371],{"class":277},[129,8364,8365,8368,8370,8372,8375,8377,8380,8382,8385,8387,8390,8392,8394,8396,8398,8400,8402,8405,8407,8409,8411],{"class":265,"line":712},[129,8366,8367],{"class":1376},"      name",[129,8369,1380],{"class":277},[129,8371,6794],{"class":277},[129,8373,8374],{"class":273},"raw",[129,8376,362],{"class":277},[129,8378,8379],{"class":273},"properties",[129,8381,362],{"class":277},[129,8383,8384],{"class":273},"firstname",[129,8386,362],{"class":277},[129,8388,8389],{"class":273},"value",[129,8391,4028],{"class":277},[129,8393,4165],{"class":277},[129,8395,8374],{"class":273},[129,8397,362],{"class":277},[129,8399,8379],{"class":273},[129,8401,362],{"class":277},[129,8403,8404],{"class":273},"lastname",[129,8406,362],{"class":277},[129,8408,8389],{"class":273},[129,8410,4175],{"class":277},[129,8412,1386],{"class":277},[129,8414,8415,8418,8420,8422,8424,8426,8428,8430,8432,8434],{"class":265,"line":1521},[129,8416,8417],{"class":1376},"      email",[129,8419,1380],{"class":277},[129,8421,8281],{"class":273},[129,8423,362],{"class":277},[129,8425,8379],{"class":273},[129,8427,362],{"class":277},[129,8429,8125],{"class":273},[129,8431,362],{"class":277},[129,8433,8389],{"class":273},[129,8435,1386],{"class":277},[129,8437,8438],{"class":265,"line":1527},[129,8439,6516],{"class":277},[129,8441,8442],{"class":265,"line":2295},[129,8443,1524],{"class":277},[129,8445,8446],{"class":265,"line":2300},[129,8447,1530],{"class":277},[11,8449,8450],{},"Probably the most practically useful one here. Very good when you integrate a third-party API - 15 minutes now, a lot of pain avoided when their API changes or you switch providers.",[2001,8452],{},[40,8454,8456],{"id":8455},"so-do-you-actually-use-these","So do you actually use these?",[11,8458,8459],{},"Breakdown for a Nuxt fullstack developer:",[59,8461,8462,8475],{},[62,8463,8464],{},[65,8465,8466,8469,8472],{},[68,8467,8468],{},"Pattern",[68,8470,8471],{},"Real-world use",[68,8473,8474],{},"In Nuxt specifically",[78,8476,8477,8489,8501,8513,8527,8539,8551],{},[65,8478,8479,8483,8486],{},[83,8480,8481],{},[118,8482,1681],{},[83,8484,8485],{},"Daily - it's how reactivity works",[83,8487,8488],{},"Vue's entire reactive system",[65,8490,8491,8495,8498],{},[83,8492,8493],{},[118,8494,4220],{},[83,8496,8497],{},"When you have connections/shared resources",[83,8499,8500],{},"DB client, Redis, logger",[65,8502,8503,8507,8510],{},[83,8504,8505],{},[118,8506,7316],{},[83,8508,8509],{},"Every third-party integration",[83,8511,8512],{},"Wrapping Stripe, email providers, CRMs",[65,8514,8515,8519,8522],{},[83,8516,8517],{},[118,8518,6543],{},[83,8520,8521],{},"Middleware, HOFs, handler composition",[83,8523,8524,8526],{},[15,8525,3853],{},", custom wrappers",[65,8528,8529,8533,8536],{},[83,8530,8531],{},[118,8532,5944],{},[83,8534,8535],{},"Auth, validation, anything swappable",[83,8537,8538],{},"Auth middleware, input parsers",[65,8540,8541,8545,8548],{},[83,8542,8543],{},[118,8544,4567],{},[83,8546,8547],{},"Multiple implementations, one interface",[83,8549,8550],{},"Payment providers, notification services",[65,8552,8553,8557,8560],{},[83,8554,8555],{},[118,8556,5346],{},[83,8558,8559],{},"Mostly handled by libraries",[83,8561,8562,8563],{},"Drizzle queries, ",[15,8564,8565],{},"queryCollection",[11,8567,8568],{},"The patterns you won't explicitly write: usually Builder (ORM does it), and Factory unless you have a legitimate \"which implementation?\" problem.",[2001,8570],{},[40,8572,8574],{"id":8573},"on-interview-questions","On interview questions",[11,8576,8577],{},"The expected answer is usually: name the pattern, describe the problem it solves, give an example.",[11,8579,8580,8581,8584,8585,8588,8589,8592],{},"The more honest question to ask in return: ",[24,8582,8583],{},"\"Can you show me an example in your codebase?\""," Real production code almost never has a ",[15,8586,8587],{},"class ConcreteObserver extends AbstractObserver"," comment. It just has a ",[15,8590,8591],{},"watch()"," or an event emitter or a subscription. The pattern is there. The vocabulary isn't.",[11,8594,8595],{},"What's actually worth knowing: the problems each pattern solves, not the canonical class diagram from 1994. If you understand that Adapter solves \"I don't want to be coupled to this third-party interface\" and Strategy solves \"I need to swap algorithms at runtime\" - you'll reach for the right pattern when the problem appears, with or without naming it.",[2001,8597],{},[11,8599,8600],{},"Most of the time in Nuxt you're already using these - just not naming them. Where naming helps: Adapter when integrating third-party APIs, Strategy when you find yourself copy-pasting auth logic across handlers, Decorator when you want cross-cutting behavior without modifying every route.",[11,8602,8603,8604,8608],{},"The ",[51,8605,8607],{"href":4200,"rel":8606},[55],"Gang of Four book"," is still worth reading for the problem descriptions - they've aged well. The Java class hierarchies haven't, but recognizing the problem when you see it is useful regardless.",[2026,8610,8611],{},"html pre.shiki code .spNyl, html code.shiki .spNyl{--shiki-light:#9C3EDA;--shiki-default:#C792EA;--shiki-dark:#C792EA}html pre.shiki code .sTEyZ, html code.shiki .sTEyZ{--shiki-light:#90A4AE;--shiki-default:#EEFFFF;--shiki-dark:#BABED8}html pre.shiki code .sMK4o, html code.shiki .sMK4o{--shiki-light:#39ADB5;--shiki-default:#89DDFF;--shiki-dark:#89DDFF}html pre.shiki code .s2Zo4, html code.shiki .s2Zo4{--shiki-light:#6182B8;--shiki-default:#82AAFF;--shiki-dark:#82AAFF}html pre.shiki code .sbssI, html code.shiki .sbssI{--shiki-light:#F76D47;--shiki-default:#F78C6C;--shiki-dark:#F78C6C}html .light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html.light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .sHwdD, html code.shiki .sHwdD{--shiki-light:#90A4AE;--shiki-light-font-style:italic;--shiki-default:#546E7A;--shiki-default-font-style:italic;--shiki-dark:#676E95;--shiki-dark-font-style:italic}html pre.shiki code .sBMFI, html code.shiki .sBMFI{--shiki-light:#E2931D;--shiki-default:#FFCB6B;--shiki-dark:#FFCB6B}html pre.shiki code .sHdIc, html code.shiki .sHdIc{--shiki-light:#90A4AE;--shiki-light-font-style:italic;--shiki-default:#EEFFFF;--shiki-default-font-style:italic;--shiki-dark:#BABED8;--shiki-dark-font-style:italic}html pre.shiki code .s7zQu, html code.shiki .s7zQu{--shiki-light:#39ADB5;--shiki-light-font-style:italic;--shiki-default:#89DDFF;--shiki-default-font-style:italic;--shiki-dark:#89DDFF;--shiki-dark-font-style:italic}html pre.shiki code .swJcz, html code.shiki .swJcz{--shiki-light:#E53935;--shiki-default:#F07178;--shiki-dark:#F07178}html pre.shiki code .sfazB, html code.shiki .sfazB{--shiki-light:#91B859;--shiki-default:#C3E88D;--shiki-dark:#C3E88D}html pre.shiki code .sfNiH, html code.shiki .sfNiH{--shiki-light:#FF5370;--shiki-default:#FF9CAC;--shiki-dark:#FF9CAC}",{"title":260,"searchDepth":297,"depth":297,"links":8613},[8614,8615,8616,8617,8618,8619,8620,8621,8622],{"id":3866,"depth":297,"text":1681},{"id":4219,"depth":297,"text":4220},{"id":4566,"depth":297,"text":4567},{"id":5345,"depth":297,"text":5346},{"id":5943,"depth":297,"text":5944},{"id":6542,"depth":297,"text":6543},{"id":7315,"depth":297,"text":7316},{"id":8455,"depth":297,"text":8456},{"id":8573,"depth":297,"text":8574},"2026-02-27","Factory, Singleton, Observer, Builder, Strategy, Decorator, Adapter - seven classic patterns, honest verdict on each, and real examples in Nuxt and Nitro.",{},"/blog/design-patterns-nuxt",{"title":3845,"description":8624},"blog/design-patterns-nuxt",[1716,8630,2051,8631,8632],"TypeScript","Nuxt","Patterns","pzpXcRVdRoxpmplhKj_0fO0cnKICIOOHjzVcncAbsQk",{"id":8635,"title":8636,"body":8637,"cover":2042,"date":8623,"description":16477,"extension":2045,"meta":16478,"navigation":335,"path":16479,"readingTime":16480,"seo":16481,"stem":16482,"tags":16483,"__hash__":16487},"blog/blog/frontend-internals.md","Frontend internals: 90 concepts",{"type":8,"value":8638,"toc":16351},[8639,8653,8656,8660,8664,8669,8676,8697,8701,8708,8720,8724,8735,8758,8777,8797,8801,8811,8814,8821,8827,8831,8836,8839,8843,8848,8868,8872,8875,8878,8882,8887,8890,8896,8900,8905,8919,8923,8936,8942,8948,8952,8956,8962,8968,8980,8984,8990,8996,9000,9007,9018,9022,9027,9152,9166,9170,9174,9183,9190,9194,9227,9273,9354,9357,9361,9367,9422,9445,9449,9455,9462,9691,9847,9853,9857,9861,9875,9878,9903,10005,10008,10029,10066,10081,10085,10088,10142,10168,10172,10188,10193,10374,10377,10381,10387,10390,10394,10457,10615,10625,10629,10633,10640,10664,10669,10673,10676,10696,10704,10721,10725,10743,10776,10781,10785,10799,10832,10843,10847,10861,10885,10889,10893,10906,10909,10913,10924,10930,10934,10940,10950,10954,10958,10964,10967,10986,11033,11037,11043,11067,11070,11074,11083,11148,11158,11162,11167,11170,11173,11177,11181,11190,11290,11338,11342,11348,11412,11423,11445,11449,11456,11463,11467,11471,11538,11547,11552,11556,11564,11580,11587,11591,11599,11653,11677,11680,11686,11764,11767,11771,11779,11858,11865,11869,11873,11884,12018,12023,12027,12044,12099,12107,12111,12114,12185,12188,12191,12196,12203,12206,12213,12217,12221,12231,12249,12263,12267,12277,12297,12300,12304,12310,12403,12431,12468,12475,12479,12504,12508,12518,12527,12533,12543,12547,12553,12614,12621,12624,12629,12633,12637,12643,12703,12712,12716,12730,12748,12754,12758,12764,12770,12777,12784,12788,12804,12913,12928,12936,12940,12950,13056,13059,13072,13076,13088,13290,13296,13331,13335,13339,13345,13440,13449,13678,13686,13690,13696,13699,13703,13706,13742,13746,13752,13758,13774,13778,13784,13808,13811,13815,13822,14098,14106,14110,14116,14172,14175,14179,14185,14188,14197,14453,14457,14460,14486,14489,14493,14498,14505,14525,14540,14557,14561,14565,14570,14573,14583,14587,14593,14603,14690,14699,14703,14711,14862,14898,14902,14914,15096,15102,15106,15112,15119,15160,15164,15168,15171,15174,15210,15213,15216,15222,15372,15382,15386,15389,15392,15417,15421,15425,15428,15595,15634,15638,15644,15763,15771,15775,15781,15790,15794,15800,15823,15830,15834,15840,15852,15860,15877,15881,15887,15899,15924,15928,15932,15937,15940,15981,16008,16012,16033,16036,16046,16050,16054,16060,16106,16133,16148,16151,16155,16169,16172,16191,16194,16198,16217,16315,16329,16340,16342,16345,16348],[11,8640,8641,8642,8645,8646,8648,8649,8652],{},"Add ",[15,8643,8644],{},"will-change: transform"," to every animated element, forget that ",[15,8647,3912],{}," without cleanup creates race conditions, and wonder why their ",[15,8650,8651],{},"\u003CSuspense>"," boundaries cause more network waterfalls than they prevent. ( ͡° ͜ʖ ͡°)",[11,8654,8655],{},"90 concepts, grouped by theme, each explained, from Vue perspective throughout.",[40,8657,8659],{"id":8658},"rendering-hydration","Rendering & hydration",[2456,8661,8663],{"id":8662},"hydration","Hydration",[11,8665,8666,8668],{},[118,8667,8663],{}," is the process of attaching JavaScript event handlers and reactivity to HTML that was already rendered on the server. The server sends a fully-formed HTML string; the browser displays it immediately. Then the JavaScript bundle loads, runs, walks the existing DOM (instead of creating new elements), and \"wires up\" the Vue component tree to that DOM - attaching event listeners, watchers, and refs.",[11,8670,8671,8672,8675],{},"The browser parses and executes the full bundle, reconstructs the virtual DOM, compares it to the real DOM (they must match exactly or you get a hydration mismatch warning), and registers all event handlers. On a slow device this can take 3-10 seconds during which the page ",[24,8673,8674],{},"looks"," interactive but isn't. That gap is what FID and INP measure.",[11,8677,8678,8679,8696],{},"In Nuxt, hydration starts when ",[15,8680,8681,8684,8686,8689,8691,8694],{"className":257,"language":259,"style":260},[129,8682,8683],{"class":273},"nuxtApp",[129,8685,362],{"class":277},[129,8687,8688],{"class":273},"vueApp",[129,8690,362],{"class":277},[129,8692,8693],{"class":284},"mount",[129,8695,4140],{"class":273}," is called on the client.",[2456,8698,8700],{"id":8699},"partial-hydration","Partial hydration",[11,8702,8703,8704,8707],{},"Instead of hydrating the entire page, ",[118,8705,8706],{},"partial hydration"," hydrates only the components that actually need client-side interactivity. A static pricing table doesn't need JavaScript. A carousel does. Partial hydration lets you ship the carousel's bundle while leaving the pricing table as inert HTML.",[3733,8709,8710],{},[11,8711,8712,8715,8716,8719],{},[118,8713,8714],{},"The catch",": components opted out of hydration must produce identical output on server and client, and the framework still needs discipline about what touches server-only state. In Nuxt, ",[15,8717,8718],{},"\u003CClientOnly>"," and lazy-loaded components approximate this - but it's opt-in, not the default.",[2456,8721,8723],{"id":8722},"islands-architecture","Islands architecture",[11,8725,8726,8728,8729,8734],{},[118,8727,8723],{}," (the term comes from ",[51,8730,8733],{"href":8731,"rel":8732},"https://jasonformat.com/islands-architecture/",[55],"Jason Miller's 2019 post",") takes partial hydration to its logical conclusion. The page is mostly static HTML (\"the ocean\") with isolated interactive components (\"islands\"). Each island hydrates independently with its own bundle. No shared framework runtime between islands.",[11,8736,8737,8738,8741,8742,8745,8746,8749,8750,8753,8754,8757],{},"Astro popularized this for content sites - the entire page is zero-JS by default, you opt ",[24,8739,8740],{},"in"," to interactivity per component. Nuxt flips this around. Instead of static page + dynamic islands, Nuxt gives you a full Vue SPA with SSR where you can embed ",[118,8743,8744],{},"static islands inside a dynamic app",". Name a component ",[15,8747,8748],{},".server.vue"," (or place it in ",[15,8751,8752],{},"components/islands/",") and Nuxt renders it exclusively on the server via ",[15,8755,8756],{},"\u003CNuxtIsland>",", ships only the HTML, and never hydrates it. The component and all its dependencies - heavy markdown parsers, syntax highlighters, whatever - are completely excluded from the client bundle. Zero JS for that component.",[11,8759,8760,8761,8764,8765,8768,8769,8772,8773,8776],{},"With ",[15,8762,8763],{},"selectiveClient"," enabled, you can mark specific children ",[24,8766,8767],{},"inside"," a server component with the ",[15,8770,8771],{},"nuxt-client"," attribute - so a mostly-static island can still have one interactive counter or form inside it. You can even render islands from a remote server with the ",[15,8774,8775],{},"source"," prop, which is useful for micro-frontend patterns.",[11,8778,8779,8780,8783,8784,8786,8787,8790,8791,8796],{},"The tradeoff is real though: Astro's default is zero JS everywhere, Nuxt's default is full hydration everywhere. You're opting ",[24,8781,8782],{},"out"," of JS in Nuxt, not opting ",[24,8785,8740],{},". And the feature still lives behind ",[15,8788,8789],{},"experimental.componentIslands"," in nuxt.config - it's been increasingly stable since Nuxt 3.8 but hasn't graduated to stable as of Nuxt 4. The ",[51,8792,8795],{"href":8793,"rel":8794},"https://github.com/nuxt/nuxt/issues/19772",[55],"server components roadmap"," tracks remaining work.",[2456,8798,8800],{"id":8799},"streaming-ssr","Streaming SSR",[11,8802,8803,8804,8806,8807,8810],{},"Standard SSR blocks the response until the entire HTML is ready. ",[118,8805,8800],{}," uses HTTP chunked transfer encoding to send HTML to the browser before all async data resolves. The browser starts parsing and rendering the ",[15,8808,8809],{},"\u003Chead>"," and above-the-fold content while the server is still fetching data for the rest of the page.",[11,8812,8813],{},"The practical gain: LCP for above-the-fold content improves even when some below-fold data is slow.",[3733,8815,8816],{},[11,8817,8818,8820],{},[118,8819,8714],{},": streaming makes error handling harder because you've already started writing the response when a downstream error occurs - you can't retroactively change the HTTP status code.",[11,8822,8823,8824,362],{},"Nuxt 3/4 supports streaming via Nitro's H3 layer and Vue's ",[15,8825,8826],{},"renderToWebStream",[2456,8828,8830],{"id":8829},"concurrent-rendering","Concurrent rendering",[11,8832,8833,8835],{},[118,8834,8830],{}," means the renderer can interrupt, pause, and resume rendering work. In a traditional synchronous renderer, a large component tree blocks the main thread until rendering is complete. Concurrent rendering breaks work into units that can be interrupted if something higher priority arrives - a user interaction, for instance.",[11,8837,8838],{},"Vue 3's scheduler yields to the browser between component updates via microtask scheduling. It doesn't expose a formal \"concurrent mode\" API, but the underlying principle is the same: rendering work shouldn't monopolize the main thread.",[2456,8840,8842],{"id":8841},"time-slicing","Time slicing",[11,8844,8845,8847],{},[118,8846,8842],{}," is the mechanism behind concurrent rendering. Rendering is broken into slices timed to yield before each 16ms frame deadline. If a rendering task takes 50ms synchronously, it blocks three frames - noticeable jank. With time slicing, the renderer yields after each slice, the browser paints a frame, then rendering resumes.",[11,8849,8850,8851,8854,8855,8867],{},"In practice, Vue's scheduler handles this via ",[15,8852,8853],{},"nextTick"," batching - all component updates within a single tick are processed together, then control returns to the browser. For custom heavy work in your own code, ",[15,8856,8857,8860,8862,8865],{"className":257,"language":259,"style":260},[129,8858,8859],{"class":273},"scheduler",[129,8861,362],{"class":277},[129,8863,8864],{"class":284},"yield",[129,8866,4140],{"class":273}," is the native API to achieve the same effect.",[2456,8869,8871],{"id":8870},"selective-hydration","Selective hydration",[11,8873,8874],{},"A refinement of partial hydration where the framework prioritizes which parts of the page to hydrate based on user interaction. If a user clicks on a part of the page that hasn't hydrated yet, the framework immediately prioritizes hydrating that component over the normal top-down order.",[11,8876,8877],{},"The result: the thing the user is actually trying to interact with becomes interactive faster, even if overall hydration isn't complete. Nuxt doesn't implement this automatically yet - it's more of a target architecture than a built-in behavior.",[2456,8879,8881],{"id":8880},"server-components","Server components",[11,8883,8884,8886],{},[118,8885,8881],{}," run exclusively on the server and are never hydrated. They can access databases directly, read from the filesystem, use server-only secrets - and their code is never sent to the browser. They output serialized component descriptions that the client renderer uses to display HTML.",[11,8888,8889],{},"The key distinction from regular SSR: a server component has zero client-side JavaScript footprint. A server-rendered component (traditional SSR) still ships all its JavaScript for hydration.",[11,8891,8892,8893,8895],{},"In Nuxt 4, server components are available via the ",[15,8894,8748],{}," suffix. State management across server/client boundaries is complex - client-side interactions must stay in client components.",[2456,8897,8899],{"id":8898},"edge-rendering","Edge rendering",[11,8901,8902,8904],{},[118,8903,8899],{}," moves SSR from a centralized origin server to edge locations close to the user (Cloudflare Workers, Vercel Edge Runtime). The goal: reduce latency from \"user requests page\" to \"server starts sending HTML\" from 100-300ms to under 30ms.",[11,8906,8907,8908,8910,8911,8914,8915,8918],{},"The constraint: edge runtimes are V8 isolates, not Node.js. No ",[15,8909,2664],{},", limited ",[15,8912,8913],{},"crypto",", no native addons. Nuxt's Nitro handles this via universal adapters, but if your SSR code calls ",[15,8916,8917],{},"require('better-sqlite3')"," you're not running at the edge.",[2456,8920,8922],{"id":8921},"suspense-boundaries","Suspense boundaries",[11,8924,561,8925,8928,8929,8931,8932,8935],{},[118,8926,8927],{},"Suspense boundary"," wraps async components and shows a fallback while children load. In Vue 3, ",[15,8930,8651],{}," resolves all ",[15,8933,8934],{},"async setup()"," functions in its subtree before rendering children - no special \"throw a promise\" magic needed.",[11,8937,8938,8939,8941],{},"In streaming SSR, each Suspense boundary is a stream boundary - the server streams the outer shell, shows the fallback, then streams the resolved content when the async data is ready. Multiple ",[15,8940,8651],{}," on a page mean multiple independent streaming segments.",[11,8943,8944,8945,8947],{},"Gotcha with nesting: an inner ",[15,8946,8651],{}," boundary won't surface its fallback if an outer one hasn't resolved yet. The outer boundary takes priority and shows its own fallback.",[40,8949,8951],{"id":8950},"framework-internals","Framework internals",[2456,8953,8955],{"id":8954},"reconciliation-algorithm","Reconciliation algorithm",[11,8957,8958,8961],{},[118,8959,8960],{},"Reconciliation"," is how a virtual DOM framework decides which real DOM operations to perform when state changes. Instead of re-rendering the entire DOM, the framework compares the previous virtual DOM tree to the new one and generates a minimal list of mutations.",[11,8963,8964,8965,8967],{},"The baseline complexity of tree diffing is O(n³). Vue (and other virtual DOM frameworks) reduce this to O(n) with two heuristics: (1) elements of different types produce different trees - don't diff them, just replace; (2) elements with stable ",[15,8966,6273],{}," props can be tracked across list positions.",[3283,8969,8970],{},[11,8971,8972,8973,8975,8976,8979],{},"This is why ",[15,8974,6273],{}," in ",[15,8977,8978],{},"v-for"," isn't optional - it's the signal that enables O(n) reconciliation for list updates.",[2456,8981,8983],{"id":8982},"fiber-architecture","Fiber architecture",[11,8985,8986,8989],{},[118,8987,8988],{},"Fiber"," is the name of React's internal rendering architecture - worth understanding even from a Vue perspective because the problem it solves is universal. It replaced recursive call-stack rendering with an explicit linked list of work units (fibers), one per element. Because work is an explicit data structure rather than a call stack, the renderer can pause between units, save its position, and resume later. A call stack can't be interrupted.",[11,8991,8992,8993,8995],{},"Vue 3 solves the same problem differently. Instead of per-element fibers, Vue schedules re-renders at the component boundary - each component is an independent update unit. When state changes, Vue queues that component's re-render via ",[15,8994,8853],{},", then flushes the queue in one batch and yields to the browser. The VNode diffing within each component is still synchronous, but components themselves are the granularity of scheduling.",[2456,8997,8999],{"id":8998},"virtual-dom-diffing-complexity","Virtual DOM diffing complexity",[11,9001,9002,9003,9006],{},"Even with O(n) heuristics, diffing has practical costs. A component tree with 500 nodes means 500 fiber/vnode comparisons on every render that reaches it. The \"virtual DOM is fast\" claim needs qualification: it's fast ",[24,9004,9005],{},"compared to naively re-rendering the real DOM every time",", not compared to surgical reactive updates - which is what Svelte and Vue's fine-grained reactivity do.",[11,9008,9009,9010,9013,9014,9017],{},"For very large lists, windowing (rendering only visible items via ",[15,9011,9012],{},"vue-virtual-scroller",") beats any diffing optimization. For deep trees, ",[15,9015,9016],{},"v-memo"," prunes the diff tree before it runs - if the memoized dependencies haven't changed, Vue skips the entire subtree.",[2456,9019,9021],{"id":9020},"structural-sharing","Structural sharing",[11,9023,9024,9026],{},[118,9025,9021],{}," is how immutable data structures avoid copying entire objects on update. When you update a nested field, you create new object references along the path from root to the changed node, while sharing unchanged branches with the original.",[255,9028,9030],{"className":3922,"code":9029,"language":3924,"meta":260,"style":260},"const original = { a: { x: 1 }, b: { y: 2 } }\n\n// Update a.x to 99:\nconst updated = {\n  ...original,               // shares reference to original.b\n  a: { ...original.a, x: 99 }\n}\n\n// original.b === updated.b  -> true (shared, not copied)\n",[15,9031,9032,9077,9081,9086,9097,9110,9139,9143,9147],{"__ignoreMap":260},[129,9033,9034,9036,9039,9041,9043,9046,9048,9050,9052,9054,9056,9059,9062,9064,9066,9069,9071,9073,9075],{"class":265,"line":266},[129,9035,270],{"class":269},[129,9037,9038],{"class":273}," original ",[129,9040,278],{"class":277},[129,9042,1416],{"class":277},[129,9044,9045],{"class":1376}," a",[129,9047,1380],{"class":277},[129,9049,1416],{"class":277},[129,9051,3214],{"class":1376},[129,9053,1380],{"class":277},[129,9055,1383],{"class":290},[129,9057,9058],{"class":277}," },",[129,9060,9061],{"class":1376}," b",[129,9063,1380],{"class":277},[129,9065,1416],{"class":277},[129,9067,9068],{"class":1376}," y",[129,9070,1380],{"class":277},[129,9072,1023],{"class":290},[129,9074,4255],{"class":277},[129,9076,1476],{"class":277},[129,9078,9079],{"class":265,"line":297},[129,9080,336],{"emptyLinePlaceholder":335},[129,9082,9083],{"class":265,"line":315},[129,9084,9085],{"class":376},"// Update a.x to 99:\n",[129,9087,9088,9090,9093,9095],{"class":265,"line":332},[129,9089,270],{"class":269},[129,9091,9092],{"class":273}," updated ",[129,9094,278],{"class":277},[129,9096,1371],{"class":277},[129,9098,9099,9102,9105,9107],{"class":265,"line":339},[129,9100,9101],{"class":277},"  ...",[129,9103,9104],{"class":273},"original",[129,9106,1015],{"class":277},[129,9108,9109],{"class":376},"               // shares reference to original.b\n",[129,9111,9112,9115,9117,9119,9122,9124,9126,9128,9130,9132,9134,9137],{"class":265,"line":356},[129,9113,9114],{"class":1376},"  a",[129,9116,1380],{"class":277},[129,9118,1416],{"class":277},[129,9120,9121],{"class":277}," ...",[129,9123,9104],{"class":273},[129,9125,362],{"class":277},[129,9127,51],{"class":273},[129,9129,1015],{"class":277},[129,9131,3214],{"class":1376},[129,9133,1380],{"class":277},[129,9135,9136],{"class":290}," 99",[129,9138,1476],{"class":277},[129,9140,9141],{"class":265,"line":651},[129,9142,1530],{"class":277},[129,9144,9145],{"class":265,"line":657},[129,9146,336],{"emptyLinePlaceholder":335},[129,9148,9149],{"class":265,"line":669},[129,9150,9151],{"class":376},"// original.b === updated.b  -> true (shared, not copied)\n",[11,9153,9154,9155,9162,9163,9165],{},"Immer uses structural sharing internally when you write mutating-style code inside ",[15,9156,9157,9160],{"className":257,"language":259,"style":260},[129,9158,9159],{"class":284},"produce",[129,9161,4140],{"class":273},". This matters because referential equality checks (",[15,9164,7981],{},") are how frameworks skip unnecessary re-renders - shared branches pass equality checks automatically.",[40,9167,9169],{"id":9168},"javascript-patterns-gotchas","JavaScript patterns & gotchas",[2456,9171,9173],{"id":9172},"immutable-data-patterns","Immutable data patterns",[11,9175,9176,9179,9180,9182],{},[118,9177,9178],{},"Immutable data"," means values that never change after creation. When you need a \"modified\" version, you create a new object. The consequence in Vue: returning the same object reference from a ",[15,9181,3918],{}," means \"nothing changed, skip re-render.\" Mutating the same object in place means \"nothing changed\" to a referential equality check - a silent bug.",[11,9184,9185,9186,9189],{},"Pinia encourages mutation-style via ",[15,9187,9188],{},"$patch"," but tracks changes internally via Vue's reactivity proxy, not reference equality. This is the main difference between Pinia and pure immutable stores - you don't need to think about structural sharing, Vue's proxy handles change detection for you.",[2456,9191,9193],{"id":9192},"referential-equality","Referential equality",[11,9195,9196,9197,9199,9200,9224,9225,362],{},"In JavaScript, ",[15,9198,7981],{}," on objects and arrays checks identity - whether both sides point to the same object in memory, not whether they have the same content. ",[15,9201,9202,9204,9206,9208,9210,9212,9214,9216,9218,9220,9222],{"className":257,"language":259,"style":260},[129,9203,4796],{"class":277},[129,9205,9045],{"class":2161},[129,9207,1380],{"class":277},[129,9209,1383],{"class":290},[129,9211,4255],{"class":277},[129,9213,5116],{"class":277},[129,9215,1416],{"class":277},[129,9217,9045],{"class":1376},[129,9219,1380],{"class":277},[129,9221,1383],{"class":290},[129,9223,4255],{"class":277}," is ",[15,9226,146],{},[11,9228,9229,9230,968,9232,9234,9235,9259,9260,9263,9264,9266,9267,9269,9270,9272],{},"This bites Vue's ",[15,9231,3918],{},[15,9233,3912],{}," constantly. In ",[15,9236,9237,9239,9241,9243,9245,9248,9250,9252,9255,9257],{"className":257,"language":259,"style":260},[129,9238,3918],{"class":284},[129,9240,147],{"class":273},[129,9242,4140],{"class":277},[129,9244,456],{"class":269},[129,9246,9247],{"class":284}," transform",[129,9249,147],{"class":273},[129,9251,4796],{"class":277},[129,9253,9254],{"class":273}," id ",[129,9256,4028],{"class":277},[129,9258,5893],{"class":273}," - the ",[15,9261,9262],{},"{ id }"," literal creates a new object every evaluation, but since ",[15,9265,3190],{}," is a primitive, Vue correctly tracks ",[15,9268,3190],{}," as the reactive dependency. The problem appears when you pass object results as ",[15,9271,3912],{}," sources or compare them manually:",[255,9274,9276],{"className":3922,"code":9275,"language":3924,"meta":260,"style":260},"// This watch fires on every render - new object reference every time\nwatch(() => ({ id: user.value.id }), handler)\n\n// This doesn't - primitive reference is stable\nwatch(() => user.value.id, handler)\n",[15,9277,9278,9283,9321,9325,9330],{"__ignoreMap":260},[129,9279,9280],{"class":265,"line":266},[129,9281,9282],{"class":376},"// This watch fires on every render - new object reference every time\n",[129,9284,9285,9287,9289,9291,9293,9295,9297,9299,9301,9303,9305,9307,9309,9312,9314,9316,9318],{"class":265,"line":297},[129,9286,3912],{"class":284},[129,9288,147],{"class":273},[129,9290,4140],{"class":277},[129,9292,456],{"class":269},[129,9294,3984],{"class":273},[129,9296,4796],{"class":277},[129,9298,4643],{"class":1376},[129,9300,1380],{"class":277},[129,9302,6462],{"class":273},[129,9304,362],{"class":277},[129,9306,8389],{"class":273},[129,9308,362],{"class":277},[129,9310,9311],{"class":273},"id ",[129,9313,4028],{"class":277},[129,9315,160],{"class":273},[129,9317,1015],{"class":277},[129,9319,9320],{"class":273}," handler)\n",[129,9322,9323],{"class":265,"line":315},[129,9324,336],{"emptyLinePlaceholder":335},[129,9326,9327],{"class":265,"line":332},[129,9328,9329],{"class":376},"// This doesn't - primitive reference is stable\n",[129,9331,9332,9334,9336,9338,9340,9342,9344,9346,9348,9350,9352],{"class":265,"line":339},[129,9333,3912],{"class":284},[129,9335,147],{"class":273},[129,9337,4140],{"class":277},[129,9339,456],{"class":269},[129,9341,6462],{"class":273},[129,9343,362],{"class":277},[129,9345,8389],{"class":273},[129,9347,362],{"class":277},[129,9349,3190],{"class":273},[129,9351,1015],{"class":277},[129,9353,9320],{"class":273},[11,9355,9356],{},"Fix: stabilize references. Derive primitives first, use them as watch sources.",[2456,9358,9360],{"id":9359},"memoization-pitfalls","Memoization pitfalls",[11,9362,9363,9366],{},[118,9364,9365],{},"Memoization"," caches a function's result based on its arguments, returning cached results on subsequent calls with identical arguments. The gotchas:",[2086,9368,9369,9388,9398,9413],{},[1825,9370,9371,9374,9375,9387],{},[118,9372,9373],{},"Argument identity",": memoization uses reference equality for objects. Two separate ",[15,9376,9377,9379,9381,9383,9385],{"className":257,"language":259,"style":260},[129,9378,4796],{"class":277},[129,9380,51],{"class":2161},[129,9382,1380],{"class":277},[129,9384,154],{"class":290},[129,9386,4028],{"class":277}," objects are different cache keys - the cache is never hit.",[1825,9389,9390,9393,9394,9397],{},[118,9391,9392],{},"Cache size",": a basic ",[15,9395,9396],{},"memoize"," utility without an LRU grows unbounded with diverse inputs.",[1825,9399,9400,3556,9403,9409,9410,9412],{},[118,9401,9402],{},"Over-memoization",[15,9404,9405,9407],{"className":257,"language":259,"style":260},[129,9406,3918],{"class":284},[129,9408,4140],{"class":273}," has overhead (proxy tracking, dirty checking). Wrapping every trivial value in ",[15,9411,3918],{}," is net negative - use it for genuinely expensive derivations.",[1825,9414,9415,9418,9419,9421],{},[118,9416,9417],{},"Stale reads from non-reactive sources",": a computed that reads from a plain variable (not a ",[15,9420,4213],{}," or reactive store) silently doesn't invalidate when that variable changes.",[11,9423,9424,9425,9428,9429,9440,9441,9444],{},"Vue's ",[15,9426,9427],{},"computed()"," auto-tracks reactive dependencies via the proxy - no dependency array to declare. But a computed that calls ",[15,9430,9431,9434,9436,9438],{"className":257,"language":259,"style":260},[129,9432,9433],{"class":273},"Date",[129,9435,362],{"class":277},[129,9437,3587],{"class":284},[129,9439,4140],{"class":273},", reads ",[15,9442,9443],{},"localStorage",", or uses a non-ref module-level variable is not tracking a reactive dependency. It will cache indefinitely and never re-run.",[2456,9446,9448],{"id":9447},"stale-closure-problem","Stale closure problem",[11,9450,561,9451,9454],{},[118,9452,9453],{},"stale closure"," captures a variable's value at closure creation time. If that variable changes later, the closure still sees the old value.",[11,9456,9457,9458,9461],{},"In Vue, reactivity via ",[15,9459,9460],{},".value"," makes this less common than in other frameworks - you're dereferencing the ref at call time, not capturing a snapshot. But it still bites in two situations:",[255,9463,9465],{"className":3922,"code":9464,"language":3924,"meta":260,"style":260},"// 1. Closure over a plain variable inside a composable\nexport function useTimer() {\n  let count = 0  // plain variable, not reactive\n\n  const start = () => {\n    setInterval(() => {\n      // count incremented but nobody is watching it\n      count++\n      console.log(count)  // works locally\n    }, 1000)\n  }\n\n  // components reading count get the initial value - stale closure\n  return { count, start }\n}\n\n// Fix: use ref\nexport function useTimer() {\n  const count = ref(0)\n  const start = () => setInterval(() => count.value++, 1000)\n  return { count, start }\n}\n",[15,9466,9467,9472,9485,9499,9503,9517,9530,9535,9543,9561,9571,9575,9579,9584,9598,9602,9606,9611,9623,9639,9673,9687],{"__ignoreMap":260},[129,9468,9469],{"class":265,"line":266},[129,9470,9471],{"class":376},"// 1. Closure over a plain variable inside a composable\n",[129,9473,9474,9476,9478,9481,9483],{"class":265,"line":297},[129,9475,4050],{"class":2139},[129,9477,5060],{"class":269},[129,9479,9480],{"class":284}," useTimer",[129,9482,4140],{"class":277},[129,9484,1371],{"class":277},[129,9486,9487,9489,9492,9494,9496],{"class":265,"line":315},[129,9488,5720],{"class":269},[129,9490,9491],{"class":273}," count",[129,9493,4745],{"class":277},[129,9495,5698],{"class":290},[129,9497,9498],{"class":376},"  // plain variable, not reactive\n",[129,9500,9501],{"class":265,"line":332},[129,9502,336],{"emptyLinePlaceholder":335},[129,9504,9505,9507,9509,9511,9513,9515],{"class":265,"line":339},[129,9506,5076],{"class":269},[129,9508,6717],{"class":273},[129,9510,4745],{"class":277},[129,9512,4511],{"class":277},[129,9514,456],{"class":269},[129,9516,1371],{"class":277},[129,9518,9519,9522,9524,9526,9528],{"class":265,"line":356},[129,9520,9521],{"class":284},"    setInterval",[129,9523,147],{"class":1376},[129,9525,4140],{"class":277},[129,9527,456],{"class":269},[129,9529,1371],{"class":277},[129,9531,9532],{"class":265,"line":651},[129,9533,9534],{"class":376},"      // count incremented but nobody is watching it\n",[129,9536,9537,9540],{"class":265,"line":657},[129,9538,9539],{"class":273},"      count",[129,9541,9542],{"class":277},"++\n",[129,9544,9545,9548,9550,9552,9554,9556,9558],{"class":265,"line":669},[129,9546,9547],{"class":273},"      console",[129,9549,362],{"class":277},[129,9551,365],{"class":284},[129,9553,147],{"class":1376},[129,9555,3904],{"class":273},[129,9557,607],{"class":1376},[129,9559,9560],{"class":376},"// works locally\n",[129,9562,9563,9566,9569],{"class":265,"line":693},[129,9564,9565],{"class":277},"    },",[129,9567,9568],{"class":290}," 1000",[129,9570,294],{"class":1376},[129,9572,9573],{"class":265,"line":712},[129,9574,1524],{"class":277},[129,9576,9577],{"class":265,"line":1521},[129,9578,336],{"emptyLinePlaceholder":335},[129,9580,9581],{"class":265,"line":1527},[129,9582,9583],{"class":376},"  // components reading count get the initial value - stale closure\n",[129,9585,9586,9588,9590,9592,9594,9596],{"class":265,"line":2295},[129,9587,4520],{"class":2139},[129,9589,1416],{"class":277},[129,9591,9491],{"class":273},[129,9593,1015],{"class":277},[129,9595,6717],{"class":273},[129,9597,1476],{"class":277},[129,9599,9600],{"class":265,"line":2300},[129,9601,1530],{"class":277},[129,9603,9604],{"class":265,"line":2305},[129,9605,336],{"emptyLinePlaceholder":335},[129,9607,9608],{"class":265,"line":2311},[129,9609,9610],{"class":376},"// Fix: use ref\n",[129,9612,9613,9615,9617,9619,9621],{"class":265,"line":2329},[129,9614,4050],{"class":2139},[129,9616,5060],{"class":269},[129,9618,9480],{"class":284},[129,9620,4140],{"class":277},[129,9622,1371],{"class":277},[129,9624,9625,9627,9629,9631,9633,9635,9637],{"class":265,"line":2351},[129,9626,5076],{"class":269},[129,9628,9491],{"class":273},[129,9630,4745],{"class":277},[129,9632,3894],{"class":284},[129,9634,147],{"class":1376},[129,9636,345],{"class":290},[129,9638,294],{"class":1376},[129,9640,9641,9643,9645,9647,9649,9651,9654,9656,9658,9660,9662,9664,9666,9669,9671],{"class":265,"line":2387},[129,9642,5076],{"class":269},[129,9644,6717],{"class":273},[129,9646,4745],{"class":277},[129,9648,4511],{"class":277},[129,9650,456],{"class":269},[129,9652,9653],{"class":284}," setInterval",[129,9655,147],{"class":1376},[129,9657,4140],{"class":277},[129,9659,456],{"class":269},[129,9661,9491],{"class":273},[129,9663,362],{"class":277},[129,9665,8389],{"class":273},[129,9667,9668],{"class":277},"++,",[129,9670,9568],{"class":290},[129,9672,294],{"class":1376},[129,9674,9675,9677,9679,9681,9683,9685],{"class":265,"line":2392},[129,9676,4520],{"class":2139},[129,9678,1416],{"class":277},[129,9680,9491],{"class":273},[129,9682,1015],{"class":277},[129,9684,6717],{"class":273},[129,9686,1476],{"class":277},[129,9688,9689],{"class":265,"line":2398},[129,9690,1530],{"class":277},[255,9692,9694],{"className":3922,"code":9693,"language":3924,"meta":260,"style":260},"// 2. Async callbacks that capture reactive state at call time\nconst user = ref({ name: 'Alice' })\n\nsetTimeout(() => {\n  // This reads user.value at execution time - fine for refs\n  console.log(user.value.name)\n\n  // This captures the primitive at setTimeout call time - stale\n  const name = user.value.name  // captured now\n  setTimeout(() => console.log(name), 5000)  // may be wrong in 5s\n}, 1000)\n",[15,9695,9696,9701,9730,9734,9747,9752,9775,9779,9784,9805,9839],{"__ignoreMap":260},[129,9697,9698],{"class":265,"line":266},[129,9699,9700],{"class":376},"// 2. Async callbacks that capture reactive state at call time\n",[129,9702,9703,9705,9707,9709,9711,9713,9715,9717,9719,9721,9724,9726,9728],{"class":265,"line":297},[129,9704,270],{"class":269},[129,9706,3938],{"class":273},[129,9708,278],{"class":277},[129,9710,3894],{"class":284},[129,9712,147],{"class":273},[129,9714,4796],{"class":277},[129,9716,5407],{"class":1376},[129,9718,1380],{"class":277},[129,9720,4261],{"class":277},[129,9722,9723],{"class":427},"Alice",[129,9725,424],{"class":277},[129,9727,4255],{"class":277},[129,9729,294],{"class":273},[129,9731,9732],{"class":265,"line":315},[129,9733,336],{"emptyLinePlaceholder":335},[129,9735,9736,9739,9741,9743,9745],{"class":265,"line":332},[129,9737,9738],{"class":284},"setTimeout",[129,9740,147],{"class":273},[129,9742,4140],{"class":277},[129,9744,456],{"class":269},[129,9746,1371],{"class":277},[129,9748,9749],{"class":265,"line":339},[129,9750,9751],{"class":376},"  // This reads user.value at execution time - fine for refs\n",[129,9753,9754,9757,9759,9761,9763,9765,9767,9769,9771,9773],{"class":265,"line":356},[129,9755,9756],{"class":273},"  console",[129,9758,362],{"class":277},[129,9760,365],{"class":284},[129,9762,147],{"class":1376},[129,9764,6335],{"class":273},[129,9766,362],{"class":277},[129,9768,8389],{"class":273},[129,9770,362],{"class":277},[129,9772,8164],{"class":273},[129,9774,294],{"class":1376},[129,9776,9777],{"class":265,"line":651},[129,9778,336],{"emptyLinePlaceholder":335},[129,9780,9781],{"class":265,"line":657},[129,9782,9783],{"class":376},"  // This captures the primitive at setTimeout call time - stale\n",[129,9785,9786,9788,9790,9792,9794,9796,9798,9800,9802],{"class":265,"line":669},[129,9787,5076],{"class":269},[129,9789,5407],{"class":273},[129,9791,4745],{"class":277},[129,9793,6462],{"class":273},[129,9795,362],{"class":277},[129,9797,8389],{"class":273},[129,9799,362],{"class":277},[129,9801,8164],{"class":273},[129,9803,9804],{"class":376},"  // captured now\n",[129,9806,9807,9810,9812,9814,9816,9819,9821,9823,9825,9827,9829,9831,9834,9836],{"class":265,"line":693},[129,9808,9809],{"class":284},"  setTimeout",[129,9811,147],{"class":1376},[129,9813,4140],{"class":277},[129,9815,456],{"class":269},[129,9817,9818],{"class":273}," console",[129,9820,362],{"class":277},[129,9822,365],{"class":284},[129,9824,147],{"class":1376},[129,9826,8164],{"class":273},[129,9828,160],{"class":1376},[129,9830,1015],{"class":277},[129,9832,9833],{"class":290}," 5000",[129,9835,607],{"class":1376},[129,9837,9838],{"class":376},"// may be wrong in 5s\n",[129,9840,9841,9843,9845],{"class":265,"line":712},[129,9842,6625],{"class":277},[129,9844,9568],{"class":290},[129,9846,294],{"class":273},[11,9848,9849,9850,9852],{},"The rule: if you need reactive values inside async callbacks or timers, read ",[15,9851,9460],{}," as late as possible - at execution time, not at setup time.",[40,9854,9856],{"id":9855},"browser-event-loop","Browser & event loop",[2456,9858,9860],{"id":9859},"event-loop","Event loop",[11,9862,9863,9864,9867,9868,9871,9872,362],{},"JavaScript is single-threaded. The ",[118,9865,9866],{},"event loop"," decides what to execute next. It has two queues: the ",[118,9869,9870],{},"macrotask queue"," (task queue) and the ",[118,9873,9874],{},"microtask queue",[11,9876,9877],{},"Each event loop iteration: run the current macrotask to completion, then drain the entire microtask queue (including any queued by other microtasks), then render if needed, then pick the next macrotask.",[11,9879,9880,3556,9883,500,9885,9888,9889,9892,9893,3556,9896,500,9899,9902],{},[118,9881,9882],{},"Macrotasks",[15,9884,9738],{},[15,9886,9887],{},"setInterval",", I/O callbacks, UI events, ",[15,9890,9891],{},"MessageChannel",".\n",[118,9894,9895],{},"Microtasks",[15,9897,9898],{},"Promise.then/catch/finally",[15,9900,9901],{},"queueMicrotask",", MutationObserver callbacks.",[255,9904,9906],{"className":3922,"code":9905,"language":3924,"meta":260,"style":260},"setTimeout(() => console.log('macro'), 0)\nPromise.resolve().then(() => console.log('micro'))\nconsole.log('sync')\n// Output: sync, micro, macro\n",[15,9907,9908,9941,9981,10000],{"__ignoreMap":260},[129,9909,9910,9912,9914,9916,9918,9920,9922,9924,9926,9928,9931,9933,9935,9937,9939],{"class":265,"line":266},[129,9911,9738],{"class":284},[129,9913,147],{"class":273},[129,9915,4140],{"class":277},[129,9917,456],{"class":269},[129,9919,9818],{"class":273},[129,9921,362],{"class":277},[129,9923,365],{"class":284},[129,9925,147],{"class":273},[129,9927,424],{"class":277},[129,9929,9930],{"class":427},"macro",[129,9932,424],{"class":277},[129,9934,160],{"class":273},[129,9936,1015],{"class":277},[129,9938,5698],{"class":290},[129,9940,294],{"class":273},[129,9942,9943,9946,9948,9951,9953,9955,9958,9960,9962,9964,9966,9968,9970,9972,9974,9977,9979],{"class":265,"line":297},[129,9944,9945],{"class":2161},"Promise",[129,9947,362],{"class":277},[129,9949,9950],{"class":284},"resolve",[129,9952,4140],{"class":273},[129,9954,362],{"class":277},[129,9956,9957],{"class":284},"then",[129,9959,147],{"class":273},[129,9961,4140],{"class":277},[129,9963,456],{"class":269},[129,9965,9818],{"class":273},[129,9967,362],{"class":277},[129,9969,365],{"class":284},[129,9971,147],{"class":273},[129,9973,424],{"class":277},[129,9975,9976],{"class":427},"micro",[129,9978,424],{"class":277},[129,9980,471],{"class":273},[129,9982,9983,9985,9987,9989,9991,9993,9996,9998],{"class":265,"line":315},[129,9984,359],{"class":273},[129,9986,362],{"class":277},[129,9988,365],{"class":284},[129,9990,147],{"class":273},[129,9992,424],{"class":277},[129,9994,9995],{"class":427},"sync",[129,9997,424],{"class":277},[129,9999,294],{"class":273},[129,10001,10002],{"class":265,"line":332},[129,10003,10004],{"class":376},"// Output: sync, micro, macro\n",[11,10006,10007],{},"The \"0ms setTimeout\" means \"queue a macrotask after current code and all pending microtasks finish\" - not \"run immediately.\"",[11,10009,8972,10010,10023,10024,10028],{},[15,10011,10012,10014,10017,10019,10021],{"className":257,"language":259,"style":260},[129,10013,9738],{"class":284},[129,10015,10016],{"class":273},"(fn",[129,10018,1015],{"class":277},[129,10020,5698],{"class":290},[129,10022,160],{"class":273}," became a classic trick for \"run this after the current work finishes\" - deferring DOM reads after Vue's reactive updates flush, breaking up long computations to avoid freezing the UI, or ensuring a newly inserted element is in the DOM before querying it. It works, but it's a blunt instrument. The callback lands in the macrotask queue, which means the browser might paint a frame, run other timers, or process user input before your code runs. The delay isn't zero - it's \"whenever the event loop gets around to it\" which on a busy page can be 4-15ms (browsers clamp nested ",[15,10025,10026],{"className":257,"language":259,"style":260},[129,10027,9738],{"class":273}," to a minimum of 4ms anyway).",[11,10030,10031,10032,10039,10040,10046,10047,10054,10055,10065],{},"Better alternatives exist depending on what you're actually trying to do. ",[15,10033,10034,10036],{"className":257,"language":259,"style":260},[129,10035,9901],{"class":284},[129,10037,10038],{"class":273},"(fn)"," runs after the current task but before the next render - tighter timing, no frame gap. Vue's ",[15,10041,10042,10044],{"className":257,"language":259,"style":260},[129,10043,8853],{"class":284},[129,10045,4140],{"class":273}," is essentially this - it queues your callback as a microtask that runs after Vue's DOM updates flush. ",[15,10048,10049,10052],{"className":257,"language":259,"style":260},[129,10050,10051],{"class":284},"requestAnimationFrame",[129,10053,10038],{"class":273}," runs right before the next paint - ideal for visual work. And ",[15,10056,10057,10059,10061,10063],{"className":257,"language":259,"style":260},[129,10058,8859],{"class":273},[129,10060,362],{"class":277},[129,10062,8864],{"class":284},[129,10064,4140],{"class":273}," (newer API) explicitly yields to the browser for input processing and then resumes - the most intentional way to say \"let the user interact, then continue.\"",[11,10067,10068,10080],{},[15,10069,10070,10072,10074,10076,10078],{"className":257,"language":259,"style":260},[129,10071,9738],{"class":284},[129,10073,10016],{"class":273},[129,10075,1015],{"class":277},[129,10077,5698],{"class":290},[129,10079,160],{"class":273}," still has its place when you genuinely want macrotask semantics - for instance, breaking an infinite microtask loop or ensuring other queued events process first. But reaching for it by default is a code smell that usually means you're fighting the framework's timing instead of working with it.",[2456,10082,10084],{"id":10083},"task-starvation","Task starvation",[11,10086,10087],{},"Because the microtask queue must be fully drained before the event loop can proceed to rendering or the next macrotask, an infinitely-growing microtask queue starves the browser.",[255,10089,10091],{"className":3922,"code":10090,"language":3924,"meta":260,"style":260},"// This freezes the tab\nfunction starve() {\n  Promise.resolve().then(starve)\n}\nstarve()\n",[15,10092,10093,10098,10110,10132,10136],{"__ignoreMap":260},[129,10094,10095],{"class":265,"line":266},[129,10096,10097],{"class":376},"// This freezes the tab\n",[129,10099,10100,10103,10106,10108],{"class":265,"line":297},[129,10101,10102],{"class":269},"function",[129,10104,10105],{"class":284}," starve",[129,10107,4140],{"class":277},[129,10109,1371],{"class":277},[129,10111,10112,10115,10117,10119,10121,10123,10125,10127,10130],{"class":265,"line":315},[129,10113,10114],{"class":2161},"  Promise",[129,10116,362],{"class":277},[129,10118,9950],{"class":284},[129,10120,4140],{"class":1376},[129,10122,362],{"class":277},[129,10124,9957],{"class":284},[129,10126,147],{"class":1376},[129,10128,10129],{"class":273},"starve",[129,10131,294],{"class":1376},[129,10133,10134],{"class":265,"line":332},[129,10135,1530],{"class":277},[129,10137,10138,10140],{"class":265,"line":339},[129,10139,10129],{"class":284},[129,10141,2451],{"class":273},[11,10143,10144,10145,10157,10158,362],{},"Real-world: recursive promise chains without yield points, or RxJS observables that emit synchronously on subscribe. The fix is yielding to the macrotask queue periodically via ",[15,10146,10147,10149,10151,10153,10155],{"className":257,"language":259,"style":260},[129,10148,9738],{"class":284},[129,10150,10016],{"class":273},[129,10152,1015],{"class":277},[129,10154,5698],{"class":290},[129,10156,160],{"class":273}," or the newer ",[15,10159,10160,10162,10164,10166],{"className":257,"language":259,"style":260},[129,10161,8859],{"class":273},[129,10163,362],{"class":277},[129,10165,8864],{"class":284},[129,10167,4140],{"class":273},[2456,10169,10171],{"id":10170},"layout-thrashing","Layout thrashing",[11,10173,10174,10175,500,10178,500,10181,1862,10184,10187],{},"The browser maintains separate phases: JavaScript runs, then style recalculation, then layout, then paint. Reading layout properties like ",[15,10176,10177],{},"offsetWidth",[15,10179,10180],{},"scrollTop",[15,10182,10183],{},"getBoundingClientRect",[24,10185,10186],{},"after"," a DOM mutation forces the browser to flush its pending layout queue synchronously - this is a forced reflow.",[11,10189,10190,10192],{},[118,10191,10171],{}," is reading and writing layout properties in an interleaved loop:",[255,10194,10196],{"className":3922,"code":10195,"language":3924,"meta":260,"style":260},"// Thrashing: forces a full reflow on every iteration\nelements.forEach(el => {\n  const width = el.offsetWidth          // forces layout flush\n  el.style.width = width + 10 + 'px'  // invalidates layout\n})\n\n// Fixed: batch reads, then batch writes\nconst widths = elements.map(el => el.offsetWidth)\nelements.forEach((el, i) => el.style.width = widths[i] + 10 + 'px')\n",[15,10197,10198,10203,10222,10241,10277,10283,10287,10292,10321],{"__ignoreMap":260},[129,10199,10200],{"class":265,"line":266},[129,10201,10202],{"class":376},"// Thrashing: forces a full reflow on every iteration\n",[129,10204,10205,10208,10210,10213,10215,10218,10220],{"class":265,"line":297},[129,10206,10207],{"class":273},"elements",[129,10209,362],{"class":277},[129,10211,10212],{"class":284},"forEach",[129,10214,147],{"class":273},[129,10216,10217],{"class":452},"el",[129,10219,456],{"class":269},[129,10221,1371],{"class":277},[129,10223,10224,10226,10229,10231,10234,10236,10238],{"class":265,"line":315},[129,10225,5076],{"class":269},[129,10227,10228],{"class":273}," width",[129,10230,4745],{"class":277},[129,10232,10233],{"class":273}," el",[129,10235,362],{"class":277},[129,10237,10177],{"class":273},[129,10239,10240],{"class":376},"          // forces layout flush\n",[129,10242,10243,10246,10248,10250,10252,10255,10257,10259,10262,10265,10267,10269,10272,10274],{"class":265,"line":332},[129,10244,10245],{"class":273},"  el",[129,10247,362],{"class":277},[129,10249,2026],{"class":273},[129,10251,362],{"class":277},[129,10253,10254],{"class":273},"width",[129,10256,4745],{"class":277},[129,10258,10228],{"class":273},[129,10260,10261],{"class":277}," +",[129,10263,10264],{"class":290}," 10",[129,10266,10261],{"class":277},[129,10268,4261],{"class":277},[129,10270,10271],{"class":427},"px",[129,10273,424],{"class":277},[129,10275,10276],{"class":376},"  // invalidates layout\n",[129,10278,10279,10281],{"class":265,"line":339},[129,10280,4028],{"class":277},[129,10282,294],{"class":273},[129,10284,10285],{"class":265,"line":356},[129,10286,336],{"emptyLinePlaceholder":335},[129,10288,10289],{"class":265,"line":651},[129,10290,10291],{"class":376},"// Fixed: batch reads, then batch writes\n",[129,10293,10294,10296,10299,10301,10304,10306,10308,10310,10312,10314,10316,10318],{"class":265,"line":657},[129,10295,270],{"class":269},[129,10297,10298],{"class":273}," widths ",[129,10300,278],{"class":277},[129,10302,10303],{"class":273}," elements",[129,10305,362],{"class":277},[129,10307,447],{"class":284},[129,10309,147],{"class":273},[129,10311,10217],{"class":452},[129,10313,456],{"class":269},[129,10315,10233],{"class":273},[129,10317,362],{"class":277},[129,10319,10320],{"class":273},"offsetWidth)\n",[129,10322,10323,10325,10327,10329,10331,10333,10335,10337,10340,10342,10344,10346,10348,10350,10352,10355,10357,10360,10362,10364,10366,10368,10370,10372],{"class":265,"line":669},[129,10324,10207],{"class":273},[129,10326,362],{"class":277},[129,10328,10212],{"class":284},[129,10330,147],{"class":273},[129,10332,147],{"class":277},[129,10334,10217],{"class":452},[129,10336,1015],{"class":277},[129,10338,10339],{"class":452}," i",[129,10341,160],{"class":277},[129,10343,456],{"class":269},[129,10345,10233],{"class":273},[129,10347,362],{"class":277},[129,10349,2026],{"class":273},[129,10351,362],{"class":277},[129,10353,10354],{"class":273},"width ",[129,10356,278],{"class":277},[129,10358,10359],{"class":273}," widths[i] ",[129,10361,219],{"class":277},[129,10363,10264],{"class":290},[129,10365,10261],{"class":277},[129,10367,4261],{"class":277},[129,10369,10271],{"class":427},[129,10371,424],{"class":277},[129,10373,294],{"class":273},[11,10375,10376],{},"Virtual DOM frameworks mostly avoid this by batching all DOM mutations together - a genuine advantage of framework-managed rendering over imperative code.",[2456,10378,10380],{"id":10379},"critical-rendering-path","Critical rendering path",[11,10382,8603,10383,10386],{},[118,10384,10385],{},"critical rendering path"," is the sequence the browser must complete before displaying anything: fetch HTML, parse HTML to DOM, fetch and parse CSS to CSSOM, combine into render tree, calculate layout, paint. Any resource that must be fetched and processed before this completes delays first paint.",[11,10388,10389],{},"Inlining critical CSS (styles needed for above-the-fold content) eliminates at least one round-trip from the critical path. Nuxt and Vite do this automatically with CSS code splitting.",[2456,10391,10393],{"id":10392},"render-blocking-resources","Render blocking resources",[11,10395,10396,10397,8975,10420,10429,10430,10439,10440,1335,10442,10445,10446,2354],{},"Any ",[15,10398,10401,10403,10406,10409,10411,10413,10416,10418],{"className":10399,"language":10400,"style":260},"language-html shiki shiki-themes material-theme-lighter material-theme material-theme-palenight","html",[129,10402,3945],{"class":277},[129,10404,10405],{"class":1376},"link",[129,10407,10408],{"class":269}," rel",[129,10410,278],{"class":277},[129,10412,2258],{"class":277},[129,10414,10415],{"class":427},"stylesheet",[129,10417,2258],{"class":277},[129,10419,3956],{"class":277},[15,10421,10422,10424,10427],{"className":10399,"language":10400,"style":260},[129,10423,3945],{"class":277},[129,10425,10426],{"class":1376},"head",[129,10428,3956],{"class":277}," blocks rendering until the browser downloads and parses the CSS. ",[15,10431,10432,10434,10437],{"className":10399,"language":10400,"style":260},[129,10433,3945],{"class":277},[129,10435,10436],{"class":1376},"script",[129,10438,3956],{"class":277}," tags without ",[15,10441,4508],{},[15,10443,10444],{},"defer"," block HTML parsing entirely (the script might call ",[15,10447,10448,10451,10453,10455],{"className":257,"language":259,"style":260},[129,10449,10450],{"class":273},"document",[129,10452,362],{"class":277},[129,10454,2413],{"class":284},[129,10456,4140],{"class":273},[255,10458,10460],{"className":10399,"code":10459,"language":10400,"meta":260,"style":260},"\u003C!-- Render blocking: -->\n\u003Clink rel=\"stylesheet\" href=\"modal-only-styles.css\">\n\u003Cscript src=\"analytics.js\">\u003C/script>\n\n\u003C!-- Non-blocking: -->\n\u003Clink rel=\"stylesheet\" href=\"modal-only-styles.css\" media=\"print\" onload=\"this.media='all'\">\n\u003Cscript src=\"analytics.js\" defer>\u003C/script>\n",[15,10461,10462,10467,10497,10522,10526,10531,10590],{"__ignoreMap":260},[129,10463,10464],{"class":265,"line":266},[129,10465,10466],{"class":376},"\u003C!-- Render blocking: -->\n",[129,10468,10469,10471,10473,10475,10477,10479,10481,10483,10486,10488,10490,10493,10495],{"class":265,"line":297},[129,10470,3945],{"class":277},[129,10472,10405],{"class":1376},[129,10474,10408],{"class":269},[129,10476,278],{"class":277},[129,10478,2258],{"class":277},[129,10480,10415],{"class":427},[129,10482,2258],{"class":277},[129,10484,10485],{"class":269}," href",[129,10487,278],{"class":277},[129,10489,2258],{"class":277},[129,10491,10492],{"class":427},"modal-only-styles.css",[129,10494,2258],{"class":277},[129,10496,4676],{"class":277},[129,10498,10499,10501,10503,10506,10508,10510,10513,10515,10518,10520],{"class":265,"line":315},[129,10500,3945],{"class":277},[129,10502,10436],{"class":1376},[129,10504,10505],{"class":269}," src",[129,10507,278],{"class":277},[129,10509,2258],{"class":277},[129,10511,10512],{"class":427},"analytics.js",[129,10514,2258],{"class":277},[129,10516,10517],{"class":277},">\u003C/",[129,10519,10436],{"class":1376},[129,10521,4676],{"class":277},[129,10523,10524],{"class":265,"line":332},[129,10525,336],{"emptyLinePlaceholder":335},[129,10527,10528],{"class":265,"line":339},[129,10529,10530],{"class":376},"\u003C!-- Non-blocking: -->\n",[129,10532,10533,10535,10537,10539,10541,10543,10545,10547,10549,10551,10553,10555,10557,10560,10562,10564,10567,10569,10572,10574,10577,10580,10583,10585,10588],{"class":265,"line":356},[129,10534,3945],{"class":277},[129,10536,10405],{"class":1376},[129,10538,10408],{"class":269},[129,10540,278],{"class":277},[129,10542,2258],{"class":277},[129,10544,10415],{"class":427},[129,10546,2258],{"class":277},[129,10548,10485],{"class":269},[129,10550,278],{"class":277},[129,10552,2258],{"class":277},[129,10554,10492],{"class":427},[129,10556,2258],{"class":277},[129,10558,10559],{"class":269}," media",[129,10561,278],{"class":277},[129,10563,2258],{"class":277},[129,10565,10566],{"class":427},"print",[129,10568,2258],{"class":277},[129,10570,10571],{"class":269}," onload",[129,10573,278],{"class":277},[129,10575,10576],{"class":277},"\"this.",[129,10578,10579],{"class":273},"media",[129,10581,10582],{"class":277},"='",[129,10584,4544],{"class":427},[129,10586,10587],{"class":277},"'\"",[129,10589,4676],{"class":277},[129,10591,10592,10594,10596,10598,10600,10602,10604,10606,10609,10611,10613],{"class":265,"line":651},[129,10593,3945],{"class":277},[129,10595,10436],{"class":1376},[129,10597,10505],{"class":269},[129,10599,278],{"class":277},[129,10601,2258],{"class":277},[129,10603,10512],{"class":427},[129,10605,2258],{"class":277},[129,10607,10608],{"class":269}," defer",[129,10610,10517],{"class":277},[129,10612,10436],{"class":1376},[129,10614,4676],{"class":277},[11,10616,8603,10617,10620,10621,10624],{},[15,10618,10619],{},"media=\"print\""," trick: the browser downloads the stylesheet but doesn't block rendering for it. The ",[15,10622,10623],{},"onload"," handler switches it to all-media once loaded. This is how performance-focused sites load non-critical CSS without flash.",[40,10626,10628],{"id":10627},"compositing-painting","Compositing & painting",[2456,10630,10632],{"id":10631},"browser-compositing-layers","Browser compositing layers",[11,10634,10635,10636,10639],{},"The browser doesn't paint the page as one flat image. It splits it into ",[118,10637,10638],{},"layers"," - independent surfaces composited (combined) by the GPU. Layers can be moved, scaled, or faded without a repaint - the GPU handles it.",[11,10641,10642,10643,938,10646,10649,10650,10653,10654,500,10657,500,10660,10663],{},"Layer promotion happens automatically for: CSS ",[15,10644,10645],{},"transform",[15,10647,10648],{},"opacity"," animations, ",[15,10651,10652],{},"position: fixed"," elements, ",[15,10655,10656],{},"will-change",[15,10658,10659],{},"\u003Cvideo>",[15,10661,10662],{},"\u003Ccanvas>",", and iframes. Inspect them in Chrome DevTools > Layers panel.",[3733,10665,10666],{},[11,10667,10668],{},"Each layer consumes GPU memory. Promoting thousands of elements to \"optimize\" them crashes mobile devices.",[2456,10670,10672],{"id":10671},"paint-vs-composite-vs-layout","Paint vs composite vs layout",[11,10674,10675],{},"Three distinct operations, in order of cost:",[1822,10677,10678,10684,10690],{},[1825,10679,10680,10683],{},[118,10681,10682],{},"Layout"," (reflow): calculate size and position of every element. Triggered by changes to geometry (width, height, margin, padding, font size, adding/removing nodes). Most expensive.",[1825,10685,10686,10689],{},[118,10687,10688],{},"Paint",": fill in pixels for each layer - text, colors, shadows, borders. Triggered by visual changes that don't affect geometry (color, background). Moderate cost.",[1825,10691,10692,10695],{},[118,10693,10694],{},"Composite",": assemble layers and display. Triggered by GPU-accelerated properties (transform, opacity). Cheapest by far.",[11,10697,10698,10699,968,10701,10703],{},"For smooth 60fps animation: animate only ",[15,10700,10645],{},[15,10702,10648],{},". Both skip layout and paint entirely.",[3733,10705,10706],{},[11,10707,10708,10709,938,10712,938,10715,938,10717,10720],{},"Animating ",[15,10710,10711],{},"left",[15,10713,10714],{},"top",[15,10716,10254],{},[15,10718,10719],{},"height"," triggers layout every frame - guaranteed jank.",[2456,10722,10724],{"id":10723},"gpu-acceleration-in-css","GPU acceleration in CSS",[11,10726,10727,10728,968,10735,10742],{},"Placing a property on the compositor thread requires the browser to promote the element to its own compositing layer. ",[15,10729,10732],{"className":10730,"language":10731,"style":260},"language-css shiki shiki-themes material-theme-lighter material-theme material-theme-palenight","css",[129,10733,10734],{"class":273},"transform: translateZ(0)",[15,10736,10737,10739],{"className":10730,"language":10731,"style":260},[129,10738,10656],{"class":2161},[129,10740,10741],{"class":273},": transform"," force this.",[255,10744,10746],{"className":10730,"code":10745,"language":10731,"meta":260,"style":260},".animated-element {\n  will-change: transform;  /* promotes to GPU layer before animation starts */\n}\n",[15,10747,10748,10757,10772],{"__ignoreMap":260},[129,10749,10750,10752,10755],{"class":265,"line":266},[129,10751,362],{"class":277},[129,10753,10754],{"class":2161},"animated-element",[129,10756,1371],{"class":277},[129,10758,10759,10763,10765,10767,10769],{"class":265,"line":297},[129,10760,10762],{"class":10761},"sqsOY","  will-change",[129,10764,1380],{"class":277},[129,10766,9247],{"class":273},[129,10768,7376],{"class":277},[129,10770,10771],{"class":376},"  /* promotes to GPU layer before animation starts */\n",[129,10773,10774],{"class":265,"line":315},[129,10775,1530],{"class":277},[11,10777,10778,10780],{},[15,10779,10656],{}," should be used sparingly and only on elements that will actually animate soon. It's a promise to the browser that benefits from layer promotion. Overusing it means GPU memory spent on layers that never move.",[2456,10782,10784],{"id":10783},"css-containment","CSS containment",[11,10786,10787,3984,10789,10792,10793,10798],{},[118,10788,10784],{},[15,10790,10791],{},"contain"," property) tells the browser that a subtree is independent from the rest of the page for layout, style, and paint calculations. If an element has ",[15,10794,10795],{"className":10730,"language":10731,"style":260},[129,10796,10797],{"class":273},"contain: layout",", changes inside it can't affect anything outside - so the browser skips checking the entire document during recalculation.",[255,10800,10802],{"className":10730,"code":10801,"language":10731,"meta":260,"style":260},".card {\n  contain: content; /* layout + style + paint */\n}\n",[15,10803,10804,10813,10828],{"__ignoreMap":260},[129,10805,10806,10808,10811],{"class":265,"line":266},[129,10807,362],{"class":277},[129,10809,10810],{"class":2161},"card",[129,10812,1371],{"class":277},[129,10814,10815,10818,10820,10823,10825],{"class":265,"line":297},[129,10816,10817],{"class":10761},"  contain",[129,10819,1380],{"class":277},[129,10821,10822],{"class":273}," content",[129,10824,7376],{"class":277},[129,10826,10827],{"class":376}," /* layout + style + paint */\n",[129,10829,10830],{"class":265,"line":315},[129,10831,1530],{"class":277},[11,10833,10834,10842],{},[15,10835,10836,10839],{"className":10730,"language":10731,"style":260},[129,10837,10838],{"class":2161},"content-visibility",[129,10840,10841],{"class":273},": auto"," (built on containment) is arguably the highest-leverage single-line optimization for content-heavy pages. It skips rendering off-screen elements entirely and renders them only when they enter the viewport. The browser still reserves their layout space - scroll height stays correct.",[2456,10844,10846],{"id":10845},"subpixel-rendering","Subpixel rendering",[11,10848,10849,10850,10860],{},"On a 2x DPR (device pixel ratio) display, 1 CSS pixel = 2x2 physical pixels. When an element is positioned at a non-integer CSS pixel value (",[15,10851,10852,10855,10857],{"className":10730,"language":10731,"style":260},[129,10853,10854],{"class":273},"left: 33",[129,10856,362],{"class":277},[129,10858,10859],{"class":2161},"5px","), the browser must anti-alias across physical pixel boundaries.",[3576,10862,10863],{},[11,10864,10865,10871,10872,10884],{},[15,10866,10867,10869],{"className":257,"language":259,"style":260},[129,10868,10183],{"class":284},[129,10870,4140],{"class":273}," returns floating-point values. When using these values to position an overlay or tooltip, ",[15,10873,10874,10877,10879,10882],{"className":257,"language":259,"style":260},[129,10875,10876],{"class":273},"Math",[129,10878,362],{"class":277},[129,10880,10881],{"class":284},"round",[129,10883,4140],{"class":273}," the values first - otherwise you'll see blurry borders on non-retina displays.",[40,10886,10888],{"id":10887},"observer-apis","Observer APIs",[2456,10890,10892],{"id":10891},"intersectionobserver-internals","IntersectionObserver internals",[11,10894,10895,10898,10899,10901,10902,10905],{},[118,10896,10897],{},"IntersectionObserver"," fires callbacks when an element enters or exits the viewport (or a scroll container). The key implementation detail: it doesn't run on every scroll event. It runs at the same time as ",[15,10900,10051],{}," - once per frame, after layout but before paint. Far cheaper than ",[15,10903,10904],{},"scroll"," event listeners with manual position checks.",[11,10907,10908],{},"Gotcha: IO callbacks are asynchronous to the render cycle. If your callback modifies layout (adds elements, changes heights), you can cause jank even though the callback itself is \"cheap.\"",[2456,10910,10912],{"id":10911},"resizeobserver-loop-limits","ResizeObserver loop limits",[11,10914,10915,10918,10919,10923],{},[118,10916,10917],{},"ResizeObserver"," calls a callback when an element's size changes. The loop limit error - ",[15,10920,10922],{"color":10921},"error","ResizeObserver loop limit exceeded"," - happens when a ResizeObserver callback causes the observed element to resize, triggering another callback, infinitely. The browser detects and suppresses this.",[11,10925,10926,10927,10929],{},"This usually means your callback modifies layout in a way that affects the observed element. Fix: use ",[15,10928,10051],{}," to defer layout-changing work out of the callback, or debounce it.",[2456,10931,10933],{"id":10932},"mutationobserver-cost","MutationObserver cost",[11,10935,10936,10939],{},[118,10937,10938],{},"MutationObserver"," fires when the DOM changes - insertions, deletions, attribute changes, text content changes. Internally it's a microtask queue - mutations are batched and the callback fires as a microtask after the current task completes.",[11,10941,10942,10943,968,10946,10949],{},"The cost is proportional to what you observe. Watching a root node with ",[15,10944,10945],{},"subtree: true",[15,10947,10948],{},"childList: true"," means every DOM insertion anywhere in the document is tracked. In a component framework that creates hundreds of nodes on mount, this adds up. If you see large groups of microtask entries in DevTools traces, check your MutationObserver usage - analytics scripts are common culprits.",[40,10951,10953],{"id":10952},"build-bundling","Build & bundling",[2456,10955,10957],{"id":10956},"tree-shaking-internals","Tree shaking internals",[11,10959,10960,10963],{},[118,10961,10962],{},"Tree shaking"," is dead code elimination based on static analysis of ES module imports. Bundlers (Rollup, Vite, Webpack 5) parse the import graph, identify which exports are actually used, and exclude unused code from the bundle.",[11,10965,10966],{},"It only works with ES modules, not CommonJS, because CommonJS imports are evaluated at runtime and can't be statically analyzed. This is why migrating packages to ESM matters.",[11,10968,10969,10970,10973,10974,10977,10978,10981,10982,10985],{},"The gotcha: ",[118,10971,10972],{},"side effects",". A module that modifies ",[15,10975,10976],{},"globalThis",", registers something, or patches prototypes on import can't be safely removed even if nothing is imported from it. The ",[15,10979,10980],{},"\"sideEffects\": false"," field in ",[15,10983,10984],{},"package.json"," is an explicit declaration that all modules in the package are side-effect-free.",[255,10987,10990],{"className":10988,"code":10989,"language":2289,"meta":260,"style":260},"language-json shiki shiki-themes material-theme-lighter material-theme material-theme-palenight","{\n  \"sideEffects\": [\"*.css\", \"!src/polyfills.js\"]\n}\n",[15,10991,10992,10996,11029],{"__ignoreMap":260},[129,10993,10994],{"class":265,"line":266},[129,10995,6455],{"class":277},[129,10997,10998,11001,11004,11006,11008,11010,11012,11015,11017,11019,11022,11025,11027],{"class":265,"line":297},[129,10999,11000],{"class":277},"  \"",[129,11002,11003],{"class":269},"sideEffects",[129,11005,2258],{"class":277},[129,11007,1380],{"class":277},[129,11009,1010],{"class":277},[129,11011,2258],{"class":277},[129,11013,11014],{"class":427},"*.css",[129,11016,2258],{"class":277},[129,11018,1015],{"class":277},[129,11020,11021],{"class":277}," \"",[129,11023,11024],{"class":427},"!src/polyfills.js",[129,11026,2258],{"class":277},[129,11028,1046],{"class":277},[129,11030,11031],{"class":265,"line":315},[129,11032,1530],{"class":277},[2456,11034,11036],{"id":11035},"code-splitting-strategies","Code splitting strategies",[11,11038,11039,11042],{},[118,11040,11041],{},"Code splitting"," divides the bundle into chunks loaded on demand. Three main strategies:",[2086,11044,11045,11051,11057],{},[1825,11046,11047,11050],{},[118,11048,11049],{},"Route-based",": one chunk per page/route. The Nuxt default - each page component is automatically split.",[1825,11052,11053,11056],{},[118,11054,11055],{},"Component-based",": heavy components (rich text editor, chart library) loaded only when needed.",[1825,11058,11059,11062,11063,11066],{},[118,11060,11061],{},"Vendor splitting",": separate chunk for ",[15,11064,11065],{},"node_modules"," that changes less often than app code - better long-term caching.",[11,11068,11069],{},"The tradeoff: more chunks means more HTTP requests (mitigated by HTTP/2 multiplexing) and more waterfall risk (chunk A loads, discovers it needs chunk B, requests chunk B).",[2456,11071,11073],{"id":11072},"dynamic-import-chunking","Dynamic import chunking",[11,11075,11076,11082],{},[15,11077,11078,11080],{"className":257,"language":259,"style":260},[129,11079,2140],{"class":284},[129,11081,4140],{"class":273}," (dynamic import) tells the bundler to create a split point. Vite and Nuxt handle route-level splits automatically, but manual dynamic imports control the chunk graph.",[255,11084,11086],{"className":3922,"code":11085,"language":3924,"meta":260,"style":260},"// Loaded eagerly (in main bundle)\nimport { HeavyEditor } from './HeavyEditor'\n\n// Loaded on demand (separate chunk, hashed filename)\nconst HeavyEditor = () => import('./HeavyEditor')\n",[15,11087,11088,11093,11113,11117,11122],{"__ignoreMap":260},[129,11089,11090],{"class":265,"line":266},[129,11091,11092],{"class":376},"// Loaded eagerly (in main bundle)\n",[129,11094,11095,11097,11099,11102,11104,11106,11108,11111],{"class":265,"line":297},[129,11096,2140],{"class":2139},[129,11098,1416],{"class":277},[129,11100,11101],{"class":273}," HeavyEditor",[129,11103,4255],{"class":277},[129,11105,4258],{"class":2139},[129,11107,4261],{"class":277},[129,11109,11110],{"class":427},"./HeavyEditor",[129,11112,4267],{"class":277},[129,11114,11115],{"class":265,"line":315},[129,11116,336],{"emptyLinePlaceholder":335},[129,11118,11119],{"class":265,"line":332},[129,11120,11121],{"class":376},"// Loaded on demand (separate chunk, hashed filename)\n",[129,11123,11124,11126,11129,11131,11133,11135,11138,11140,11142,11144,11146],{"class":265,"line":339},[129,11125,270],{"class":269},[129,11127,11128],{"class":273}," HeavyEditor ",[129,11130,278],{"class":277},[129,11132,4511],{"class":277},[129,11134,456],{"class":269},[129,11136,11137],{"class":277}," import",[129,11139,147],{"class":273},[129,11141,424],{"class":277},[129,11143,11110],{"class":427},[129,11145,424],{"class":277},[129,11147,294],{"class":273},[11,11149,11150,11153,11154,11157],{},[118,11151,11152],{},"Chunk naming"," matters for caching. Vite generates content-hash filenames (",[15,11155,11156],{},"HeavyEditor.Ba3f9c2.js","). On rebuild, if only HeavyEditor changed, only that chunk's hash changes - users re-download only that file. This is why separating stable third-party code into a vendor chunk is a genuine caching win.",[2456,11159,11161],{"id":11160},"module-federation","Module federation",[11,11163,11164,11166],{},[118,11165,11161],{}," (Webpack 5+, also a Vite plugin) lets separately-deployed applications share JavaScript modules at runtime - without bundling them together at build time. Application A exposes a component; Application B loads and renders it without importing it during A's build.",[11,11168,11169],{},"The use case is micro-frontends at scale: multiple independent deployment units sharing UI components or a design system. The complexity is real: shared dependencies (Vue, Nuxt modules) must be version-compatible across remotes, TypeScript types for remote modules are awkward, and network failures need graceful fallbacks.",[11,11171,11172],{},"For most teams this is overkill. The genuine use case is large organizations with multiple independent teams that need to deploy independently while sharing UI.",[40,11174,11176],{"id":11175},"web-components-platform-apis","Web Components & platform APIs",[2456,11178,11180],{"id":11179},"shadow-dom","Shadow DOM",[11,11182,11183,11185,11186,11189],{},[118,11184,11180],{}," is browser-native encapsulation. A shadow root attached to an element creates a scoped DOM tree: CSS inside can't leak out, CSS outside can't leak in (except inherited properties and CSS custom properties), and ",[15,11187,11188],{},"document.querySelector"," can't reach inside.",[255,11191,11193],{"className":3922,"code":11192,"language":3924,"meta":260,"style":260},"const host = document.querySelector('#card')\nconst shadow = host.attachShadow({ mode: 'open' })\nshadow.innerHTML = `\n  \u003Cstyle>p { color: red }\u003C/style>\n  \u003Cp>This red doesn't affect the main document\u003C/p>\n`\n",[15,11194,11195,11223,11260,11275,11280,11285],{"__ignoreMap":260},[129,11196,11197,11199,11202,11204,11207,11209,11212,11214,11216,11219,11221],{"class":265,"line":266},[129,11198,270],{"class":269},[129,11200,11201],{"class":273}," host ",[129,11203,278],{"class":277},[129,11205,11206],{"class":273}," document",[129,11208,362],{"class":277},[129,11210,11211],{"class":284},"querySelector",[129,11213,147],{"class":273},[129,11215,424],{"class":277},[129,11217,11218],{"class":427},"#card",[129,11220,424],{"class":277},[129,11222,294],{"class":273},[129,11224,11225,11227,11230,11232,11235,11237,11240,11242,11244,11247,11249,11251,11254,11256,11258],{"class":265,"line":297},[129,11226,270],{"class":269},[129,11228,11229],{"class":273}," shadow ",[129,11231,278],{"class":277},[129,11233,11234],{"class":273}," host",[129,11236,362],{"class":277},[129,11238,11239],{"class":284},"attachShadow",[129,11241,147],{"class":273},[129,11243,4796],{"class":277},[129,11245,11246],{"class":1376}," mode",[129,11248,1380],{"class":277},[129,11250,4261],{"class":277},[129,11252,11253],{"class":427},"open",[129,11255,424],{"class":277},[129,11257,4255],{"class":277},[129,11259,294],{"class":273},[129,11261,11262,11265,11267,11270,11272],{"class":265,"line":315},[129,11263,11264],{"class":273},"shadow",[129,11266,362],{"class":277},[129,11268,11269],{"class":273},"innerHTML ",[129,11271,278],{"class":277},[129,11273,11274],{"class":277}," `\n",[129,11276,11277],{"class":265,"line":332},[129,11278,11279],{"class":427},"  \u003Cstyle>p { color: red }\u003C/style>\n",[129,11281,11282],{"class":265,"line":339},[129,11283,11284],{"class":427},"  \u003Cp>This red doesn't affect the main document\u003C/p>\n",[129,11286,11287],{"class":265,"line":356},[129,11288,11289],{"class":277},"`\n",[11,11291,11292,11295,11296,11299,11300,11303,11304,11306,11307,11327,11328,11337],{},[15,11293,11294],{},"mode: 'open'"," means JavaScript can access the shadow root via ",[15,11297,11298],{},"element.shadowRoot",". ",[15,11301,11302],{},"mode: 'closed'"," returns ",[15,11305,3961],{},". Browsers use closed shadow roots for their own UI elements - the native ",[15,11308,11309,11311,11314,11317,11319,11321,11323,11325],{"className":10399,"language":10400,"style":260},[129,11310,3945],{"class":277},[129,11312,11313],{"class":1376},"input",[129,11315,11316],{"class":269}," type",[129,11318,278],{"class":277},[129,11320,2258],{"class":277},[129,11322,5599],{"class":427},[129,11324,2258],{"class":277},[129,11326,3956],{"class":277}," datepicker, ",[15,11329,11330,11332,11335],{"className":10399,"language":10400,"style":260},[129,11331,3945],{"class":277},[129,11333,11334],{"class":1376},"video",[129,11336,3956],{"class":277}," controls.",[2456,11339,11341],{"id":11340},"custom-elements-lifecycle","Custom Elements lifecycle",[11,11343,11344,11347],{},[118,11345,11346],{},"Custom Elements"," are user-defined HTML elements with lifecycle callbacks:",[59,11349,11350,11360],{},[62,11351,11352],{},[65,11353,11354,11357],{},[68,11355,11356],{},"Callback",[68,11358,11359],{},"When it fires",[78,11361,11362,11372,11382,11392,11402],{},[65,11363,11364,11369],{},[83,11365,11366],{},[15,11367,11368],{},"constructor()",[83,11370,11371],{},"Element created in memory",[65,11373,11374,11379],{},[83,11375,11376],{},[15,11377,11378],{},"connectedCallback()",[83,11380,11381],{},"Element attached to the document",[65,11383,11384,11389],{},[83,11385,11386],{},[15,11387,11388],{},"disconnectedCallback()",[83,11390,11391],{},"Element removed from the document",[65,11393,11394,11399],{},[83,11395,11396],{},[15,11397,11398],{},"attributeChangedCallback(name, old, new)",[83,11400,11401],{},"Observed attribute changed",[65,11403,11404,11409],{},[83,11405,11406],{},[15,11407,11408],{},"adoptedCallback()",[83,11410,11411],{},"Element moved to a new document",[11,11413,11414,11415,11418,11419,11422],{},"Critical gotcha: ",[15,11416,11417],{},"attributeChangedCallback"," only fires for attributes listed in ",[15,11420,11421],{},"static observedAttributes",". Forget to declare one and mutations to it are silently ignored.",[11,11424,11425,11428,11429,11431,11432,11444],{},[15,11426,11427],{},"connectedCallback"," can fire before children are parsed. If your element needs to read its child nodes in ",[15,11430,11427],{},", use ",[15,11433,11434,11436,11438,11440,11442],{"className":257,"language":259,"style":260},[129,11435,9738],{"class":284},[129,11437,10016],{"class":273},[129,11439,1015],{"class":277},[129,11441,5698],{"class":290},[129,11443,160],{"class":273}," or a MutationObserver - the browser creates elements as it encounters opening tags during HTML parsing.",[2456,11446,11448],{"id":11447},"web-components-interoperability","Web Components interoperability",[11,11450,11451,11452,11455],{},"Using Web Components inside Vue works well for most cases. The friction points: custom elements dispatch ",[15,11453,11454],{},"CustomEvent"," (not Vue component events), attribute vs property setting (Vue sets properties by default; HTML attributes are strings), and SSR (custom elements can't run server-side without a DOM polyfill).",[11,11457,11458,11459,11462],{},"Vue 3 has native Web Components support via ",[15,11460,11461],{},"defineCustomElement",", which compiles a Vue SFC into a custom element that works anywhere. For Nuxt's Islands pattern or design system components that need to work outside Vue, this is a legitimate approach. For a purely internal Vue codebase, there's no real benefit over standard Vue components.",[40,11464,11466],{"id":11465},"threading-workers","Threading & workers",[2456,11468,11470],{"id":11469},"web-workers-vs-service-workers","Web Workers vs Service Workers",[59,11472,11473,11485],{},[62,11474,11475],{},[65,11476,11477,11479,11482],{},[68,11478],{},[68,11480,11481],{},"Web Worker",[68,11483,11484],{},"Service Worker",[78,11486,11487,11498,11507,11516,11527],{},[65,11488,11489,11492,11495],{},[83,11490,11491],{},"Runs in",[83,11493,11494],{},"Background thread",[83,11496,11497],{},"Background thread + survives page close",[65,11499,11500,11503,11505],{},[83,11501,11502],{},"DOM access",[83,11504,1732],{},[83,11506,1732],{},[65,11508,11509,11512,11514],{},[83,11510,11511],{},"Network interception",[83,11513,1732],{},[83,11515,3437],{},[65,11517,11518,11521,11524],{},[83,11519,11520],{},"Scope",[83,11522,11523],{},"Per-page",[83,11525,11526],{},"Per-origin",[65,11528,11529,11532,11535],{},[83,11530,11531],{},"Use case",[83,11533,11534],{},"CPU-heavy computation",[83,11536,11537],{},"Offline caching, background sync, push notifications",[11,11539,561,11540,11542,11543,11546],{},[118,11541,11481],{}," is a background thread for your page. You create it, communicate with ",[15,11544,11545],{},"postMessage",", and it's gone when the page closes. CPU-heavy tasks (image processing, cryptography, parsing large datasets) belong here.",[11,11548,561,11549,11551],{},[118,11550,11484],{}," is a persistent proxy between your page and the network. It intercepts all fetch requests from your origin, can return cached responses, and survives between page visits. It's the foundation of PWA offline capability.",[2456,11553,11555],{"id":11554},"sharedarraybuffer","SharedArrayBuffer",[11,11557,11558,11560,11561,11563],{},[118,11559,11555],{}," lets multiple threads (main thread + workers) read and write the same block of memory simultaneously. Without it, every ",[15,11562,11545],{}," serializes and copies data. With SharedArrayBuffer you have true shared memory - including all the race condition complexity that comes with it.",[11,11565,11566,11567,11572,11573,968,11576,11579],{},"Due to ",[51,11568,11571],{"href":11569,"rel":11570},"https://developer.chrome.com/blog/enabling-shared-array-buffers/",[55],"Spectre exploitation risks",", SharedArrayBuffer requires cross-origin isolation: ",[15,11574,11575],{},"Cross-Origin-Opener-Policy: same-origin",[15,11577,11578],{},"Cross-Origin-Embedder-Policy: require-corp"," headers. This breaks third-party iframes and CDN-hosted assets unless they opt in explicitly.",[11,11581,11582,11583,11586],{},"Use ",[15,11584,11585],{},"Atomics"," for synchronization when multiple threads access the same SharedArrayBuffer.",[2456,11588,11590],{"id":11589},"transferable-objects","Transferable objects",[11,11592,11593,11595,11596,11598],{},[15,11594,11545],{}," with large data (a 100MB ArrayBuffer) copies the data - O(n) time and memory. ",[118,11597,11590],{}," transfer ownership instead of copying: the sender's reference becomes unusable, and the transfer is O(1).",[255,11600,11602],{"className":3922,"code":11601,"language":3924,"meta":260,"style":260},"const buffer = new ArrayBuffer(100_000_000)\nworker.postMessage({ data: buffer }, [buffer])\n// buffer is now detached in the main thread - it was transferred\n",[15,11603,11604,11623,11648],{"__ignoreMap":260},[129,11605,11606,11608,11610,11612,11614,11616,11618,11621],{"class":265,"line":266},[129,11607,270],{"class":269},[129,11609,274],{"class":273},[129,11611,278],{"class":277},[129,11613,281],{"class":277},[129,11615,285],{"class":284},[129,11617,147],{"class":273},[129,11619,11620],{"class":290},"100_000_000",[129,11622,294],{"class":273},[129,11624,11625,11628,11630,11632,11634,11636,11639,11641,11643,11645],{"class":265,"line":297},[129,11626,11627],{"class":273},"worker",[129,11629,362],{"class":277},[129,11631,11545],{"class":284},[129,11633,147],{"class":273},[129,11635,4796],{"class":277},[129,11637,11638],{"class":1376}," data",[129,11640,1380],{"class":277},[129,11642,274],{"class":273},[129,11644,6625],{"class":277},[129,11646,11647],{"class":273}," [buffer])\n",[129,11649,11650],{"class":265,"line":315},[129,11651,11652],{"class":376},"// buffer is now detached in the main thread - it was transferred\n",[11,11654,11655,11656,500,11659,500,11662,500,11665,500,11668,500,11671,500,11674,362],{},"Transferable types: ",[15,11657,11658],{},"ArrayBuffer",[15,11660,11661],{},"MessagePort",[15,11663,11664],{},"ImageBitmap",[15,11666,11667],{},"OffscreenCanvas",[15,11669,11670],{},"ReadableStream",[15,11672,11673],{},"WritableStream",[15,11675,11676],{},"TransformStream",[2456,11678,11667],{"id":11679},"offscreencanvas",[11,11681,561,11682,11685],{},[118,11683,11684],{},"Canvas"," not attached to the document. You can transfer it to a Web Worker and run all rendering there - completely off the main thread. This is the right approach for heavy canvas animations that would otherwise cause jank by occupying the main thread.",[255,11687,11689],{"className":3922,"code":11688,"language":3924,"meta":260,"style":260},"const canvas = document.querySelector('canvas')\nconst offscreen = canvas.transferControlToOffscreen()\nworker.postMessage({ canvas: offscreen }, [offscreen])\n// All canvas operations now happen in the worker\n",[15,11690,11691,11717,11736,11759],{"__ignoreMap":260},[129,11692,11693,11695,11698,11700,11702,11704,11706,11708,11710,11713,11715],{"class":265,"line":266},[129,11694,270],{"class":269},[129,11696,11697],{"class":273}," canvas ",[129,11699,278],{"class":277},[129,11701,11206],{"class":273},[129,11703,362],{"class":277},[129,11705,11211],{"class":284},[129,11707,147],{"class":273},[129,11709,424],{"class":277},[129,11711,11712],{"class":427},"canvas",[129,11714,424],{"class":277},[129,11716,294],{"class":273},[129,11718,11719,11721,11724,11726,11729,11731,11734],{"class":265,"line":297},[129,11720,270],{"class":269},[129,11722,11723],{"class":273}," offscreen ",[129,11725,278],{"class":277},[129,11727,11728],{"class":273}," canvas",[129,11730,362],{"class":277},[129,11732,11733],{"class":284},"transferControlToOffscreen",[129,11735,2451],{"class":273},[129,11737,11738,11740,11742,11744,11746,11748,11750,11752,11754,11756],{"class":265,"line":315},[129,11739,11627],{"class":273},[129,11741,362],{"class":277},[129,11743,11545],{"class":284},[129,11745,147],{"class":273},[129,11747,4796],{"class":277},[129,11749,11728],{"class":1376},[129,11751,1380],{"class":277},[129,11753,11723],{"class":273},[129,11755,6625],{"class":277},[129,11757,11758],{"class":273}," [offscreen])\n",[129,11760,11761],{"class":265,"line":332},[129,11762,11763],{"class":376},"// All canvas operations now happen in the worker\n",[11,11765,11766],{},"Three.js supports OffscreenCanvas. Pixi.js has experimental support. For production use, test carefully - support is solid on desktop, more variable on mobile.",[2456,11768,11770],{"id":11769},"webassembly-integration","WebAssembly integration",[11,11772,11773,11776,11777,362],{},[118,11774,11775],{},"WebAssembly"," (WASM) is a binary instruction format that runs in browsers at near-native speed. Languages compiled to WASM (C, C++, Rust, Go) can run at speeds 10-100x faster than equivalent JavaScript for CPU-bound tasks. WASM doesn't access the DOM - communication happens via shared memory or ",[15,11778,11545],{},[255,11780,11782],{"className":3922,"code":11781,"language":3924,"meta":260,"style":260},"const wasm = await WebAssembly.instantiateStreaming(fetch('/module.wasm'), imports)\nconst result = wasm.instance.exports.process(inputPtr, inputLength)\n",[15,11783,11784,11824],{"__ignoreMap":260},[129,11785,11786,11788,11791,11793,11795,11798,11800,11803,11805,11808,11810,11812,11815,11817,11819,11821],{"class":265,"line":266},[129,11787,270],{"class":269},[129,11789,11790],{"class":273}," wasm ",[129,11792,278],{"class":277},[129,11794,4779],{"class":2139},[129,11796,11797],{"class":273}," WebAssembly",[129,11799,362],{"class":277},[129,11801,11802],{"class":284},"instantiateStreaming",[129,11804,147],{"class":273},[129,11806,11807],{"class":284},"fetch",[129,11809,147],{"class":273},[129,11811,424],{"class":277},[129,11813,11814],{"class":427},"/module.wasm",[129,11816,424],{"class":277},[129,11818,160],{"class":273},[129,11820,1015],{"class":277},[129,11822,11823],{"class":273}," imports)\n",[129,11825,11826,11828,11831,11833,11836,11838,11841,11843,11846,11848,11850,11853,11855],{"class":265,"line":297},[129,11827,270],{"class":269},[129,11829,11830],{"class":273}," result ",[129,11832,278],{"class":277},[129,11834,11835],{"class":273}," wasm",[129,11837,362],{"class":277},[129,11839,11840],{"class":273},"instance",[129,11842,362],{"class":277},[129,11844,11845],{"class":273},"exports",[129,11847,362],{"class":277},[129,11849,4755],{"class":284},[129,11851,11852],{"class":273},"(inputPtr",[129,11854,1015],{"class":277},[129,11856,11857],{"class":273}," inputLength)\n",[11,11859,11860,11861,11864],{},"In Nuxt with Vite, WASM files can be imported directly. Real use cases: image/video processing, audio DSP, cryptography, SQLite in the browser (",[15,11862,11863],{},"sql.js","), PDF rendering. WASM it's for specific CPU-bound tasks where JS's JIT isn't fast enough.",[40,11866,11868],{"id":11867},"storage-offline","Storage & offline",[2456,11870,11872],{"id":11871},"indexeddb","IndexedDB",[11,11874,11875,11877,11878,11883],{},[118,11876,11872],{}," is a transactional key-value store with structured data (not just strings like localStorage). It's asynchronous, supports gigabytes of data, and has complex querying via indexes. The raw API is notoriously verbose. ",[51,11879,11882],{"href":11880,"rel":11881},"https://dexie.org",[55],"Dexie.js"," wraps it in a usable Promise-based API:",[255,11885,11887],{"className":3922,"code":11886,"language":3924,"meta":260,"style":260},"import Dexie from 'dexie'\nconst db = new Dexie('AppDB')\ndb.version(1).stores({ posts: '++id, slug, publishedAt' })\nconst recent = await db.posts.where('publishedAt').above(lastWeek).toArray()\n",[15,11888,11889,11905,11929,11970],{"__ignoreMap":260},[129,11890,11891,11893,11896,11898,11900,11903],{"class":265,"line":266},[129,11892,2140],{"class":2139},[129,11894,11895],{"class":273}," Dexie ",[129,11897,2589],{"class":2139},[129,11899,4261],{"class":277},[129,11901,11902],{"class":427},"dexie",[129,11904,4267],{"class":277},[129,11906,11907,11909,11911,11913,11915,11918,11920,11922,11925,11927],{"class":265,"line":297},[129,11908,270],{"class":269},[129,11910,4324],{"class":273},[129,11912,278],{"class":277},[129,11914,281],{"class":277},[129,11916,11917],{"class":284}," Dexie",[129,11919,147],{"class":273},[129,11921,424],{"class":277},[129,11923,11924],{"class":427},"AppDB",[129,11926,424],{"class":277},[129,11928,294],{"class":273},[129,11930,11931,11934,11936,11939,11941,11943,11945,11947,11950,11952,11954,11957,11959,11961,11964,11966,11968],{"class":265,"line":315},[129,11932,11933],{"class":273},"db",[129,11935,362],{"class":277},[129,11937,11938],{"class":284},"version",[129,11940,147],{"class":273},[129,11942,154],{"class":290},[129,11944,160],{"class":273},[129,11946,362],{"class":277},[129,11948,11949],{"class":284},"stores",[129,11951,147],{"class":273},[129,11953,4796],{"class":277},[129,11955,11956],{"class":1376}," posts",[129,11958,1380],{"class":277},[129,11960,4261],{"class":277},[129,11962,11963],{"class":427},"++id, slug, publishedAt",[129,11965,424],{"class":277},[129,11967,4255],{"class":277},[129,11969,294],{"class":273},[129,11971,11972,11974,11977,11979,11981,11983,11985,11988,11990,11992,11994,11996,11999,12001,12003,12005,12008,12011,12013,12016],{"class":265,"line":332},[129,11973,270],{"class":269},[129,11975,11976],{"class":273}," recent ",[129,11978,278],{"class":277},[129,11980,4779],{"class":2139},[129,11982,4479],{"class":273},[129,11984,362],{"class":277},[129,11986,11987],{"class":273},"posts",[129,11989,362],{"class":277},[129,11991,5436],{"class":284},[129,11993,147],{"class":273},[129,11995,424],{"class":277},[129,11997,11998],{"class":427},"publishedAt",[129,12000,424],{"class":277},[129,12002,160],{"class":273},[129,12004,362],{"class":277},[129,12006,12007],{"class":284},"above",[129,12009,12010],{"class":273},"(lastWeek)",[129,12012,362],{"class":277},[129,12014,12015],{"class":284},"toArray",[129,12017,2451],{"class":273},[3733,12019,12020],{},[11,12021,12022],{},"IDB operations in a Service Worker share the same database as the page, but simultaneous version upgrades from both contexts cause blocking. Test your upgrade logic carefully.",[2456,12024,12026],{"id":12025},"service-worker-lifecycle-traps","Service Worker lifecycle traps",[11,12028,12029,12030,12033,12034,12033,12037,12033,12040,12043],{},"A Service Worker transitions: ",[15,12031,12032],{},"installing"," -> ",[15,12035,12036],{},"installed",[15,12038,12039],{},"activating",[15,12041,12042],{},"activated",". The traps:",[2086,12045,12046,12056,12077],{},[1825,12047,12048,12051,12052,12055],{},[118,12049,12050],{},"Stuck in installing",": if the SW file fails to download or throws during ",[15,12053,12054],{},"install",", it gets stuck. The old SW keeps serving.",[1825,12057,12058,12061,12062,8975,12074,12076],{},[118,12059,12060],{},"Waiting state",": a new SW installs but waits for all existing pages to close before activating. This is correct - two SW versions running simultaneously would conflict. ",[15,12063,12064,12067,12069,12072],{"className":257,"language":259,"style":260},[129,12065,12066],{"class":273},"self",[129,12068,362],{"class":277},[129,12070,12071],{"class":284},"skipWaiting",[129,12073,4140],{"class":273},[15,12075,12054],{}," forces immediate activation (can break things if old and new SW expect different cache shapes).",[1825,12078,12079,12082,12083,8975,12095,12098],{},[118,12080,12081],{},"Outdated clients",": an activated SW may serve cached JS that loads the old SW version. ",[15,12084,12085,12088,12090,12093],{"className":257,"language":259,"style":260},[129,12086,12087],{"class":273},"clients",[129,12089,362],{"class":277},[129,12091,12092],{"class":284},"claim",[129,12094,4140],{"class":273},[15,12096,12097],{},"activate"," takes control of all open clients immediately.",[11,12100,12101,12106],{},[51,12102,12105],{"href":12103,"rel":12104},"https://developer.chrome.com/docs/workbox/",[55],"Workbox"," handles most of this correctly by default.",[2456,12108,12110],{"id":12109},"cache-invalidation-strategies","Cache invalidation strategies",[11,12112,12113],{},"Five strategies, all implemented by Workbox:",[59,12115,12116,12128],{},[62,12117,12118],{},[65,12119,12120,12122,12125],{},[68,12121,5944],{},[68,12123,12124],{},"Logic",[68,12126,12127],{},"Best for",[78,12129,12130,12141,12152,12163,12174],{},[65,12131,12132,12135,12138],{},[83,12133,12134],{},"Cache first",[83,12136,12137],{},"Cache hit? Return it. Miss? Fetch & cache.",[83,12139,12140],{},"Static assets with hashed filenames",[65,12142,12143,12146,12149],{},[83,12144,12145],{},"Network first",[83,12147,12148],{},"Try network. Fail? Return cache.",[83,12150,12151],{},"API data that must be fresh",[65,12153,12154,12157,12160],{},[83,12155,12156],{},"Stale-while-revalidate",[83,12158,12159],{},"Return cache immediately, refresh in background",[83,12161,12162],{},"Semi-static content, UI shell",[65,12164,12165,12168,12171],{},[83,12166,12167],{},"Cache only",[83,12169,12170],{},"Never network.",[83,12172,12173],{},"Pre-cached offline app shell",[65,12175,12176,12179,12182],{},[83,12177,12178],{},"Network only",[83,12180,12181],{},"Never cache.",[83,12183,12184],{},"Non-idempotent requests",[11,12186,12187],{},"The right strategy is per-resource-type, not per-site. A genuinely useful offline experience maps each resource type to the appropriate strategy.",[2456,12189,12156],{"id":12190},"stale-while-revalidate",[11,12192,12193,12195],{},[118,12194,12156],{}," returns a cached response immediately, then fires a background request to refresh it. The user sees content instantly; the next load (or navigation if the background request completes in time) shows fresh data.",[255,12197,12201],{"className":12198,"code":12200,"language":3237},[12199],"language-text","Cache-Control: max-age=3600, stale-while-revalidate=86400\n",[15,12202,12200],{"__ignoreMap":260},[11,12204,12205],{},"This means: treat as fresh for 1 hour, serve stale for up to 24 hours while refreshing in background. Browsers and CDNs handle this automatically when the header is present.",[11,12207,12208,12209,12212],{},"The SWR hook library borrows its name from this pattern. ",[15,12210,12211],{},"useSWR"," returns cached data immediately and fetches fresh data in the background.",[40,12214,12216],{"id":12215},"networking-resources","Networking & resources",[2456,12218,12220],{"id":12219},"etag-vs-cache-control","ETag vs Cache-Control",[11,12222,12223,12226,12227,12230],{},[15,12224,12225],{},"Cache-Control"," controls whether the browser sends a request at all. ",[15,12228,12229],{},"ETag"," controls what happens when it does.",[11,12232,12233,12236,12237,12240,12241,12244,12245,12248],{},[15,12234,12235],{},"Cache-Control: max-age=31536000, immutable"," means \"don't even check for a year.\" ",[15,12238,12239],{},"Cache-Control: no-cache"," means \"always check.\" When the browser does check, it sends ",[15,12242,12243],{},"If-None-Match: [etag-value]"," - if it matches the server's current ETag, the response is ",[15,12246,12247],{},"304 Not Modified"," with no body.",[11,12250,12251,12252,12255,12256,12259,12260,12262],{},"For static assets with content-hash filenames (which Nuxt/Vite generates: ",[15,12253,12254],{},"main.Ba3f9c2.js","), use ",[15,12257,12258],{},"Cache-Control: public, max-age=31536000, immutable",". No ETag needed - the filename encodes freshness. For HTML, use ",[15,12261,12239],{}," + ETag for conditional GETs.",[2456,12264,12266],{"id":12265},"http3-and-quic","HTTP/3 and QUIC",[11,12268,12269,12272,12273,12276],{},[118,12270,12271],{},"HTTP/3"," replaces TCP with ",[118,12274,12275],{},"QUIC"," (UDP-based) as the transport layer.",[1822,12278,12279,12285,12291],{},[1825,12280,12281,12284],{},[118,12282,12283],{},"No head-of-line blocking at transport level",": HTTP/2 multiplexes streams over a single TCP connection. One lost TCP packet stalls all HTTP/2 streams. QUIC has independent streams - a lost packet blocks only that specific stream.",[1825,12286,12287,12290],{},[118,12288,12289],{},"Faster connection setup",": QUIC + TLS 1.3 takes 1 RTT (vs 3+ for TCP+TLS). Repeated connections from known clients take 0 RTT.",[1825,12292,12293,12296],{},[118,12294,12295],{},"Connection migration",": QUIC uses a connection ID, not the IP:port tuple. A mobile user switching WiFi to cellular keeps their QUIC connection alive.",[11,12298,12299],{},"HTTP/3 is supported in all modern browsers. Cloudflare, Fastly, and most CDNs support it. Nuxt app behind Cloudflare is probably already serving HTTP/3.",[2456,12301,12303],{"id":12302},"priority-hints","Priority hints",[11,12305,8603,12306,12309],{},[15,12307,12308],{},"fetchpriority"," attribute tells the browser which resources are most important so it can schedule fetches accordingly.",[255,12311,12313],{"className":10399,"code":12312,"language":10400,"meta":260,"style":260},"\u003C!-- Hero image is the LCP element - needs high priority -->\n\u003Cimg src=\"/hero.jpg\" fetchpriority=\"high\">\n\n\u003C!-- Below-fold images don't need to compete with LCP -->\n\u003Cimg src=\"/card.jpg\" fetchpriority=\"low\" loading=\"lazy\">\n",[15,12314,12315,12320,12352,12356,12361],{"__ignoreMap":260},[129,12316,12317],{"class":265,"line":266},[129,12318,12319],{"class":376},"\u003C!-- Hero image is the LCP element - needs high priority -->\n",[129,12321,12322,12324,12327,12329,12331,12333,12336,12338,12341,12343,12345,12348,12350],{"class":265,"line":297},[129,12323,3945],{"class":277},[129,12325,12326],{"class":1376},"img",[129,12328,10505],{"class":269},[129,12330,278],{"class":277},[129,12332,2258],{"class":277},[129,12334,12335],{"class":427},"/hero.jpg",[129,12337,2258],{"class":277},[129,12339,12340],{"class":269}," fetchpriority",[129,12342,278],{"class":277},[129,12344,2258],{"class":277},[129,12346,12347],{"class":427},"high",[129,12349,2258],{"class":277},[129,12351,4676],{"class":277},[129,12353,12354],{"class":265,"line":315},[129,12355,336],{"emptyLinePlaceholder":335},[129,12357,12358],{"class":265,"line":332},[129,12359,12360],{"class":376},"\u003C!-- Below-fold images don't need to compete with LCP -->\n",[129,12362,12363,12365,12367,12369,12371,12373,12376,12378,12380,12382,12384,12387,12389,12392,12394,12396,12399,12401],{"class":265,"line":339},[129,12364,3945],{"class":277},[129,12366,12326],{"class":1376},[129,12368,10505],{"class":269},[129,12370,278],{"class":277},[129,12372,2258],{"class":277},[129,12374,12375],{"class":427},"/card.jpg",[129,12377,2258],{"class":277},[129,12379,12340],{"class":269},[129,12381,278],{"class":277},[129,12383,2258],{"class":277},[129,12385,12386],{"class":427},"low",[129,12388,2258],{"class":277},[129,12390,12391],{"class":269}," loading",[129,12393,278],{"class":277},[129,12395,2258],{"class":277},[129,12397,12398],{"class":427},"lazy",[129,12400,2258],{"class":277},[129,12402,4676],{"class":277},[11,12404,12405,12406,968,12425,1380],{},"Also works with ",[15,12407,12408,12410,12412,12414,12416,12418,12421,12423],{"className":10399,"language":10400,"style":260},[129,12409,3945],{"class":277},[129,12411,10405],{"class":1376},[129,12413,10408],{"class":269},[129,12415,278],{"class":277},[129,12417,2258],{"class":277},[129,12419,12420],{"class":427},"preload",[129,12422,2258],{"class":277},[129,12424,3956],{"class":277},[15,12426,12427,12429],{"className":257,"language":259,"style":260},[129,12428,11807],{"class":284},[129,12430,4140],{"class":273},[255,12432,12434],{"className":3922,"code":12433,"language":3924,"meta":260,"style":260},"fetch('/api/critical-data', { priority: 'high' })\n",[15,12435,12436],{"__ignoreMap":260},[129,12437,12438,12440,12442,12444,12447,12449,12451,12453,12456,12458,12460,12462,12464,12466],{"class":265,"line":266},[129,12439,11807],{"class":284},[129,12441,147],{"class":273},[129,12443,424],{"class":277},[129,12445,12446],{"class":427},"/api/critical-data",[129,12448,424],{"class":277},[129,12450,1015],{"class":277},[129,12452,1416],{"class":277},[129,12454,12455],{"class":1376}," priority",[129,12457,1380],{"class":277},[129,12459,4261],{"class":277},[129,12461,12347],{"class":427},[129,12463,424],{"class":277},[129,12465,4255],{"class":277},[129,12467,294],{"class":273},[11,12469,12470,12471,12474],{},"Without explicit hints, the browser guesses based on resource type and position in HTML. An LCP image discovered late (via CSS background, injected by JavaScript) gets a low priority guess. ",[15,12472,12473],{},"fetchpriority=\"high\""," overrides that.",[2456,12476,12478],{"id":12477},"preload-vs-prefetch-vs-preconnect","Preload vs Prefetch vs Preconnect",[1822,12480,12481,12488,12496],{},[1825,12482,12483,12487],{},[118,12484,12485],{},[15,12486,12420],{}," - fetch this resource immediately, before the parser finds it. Use for critical current-page resources: LCP image, key font, critical script. Wrong use: preloading resources the page doesn't actually need (DevTools warns).",[1825,12489,12490,12495],{},[118,12491,12492],{},[15,12493,12494],{},"prefetch"," - fetch in idle time, for likely future navigation. Lower priority than current-page resources. Nuxt's router automatically prefetches linked pages on hover.",[1825,12497,12498,12503],{},[118,12499,12500],{},[15,12501,12502],{},"preconnect"," - establish the TCP+TLS connection to a domain early. No resource fetched, just the connection warmed up. Use for third-party origins you know you'll need. Limit to 3-4 - each connection has memory cost.",[2456,12505,12507],{"id":12506},"cors-preflight","CORS preflight",[11,12509,12510,12513,12514,12517],{},[118,12511,12512],{},"CORS"," restricts cross-origin HTTP requests. A \"preflight\" is an ",[15,12515,12516],{},"OPTIONS"," request the browser automatically sends before certain cross-origin requests to ask \"are you willing to handle this?\"",[11,12519,12520,12521,500,12524,2354],{},"Preflight triggers for: non-simple methods (PUT, DELETE, PATCH), custom headers (",[15,12522,12523],{},"Authorization",[15,12525,12526],{},"Content-Type: application/json",[255,12528,12531],{"className":12529,"code":12530,"language":3237},[12199],"OPTIONS /api/data HTTP/1.1\nOrigin: https://app.example.com\nAccess-Control-Request-Method: POST\n\nHTTP/1.1 204 No Content\nAccess-Control-Allow-Origin: https://app.example.com\nAccess-Control-Allow-Methods: POST\nAccess-Control-Max-Age: 86400\n",[15,12532,12530],{"__ignoreMap":260},[11,12534,12535,12538,12539,12542],{},[15,12536,12537],{},"Access-Control-Max-Age"," is often forgotten. Without it, the browser sends a preflight for ",[24,12540,12541],{},"every"," non-simple request. On an API that receives 10 requests per session, that's 10 extra OPTIONS round-trips.",[2456,12544,12546],{"id":12545},"speculative-prerendering","Speculative prerendering",[11,12548,12549,12552],{},[118,12550,12551],{},"Speculation rules"," (newer browser API) allow pages to be fully prerendered - including JavaScript execution - in a hidden background tab.",[255,12554,12556],{"className":10399,"code":12555,"language":10400,"meta":260,"style":260},"\u003Cscript type=\"speculationrules\">\n{\n  \"prerender\": [{\n    \"where\": { \"href_matches\": \"/product/*\" },\n    \"eagerness\": \"moderate\"\n  }]\n}\n\u003C/script>\n",[15,12557,12558,12577,12581,12586,12591,12596,12601,12605],{"__ignoreMap":260},[129,12559,12560,12562,12564,12566,12568,12570,12573,12575],{"class":265,"line":266},[129,12561,3945],{"class":277},[129,12563,10436],{"class":1376},[129,12565,11316],{"class":269},[129,12567,278],{"class":277},[129,12569,2258],{"class":277},[129,12571,12572],{"class":427},"speculationrules",[129,12574,2258],{"class":277},[129,12576,4676],{"class":277},[129,12578,12579],{"class":265,"line":297},[129,12580,6455],{"class":273},[129,12582,12583],{"class":265,"line":315},[129,12584,12585],{"class":273},"  \"prerender\": [{\n",[129,12587,12588],{"class":265,"line":332},[129,12589,12590],{"class":273},"    \"where\": { \"href_matches\": \"/product/*\" },\n",[129,12592,12593],{"class":265,"line":339},[129,12594,12595],{"class":273},"    \"eagerness\": \"moderate\"\n",[129,12597,12598],{"class":265,"line":356},[129,12599,12600],{"class":273},"  }]\n",[129,12602,12603],{"class":265,"line":651},[129,12604,1530],{"class":273},[129,12606,12607,12610,12612],{"class":265,"line":657},[129,12608,12609],{"class":277},"\u003C/",[129,12611,10436],{"class":1376},[129,12613,4676],{"class":277},[11,12615,12616,12617,12620],{},"When the user navigates to ",[15,12618,12619],{},"/product/123",", the page is already rendered and ready to display instantly. This is a step beyond prefetch - prerendering runs the full page lifecycle.",[11,12622,12623],{},"Chrome 108+ supports it.",[3733,12625,12626],{},[11,12627,12628],{},"Prerendering fires analytics beacons and initializes third-party scripts for pages the user may never visit.",[40,12630,12632],{"id":12631},"security","Security",[2456,12634,12636],{"id":12635},"samesite-cookie-modes","SameSite cookie modes",[11,12638,12639,12642],{},[15,12640,12641],{},"SameSite"," controls when the browser sends cookies with cross-site requests:",[59,12644,12645,12657],{},[62,12646,12647],{},[65,12648,12649,12651,12654],{},[68,12650,2471],{},[68,12652,12653],{},"Cookie sent when",[68,12655,12656],{},"Notes",[78,12658,12659,12672,12686],{},[65,12660,12661,12666,12669],{},[83,12662,12663],{},[15,12664,12665],{},"Strict",[83,12667,12668],{},"Only same-site requests",[83,12670,12671],{},"Best CSRF protection, breaks OAuth flows",[65,12673,12674,12680,12683],{},[83,12675,12676,12679],{},[15,12677,12678],{},"Lax"," (default)",[83,12681,12682],{},"Same-site + top-level GET navigations",[83,12684,12685],{},"Good balance; Chrome default since 2020",[65,12687,12688,12693,12696],{},[83,12689,12690],{},[15,12691,12692],{},"None",[83,12694,12695],{},"All cross-site requests",[83,12697,12698,12699,12702],{},"Requires ",[15,12700,12701],{},"Secure"," flag; needed for embedded iframes, payment widgets",[11,12704,12705,12706,12708,12709,12711],{},"Since Chrome 80 (2020), cookies without a ",[15,12707,12641],{}," attribute default to ",[15,12710,12678],{},". This broke a lot of third-party integrations that relied on cookies being sent with cross-site requests.",[2456,12713,12715],{"id":12714},"csrf-vs-xss-mitigation","CSRF vs XSS mitigation",[11,12717,12718,12721,12722,12725,12726,12729],{},[118,12719,12720],{},"CSRF"," (Cross-Site Request Forgery): an attacker tricks a user's browser into making an authenticated request to a site they're logged into. Mitigations: ",[15,12723,12724],{},"SameSite=Lax/Strict"," cookies (modern), CSRF tokens in forms (traditional), checking the ",[15,12727,12728],{},"Origin"," header.",[11,12731,12732,12735,12736,12739,12740,12743,12744,12747],{},[118,12733,12734],{},"XSS"," (Cross-Site Scripting): an attacker injects JavaScript into a page that runs in a victim's browser, stealing session cookies or performing actions as the user. Mitigations: Content Security Policy, sanitizing user input before rendering, using ",[15,12737,12738],{},"textContent"," not ",[15,12741,12742],{},"innerHTML"," for untrusted data, ",[15,12745,12746],{},"HttpOnly"," cookies (unreadable by JS even if XSS succeeds).",[11,12749,12750,12751,12753],{},"XSS is generally worse than CSRF: successful XSS means arbitrary JavaScript execution, which defeats CSRF tokens and ",[15,12752,12641],{}," cookies entirely.",[2456,12755,12757],{"id":12756},"content-security-policy-csp","Content Security Policy (CSP)",[11,12759,12760,12763],{},[118,12761,12762],{},"CSP"," is a response header that tells the browser which sources are trusted for scripts, styles, images, fonts, and more.",[255,12765,12768],{"className":12766,"code":12767,"language":3237},[12199],"Content-Security-Policy:\n  default-src 'self';\n  script-src 'self' 'nonce-abc123';\n  style-src 'self' 'unsafe-inline';\n  img-src 'self' data: https://images.cdn.com;\n",[15,12769,12767],{"__ignoreMap":260},[11,12771,12772,12773,12776],{},"Nonce-based CSP: the server generates a random nonce per request, includes it in the header, and adds ",[15,12774,12775],{},"nonce=\"abc123\""," to each legitimate inline script. The browser executes only scripts with a matching nonce - injected scripts can't know the nonce.",[11,12778,12779,12780,12783],{},"Nuxt 4 has built-in CSP support in Nitro. Start with ",[15,12781,12782],{},"Content-Security-Policy-Report-Only"," in report mode before enforcing - production CSP needs careful iteration.",[2456,12785,12787],{"id":12786},"trusted-types","Trusted Types",[11,12789,12790,12792,12793,500,12795,1653,12798,12801,12802,362],{},[118,12791,12787],{}," prevents DOM XSS by requiring typed objects (not raw strings) to be passed to dangerous sinks like ",[15,12794,12742],{},[15,12796,12797],{},"eval",[15,12799,12800],{},"document.write",". You define a policy that sanitizes strings into Trusted HTML; the browser enforces only Trusted HTML can be assigned to ",[15,12803,12742],{},[255,12805,12807],{"className":3922,"code":12806,"language":3924,"meta":260,"style":260},"const policy = trustedTypes.createPolicy('default', {\n  createHTML: str => DOMPurify.sanitize(str)\n})\n\nelement.innerHTML = policy.createHTML(userContent)  // OK\nelement.innerHTML = userContent                      // TypeError\n",[15,12808,12809,12839,12862,12868,12872,12897],{"__ignoreMap":260},[129,12810,12811,12813,12816,12818,12821,12823,12826,12828,12830,12833,12835,12837],{"class":265,"line":266},[129,12812,270],{"class":269},[129,12814,12815],{"class":273}," policy ",[129,12817,278],{"class":277},[129,12819,12820],{"class":273}," trustedTypes",[129,12822,362],{"class":277},[129,12824,12825],{"class":284},"createPolicy",[129,12827,147],{"class":273},[129,12829,424],{"class":277},[129,12831,12832],{"class":427},"default",[129,12834,424],{"class":277},[129,12836,1015],{"class":277},[129,12838,1371],{"class":277},[129,12840,12841,12844,12846,12849,12851,12854,12856,12859],{"class":265,"line":297},[129,12842,12843],{"class":284},"  createHTML",[129,12845,1380],{"class":277},[129,12847,12848],{"class":452}," str",[129,12850,456],{"class":269},[129,12852,12853],{"class":273}," DOMPurify",[129,12855,362],{"class":277},[129,12857,12858],{"class":284},"sanitize",[129,12860,12861],{"class":273},"(str)\n",[129,12863,12864,12866],{"class":265,"line":315},[129,12865,4028],{"class":277},[129,12867,294],{"class":273},[129,12869,12870],{"class":265,"line":332},[129,12871,336],{"emptyLinePlaceholder":335},[129,12873,12874,12877,12879,12881,12883,12886,12888,12891,12894],{"class":265,"line":339},[129,12875,12876],{"class":273},"element",[129,12878,362],{"class":277},[129,12880,11269],{"class":273},[129,12882,278],{"class":277},[129,12884,12885],{"class":273}," policy",[129,12887,362],{"class":277},[129,12889,12890],{"class":284},"createHTML",[129,12892,12893],{"class":273},"(userContent)  ",[129,12895,12896],{"class":376},"// OK\n",[129,12898,12899,12901,12903,12905,12907,12910],{"class":265,"line":356},[129,12900,12876],{"class":273},[129,12902,362],{"class":277},[129,12904,11269],{"class":273},[129,12906,278],{"class":277},[129,12908,12909],{"class":273}," userContent                      ",[129,12911,12912],{"class":376},"// TypeError\n",[11,12914,9424,12915,12918,12919,12921,12922,12924,12925,12927],{},[15,12916,12917],{},"v-html"," uses ",[15,12920,12742],{}," directly and bypasses Trusted Types. If you need ",[15,12923,12917],{}," with any user-controlled content, you need explicit sanitization (DOMPurify) before passing to ",[15,12926,12917],{},", or a custom Trusted Types policy.",[3576,12929,12930],{},[11,12931,12932,12933,12935],{},"In most cases, the right answer is to not use ",[15,12934,12917],{}," with user content at all.",[2456,12937,12939],{"id":12938},"dom-clobbering","DOM clobbering",[11,12941,12942,12944,12945,1335,12947,12949],{},[118,12943,12939],{}," happens when HTML elements with specific ",[15,12946,3190],{},[15,12948,8164],{}," attributes override global JavaScript variables.",[255,12951,12953],{"className":10399,"code":12952,"language":10400,"meta":260,"style":260},"\u003Cform id=\"settings\">\n  \u003Cinput name=\"action\" value=\"attacker-controlled\">\n\u003C/form>\n\u003Cscript>\n  // Developer expects settings.action to be a string from app config\n  // but settings.action is the \u003Cinput> element\n  fetch(settings.action)  // fetch([object HTMLInputElement]) - broken\n\u003C/script>\n",[15,12954,12955,12975,13006,13014,13022,13027,13032,13048],{"__ignoreMap":260},[129,12956,12957,12959,12962,12964,12966,12968,12971,12973],{"class":265,"line":266},[129,12958,3945],{"class":277},[129,12960,12961],{"class":1376},"form",[129,12963,4643],{"class":269},[129,12965,278],{"class":277},[129,12967,2258],{"class":277},[129,12969,12970],{"class":427},"settings",[129,12972,2258],{"class":277},[129,12974,4676],{"class":277},[129,12976,12977,12980,12982,12984,12986,12988,12991,12993,12995,12997,12999,13002,13004],{"class":265,"line":297},[129,12978,12979],{"class":277},"  \u003C",[129,12981,11313],{"class":1376},[129,12983,5407],{"class":269},[129,12985,278],{"class":277},[129,12987,2258],{"class":277},[129,12989,12990],{"class":427},"action",[129,12992,2258],{"class":277},[129,12994,1419],{"class":269},[129,12996,278],{"class":277},[129,12998,2258],{"class":277},[129,13000,13001],{"class":427},"attacker-controlled",[129,13003,2258],{"class":277},[129,13005,4676],{"class":277},[129,13007,13008,13010,13012],{"class":265,"line":315},[129,13009,12609],{"class":277},[129,13011,12961],{"class":1376},[129,13013,4676],{"class":277},[129,13015,13016,13018,13020],{"class":265,"line":332},[129,13017,3945],{"class":277},[129,13019,10436],{"class":1376},[129,13021,4676],{"class":277},[129,13023,13024],{"class":265,"line":339},[129,13025,13026],{"class":376},"  // Developer expects settings.action to be a string from app config\n",[129,13028,13029],{"class":265,"line":356},[129,13030,13031],{"class":376},"  // but settings.action is the \u003Cinput> element\n",[129,13033,13034,13037,13040,13042,13045],{"class":265,"line":651},[129,13035,13036],{"class":284},"  fetch",[129,13038,13039],{"class":273},"(settings",[129,13041,362],{"class":277},[129,13043,13044],{"class":273},"action)  ",[129,13046,13047],{"class":376},"// fetch([object HTMLInputElement]) - broken\n",[129,13049,13050,13052,13054],{"class":265,"line":657},[129,13051,12609],{"class":277},[129,13053,10436],{"class":1376},[129,13055,4676],{"class":277},[11,13057,13058],{},"Modern CSP prevents many clobbering attacks.",[3576,13060,13061],{},[11,13062,13063,13064,13067,13068,13071],{},"Never use bare ",[15,13065,13066],{},"window"," property access for configuration; always use explicit ",[15,13069,13070],{},"document.getElementById"," with type checking.",[2456,13073,13075],{"id":13074},"prototype-pollution","Prototype pollution",[11,13077,13078,13080,13081,1335,13084,13087],{},[118,13079,13075],{}," attacks modify ",[15,13082,13083],{},"Object.prototype",[15,13085,13086],{},"Array.prototype",", affecting every object in the runtime.",[255,13089,13091],{"className":3922,"code":13090,"language":3924,"meta":260,"style":260},"// Vulnerable deep merge\nfunction merge(target, source) {\n  for (const key of Object.keys(source)) {\n    if (typeof source[key] === 'object') merge(target[key] ??= {}, source[key])\n    else target[key] = source[key]\n  }\n}\n\n// Attacker input:\nmerge({}, JSON.parse('{\"__proto__\": {\"isAdmin\": true}}'))\n({}).isAdmin  // true - every object is now \"admin\"\n",[15,13092,13093,13098,13119,13148,13204,13228,13232,13236,13240,13245,13273],{"__ignoreMap":260},[129,13094,13095],{"class":265,"line":266},[129,13096,13097],{"class":376},"// Vulnerable deep merge\n",[129,13099,13100,13102,13105,13107,13110,13112,13115,13117],{"class":265,"line":297},[129,13101,10102],{"class":269},[129,13103,13104],{"class":284}," merge",[129,13106,147],{"class":277},[129,13108,13109],{"class":452},"target",[129,13111,1015],{"class":277},[129,13113,13114],{"class":452}," source",[129,13116,160],{"class":277},[129,13118,1371],{"class":277},[129,13120,13121,13123,13125,13127,13129,13131,13134,13136,13139,13141,13143,13146],{"class":265,"line":315},[129,13122,6437],{"class":2139},[129,13124,3984],{"class":1376},[129,13126,270],{"class":269},[129,13128,6243],{"class":273},[129,13130,6447],{"class":277},[129,13132,13133],{"class":273}," Object",[129,13135,362],{"class":277},[129,13137,13138],{"class":284},"keys",[129,13140,147],{"class":1376},[129,13142,8775],{"class":273},[129,13144,13145],{"class":1376},")) ",[129,13147,6455],{"class":277},[129,13149,13150,13152,13154,13157,13159,13161,13163,13165,13167,13169,13172,13174,13176,13179,13181,13183,13185,13187,13189,13192,13195,13197,13199,13201],{"class":265,"line":332},[129,13151,6479],{"class":2139},[129,13153,3984],{"class":1376},[129,13155,13156],{"class":277},"typeof",[129,13158,13114],{"class":273},[129,13160,4128],{"class":1376},[129,13162,6273],{"class":273},[129,13164,348],{"class":1376},[129,13166,7981],{"class":277},[129,13168,4261],{"class":277},[129,13170,13171],{"class":427},"object",[129,13173,424],{"class":277},[129,13175,4005],{"class":1376},[129,13177,13178],{"class":284},"merge",[129,13180,147],{"class":1376},[129,13182,13109],{"class":273},[129,13184,4128],{"class":1376},[129,13186,6273],{"class":273},[129,13188,348],{"class":1376},[129,13190,13191],{"class":277},"??=",[129,13193,13194],{"class":277}," {},",[129,13196,13114],{"class":273},[129,13198,4128],{"class":1376},[129,13200,6273],{"class":273},[129,13202,13203],{"class":1376},"])\n",[129,13205,13206,13209,13212,13214,13216,13218,13220,13222,13224,13226],{"class":265,"line":339},[129,13207,13208],{"class":2139},"    else",[129,13210,13211],{"class":273}," target",[129,13213,4128],{"class":1376},[129,13215,6273],{"class":273},[129,13217,348],{"class":1376},[129,13219,278],{"class":277},[129,13221,13114],{"class":273},[129,13223,4128],{"class":1376},[129,13225,6273],{"class":273},[129,13227,1046],{"class":1376},[129,13229,13230],{"class":265,"line":356},[129,13231,1524],{"class":277},[129,13233,13234],{"class":265,"line":651},[129,13235,1530],{"class":277},[129,13237,13238],{"class":265,"line":657},[129,13239,336],{"emptyLinePlaceholder":335},[129,13241,13242],{"class":265,"line":669},[129,13243,13244],{"class":376},"// Attacker input:\n",[129,13246,13247,13249,13251,13254,13257,13259,13262,13264,13266,13269,13271],{"class":265,"line":693},[129,13248,13178],{"class":284},[129,13250,147],{"class":273},[129,13252,13253],{"class":277},"{},",[129,13255,13256],{"class":273}," JSON",[129,13258,362],{"class":277},[129,13260,13261],{"class":284},"parse",[129,13263,147],{"class":273},[129,13265,424],{"class":277},[129,13267,13268],{"class":427},"{\"__proto__\": {\"isAdmin\": true}}",[129,13270,424],{"class":277},[129,13272,471],{"class":273},[129,13274,13275,13277,13280,13282,13284,13287],{"class":265,"line":712},[129,13276,147],{"class":273},[129,13278,13279],{"class":277},"{}",[129,13281,160],{"class":273},[129,13283,362],{"class":277},[129,13285,13286],{"class":273},"isAdmin  ",[129,13288,13289],{"class":376},"// true - every object is now \"admin\"\n",[11,13291,13292,13295],{},[15,13293,13294],{},"lodash.merge"," had this vulnerability.",[3576,13297,13298],{},[11,13299,13300,13301,500,13304,500,13307,13310,13311,13326,13327,13330],{},"Validate keys against ",[15,13302,13303],{},"__proto__",[15,13305,13306],{},"constructor",[15,13308,13309],{},"prototype","; use ",[15,13312,13313,13316,13318,13320,13322,13324],{"className":257,"language":259,"style":260},[129,13314,13315],{"class":273},"Object",[129,13317,362],{"class":277},[129,13319,4791],{"class":284},[129,13321,147],{"class":273},[129,13323,3961],{"class":277},[129,13325,160],{"class":273}," for dictionary objects; use ",[15,13328,13329],{},"structuredClone"," for deep copying untrusted input.",[40,13332,13334],{"id":13333},"concurrency-state-architecture","Concurrency & state architecture",[2456,13336,13338],{"id":13337},"race-conditions-in-ui-state","Race conditions in UI state",[11,13340,561,13341,13344],{},[118,13342,13343],{},"race condition"," in UI happens when two async operations compete and their completion order isn't guaranteed. Classic example: the user types in a search box, two requests fire, the slower one resolves last and overwrites the correct (newer) result.",[255,13346,13348],{"className":3922,"code":13347,"language":3924,"meta":260,"style":260},"// Race condition - last fetch wins, not last query\nconst results = ref([])\nconst search = async (query: string) => {\n  const data = await $fetch(`/api/search?q=${query}`)\n  results.value = data  // may overwrite a newer result\n}\n",[15,13349,13350,13355,13368,13393,13420,13436],{"__ignoreMap":260},[129,13351,13352],{"class":265,"line":266},[129,13353,13354],{"class":376},"// Race condition - last fetch wins, not last query\n",[129,13356,13357,13359,13361,13363,13365],{"class":265,"line":297},[129,13358,270],{"class":269},[129,13360,5373],{"class":273},[129,13362,278],{"class":277},[129,13364,3894],{"class":284},[129,13366,13367],{"class":273},"([])\n",[129,13369,13370,13372,13375,13377,13379,13381,13383,13385,13387,13389,13391],{"class":265,"line":315},[129,13371,270],{"class":269},[129,13373,13374],{"class":273}," search ",[129,13376,278],{"class":277},[129,13378,6020],{"class":269},[129,13380,3984],{"class":277},[129,13382,5781],{"class":452},[129,13384,1380],{"class":277},[129,13386,4622],{"class":2161},[129,13388,160],{"class":277},[129,13390,456],{"class":269},[129,13392,1371],{"class":277},[129,13394,13395,13397,13399,13401,13403,13405,13407,13409,13412,13414,13416,13418],{"class":265,"line":332},[129,13396,5076],{"class":269},[129,13398,11638],{"class":273},[129,13400,4745],{"class":277},[129,13402,4779],{"class":2139},[129,13404,8288],{"class":284},[129,13406,147],{"class":1376},[129,13408,4125],{"class":277},[129,13410,13411],{"class":427},"/api/search?q=",[129,13413,4131],{"class":277},[129,13415,5781],{"class":273},[129,13417,4175],{"class":277},[129,13419,294],{"class":1376},[129,13421,13422,13425,13427,13429,13431,13433],{"class":265,"line":339},[129,13423,13424],{"class":273},"  results",[129,13426,362],{"class":277},[129,13428,8389],{"class":273},[129,13430,4745],{"class":277},[129,13432,11638],{"class":273},[129,13434,13435],{"class":376},"  // may overwrite a newer result\n",[129,13437,13438],{"class":265,"line":356},[129,13439,1530],{"class":277},[11,13441,13442,13443,13445,13446,1380],{},"The Vue-idiomatic fix is ",[15,13444,3912],{}," with ",[15,13447,13448],{},"onCleanup",[255,13450,13452],{"className":3922,"code":13451,"language":3924,"meta":260,"style":260},"const query = ref('')\nconst results = ref([])\n\nwatch(query, async (q, _, onCleanup) => {\n  const controller = new AbortController()\n  // onCleanup runs before the next watch execution\n  onCleanup(() => controller.abort())\n\n  try {\n    results.value = await $fetch(`/api/search?q=${q}`, {\n      signal: controller.signal\n    })\n  } catch (e) {\n    if (e.name !== 'AbortError') throw e\n  }\n})\n",[15,13453,13454,13471,13483,13487,13519,13535,13540,13561,13565,13572,13603,13617,13623,13638,13668,13672],{"__ignoreMap":260},[129,13455,13456,13458,13461,13463,13465,13467,13469],{"class":265,"line":266},[129,13457,270],{"class":269},[129,13459,13460],{"class":273}," query ",[129,13462,278],{"class":277},[129,13464,3894],{"class":284},[129,13466,147],{"class":273},[129,13468,440],{"class":277},[129,13470,294],{"class":273},[129,13472,13473,13475,13477,13479,13481],{"class":265,"line":297},[129,13474,270],{"class":269},[129,13476,5373],{"class":273},[129,13478,278],{"class":277},[129,13480,3894],{"class":284},[129,13482,13367],{"class":273},[129,13484,13485],{"class":265,"line":315},[129,13486,336],{"emptyLinePlaceholder":335},[129,13488,13489,13491,13494,13496,13498,13500,13503,13505,13508,13510,13513,13515,13517],{"class":265,"line":332},[129,13490,3912],{"class":284},[129,13492,13493],{"class":273},"(query",[129,13495,1015],{"class":277},[129,13497,6020],{"class":269},[129,13499,3984],{"class":277},[129,13501,13502],{"class":452},"q",[129,13504,1015],{"class":277},[129,13506,13507],{"class":452}," _",[129,13509,1015],{"class":277},[129,13511,13512],{"class":452}," onCleanup",[129,13514,160],{"class":277},[129,13516,456],{"class":269},[129,13518,1371],{"class":277},[129,13520,13521,13523,13526,13528,13530,13533],{"class":265,"line":339},[129,13522,5076],{"class":269},[129,13524,13525],{"class":273}," controller",[129,13527,4745],{"class":277},[129,13529,281],{"class":277},[129,13531,13532],{"class":284}," AbortController",[129,13534,2451],{"class":1376},[129,13536,13537],{"class":265,"line":356},[129,13538,13539],{"class":376},"  // onCleanup runs before the next watch execution\n",[129,13541,13542,13545,13547,13549,13551,13553,13555,13558],{"class":265,"line":651},[129,13543,13544],{"class":284},"  onCleanup",[129,13546,147],{"class":1376},[129,13548,4140],{"class":277},[129,13550,456],{"class":269},[129,13552,13525],{"class":273},[129,13554,362],{"class":277},[129,13556,13557],{"class":284},"abort",[129,13559,13560],{"class":1376},"())\n",[129,13562,13563],{"class":265,"line":657},[129,13564,336],{"emptyLinePlaceholder":335},[129,13566,13567,13570],{"class":265,"line":669},[129,13568,13569],{"class":2139},"  try",[129,13571,1371],{"class":277},[129,13573,13574,13577,13579,13581,13583,13585,13587,13589,13591,13593,13595,13597,13599,13601],{"class":265,"line":693},[129,13575,13576],{"class":273},"    results",[129,13578,362],{"class":277},[129,13580,8389],{"class":273},[129,13582,4745],{"class":277},[129,13584,4779],{"class":2139},[129,13586,8288],{"class":284},[129,13588,147],{"class":1376},[129,13590,4125],{"class":277},[129,13592,13411],{"class":427},[129,13594,4131],{"class":277},[129,13596,13502],{"class":273},[129,13598,4175],{"class":277},[129,13600,1015],{"class":277},[129,13602,1371],{"class":277},[129,13604,13605,13608,13610,13612,13614],{"class":265,"line":712},[129,13606,13607],{"class":1376},"      signal",[129,13609,1380],{"class":277},[129,13611,13525],{"class":273},[129,13613,362],{"class":277},[129,13615,13616],{"class":273},"signal\n",[129,13618,13619,13621],{"class":265,"line":1521},[129,13620,7619],{"class":277},[129,13622,294],{"class":1376},[129,13624,13625,13627,13630,13632,13634,13636],{"class":265,"line":1527},[129,13626,4182],{"class":277},[129,13628,13629],{"class":2139}," catch",[129,13631,3984],{"class":1376},[129,13633,188],{"class":273},[129,13635,4005],{"class":1376},[129,13637,6455],{"class":277},[129,13639,13640,13642,13644,13646,13648,13650,13653,13655,13658,13660,13662,13665],{"class":265,"line":2295},[129,13641,6479],{"class":2139},[129,13643,3984],{"class":1376},[129,13645,188],{"class":273},[129,13647,362],{"class":277},[129,13649,8164],{"class":273},[129,13651,13652],{"class":277}," !==",[129,13654,4261],{"class":277},[129,13656,13657],{"class":427},"AbortError",[129,13659,424],{"class":277},[129,13661,4005],{"class":1376},[129,13663,13664],{"class":2139},"throw",[129,13666,13667],{"class":273}," e\n",[129,13669,13670],{"class":265,"line":2300},[129,13671,1524],{"class":277},[129,13673,13674,13676],{"class":265,"line":2305},[129,13675,4028],{"class":277},[129,13677,294],{"class":273},[11,13679,13680,13682,13683,13685],{},[15,13681,13448],{}," fires before the next watch run (when ",[15,13684,5781],{}," changes again), aborting the in-flight request before a new one starts.",[2456,13687,13689],{"id":13688},"tearing-in-concurrent-ui","Tearing in concurrent UI",[11,13691,13692,13695],{},[118,13693,13694],{},"Tearing"," is a concurrent rendering artifact where different parts of the UI read the same shared mutable state at different points during a single render pass, resulting in a UI that's internally inconsistent. Component A reads the old value, rendering pauses, the store updates, Component B reads the new value - both states appear on screen simultaneously.",[11,13697,13698],{},"Vue's reactivity system is synchronous in its dependency tracking and doesn't have this problem by design. When state changes, Vue tracks which components depend on it and schedules their re-renders atomically - there's no \"render pause\" mid-component that could expose a torn state. It's worth knowing the term because you'll encounter it in library documentation and job interviews, even if it's not a Vue-specific problem to solve.",[2456,13700,13702],{"id":13701},"scheduler-priorities","Scheduler priorities",[11,13704,13705],{},"Concurrent frameworks maintain a priority system for rendering tasks - some updates are more urgent than others. User input needs to feel instant; a background data sync can wait.",[11,13707,13708,13709,13711,13712,13445,13714,13728,13729,13741],{},"Vue's scheduler doesn't expose priority levels as a public API, but it has implicit ordering: watchers triggered by user interaction run before passive watchers, and component re-renders are batched and flushed via ",[15,13710,8853],{}," (a microtask), which means the browser gets control between ticks. For manual control, ",[15,13713,3915],{},[15,13715,13716,13719,13721,13723,13726],{"className":257,"language":259,"style":260},[129,13717,13718],{"class":2161},"flush",[129,13720,1380],{"class":277},[129,13722,4261],{"class":277},[129,13724,13725],{"class":427},"post",[129,13727,424],{"class":277}," runs after the DOM update, ",[15,13730,13731,13733,13735,13737,13739],{"className":257,"language":259,"style":260},[129,13732,13718],{"class":2161},[129,13734,1380],{"class":277},[129,13736,4261],{"class":277},[129,13738,9995],{"class":427},[129,13740,424],{"class":277}," runs immediately. These aren't \"priority\" in a formal sense, but they let you control the execution order around renders.",[2456,13743,13745],{"id":13744},"render-waterfalls","Render waterfalls",[11,13747,561,13748,13751],{},[118,13749,13750],{},"render waterfall"," is when rendering is blocked in sequence: component A renders, starts a data fetch, waits, then component B renders, starts another fetch, waits...",[255,13753,13756],{"className":13754,"code":13755,"language":3237},[12199],"Component A renders -> fetch /api/user (200ms wait)\n-> Component B renders -> fetch /api/posts (150ms wait)\nTotal: 350ms sequential loading\n",[15,13757,13755],{"__ignoreMap":260},[3576,13759,13760],{},[11,13761,13762,13765,13766,13769,13770,13773],{},[118,13763,13764],{},"Solutions",": hoist all data fetches to the route level (fetch in parallel with ",[15,13767,13768],{},"Promise.all","), or use Suspense with streaming so each component's data resolves independently without blocking siblings. In Nuxt, ",[15,13771,13772],{},"useAsyncData"," with deduplication plus route-level parallel fetches prevents waterfalls.",[2456,13775,13777],{"id":13776},"micro-frontend-orchestration","Micro-frontend orchestration",[11,13779,13780,13783],{},[118,13781,13782],{},"Micro-frontends"," decompose a frontend application into independently deployable parts. Orchestration approaches:",[1822,13785,13786,13792,13798],{},[1825,13787,13788,13791],{},[118,13789,13790],{},"Server-side composition",": Nginx/CDN assembles HTML fragments from multiple services (SSI, Edge Side Includes). Simple but limited runtime interactivity.",[1825,13793,13794,13797],{},[118,13795,13796],{},"Build-time composition",": all fragments bundled together at deploy time. Defeats \"independent deployment.\"",[1825,13799,13800,13803,13804,13807],{},[118,13801,13802],{},"Runtime composition",": Module Federation, ",[15,13805,13806],{},"import()"," from remote URLs, iframe embedding. True independence but complex dependency management.",[11,13809,13810],{},"Most organizations overestimate their need for micro-frontends and underestimate the coordination overhead. The genuine use case is large organizations with genuinely independent teams.",[2456,13812,13814],{"id":13813},"finite-state-modeling","Finite state modeling",[11,13816,13817,13818,13821],{},"UI has states. Most bugs are transitions between states that weren't anticipated. ",[118,13819,13820],{},"Finite state machines"," make all possible states and transitions explicit, preventing invalid state combinations.",[255,13823,13825],{"className":3922,"code":13824,"language":3924,"meta":260,"style":260},"// Without FSM: booleans that can be in invalid combinations\nconst isLoading = ref(false)\nconst isError = ref(false)\nconst isSuccess = ref(false)\n// isLoading && isSuccess at the same time? Possible, shouldn't be.\n\n// With XState: all states explicit, invalid combinations impossible\nconst machine = createMachine({\n  initial: 'idle',\n  states: {\n    idle:    { on: { FETCH: 'loading' } },\n    loading: { on: { RESOLVE: 'success', REJECT: 'error' } },\n    success: { on: { FETCH: 'loading' } },\n    error:   { on: { RETRY: 'loading', GIVE_UP: 'idle' } },\n  }\n})\n",[15,13826,13827,13832,13849,13866,13883,13888,13892,13897,13913,13929,13938,13971,14015,14044,14088,14092],{"__ignoreMap":260},[129,13828,13829],{"class":265,"line":266},[129,13830,13831],{"class":376},"// Without FSM: booleans that can be in invalid combinations\n",[129,13833,13834,13836,13839,13841,13843,13845,13847],{"class":265,"line":297},[129,13835,270],{"class":269},[129,13837,13838],{"class":273}," isLoading ",[129,13840,278],{"class":277},[129,13842,3894],{"class":284},[129,13844,147],{"class":273},[129,13846,146],{"class":4822},[129,13848,294],{"class":273},[129,13850,13851,13853,13856,13858,13860,13862,13864],{"class":265,"line":315},[129,13852,270],{"class":269},[129,13854,13855],{"class":273}," isError ",[129,13857,278],{"class":277},[129,13859,3894],{"class":284},[129,13861,147],{"class":273},[129,13863,146],{"class":4822},[129,13865,294],{"class":273},[129,13867,13868,13870,13873,13875,13877,13879,13881],{"class":265,"line":332},[129,13869,270],{"class":269},[129,13871,13872],{"class":273}," isSuccess ",[129,13874,278],{"class":277},[129,13876,3894],{"class":284},[129,13878,147],{"class":273},[129,13880,146],{"class":4822},[129,13882,294],{"class":273},[129,13884,13885],{"class":265,"line":339},[129,13886,13887],{"class":376},"// isLoading && isSuccess at the same time? Possible, shouldn't be.\n",[129,13889,13890],{"class":265,"line":356},[129,13891,336],{"emptyLinePlaceholder":335},[129,13893,13894],{"class":265,"line":651},[129,13895,13896],{"class":376},"// With XState: all states explicit, invalid combinations impossible\n",[129,13898,13899,13901,13904,13906,13909,13911],{"class":265,"line":657},[129,13900,270],{"class":269},[129,13902,13903],{"class":273}," machine ",[129,13905,278],{"class":277},[129,13907,13908],{"class":284}," createMachine",[129,13910,147],{"class":273},[129,13912,6455],{"class":277},[129,13914,13915,13918,13920,13922,13925,13927],{"class":265,"line":669},[129,13916,13917],{"class":1376},"  initial",[129,13919,1380],{"class":277},[129,13921,4261],{"class":277},[129,13923,13924],{"class":427},"idle",[129,13926,424],{"class":277},[129,13928,1386],{"class":277},[129,13930,13931,13934,13936],{"class":265,"line":693},[129,13932,13933],{"class":1376},"  states",[129,13935,1380],{"class":277},[129,13937,1371],{"class":277},[129,13939,13940,13943,13945,13948,13951,13953,13955,13958,13960,13962,13965,13967,13969],{"class":265,"line":712},[129,13941,13942],{"class":1376},"    idle",[129,13944,1380],{"class":277},[129,13946,13947],{"class":277},"    {",[129,13949,13950],{"class":1376}," on",[129,13952,1380],{"class":277},[129,13954,1416],{"class":277},[129,13956,13957],{"class":1376}," FETCH",[129,13959,1380],{"class":277},[129,13961,4261],{"class":277},[129,13963,13964],{"class":427},"loading",[129,13966,424],{"class":277},[129,13968,4255],{"class":277},[129,13970,1444],{"class":277},[129,13972,13973,13976,13978,13980,13982,13984,13986,13989,13991,13993,13996,13998,14000,14003,14005,14007,14009,14011,14013],{"class":265,"line":1521},[129,13974,13975],{"class":1376},"    loading",[129,13977,1380],{"class":277},[129,13979,1416],{"class":277},[129,13981,13950],{"class":1376},[129,13983,1380],{"class":277},[129,13985,1416],{"class":277},[129,13987,13988],{"class":1376}," RESOLVE",[129,13990,1380],{"class":277},[129,13992,4261],{"class":277},[129,13994,13995],{"class":427},"success",[129,13997,424],{"class":277},[129,13999,1015],{"class":277},[129,14001,14002],{"class":1376}," REJECT",[129,14004,1380],{"class":277},[129,14006,4261],{"class":277},[129,14008,10921],{"class":427},[129,14010,424],{"class":277},[129,14012,4255],{"class":277},[129,14014,1444],{"class":277},[129,14016,14017,14020,14022,14024,14026,14028,14030,14032,14034,14036,14038,14040,14042],{"class":265,"line":1527},[129,14018,14019],{"class":1376},"    success",[129,14021,1380],{"class":277},[129,14023,1416],{"class":277},[129,14025,13950],{"class":1376},[129,14027,1380],{"class":277},[129,14029,1416],{"class":277},[129,14031,13957],{"class":1376},[129,14033,1380],{"class":277},[129,14035,4261],{"class":277},[129,14037,13964],{"class":427},[129,14039,424],{"class":277},[129,14041,4255],{"class":277},[129,14043,1444],{"class":277},[129,14045,14046,14049,14051,14054,14056,14058,14060,14063,14065,14067,14069,14071,14073,14076,14078,14080,14082,14084,14086],{"class":265,"line":2295},[129,14047,14048],{"class":1376},"    error",[129,14050,1380],{"class":277},[129,14052,14053],{"class":277},"   {",[129,14055,13950],{"class":1376},[129,14057,1380],{"class":277},[129,14059,1416],{"class":277},[129,14061,14062],{"class":1376}," RETRY",[129,14064,1380],{"class":277},[129,14066,4261],{"class":277},[129,14068,13964],{"class":427},[129,14070,424],{"class":277},[129,14072,1015],{"class":277},[129,14074,14075],{"class":1376}," GIVE_UP",[129,14077,1380],{"class":277},[129,14079,4261],{"class":277},[129,14081,13924],{"class":427},[129,14083,424],{"class":277},[129,14085,4255],{"class":277},[129,14087,1444],{"class":277},[129,14089,14090],{"class":265,"line":2300},[129,14091,1524],{"class":277},[129,14093,14094,14096],{"class":265,"line":2305},[129,14095,4028],{"class":277},[129,14097,294],{"class":273},[11,14099,14100,14105],{},[51,14101,14104],{"href":14102,"rel":14103},"https://stately.ai/xstate",[55],"XState"," implements FSMs and statecharts for JavaScript. For simpler cases, a reducer pattern with Pinia achieves similar explicitness without the full XState machinery.",[2456,14107,14109],{"id":14108},"event-sourcing-in-frontend","Event sourcing in frontend",[11,14111,14112,14115],{},[118,14113,14114],{},"Event sourcing"," stores state as a sequence of events rather than the current value. Current state is derived by replaying events from the beginning.",[11,14117,14118,14119,14132,14133,14171],{},"In frontend: instead of storing ",[15,14120,14121,14123,14126,14128,14130],{"className":257,"language":259,"style":260},[129,14122,4796],{"class":277},[129,14124,14125],{"class":2161}," quantity",[129,14127,1380],{"class":277},[129,14129,1018],{"class":290},[129,14131,4255],{"class":277},", store ",[15,14134,14135,14137,14139,14142,14144,14146,14148,14150,14152,14154,14156,14158,14160,14162,14164,14166,14168],{"className":257,"language":259,"style":260},[129,14136,4128],{"class":273},[129,14138,4796],{"class":277},[129,14140,14141],{"class":1376},"add",[129,14143,1380],{"class":277},[129,14145,1383],{"class":290},[129,14147,6625],{"class":277},[129,14149,1416],{"class":277},[129,14151,14141],{"class":1376},[129,14153,1380],{"class":277},[129,14155,1383],{"class":290},[129,14157,6625],{"class":277},[129,14159,1416],{"class":277},[129,14161,14141],{"class":1376},[129,14163,1380],{"class":277},[129,14165,1383],{"class":290},[129,14167,4028],{"class":277},[129,14169,14170],{"class":273},"]",". Undo/redo is dropping the last event and recomputing. Pinia with a custom action logger can approximate this - store a list of dispatched actions and re-run them from a snapshot to \"time-travel.\" Git is event sourcing. Any undo history is event sourcing.",[11,14173,14174],{},"The downside: replaying many events is expensive. In practice, snapshots at regular intervals plus replay from the last snapshot keep it performant.",[2456,14176,14178],{"id":14177},"optimistic-ui-rollback-strategy","Optimistic UI rollback strategy",[11,14180,14181,14184],{},[118,14182,14183],{},"Optimistic UI"," applies state changes before the server confirms them, assuming success. The rollback strategy when the server fails matters more than the happy path.",[11,14186,14187],{},"The complexity: if the user makes additional changes during a pending request, naively reverting all state loses their legitimate input. The correct approach: rollback only the specific optimistic mutation, leaving subsequent changes intact.",[11,14189,14190,14191,1335,14194,1380],{},"In Nuxt, the pattern with ",[15,14192,14193],{},"useFetch",[15,14195,14196],{},"$fetch",[255,14198,14200],{"className":3922,"code":14199,"language":3924,"meta":260,"style":260},"const items = ref([...serverItems])\n\nasync function addItem(item) {\n  // 1. Save previous state\n  const previous = [...items.value]\n  // 2. Apply optimistically\n  items.value.push({ ...item, _pending: true })\n  try {\n    const saved = await $fetch('/api/items', { method: 'POST', body: item })\n    // 3. Replace optimistic entry with server-confirmed one\n    items.value = items.value.map(i => i._pending ? saved : i)\n  } catch {\n    // 4. Rollback only this mutation\n    items.value = previous\n  }\n}\n",[15,14201,14202,14222,14226,14244,14249,14271,14276,14310,14316,14367,14372,14419,14427,14432,14445,14449],{"__ignoreMap":260},[129,14203,14204,14206,14209,14211,14213,14216,14219],{"class":265,"line":266},[129,14205,270],{"class":269},[129,14207,14208],{"class":273}," items ",[129,14210,278],{"class":277},[129,14212,3894],{"class":284},[129,14214,14215],{"class":273},"([",[129,14217,14218],{"class":277},"...",[129,14220,14221],{"class":273},"serverItems])\n",[129,14223,14224],{"class":265,"line":297},[129,14225,336],{"emptyLinePlaceholder":335},[129,14227,14228,14230,14232,14235,14237,14240,14242],{"class":265,"line":315},[129,14229,4508],{"class":269},[129,14231,5060],{"class":269},[129,14233,14234],{"class":284}," addItem",[129,14236,147],{"class":277},[129,14238,14239],{"class":452},"item",[129,14241,160],{"class":277},[129,14243,1371],{"class":277},[129,14245,14246],{"class":265,"line":332},[129,14247,14248],{"class":376},"  // 1. Save previous state\n",[129,14250,14251,14253,14256,14258,14260,14262,14265,14267,14269],{"class":265,"line":339},[129,14252,5076],{"class":269},[129,14254,14255],{"class":273}," previous",[129,14257,4745],{"class":277},[129,14259,1010],{"class":1376},[129,14261,14218],{"class":277},[129,14263,14264],{"class":273},"items",[129,14266,362],{"class":277},[129,14268,8389],{"class":273},[129,14270,1046],{"class":1376},[129,14272,14273],{"class":265,"line":356},[129,14274,14275],{"class":376},"  // 2. Apply optimistically\n",[129,14277,14278,14281,14283,14285,14287,14289,14291,14293,14295,14297,14299,14302,14304,14306,14308],{"class":265,"line":651},[129,14279,14280],{"class":273},"  items",[129,14282,362],{"class":277},[129,14284,8389],{"class":273},[129,14286,362],{"class":277},[129,14288,596],{"class":284},[129,14290,147],{"class":1376},[129,14292,4796],{"class":277},[129,14294,9121],{"class":277},[129,14296,14239],{"class":273},[129,14298,1015],{"class":277},[129,14300,14301],{"class":1376}," _pending",[129,14303,1380],{"class":277},[129,14305,4823],{"class":4822},[129,14307,4255],{"class":277},[129,14309,294],{"class":1376},[129,14311,14312,14314],{"class":265,"line":657},[129,14313,13569],{"class":2139},[129,14315,1371],{"class":277},[129,14317,14318,14320,14323,14325,14327,14329,14331,14333,14336,14338,14340,14342,14345,14347,14349,14351,14353,14355,14358,14360,14363,14365],{"class":265,"line":669},[129,14319,4739],{"class":269},[129,14321,14322],{"class":273}," saved",[129,14324,4745],{"class":277},[129,14326,4779],{"class":2139},[129,14328,8288],{"class":284},[129,14330,147],{"class":1376},[129,14332,424],{"class":277},[129,14334,14335],{"class":427},"/api/items",[129,14337,424],{"class":277},[129,14339,1015],{"class":277},[129,14341,1416],{"class":277},[129,14343,14344],{"class":1376}," method",[129,14346,1380],{"class":277},[129,14348,4261],{"class":277},[129,14350,3142],{"class":427},[129,14352,424],{"class":277},[129,14354,1015],{"class":277},[129,14356,14357],{"class":1376}," body",[129,14359,1380],{"class":277},[129,14361,14362],{"class":273}," item",[129,14364,4255],{"class":277},[129,14366,294],{"class":1376},[129,14368,14369],{"class":265,"line":693},[129,14370,14371],{"class":376},"    // 3. Replace optimistic entry with server-confirmed one\n",[129,14373,14374,14377,14379,14381,14383,14386,14388,14390,14392,14394,14396,14398,14400,14402,14404,14407,14410,14412,14415,14417],{"class":265,"line":712},[129,14375,14376],{"class":273},"    items",[129,14378,362],{"class":277},[129,14380,8389],{"class":273},[129,14382,4745],{"class":277},[129,14384,14385],{"class":273}," items",[129,14387,362],{"class":277},[129,14389,8389],{"class":273},[129,14391,362],{"class":277},[129,14393,447],{"class":284},[129,14395,147],{"class":1376},[129,14397,169],{"class":452},[129,14399,456],{"class":269},[129,14401,10339],{"class":273},[129,14403,362],{"class":277},[129,14405,14406],{"class":273},"_pending",[129,14408,14409],{"class":277}," ?",[129,14411,14322],{"class":273},[129,14413,14414],{"class":277}," :",[129,14416,10339],{"class":273},[129,14418,294],{"class":1376},[129,14420,14421,14423,14425],{"class":265,"line":1521},[129,14422,4182],{"class":277},[129,14424,13629],{"class":2139},[129,14426,1371],{"class":277},[129,14428,14429],{"class":265,"line":1527},[129,14430,14431],{"class":376},"    // 4. Rollback only this mutation\n",[129,14433,14434,14436,14438,14440,14442],{"class":265,"line":2295},[129,14435,14376],{"class":273},[129,14437,362],{"class":277},[129,14439,8389],{"class":273},[129,14441,4745],{"class":277},[129,14443,14444],{"class":273}," previous\n",[129,14446,14447],{"class":265,"line":2300},[129,14448,1524],{"class":277},[129,14450,14451],{"class":265,"line":2305},[129,14452,1530],{"class":277},[2456,14454,14456],{"id":14455},"offline-conflict-resolution","Offline conflict resolution",[11,14458,14459],{},"When a user edits data offline and reconnects, the server may have changed while they were away. Strategies:",[1822,14461,14462,14468,14474,14480],{},[1825,14463,14464,14467],{},[118,14465,14466],{},"Last-write-wins",": most recent timestamp wins. Simple, loses data silently.",[1825,14469,14470,14473],{},[118,14471,14472],{},"First-write-wins",": server rejects updates to data that changed since last sync. Client must re-fetch and re-apply.",[1825,14475,14476,14479],{},[118,14477,14478],{},"Manual merge",": show the user both versions. Correct but requires UI.",[1825,14481,14482,14485],{},[118,14483,14484],{},"CRDTs",": data structures that merge automatically without conflicts (see below).",[11,14487,14488],{},"For most apps: optimistic mutations + server-wins conflict resolution + a sync status indicator is sufficient.",[2456,14490,14492],{"id":14491},"crdt-basics-for-collaboration","CRDT basics for collaboration",[11,14494,14495,14497],{},[118,14496,14484],{}," (Conflict-free Replicated Data Types) are data structures that merge automatically without coordination. Any two replicas converge to the same state when merged, regardless of operation order.",[11,14499,14500,14501,14504],{},"The simplest: a ",[118,14502,14503],{},"G-Counter"," (grow-only counter). Each peer has its own counter; merge is element-wise max:",[255,14506,14508],{"className":3922,"code":14507,"language":3924,"meta":260,"style":260},"// Peer A: { A: 3, B: 1 }\n// Peer B: { A: 2, B: 2 }\n// Merge:  { A: 3, B: 2 }  - deterministic regardless of merge order\n",[15,14509,14510,14515,14520],{"__ignoreMap":260},[129,14511,14512],{"class":265,"line":266},[129,14513,14514],{"class":376},"// Peer A: { A: 3, B: 1 }\n",[129,14516,14517],{"class":265,"line":297},[129,14518,14519],{"class":376},"// Peer B: { A: 2, B: 2 }\n",[129,14521,14522],{"class":265,"line":315},[129,14523,14524],{"class":376},"// Merge:  { A: 3, B: 2 }  - deterministic regardless of merge order\n",[11,14526,14527,14528,14531,14532,14535,14536,14539],{},"More useful: ",[118,14529,14530],{},"LWW-Register"," (last-write-wins with vector clocks), ",[118,14533,14534],{},"OR-Set"," (handles add/remove conflicts), ",[118,14537,14538],{},"RGA"," (for text editing).",[11,14541,14542,968,14547,14552,14553,14556],{},[51,14543,14546],{"href":14544,"rel":14545},"https://yjs.dev",[55],"Yjs",[51,14548,14551],{"href":14549,"rel":14550},"https://automerge.org",[55],"Automerge"," are production-grade CRDT libraries. Yjs powers collaborative editing in Notion alternatives and code editors. The Nuxt integration pattern: Yjs + ",[15,14554,14555],{},"y-websocket"," server + a Vue binding for your content type.",[40,14558,14560],{"id":14559},"async-streams","Async & streams",[2456,14562,14564],{"id":14563},"webrtc","WebRTC",[11,14566,14567,14569],{},[118,14568,14564],{}," provides browser APIs for peer-to-peer audio, video, and data channels. No server required for actual data transfer - only for signaling (exchanging offers and ICE candidates).",[11,14571,14572],{},"The connection setup: both peers exchange SDP (Session Description Protocol) offers through a signaling server (WebSocket, any protocol works). STUN servers help discover public IP:port pairs; TURN servers relay traffic when P2P fails (symmetric NAT, firewalls).",[11,14574,14575,14578,14579,14582],{},[15,14576,14577],{},"RTCDataChannel"," is the P2P data primitive - think WebSocket but between browsers. ",[15,14580,14581],{},"RTCPeerConnection"," with media streams handles audio/video. The hard parts are NAT traversal reliability and signaling server uptime.",[2456,14584,14586],{"id":14585},"backpressure-in-streams-api","Backpressure in streams API",[11,14588,14589,14592],{},[118,14590,14591],{},"Backpressure"," is the mechanism by which a data consumer signals to a producer to slow down. Without it, a fast producer + slow consumer leads to unbounded buffer growth and memory exhaustion.",[11,14594,14595,14596,14599,14600,14602],{},"The WHATWG Streams API has backpressure built in. The ",[15,14597,14598],{},"pull"," method on ",[15,14601,11670],{}," is called only when the downstream consumer is ready for more data:",[255,14604,14606],{"className":3922,"code":14605,"language":3924,"meta":260,"style":260},"const readable = new ReadableStream({\n  async pull(controller) {\n    // Called only when downstream has consumed previous chunks\n    const chunk = await getNextChunk()\n    controller.enqueue(chunk)\n  }\n})\n",[15,14607,14608,14626,14642,14647,14663,14680,14684],{"__ignoreMap":260},[129,14609,14610,14612,14615,14617,14619,14622,14624],{"class":265,"line":266},[129,14611,270],{"class":269},[129,14613,14614],{"class":273}," readable ",[129,14616,278],{"class":277},[129,14618,281],{"class":277},[129,14620,14621],{"class":284}," ReadableStream",[129,14623,147],{"class":273},[129,14625,6455],{"class":277},[129,14627,14628,14630,14633,14635,14638,14640],{"class":265,"line":297},[129,14629,4703],{"class":269},[129,14631,14632],{"class":1376}," pull",[129,14634,147],{"class":277},[129,14636,14637],{"class":452},"controller",[129,14639,160],{"class":277},[129,14641,1371],{"class":277},[129,14643,14644],{"class":265,"line":315},[129,14645,14646],{"class":376},"    // Called only when downstream has consumed previous chunks\n",[129,14648,14649,14651,14654,14656,14658,14661],{"class":265,"line":332},[129,14650,4739],{"class":269},[129,14652,14653],{"class":273}," chunk",[129,14655,4745],{"class":277},[129,14657,4779],{"class":2139},[129,14659,14660],{"class":284}," getNextChunk",[129,14662,2451],{"class":1376},[129,14664,14665,14668,14670,14673,14675,14678],{"class":265,"line":339},[129,14666,14667],{"class":273},"    controller",[129,14669,362],{"class":277},[129,14671,14672],{"class":284},"enqueue",[129,14674,147],{"class":1376},[129,14676,14677],{"class":273},"chunk",[129,14679,294],{"class":1376},[129,14681,14682],{"class":265,"line":356},[129,14683,1524],{"class":277},[129,14685,14686,14688],{"class":265,"line":651},[129,14687,4028],{"class":277},[129,14689,294],{"class":273},[11,14691,14692,14698],{},[15,14693,14694,14696],{"className":257,"language":259,"style":260},[129,14695,11807],{"class":284},[129,14697,4140],{"class":273}," with streaming response bodies participates in this mechanism - reading the response body slowly applies backpressure to the HTTP connection.",[2456,14700,14702],{"id":14701},"abortcontroller","AbortController",[11,14704,14705,14707,14708,362],{},[15,14706,14702],{}," provides a signal that can cancel fetch requests, streams, and any operation that accepts a ",[15,14709,14710],{},"signal",[255,14712,14714],{"className":3922,"code":14713,"language":3924,"meta":260,"style":260},"const controller = new AbortController()\nsetTimeout(() => controller.abort(), 5000)\n\ntry {\n  const res = await fetch('/api/data', { signal: controller.signal })\n} catch (e) {\n  if (e.name === 'AbortError') console.log('Request cancelled')\n}\n",[15,14715,14716,14731,14755,14759,14766,14808,14819,14858],{"__ignoreMap":260},[129,14717,14718,14720,14723,14725,14727,14729],{"class":265,"line":266},[129,14719,270],{"class":269},[129,14721,14722],{"class":273}," controller ",[129,14724,278],{"class":277},[129,14726,281],{"class":277},[129,14728,13532],{"class":284},[129,14730,2451],{"class":273},[129,14732,14733,14735,14737,14739,14741,14743,14745,14747,14749,14751,14753],{"class":265,"line":297},[129,14734,9738],{"class":284},[129,14736,147],{"class":273},[129,14738,4140],{"class":277},[129,14740,456],{"class":269},[129,14742,13525],{"class":273},[129,14744,362],{"class":277},[129,14746,13557],{"class":284},[129,14748,4140],{"class":273},[129,14750,1015],{"class":277},[129,14752,9833],{"class":290},[129,14754,294],{"class":273},[129,14756,14757],{"class":265,"line":315},[129,14758,336],{"emptyLinePlaceholder":335},[129,14760,14761,14764],{"class":265,"line":332},[129,14762,14763],{"class":2139},"try",[129,14765,1371],{"class":277},[129,14767,14768,14770,14773,14775,14777,14780,14782,14784,14787,14789,14791,14793,14796,14798,14800,14802,14804,14806],{"class":265,"line":339},[129,14769,5076],{"class":269},[129,14771,14772],{"class":273}," res",[129,14774,4745],{"class":277},[129,14776,4779],{"class":2139},[129,14778,14779],{"class":284}," fetch",[129,14781,147],{"class":1376},[129,14783,424],{"class":277},[129,14785,14786],{"class":427},"/api/data",[129,14788,424],{"class":277},[129,14790,1015],{"class":277},[129,14792,1416],{"class":277},[129,14794,14795],{"class":1376}," signal",[129,14797,1380],{"class":277},[129,14799,13525],{"class":273},[129,14801,362],{"class":277},[129,14803,14710],{"class":273},[129,14805,4255],{"class":277},[129,14807,294],{"class":1376},[129,14809,14810,14812,14814,14817],{"class":265,"line":356},[129,14811,4028],{"class":277},[129,14813,13629],{"class":2139},[129,14815,14816],{"class":273}," (e) ",[129,14818,6455],{"class":277},[129,14820,14821,14823,14825,14827,14829,14831,14833,14835,14837,14839,14841,14843,14845,14847,14849,14851,14854,14856],{"class":265,"line":651},[129,14822,3998],{"class":2139},[129,14824,3984],{"class":1376},[129,14826,188],{"class":273},[129,14828,362],{"class":277},[129,14830,8164],{"class":273},[129,14832,5116],{"class":277},[129,14834,4261],{"class":277},[129,14836,13657],{"class":427},[129,14838,424],{"class":277},[129,14840,4005],{"class":1376},[129,14842,359],{"class":273},[129,14844,362],{"class":277},[129,14846,365],{"class":284},[129,14848,147],{"class":1376},[129,14850,424],{"class":277},[129,14852,14853],{"class":427},"Request cancelled",[129,14855,424],{"class":277},[129,14857,294],{"class":1376},[129,14859,14860],{"class":265,"line":657},[129,14861,1530],{"class":277},[11,14863,14864,14881,14882,14885,14886,14888,14889,14891,14892,14894,14895,14897],{},[15,14865,14866,14869,14871,14874,14876,14879],{"className":257,"language":259,"style":260},[129,14867,14868],{"class":273},"AbortSignal",[129,14870,362],{"class":277},[129,14872,14873],{"class":284},"timeout",[129,14875,147],{"class":273},[129,14877,14878],{"class":290},"5000",[129,14880,160],{"class":273}," is a shorthand for timeout-based cancellation without creating a manual controller. In Vue composables, abort in ",[15,14883,14884],{},"onUnmounted",". In Nuxt's ",[15,14887,13772],{},", abort is handled internally when a component unmounts or the key changes - but manual ",[15,14890,14196],{}," calls inside ",[15,14893,3912],{}," need explicit abort logic via ",[15,14896,13448],{}," (as shown in the race conditions section above).",[2456,14899,14901],{"id":14900},"streaming-fetch-response-handling","Streaming fetch response handling",[11,14903,14904,14906,14907,13445,14910,14913],{},[15,14905,11807],{}," returns a ",[15,14908,14909],{},"Response",[15,14911,14912],{},"body: ReadableStream",". You can process it incrementally rather than waiting for the full response:",[255,14915,14917],{"className":3922,"code":14916,"language":3924,"meta":260,"style":260},"const response = await fetch('/api/large-data')\nconst reader = response.body.getReader()\nconst decoder = new TextDecoder()\n\nwhile (true) {\n  const { done, value } = await reader.read()\n  if (done) break\n  const chunk = decoder.decode(value, { stream: true })\n  processChunk(chunk)  // handle each chunk as it arrives\n}\n",[15,14918,14919,14943,14967,14983,14987,15001,15029,15043,15078,15092],{"__ignoreMap":260},[129,14920,14921,14923,14926,14928,14930,14932,14934,14936,14939,14941],{"class":265,"line":266},[129,14922,270],{"class":269},[129,14924,14925],{"class":273}," response ",[129,14927,278],{"class":277},[129,14929,4779],{"class":2139},[129,14931,14779],{"class":284},[129,14933,147],{"class":273},[129,14935,424],{"class":277},[129,14937,14938],{"class":427},"/api/large-data",[129,14940,424],{"class":277},[129,14942,294],{"class":273},[129,14944,14945,14947,14950,14952,14955,14957,14960,14962,14965],{"class":265,"line":297},[129,14946,270],{"class":269},[129,14948,14949],{"class":273}," reader ",[129,14951,278],{"class":277},[129,14953,14954],{"class":273}," response",[129,14956,362],{"class":277},[129,14958,14959],{"class":273},"body",[129,14961,362],{"class":277},[129,14963,14964],{"class":284},"getReader",[129,14966,2451],{"class":273},[129,14968,14969,14971,14974,14976,14978,14981],{"class":265,"line":315},[129,14970,270],{"class":269},[129,14972,14973],{"class":273}," decoder ",[129,14975,278],{"class":277},[129,14977,281],{"class":277},[129,14979,14980],{"class":284}," TextDecoder",[129,14982,2451],{"class":273},[129,14984,14985],{"class":265,"line":332},[129,14986,336],{"emptyLinePlaceholder":335},[129,14988,14989,14992,14994,14997,14999],{"class":265,"line":339},[129,14990,14991],{"class":2139},"while",[129,14993,3984],{"class":273},[129,14995,14996],{"class":4822},"true",[129,14998,4005],{"class":273},[129,15000,6455],{"class":277},[129,15002,15003,15005,15007,15010,15012,15014,15016,15018,15020,15023,15025,15027],{"class":265,"line":356},[129,15004,5076],{"class":269},[129,15006,1416],{"class":277},[129,15008,15009],{"class":273}," done",[129,15011,1015],{"class":277},[129,15013,1419],{"class":273},[129,15015,4255],{"class":277},[129,15017,4745],{"class":277},[129,15019,4779],{"class":2139},[129,15021,15022],{"class":273}," reader",[129,15024,362],{"class":277},[129,15026,2238],{"class":284},[129,15028,2451],{"class":1376},[129,15030,15031,15033,15035,15038,15040],{"class":265,"line":651},[129,15032,3998],{"class":2139},[129,15034,3984],{"class":1376},[129,15036,15037],{"class":273},"done",[129,15039,4005],{"class":1376},[129,15041,15042],{"class":2139},"break\n",[129,15044,15045,15047,15049,15051,15054,15056,15059,15061,15063,15065,15067,15070,15072,15074,15076],{"class":265,"line":657},[129,15046,5076],{"class":269},[129,15048,14653],{"class":273},[129,15050,4745],{"class":277},[129,15052,15053],{"class":273}," decoder",[129,15055,362],{"class":277},[129,15057,15058],{"class":284},"decode",[129,15060,147],{"class":1376},[129,15062,8389],{"class":273},[129,15064,1015],{"class":277},[129,15066,1416],{"class":277},[129,15068,15069],{"class":1376}," stream",[129,15071,1380],{"class":277},[129,15073,4823],{"class":4822},[129,15075,4255],{"class":277},[129,15077,294],{"class":1376},[129,15079,15080,15083,15085,15087,15089],{"class":265,"line":669},[129,15081,15082],{"class":284},"  processChunk",[129,15084,147],{"class":1376},[129,15086,14677],{"class":273},[129,15088,607],{"class":1376},[129,15090,15091],{"class":376},"// handle each chunk as it arrives\n",[129,15093,15094],{"class":265,"line":693},[129,15095,1530],{"class":277},[11,15097,15098,15099,15101],{},"This is how LLM streaming responses work in chat UIs - each token arrives as a chunk. In Nuxt with ",[15,15100,14196],{}," (ofetch), you can access the underlying response stream for the same pattern.",[2456,15103,15105],{"id":15104},"priority-inversion-in-async-code","Priority inversion in async code",[11,15107,15108,15111],{},[118,15109,15110],{},"Priority inversion"," is when a high-priority task is blocked by a low-priority one. In async code: a critical user interaction is queued behind background processing that shouldn't block it.",[11,15113,15114,15115,15118],{},"The browser's event loop doesn't inherently prioritize tasks (except microtasks over macrotasks). A ",[15,15116,15117],{},"setTimeout(heavyWork, 0)"," that runs for 200ms blocks a subsequent click handler.",[11,15120,15121,15122,1335,15132,15159],{},"Fix: break heavy work into chunks with ",[15,15123,15124,15126,15128,15130],{"className":257,"language":259,"style":260},[129,15125,8859],{"class":273},[129,15127,362],{"class":277},[129,15129,8864],{"class":284},[129,15131,4140],{"class":273},[15,15133,15134,15136,15138,15140,15142,15145,15147,15150,15153,15155,15157],{"className":257,"language":259,"style":260},[129,15135,8083],{"class":2139},[129,15137,281],{"class":277},[129,15139,4637],{"class":2161},[129,15141,147],{"class":273},[129,15143,15144],{"class":452},"r",[129,15146,456],{"class":269},[129,15148,15149],{"class":284}," setTimeout",[129,15151,15152],{"class":273},"(r",[129,15154,1015],{"class":277},[129,15156,5698],{"class":290},[129,15158,5893],{"class":273}," between chunks. This yields to the macrotask queue, giving the event loop a chance to process the pending click before continuing.",[40,15161,15163],{"id":15162},"memory-garbage-collection","Memory & garbage collection",[2456,15165,15167],{"id":15166},"browser-memory-leak-detection","Browser memory leak detection",[11,15169,15170],{},"JavaScript garbage-collects objects with no remaining references. A leak is a reference you didn't intend to keep that prevents collection.",[11,15172,15173],{},"Common patterns:",[1822,15175,15176,15189,15195,15204],{},[1825,15177,15178,3556,15181,15184,15185,15188],{},[118,15179,15180],{},"Event listeners not removed",[15,15182,15183],{},"addEventListener"," without ",[15,15186,15187],{},"removeEventListener"," keeps the callback and its closure alive",[1825,15190,15191,15194],{},[118,15192,15193],{},"Detached DOM nodes",": elements removed from the document but still referenced",[1825,15196,15197,15200,15201,15203],{},[118,15198,15199],{},"Growing caches",": unbounded ",[15,15202,2016],{},"s or arrays without eviction",[1825,15205,15206,15209],{},[118,15207,15208],{},"Closures over large objects",": a small callback that captures a large scope",[11,15211,15212],{},"To detect: Chrome DevTools > Memory > Take Heap Snapshot. Snapshot before an action, perform the action, navigate away, snapshot again. Objects that grew indicate a leak. The \"Detached DOM tree\" filter shows detached nodes directly.",[2456,15214,15193],{"id":15215},"detached-dom-nodes",[11,15217,561,15218,15221],{},[118,15219,15220],{},"detached node"," is a DOM element removed from the document but still referenced by JavaScript - a global variable, a closure, an event listener.",[255,15223,15225],{"className":3922,"code":15224,"language":3924,"meta":260,"style":260},"const cache: HTMLElement[] = []\n\nfunction buildWidget() {\n  const el = document.createElement('div')\n  cache.push(el)  // el will never be GC'd even if removed from DOM\n  document.body.appendChild(el)\n}\n\nfunction removeWidget(el: HTMLElement) {\n  el.remove()\n  // cache still holds reference - el is detached but not collected\n}\n",[15,15226,15227,15246,15250,15261,15287,15305,15325,15329,15333,15352,15363,15368],{"__ignoreMap":260},[129,15228,15229,15231,15234,15236,15239,15242,15244],{"class":265,"line":266},[129,15230,270],{"class":269},[129,15232,15233],{"class":273}," cache",[129,15235,1380],{"class":277},[129,15237,15238],{"class":2161}," HTMLElement",[129,15240,15241],{"class":273},"[] ",[129,15243,278],{"class":277},[129,15245,587],{"class":273},[129,15247,15248],{"class":265,"line":297},[129,15249,336],{"emptyLinePlaceholder":335},[129,15251,15252,15254,15257,15259],{"class":265,"line":315},[129,15253,10102],{"class":269},[129,15255,15256],{"class":284}," buildWidget",[129,15258,4140],{"class":277},[129,15260,1371],{"class":277},[129,15262,15263,15265,15267,15269,15271,15273,15276,15278,15280,15283,15285],{"class":265,"line":332},[129,15264,5076],{"class":269},[129,15266,10233],{"class":273},[129,15268,4745],{"class":277},[129,15270,11206],{"class":273},[129,15272,362],{"class":277},[129,15274,15275],{"class":284},"createElement",[129,15277,147],{"class":1376},[129,15279,424],{"class":277},[129,15281,15282],{"class":427},"div",[129,15284,424],{"class":277},[129,15286,294],{"class":1376},[129,15288,15289,15292,15294,15296,15298,15300,15302],{"class":265,"line":339},[129,15290,15291],{"class":273},"  cache",[129,15293,362],{"class":277},[129,15295,596],{"class":284},[129,15297,147],{"class":1376},[129,15299,10217],{"class":273},[129,15301,607],{"class":1376},[129,15303,15304],{"class":376},"// el will never be GC'd even if removed from DOM\n",[129,15306,15307,15310,15312,15314,15316,15319,15321,15323],{"class":265,"line":356},[129,15308,15309],{"class":273},"  document",[129,15311,362],{"class":277},[129,15313,14959],{"class":273},[129,15315,362],{"class":277},[129,15317,15318],{"class":284},"appendChild",[129,15320,147],{"class":1376},[129,15322,10217],{"class":273},[129,15324,294],{"class":1376},[129,15326,15327],{"class":265,"line":651},[129,15328,1530],{"class":277},[129,15330,15331],{"class":265,"line":657},[129,15332,336],{"emptyLinePlaceholder":335},[129,15334,15335,15337,15340,15342,15344,15346,15348,15350],{"class":265,"line":669},[129,15336,10102],{"class":269},[129,15338,15339],{"class":284}," removeWidget",[129,15341,147],{"class":277},[129,15343,10217],{"class":452},[129,15345,1380],{"class":277},[129,15347,15238],{"class":2161},[129,15349,160],{"class":277},[129,15351,1371],{"class":277},[129,15353,15354,15356,15358,15361],{"class":265,"line":693},[129,15355,10245],{"class":273},[129,15357,362],{"class":277},[129,15359,15360],{"class":284},"remove",[129,15362,2451],{"class":1376},[129,15364,15365],{"class":265,"line":712},[129,15366,15367],{"class":376},"  // cache still holds reference - el is detached but not collected\n",[129,15369,15370],{"class":265,"line":1521},[129,15371,1530],{"class":277},[11,15373,15374,15375,968,15378,15381],{},"Fix: clear references when removing elements. ",[15,15376,15377],{},"WeakRef",[15,15379,15380],{},"WeakMap"," hold references that don't prevent GC - useful for caches keyed by DOM nodes.",[2456,15383,15385],{"id":15384},"garbage-collection-timing","Garbage collection timing",[11,15387,15388],{},"JavaScript GC is not deterministic. The browser decides when to run it - typically under memory pressure. Major GC (mark-and-sweep of the entire heap) causes pauses that appear as long tasks in DevTools.",[11,15390,15391],{},"You can't force GC in production. What you can do: minimize allocation pressure. Object pooling (reusing objects instead of creating new ones each frame) reduces GC frequency. For animation code running at 60fps, avoid heap allocations inside the render loop.",[11,15393,15394,500,15396,15416],{},[15,15395,13329],{},[15,15397,15398,15401,15403,15405,15408,15410,15413],{"className":257,"language":259,"style":260},[129,15399,15400],{"class":273},"JSON",[129,15402,362],{"class":277},[129,15404,13261],{"class":284},[129,15406,15407],{"class":273},"(JSON",[129,15409,362],{"class":277},[129,15411,15412],{"class":284},"stringify",[129,15414,15415],{"class":273},"())",", and spread operators all allocate. In hot paths, mutate in place.",[40,15418,15420],{"id":15419},"web-vitals-performance-apis","Web vitals & performance APIs",[2456,15422,15424],{"id":15423},"performanceobserver-api","PerformanceObserver API",[11,15426,15427],{},"The programmatic way to observe performance metrics:",[255,15429,15431],{"className":3922,"code":15430,"language":3924,"meta":260,"style":260},"const observer = new PerformanceObserver((list) => {\n  for (const entry of list.getEntries()) {\n    console.log(entry.entryType, entry.name, entry.startTime, entry.duration)\n  }\n})\nobserver.observe({\n  entryTypes: ['largest-contentful-paint', 'long-animation-frame', 'layout-shift']\n})\n",[15,15432,15433,15459,15485,15530,15534,15540,15553,15589],{"__ignoreMap":260},[129,15434,15435,15437,15440,15442,15444,15447,15449,15451,15453,15455,15457],{"class":265,"line":266},[129,15436,270],{"class":269},[129,15438,15439],{"class":273}," observer ",[129,15441,278],{"class":277},[129,15443,281],{"class":277},[129,15445,15446],{"class":284}," PerformanceObserver",[129,15448,147],{"class":273},[129,15450,147],{"class":277},[129,15452,21],{"class":452},[129,15454,160],{"class":277},[129,15456,456],{"class":269},[129,15458,1371],{"class":277},[129,15460,15461,15463,15465,15467,15470,15472,15475,15477,15480,15483],{"class":265,"line":297},[129,15462,6437],{"class":2139},[129,15464,3984],{"class":1376},[129,15466,270],{"class":269},[129,15468,15469],{"class":273}," entry",[129,15471,6447],{"class":277},[129,15473,15474],{"class":273}," list",[129,15476,362],{"class":277},[129,15478,15479],{"class":284},"getEntries",[129,15481,15482],{"class":1376},"()) ",[129,15484,6455],{"class":277},[129,15486,15487,15489,15491,15493,15495,15498,15500,15503,15505,15507,15509,15511,15513,15515,15517,15520,15522,15524,15526,15528],{"class":265,"line":315},[129,15488,4116],{"class":273},[129,15490,362],{"class":277},[129,15492,365],{"class":284},[129,15494,147],{"class":1376},[129,15496,15497],{"class":273},"entry",[129,15499,362],{"class":277},[129,15501,15502],{"class":273},"entryType",[129,15504,1015],{"class":277},[129,15506,15469],{"class":273},[129,15508,362],{"class":277},[129,15510,8164],{"class":273},[129,15512,1015],{"class":277},[129,15514,15469],{"class":273},[129,15516,362],{"class":277},[129,15518,15519],{"class":273},"startTime",[129,15521,1015],{"class":277},[129,15523,15469],{"class":273},[129,15525,362],{"class":277},[129,15527,6797],{"class":273},[129,15529,294],{"class":1376},[129,15531,15532],{"class":265,"line":332},[129,15533,1524],{"class":277},[129,15535,15536,15538],{"class":265,"line":339},[129,15537,4028],{"class":277},[129,15539,294],{"class":273},[129,15541,15542,15544,15546,15549,15551],{"class":265,"line":356},[129,15543,3866],{"class":273},[129,15545,362],{"class":277},[129,15547,15548],{"class":284},"observe",[129,15550,147],{"class":273},[129,15552,6455],{"class":277},[129,15554,15555,15558,15560,15562,15564,15567,15569,15571,15573,15576,15578,15580,15582,15585,15587],{"class":265,"line":651},[129,15556,15557],{"class":1376},"  entryTypes",[129,15559,1380],{"class":277},[129,15561,1010],{"class":273},[129,15563,424],{"class":277},[129,15565,15566],{"class":427},"largest-contentful-paint",[129,15568,424],{"class":277},[129,15570,1015],{"class":277},[129,15572,4261],{"class":277},[129,15574,15575],{"class":427},"long-animation-frame",[129,15577,424],{"class":277},[129,15579,1015],{"class":277},[129,15581,4261],{"class":277},[129,15583,15584],{"class":427},"layout-shift",[129,15586,424],{"class":277},[129,15588,1046],{"class":273},[129,15590,15591,15593],{"class":265,"line":657},[129,15592,4028],{"class":277},[129,15594,294],{"class":273},[11,15596,15597,15598,500,15601,500,15604,500,15607,500,15610,500,15613,500,15615,500,15617,500,15620,500,15622,500,15625,15627,15628,15633],{},"Entry types include: ",[15,15599,15600],{},"navigation",[15,15602,15603],{},"resource",[15,15605,15606],{},"mark",[15,15608,15609],{},"measure",[15,15611,15612],{},"paint",[15,15614,15566],{},[15,15616,15584],{},[15,15618,15619],{},"long-task",[15,15621,15575],{},[15,15623,15624],{},"first-input",[15,15626,12876],{},". This is how the ",[51,15629,15632],{"href":15630,"rel":15631},"https://github.com/GoogleChrome/web-vitals",[55],"web-vitals"," library collects real user metrics (RUM).",[2456,15635,15637],{"id":15636},"long-tasks-api","Long tasks API",[11,15639,561,15640,15643],{},[118,15641,15642],{},"long task"," is any task that blocks the main thread for more than 50ms. It's the primary cause of input delay.",[255,15645,15647],{"className":3922,"code":15646,"language":3924,"meta":260,"style":260},"new PerformanceObserver((list) => {\n  for (const task of list.getEntries()) {\n    console.log('Long task:', task.duration, 'ms')\n  }\n}).observe({ entryTypes: ['longtask'] })\n",[15,15648,15649,15667,15690,15725,15729],{"__ignoreMap":260},[129,15650,15651,15653,15655,15657,15659,15661,15663,15665],{"class":265,"line":266},[129,15652,4134],{"class":277},[129,15654,15446],{"class":284},[129,15656,147],{"class":273},[129,15658,147],{"class":277},[129,15660,21],{"class":452},[129,15662,160],{"class":277},[129,15664,456],{"class":269},[129,15666,1371],{"class":277},[129,15668,15669,15671,15673,15675,15678,15680,15682,15684,15686,15688],{"class":265,"line":297},[129,15670,6437],{"class":2139},[129,15672,3984],{"class":1376},[129,15674,270],{"class":269},[129,15676,15677],{"class":273}," task",[129,15679,6447],{"class":277},[129,15681,15474],{"class":273},[129,15683,362],{"class":277},[129,15685,15479],{"class":284},[129,15687,15482],{"class":1376},[129,15689,6455],{"class":277},[129,15691,15692,15694,15696,15698,15700,15702,15705,15707,15709,15711,15713,15715,15717,15719,15721,15723],{"class":265,"line":315},[129,15693,4116],{"class":273},[129,15695,362],{"class":277},[129,15697,365],{"class":284},[129,15699,147],{"class":1376},[129,15701,424],{"class":277},[129,15703,15704],{"class":427},"Long task:",[129,15706,424],{"class":277},[129,15708,1015],{"class":277},[129,15710,15677],{"class":273},[129,15712,362],{"class":277},[129,15714,6797],{"class":273},[129,15716,1015],{"class":277},[129,15718,4261],{"class":277},[129,15720,6813],{"class":427},[129,15722,424],{"class":277},[129,15724,294],{"class":1376},[129,15726,15727],{"class":265,"line":332},[129,15728,1524],{"class":277},[129,15730,15731,15733,15735,15737,15739,15741,15743,15746,15748,15750,15752,15755,15757,15759,15761],{"class":265,"line":339},[129,15732,4028],{"class":277},[129,15734,160],{"class":273},[129,15736,362],{"class":277},[129,15738,15548],{"class":284},[129,15740,147],{"class":273},[129,15742,4796],{"class":277},[129,15744,15745],{"class":1376}," entryTypes",[129,15747,1380],{"class":277},[129,15749,1010],{"class":273},[129,15751,424],{"class":277},[129,15753,15754],{"class":427},"longtask",[129,15756,424],{"class":277},[129,15758,348],{"class":273},[129,15760,4028],{"class":277},[129,15762,294],{"class":273},[11,15764,15765,15767,15768,15770],{},[15,15766,15575],{}," (LoAF) is the successor to ",[15,15769,15754],{}," - it tracks animation frames that took longer than 50ms and includes attribution (which script caused it), making it far more actionable for debugging.",[2456,15772,15774],{"id":15773},"first-input-delay-fid","First Input Delay (FID)",[11,15776,15777,15780],{},[118,15778,15779],{},"FID"," measured the delay between a user's first interaction (click, tap, key press) and the browser's response. Specifically: the time the browser was blocked by a long task when the input arrived.",[11,15782,15783,15784,15789],{},"FID is dead as a Core Web Vital. Google ",[51,15785,15788],{"href":15786,"rel":15787},"https://web.dev/blog/inp-cwv-march-2024",[55],"replaced it with INP in March 2024",". FID measured only the first interaction on a page; INP measures all interactions throughout the session.",[2456,15791,15793],{"id":15792},"interaction-to-next-paint-inp","Interaction to Next Paint (INP)",[11,15795,15796,15799],{},[118,15797,15798],{},"INP"," is the Core Web Vital that replaced FID. It reports the worst interaction latency of the entire page session (with some outlier exclusion). An \"interaction\" is a click, tap, or keyboard press - measured from input to the next paint after all associated processing completes.",[1822,15801,15802,15809,15816],{},[1825,15803,15804,15805,15808],{},"🟢 ",[118,15806,15807],{},"Good",": \u003C200ms.",[1825,15810,15811,15812,15815],{},"🟡 ",[118,15813,15814],{},"Needs improvement",": 200-500ms.",[1825,15817,15818,15819,15822],{},"🔴 ",[118,15820,15821],{},"Poor",": >500ms.",[11,15824,15825,15826,15829],{},"Common INP culprits: long tasks in event handlers, synchronous XHR, layout thrashing triggered by interactions, large JavaScript evaluated on click. Fix: break up event handler work with ",[15,15827,15828],{},"scheduler.yield()",", defer non-essential work to after the next paint.",[2456,15831,15833],{"id":15832},"cumulative-layout-shift-cls","Cumulative Layout Shift (CLS)",[11,15835,15836,15839],{},[118,15837,15838],{},"CLS"," measures visual instability - elements that unexpectedly move during page load. Calculated as sum of (impact fraction * distance fraction) for unexpected shifts.",[1822,15841,15842,15847],{},[1825,15843,15804,15844,15846],{},[118,15845,15807],{},": \u003C0.1.",[1825,15848,15818,15849,15851],{},[118,15850,15821],{},": >0.25.",[11,15853,15854,15855,938,15857,15859],{},"Biggest causes: images without ",[15,15856,10254],{},[15,15858,10719],{}," attributes, ads injected into content without reserved space, web fonts causing text reflow (FOUT), dynamically injected content above existing content.",[3576,15861,15862],{},[11,15863,15864,15865,15868,15869,15872,15873,15876],{},"In Nuxt: ",[15,15866,15867],{},"@nuxt/image"," automatically includes dimensions. ",[15,15870,15871],{},"@nuxt/fonts"," with Fontaine handles font CLS. For injected content, use CSS ",[15,15874,15875],{},"min-height"," on container elements to reserve space.",[2456,15878,15880],{"id":15879},"largest-contentful-paint-lcp","Largest Contentful Paint (LCP)",[11,15882,15883,15886],{},[118,15884,15885],{},"LCP"," marks the render time of the largest visible element in the viewport - typically a hero image or large heading. It's the best proxy for \"when does the page feel loaded.\"",[1822,15888,15889,15894],{},[1825,15890,15804,15891,15893],{},[118,15892,15807],{},": \u003C2.5s.",[1825,15895,15818,15896,15898],{},[118,15897,15821],{},": >4s.",[11,15900,15901,15902,15904,15905,15923],{},"LCP is blocked by: slow TTFB, render-blocking resources, slow resource download, client-side rendering. In Nuxt with SSR, LCP is typically the hero image. Strategies: ",[15,15903,12473],{}," on the hero image, serve from CDN, WebP/AVIF format, ",[15,15906,15907,15909,15911,15913,15915,15917,15919,15921],{"className":10399,"language":10400,"style":260},[129,15908,3945],{"class":277},[129,15910,10405],{"class":1376},[129,15912,10408],{"class":269},[129,15914,278],{"class":277},[129,15916,2258],{"class":277},[129,15918,12420],{"class":427},[129,15920,2258],{"class":277},[129,15922,3956],{"class":277}," in the document head.",[40,15925,15927],{"id":15926},"rendering-correctness","Rendering correctness",[2456,15929,15931],{"id":15930},"deterministic-rendering","Deterministic rendering",[11,15933,15934,15936],{},[118,15935,15931],{}," means given the same state input, the render output is always identical - no randomness, no timestamp-based differences, no locale-dependent formatting unless explicitly parameterized.",[11,15938,15939],{},"Non-deterministic rendering causes hydration mismatches in SSR: the server renders with one value, the client re-renders with a different value, Vue warns and potentially re-renders the entire subtree.",[11,15941,15942,15943,500,15954,500,15964,15972,15973,500,15975,500,15978,15980],{},"Common causes in Nuxt: ",[15,15944,15945,15947,15949,15952],{"className":257,"language":259,"style":260},[129,15946,10876],{"class":273},[129,15948,362],{"class":277},[129,15950,15951],{"class":284},"random",[129,15953,4140],{"class":273},[15,15955,15956,15958,15960,15962],{"className":257,"language":259,"style":260},[129,15957,9433],{"class":273},[129,15959,362],{"class":277},[129,15961,3587],{"class":284},[129,15963,4140],{"class":273},[15,15965,15966,15968,15970],{"className":257,"language":259,"style":260},[129,15967,4134],{"class":277},[129,15969,4137],{"class":284},[129,15971,4140],{"class":273}," called in component logic, browser-only APIs (",[15,15974,13066],{},[15,15976,15977],{},"navigator",[15,15979,9443],{},") accessed in server-side code.",[3576,15982,15983],{},[11,15984,15985,15986,15999,16000,16007],{},"Guard browser APIs with ",[15,15987,15988,15990,15992,15995,15997],{"className":257,"language":259,"style":260},[129,15989,2140],{"class":2139},[129,15991,362],{"class":277},[129,15993,15994],{"class":273},"meta",[129,15996,362],{"class":277},[129,15998,7465],{"class":273},", pass server-generated values as props, use ",[15,16001,16002,16005],{"className":257,"language":259,"style":260},[129,16003,16004],{"class":284},"useId",[129,16006,4140],{"class":273}," for component IDs.",[2456,16009,16011],{"id":16010},"idempotent-ui-actions","Idempotent UI actions",[11,16013,16014,16015,16018,16019,16032],{},"An ",[118,16016,16017],{},"idempotent"," action produces the same result regardless of how many times it's executed. Setting state to ",[15,16020,16021,16023,16026,16028,16030],{"className":257,"language":259,"style":260},[129,16022,4796],{"class":277},[129,16024,16025],{"class":2161}," active",[129,16027,1380],{"class":277},[129,16029,4823],{"class":4822},[129,16031,4255],{"class":277}," is idempotent - clicking five times has the same result as once. A toggle is not idempotent.",[11,16034,16035],{},"Idempotency matters for: optimistic UI resends (does retrying a failed mutation cause double-updates?), Service Worker precaching (installing the same asset twice must be safe), analytics deduplication.",[11,16037,16038,16039,500,16042,16045],{},"For mutations, using IDs and upsert semantics (",[15,16040,16041],{},"PUT /resource/id",[15,16043,16044],{},"INSERT OR REPLACE",") instead of append semantics makes server handlers idempotent.",[40,16047,16049],{"id":16048},"accessibility-internals","Accessibility internals",[2456,16051,16053],{"id":16052},"accessibility-tree","Accessibility tree",[11,16055,8603,16056,16059],{},[118,16057,16058],{},"accessibility tree"," is a parallel structure the browser builds from the DOM, populated with semantic information: role, name, description, state. Screen readers and assistive technologies read this tree, not the DOM directly.",[255,16061,16064],{"className":10399,"code":16062,"filename":16063,"language":10400,"meta":260,"style":260},"\u003Cbutton class=\"btn\" id=\"save\">Save\u003C/button>\n","DOM",[15,16065,16066],{"__ignoreMap":260},[129,16067,16068,16070,16073,16075,16077,16079,16082,16084,16086,16088,16090,16093,16095,16097,16100,16102,16104],{"class":265,"line":266},[129,16069,3945],{"class":277},[129,16071,16072],{"class":1376},"button",[129,16074,7254],{"class":269},[129,16076,278],{"class":277},[129,16078,2258],{"class":277},[129,16080,16081],{"class":427},"btn",[129,16083,2258],{"class":277},[129,16085,4643],{"class":269},[129,16087,278],{"class":277},[129,16089,2258],{"class":277},[129,16091,16092],{"class":427},"save",[129,16094,2258],{"class":277},[129,16096,3956],{"class":277},[129,16098,16099],{"class":273},"Save",[129,16101,12609],{"class":277},[129,16103,16072],{"class":1376},[129,16105,4676],{"class":277},[255,16107,16111],{"className":16108,"code":16109,"filename":16053,"language":16110,"meta":260,"style":260},"language-txt shiki shiki-themes material-theme-lighter material-theme material-theme-palenight","  button (role)\n    name: \"Save\" (from text content)\n    focusable: true\n    disabled: false\n","txt",[15,16112,16113,16118,16123,16128],{"__ignoreMap":260},[129,16114,16115],{"class":265,"line":266},[129,16116,16117],{},"  button (role)\n",[129,16119,16120],{"class":265,"line":297},[129,16121,16122],{},"    name: \"Save\" (from text content)\n",[129,16124,16125],{"class":265,"line":315},[129,16126,16127],{},"    focusable: true\n",[129,16129,16130],{"class":265,"line":332},[129,16131,16132],{},"    disabled: false\n",[3283,16134,16135],{},[11,16136,16137,968,16140,16143,16144,16147],{},[15,16138,16139],{},"display: none",[15,16141,16142],{},"visibility: hidden"," remove nodes from the accessibility tree. ",[15,16145,16146],{},"opacity: 0"," does not - the element is invisible but still announced. CSS generally doesn't affect the accessibility tree.",[11,16149,16150],{},"Inspect it in Chrome DevTools > Elements > Accessibility tab.",[2456,16152,16154],{"id":16153},"aria-live-regions-internals","ARIA live regions internals",[11,16156,16157,16160,16161,16164,16165,16168],{},[118,16158,16159],{},"ARIA live regions"," tell screen readers to announce content updates without focus moving. ",[15,16162,16163],{},"aria-live=\"polite\""," waits for the user to finish their current interaction. ",[15,16166,16167],{},"aria-live=\"assertive\""," interrupts immediately.",[11,16170,16171],{},"The implementation: the browser watches for DOM changes inside live region elements and queues announcements in the screen reader's speech queue. The gotchas:",[2086,16173,16174,16185,16188],{},[1825,16175,16176,16177,16180,16181,16184],{},"You can't add ",[15,16178,16179],{},"aria-live"," to an element that already has content and expect initial announcement - only ",[24,16182,16183],{},"changes"," are announced.",[1825,16186,16187],{},"Initial page content is never announced via live regions - only subsequent mutations.",[1825,16189,16190],{},"Removing and re-adding a live region element with different content triggers announcements; modifying content in place also triggers.",[11,16192,16193],{},"For Nuxt SPA navigation: announce page title changes via a visually-hidden live region, because screen readers don't detect route changes automatically.",[2456,16195,16197],{"id":16196},"pointer-events","Pointer events",[11,16199,8603,16200,16203,16204,500,16207,500,16210,500,16213,16216],{},[118,16201,16202],{},"Pointer Events API"," unifies mouse, touch, and stylus input into a single event model. ",[15,16205,16206],{},"pointerdown",[15,16208,16209],{},"pointermove",[15,16211,16212],{},"pointerup",[15,16214,16215],{},"pointercancel"," work for all input types.",[255,16218,16220],{"className":3922,"code":16219,"language":3924,"meta":260,"style":260},"el.addEventListener('pointerdown', (e) => {\n  e.pointerId      // unique ID per active pointer (multitouch support)\n  e.pointerType    // 'mouse' | 'touch' | 'pen'\n  e.pressure       // 0-1, stylus pressure\n  e.tiltX, e.tiltY // stylus angle\n})\n",[15,16221,16222,16250,16263,16275,16287,16309],{"__ignoreMap":260},[129,16223,16224,16226,16228,16230,16232,16234,16236,16238,16240,16242,16244,16246,16248],{"class":265,"line":266},[129,16225,10217],{"class":273},[129,16227,362],{"class":277},[129,16229,15183],{"class":284},[129,16231,147],{"class":273},[129,16233,424],{"class":277},[129,16235,16206],{"class":427},[129,16237,424],{"class":277},[129,16239,1015],{"class":277},[129,16241,3984],{"class":277},[129,16243,188],{"class":452},[129,16245,160],{"class":277},[129,16247,456],{"class":269},[129,16249,1371],{"class":277},[129,16251,16252,16255,16257,16260],{"class":265,"line":297},[129,16253,16254],{"class":273},"  e",[129,16256,362],{"class":277},[129,16258,16259],{"class":273},"pointerId",[129,16261,16262],{"class":376},"      // unique ID per active pointer (multitouch support)\n",[129,16264,16265,16267,16269,16272],{"class":265,"line":315},[129,16266,16254],{"class":273},[129,16268,362],{"class":277},[129,16270,16271],{"class":273},"pointerType",[129,16273,16274],{"class":376},"    // 'mouse' | 'touch' | 'pen'\n",[129,16276,16277,16279,16281,16284],{"class":265,"line":332},[129,16278,16254],{"class":273},[129,16280,362],{"class":277},[129,16282,16283],{"class":273},"pressure",[129,16285,16286],{"class":376},"       // 0-1, stylus pressure\n",[129,16288,16289,16291,16293,16296,16298,16301,16303,16306],{"class":265,"line":339},[129,16290,16254],{"class":273},[129,16292,362],{"class":277},[129,16294,16295],{"class":273},"tiltX",[129,16297,1015],{"class":277},[129,16299,16300],{"class":273}," e",[129,16302,362],{"class":277},[129,16304,16305],{"class":273},"tiltY",[129,16307,16308],{"class":376}," // stylus angle\n",[129,16310,16311,16313],{"class":265,"line":356},[129,16312,4028],{"class":277},[129,16314,294],{"class":273},[11,16316,16317,16320,16321,16324,16325,16328],{},[15,16318,16319],{},"touch-action"," CSS controls which pointer events get handled natively vs passed to JavaScript. ",[15,16322,16323],{},"touch-action: none"," means JS handles everything - needed for custom drag-and-drop. ",[15,16326,16327],{},"touch-action: pan-y"," means the browser handles vertical scroll; JS gets horizontal events. Forgetting this causes 300ms tap delays on mobile or scroll interference in drag interactions.",[11,16330,16331,16339],{},[15,16332,16333,16336],{"className":257,"language":259,"style":260},[129,16334,16335],{"class":284},"setPointerCapture",[129,16337,16338],{"class":273},"(pointerId)"," keeps pointer events firing on an element even when the pointer moves outside it - essential for drag handles where the user moves faster than the hit target.",[2001,16341],{},[11,16343,16344],{},"Most frontend complexity exists because browsers evolved over 30 years, JavaScript was designed in 10 days, and the two have been fighting for alignment ever since. The developers who write genuinely fast, reliable, accessible software aren't the ones who know every API by heart - they're the ones with accurate mental models of what's happening below their code.",[11,16346,16347],{},"This is a starting point for building those models tho.",[2026,16349,16350],{},"html pre.shiki code .sTEyZ, html code.shiki .sTEyZ{--shiki-light:#90A4AE;--shiki-default:#EEFFFF;--shiki-dark:#BABED8}html pre.shiki code .sMK4o, html code.shiki .sMK4o{--shiki-light:#39ADB5;--shiki-default:#89DDFF;--shiki-dark:#89DDFF}html pre.shiki code .s2Zo4, html code.shiki .s2Zo4{--shiki-light:#6182B8;--shiki-default:#82AAFF;--shiki-dark:#82AAFF}html .light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html.light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .spNyl, html code.shiki .spNyl{--shiki-light:#9C3EDA;--shiki-default:#C792EA;--shiki-dark:#C792EA}html pre.shiki code .swJcz, html code.shiki .swJcz{--shiki-light:#E53935;--shiki-default:#F07178;--shiki-dark:#F07178}html pre.shiki code .sbssI, html code.shiki .sbssI{--shiki-light:#F76D47;--shiki-default:#F78C6C;--shiki-dark:#F78C6C}html pre.shiki code .sHwdD, html code.shiki .sHwdD{--shiki-light:#90A4AE;--shiki-light-font-style:italic;--shiki-default:#546E7A;--shiki-default-font-style:italic;--shiki-dark:#676E95;--shiki-dark-font-style:italic}html pre.shiki code .sBMFI, html code.shiki .sBMFI{--shiki-light:#E2931D;--shiki-default:#FFCB6B;--shiki-dark:#FFCB6B}html pre.shiki code .s7zQu, html code.shiki .s7zQu{--shiki-light:#39ADB5;--shiki-light-font-style:italic;--shiki-default:#89DDFF;--shiki-default-font-style:italic;--shiki-dark:#89DDFF;--shiki-dark-font-style:italic}html pre.shiki code .sfazB, html code.shiki .sfazB{--shiki-light:#91B859;--shiki-default:#C3E88D;--shiki-dark:#C3E88D}html pre.shiki code .sHdIc, html code.shiki .sHdIc{--shiki-light:#90A4AE;--shiki-light-font-style:italic;--shiki-default:#EEFFFF;--shiki-default-font-style:italic;--shiki-dark:#BABED8;--shiki-dark-font-style:italic}html pre.shiki code .sqsOY, html code.shiki .sqsOY{--shiki-light:#8796B0;--shiki-default:#B2CCD6;--shiki-dark:#B2CCD6}html pre.shiki code .sfNiH, html code.shiki .sfNiH{--shiki-light:#FF5370;--shiki-default:#FF9CAC;--shiki-dark:#FF9CAC}",{"title":260,"searchDepth":297,"depth":297,"links":16352},[16353,16365,16371,16377,16384,16391,16396,16402,16407,16414,16420,16428,16436,16448,16455,16460,16468,16472],{"id":8658,"depth":297,"text":8659,"children":16354},[16355,16356,16357,16358,16359,16360,16361,16362,16363,16364],{"id":8662,"depth":315,"text":8663},{"id":8699,"depth":315,"text":8700},{"id":8722,"depth":315,"text":8723},{"id":8799,"depth":315,"text":8800},{"id":8829,"depth":315,"text":8830},{"id":8841,"depth":315,"text":8842},{"id":8870,"depth":315,"text":8871},{"id":8880,"depth":315,"text":8881},{"id":8898,"depth":315,"text":8899},{"id":8921,"depth":315,"text":8922},{"id":8950,"depth":297,"text":8951,"children":16366},[16367,16368,16369,16370],{"id":8954,"depth":315,"text":8955},{"id":8982,"depth":315,"text":8983},{"id":8998,"depth":315,"text":8999},{"id":9020,"depth":315,"text":9021},{"id":9168,"depth":297,"text":9169,"children":16372},[16373,16374,16375,16376],{"id":9172,"depth":315,"text":9173},{"id":9192,"depth":315,"text":9193},{"id":9359,"depth":315,"text":9360},{"id":9447,"depth":315,"text":9448},{"id":9855,"depth":297,"text":9856,"children":16378},[16379,16380,16381,16382,16383],{"id":9859,"depth":315,"text":9860},{"id":10083,"depth":315,"text":10084},{"id":10170,"depth":315,"text":10171},{"id":10379,"depth":315,"text":10380},{"id":10392,"depth":315,"text":10393},{"id":10627,"depth":297,"text":10628,"children":16385},[16386,16387,16388,16389,16390],{"id":10631,"depth":315,"text":10632},{"id":10671,"depth":315,"text":10672},{"id":10723,"depth":315,"text":10724},{"id":10783,"depth":315,"text":10784},{"id":10845,"depth":315,"text":10846},{"id":10887,"depth":297,"text":10888,"children":16392},[16393,16394,16395],{"id":10891,"depth":315,"text":10892},{"id":10911,"depth":315,"text":10912},{"id":10932,"depth":315,"text":10933},{"id":10952,"depth":297,"text":10953,"children":16397},[16398,16399,16400,16401],{"id":10956,"depth":315,"text":10957},{"id":11035,"depth":315,"text":11036},{"id":11072,"depth":315,"text":11073},{"id":11160,"depth":315,"text":11161},{"id":11175,"depth":297,"text":11176,"children":16403},[16404,16405,16406],{"id":11179,"depth":315,"text":11180},{"id":11340,"depth":315,"text":11341},{"id":11447,"depth":315,"text":11448},{"id":11465,"depth":297,"text":11466,"children":16408},[16409,16410,16411,16412,16413],{"id":11469,"depth":315,"text":11470},{"id":11554,"depth":315,"text":11555},{"id":11589,"depth":315,"text":11590},{"id":11679,"depth":315,"text":11667},{"id":11769,"depth":315,"text":11770},{"id":11867,"depth":297,"text":11868,"children":16415},[16416,16417,16418,16419],{"id":11871,"depth":315,"text":11872},{"id":12025,"depth":315,"text":12026},{"id":12109,"depth":315,"text":12110},{"id":12190,"depth":315,"text":12156},{"id":12215,"depth":297,"text":12216,"children":16421},[16422,16423,16424,16425,16426,16427],{"id":12219,"depth":315,"text":12220},{"id":12265,"depth":315,"text":12266},{"id":12302,"depth":315,"text":12303},{"id":12477,"depth":315,"text":12478},{"id":12506,"depth":315,"text":12507},{"id":12545,"depth":315,"text":12546},{"id":12631,"depth":297,"text":12632,"children":16429},[16430,16431,16432,16433,16434,16435],{"id":12635,"depth":315,"text":12636},{"id":12714,"depth":315,"text":12715},{"id":12756,"depth":315,"text":12757},{"id":12786,"depth":315,"text":12787},{"id":12938,"depth":315,"text":12939},{"id":13074,"depth":315,"text":13075},{"id":13333,"depth":297,"text":13334,"children":16437},[16438,16439,16440,16441,16442,16443,16444,16445,16446,16447],{"id":13337,"depth":315,"text":13338},{"id":13688,"depth":315,"text":13689},{"id":13701,"depth":315,"text":13702},{"id":13744,"depth":315,"text":13745},{"id":13776,"depth":315,"text":13777},{"id":13813,"depth":315,"text":13814},{"id":14108,"depth":315,"text":14109},{"id":14177,"depth":315,"text":14178},{"id":14455,"depth":315,"text":14456},{"id":14491,"depth":315,"text":14492},{"id":14559,"depth":297,"text":14560,"children":16449},[16450,16451,16452,16453,16454],{"id":14563,"depth":315,"text":14564},{"id":14585,"depth":315,"text":14586},{"id":14701,"depth":315,"text":14702},{"id":14900,"depth":315,"text":14901},{"id":15104,"depth":315,"text":15105},{"id":15162,"depth":297,"text":15163,"children":16456},[16457,16458,16459],{"id":15166,"depth":315,"text":15167},{"id":15215,"depth":315,"text":15193},{"id":15384,"depth":315,"text":15385},{"id":15419,"depth":297,"text":15420,"children":16461},[16462,16463,16464,16465,16466,16467],{"id":15423,"depth":315,"text":15424},{"id":15636,"depth":315,"text":15637},{"id":15773,"depth":315,"text":15774},{"id":15792,"depth":315,"text":15793},{"id":15832,"depth":315,"text":15833},{"id":15879,"depth":315,"text":15880},{"id":15926,"depth":297,"text":15927,"children":16469},[16470,16471],{"id":15930,"depth":315,"text":15931},{"id":16010,"depth":315,"text":16011},{"id":16048,"depth":297,"text":16049,"children":16473},[16474,16475,16476],{"id":16052,"depth":315,"text":16053},{"id":16153,"depth":315,"text":16154},{"id":16196,"depth":315,"text":16197},"Hydration, fiber, QUIC, CRDTs, compositing, INP - from Vue perspective",{},"/blog/frontend-internals",50,{"title":8636,"description":16477},"blog/frontend-internals",[1716,16484,16485,2051,12632,16486],"Performance","Browser","Vue","QX9gnVBXayeQ-pxnNYqGLCMGuEHuf8krcvml6d2QCGs",{"id":16489,"title":16490,"body":16491,"cover":2042,"date":8623,"description":17758,"extension":2045,"meta":17759,"navigation":335,"path":17760,"readingTime":651,"seo":17761,"stem":17762,"tags":17763,"__hash__":17767},"blog/blog/geolocation-element.md","The \u003Cgeolocation> element",{"type":8,"value":16492,"toc":17745},[16493,16513,16524,16534,16544,16547,16564,16567,16574,16581,16631,16774,16781,16790,16799,16805,16811,16815,16826,16875,16878,16882,16885,16893,16912,16926,17040,17044,17047,17079,17086,17093,17214,17218,17223,17307,17320,17323,17350,17363,17366,17370,17384,17642,17702,17720,17727,17737,17739,17742],[11,16494,16495,16496,16512],{},"The Geolocation JS API has had the same UX problem since 2008 and nobody fixed it. ",[15,16497,16498,16500,16502,16505,16507,16510],{"className":257,"language":259,"style":260},[129,16499,15977],{"class":273},[129,16501,362],{"class":277},[129,16503,16504],{"class":273},"geolocation",[129,16506,362],{"class":277},[129,16508,16509],{"class":284},"getCurrentPosition",[129,16511,4140],{"class":273}," fires a browser permission prompt the instant a script calls it - which is usually within seconds of page load, before the user has done anything that signals they want location features. They click \"Block.\" Permission denied, permanently, unless they find the lock icon in the address bar and dig through settings.",[11,16514,16515,16516,16523],{},"Chrome 144 ships ",[51,16517,16520],{"href":16518,"rel":16519},"https://developer.chrome.com/blog/geolocation-html-element",[55],[15,16521,16522],{},"\u003Cgeolocation>",", an HTML element that changes when and how location access is requested.",[40,16525,16527,16528],{"id":16526},"the-actual-problem-with-getcurrentposition","The actual problem with ",[15,16529,16530,16532],{"className":257,"language":259,"style":260},[129,16531,16509],{"class":284},[129,16533,4140],{"class":273},[11,16535,16536,16537,16543],{},"The API isn't technically broken. The problem is timing and framing. The browser shows a permission dialog the moment a script calls ",[15,16538,16539,16541],{"className":257,"language":259,"style":260},[129,16540,16509],{"class":284},[129,16542,4140],{"class":273}," - regardless of what the user was doing or whether they'd expressed any intent to use location features. Most sites call it on page load or component mount.",[11,16545,16546],{},"Once the user clicks \"Block\", the recovery path is brutal:",[2086,16548,16549,16552,16555,16558,16561],{},[1825,16550,16551],{},"Notice the permission icon in the address bar",[1825,16553,16554],{},"Click it",[1825,16556,16557],{},"Find the location toggle",[1825,16559,16560],{},"Re-enable it",[1825,16562,16563],{},"Reload the page",[11,16565,16566],{},"Nobody does this. The location feature is dead for that user on that site, permanently.",[40,16568,16570,16571,16573],{"id":16569},"what-geolocation-actually-does","What ",[15,16572,16522],{}," actually does",[11,16575,16576,16577,16580],{},"The element is a browser-rendered button. The browser controls what it looks like and what it does. When the user clicks it, the browser handles the permission request, the data retrieval, and the error states. The site listens for a ",[15,16578,16579],{},"location"," event:",[255,16582,16584],{"className":10399,"code":16583,"language":10400,"meta":260,"style":260},"\u003Cgeolocation\n  onlocation=\"handleLocation(event)\"\n  accuracymode=\"precise\">\n\u003C/geolocation>\n",[15,16585,16586,16593,16607,16623],{"__ignoreMap":260},[129,16587,16588,16590],{"class":265,"line":266},[129,16589,3945],{"class":277},[129,16591,16592],{"class":1376},"geolocation\n",[129,16594,16595,16598,16600,16602,16605],{"class":265,"line":297},[129,16596,16597],{"class":269},"  onlocation",[129,16599,278],{"class":277},[129,16601,2258],{"class":277},[129,16603,16604],{"class":427},"handleLocation(event)",[129,16606,2292],{"class":277},[129,16608,16609,16612,16614,16616,16619,16621],{"class":265,"line":315},[129,16610,16611],{"class":269},"  accuracymode",[129,16613,278],{"class":277},[129,16615,2258],{"class":277},[129,16617,16618],{"class":427},"precise",[129,16620,2258],{"class":277},[129,16622,4676],{"class":277},[129,16624,16625,16627,16629],{"class":265,"line":332},[129,16626,12609],{"class":277},[129,16628,16504],{"class":1376},[129,16630,4676],{"class":277},[255,16632,16636],{"className":16633,"code":16634,"language":16635,"meta":260,"style":260},"language-js shiki shiki-themes material-theme-lighter material-theme material-theme-palenight","function handleLocation(event) {\n  if (event.target.position) {\n    const { latitude, longitude } = event.target.position.coords\n    // do something with coords\n  } else if (event.target.error) {\n    console.error(event.target.error.message)\n  }\n}\n","js",[15,16637,16638,16653,16674,16708,16713,16739,16766,16770],{"__ignoreMap":260},[129,16639,16640,16642,16645,16647,16649,16651],{"class":265,"line":266},[129,16641,10102],{"class":269},[129,16643,16644],{"class":284}," handleLocation",[129,16646,147],{"class":277},[129,16648,4100],{"class":452},[129,16650,160],{"class":277},[129,16652,1371],{"class":277},[129,16654,16655,16657,16659,16661,16663,16665,16667,16670,16672],{"class":265,"line":297},[129,16656,3998],{"class":2139},[129,16658,3984],{"class":1376},[129,16660,4100],{"class":273},[129,16662,362],{"class":277},[129,16664,13109],{"class":273},[129,16666,362],{"class":277},[129,16668,16669],{"class":273},"position",[129,16671,4005],{"class":1376},[129,16673,6455],{"class":277},[129,16675,16676,16678,16680,16683,16685,16688,16690,16692,16695,16697,16699,16701,16703,16705],{"class":265,"line":315},[129,16677,4739],{"class":269},[129,16679,1416],{"class":277},[129,16681,16682],{"class":273}," latitude",[129,16684,1015],{"class":277},[129,16686,16687],{"class":273}," longitude",[129,16689,4255],{"class":277},[129,16691,4745],{"class":277},[129,16693,16694],{"class":273}," event",[129,16696,362],{"class":277},[129,16698,13109],{"class":273},[129,16700,362],{"class":277},[129,16702,16669],{"class":273},[129,16704,362],{"class":277},[129,16706,16707],{"class":273},"coords\n",[129,16709,16710],{"class":265,"line":332},[129,16711,16712],{"class":376},"    // do something with coords\n",[129,16714,16715,16717,16720,16723,16725,16727,16729,16731,16733,16735,16737],{"class":265,"line":339},[129,16716,4182],{"class":277},[129,16718,16719],{"class":2139}," else",[129,16721,16722],{"class":2139}," if",[129,16724,3984],{"class":1376},[129,16726,4100],{"class":273},[129,16728,362],{"class":277},[129,16730,13109],{"class":273},[129,16732,362],{"class":277},[129,16734,10921],{"class":273},[129,16736,4005],{"class":1376},[129,16738,6455],{"class":277},[129,16740,16741,16743,16745,16747,16749,16751,16753,16755,16757,16759,16761,16764],{"class":265,"line":356},[129,16742,4116],{"class":273},[129,16744,362],{"class":277},[129,16746,10921],{"class":284},[129,16748,147],{"class":1376},[129,16750,4100],{"class":273},[129,16752,362],{"class":277},[129,16754,13109],{"class":273},[129,16756,362],{"class":277},[129,16758,10921],{"class":273},[129,16760,362],{"class":277},[129,16762,16763],{"class":273},"message",[129,16765,294],{"class":1376},[129,16767,16768],{"class":265,"line":651},[129,16769,1524],{"class":277},[129,16771,16772],{"class":265,"line":657},[129,16773,1530],{"class":277},[11,16775,16776,16777,16780],{},"The browser renders the element as a styled button. The user clicking it is a ",[118,16778,16779],{},"user gesture"," - semantically different from a script running. The browser enforces that distinction at the platform level. Scripts cannot programmatically trigger the permission prompt.",[11,16782,16783,16784,1380],{},"Three behaviors that differ from ",[15,16785,16786,16788],{"className":257,"language":259,"style":260},[129,16787,16509],{"class":284},[129,16789,4140],{"class":273},[11,16791,16792,16795,16796,16798],{},[118,16793,16794],{},"Permission blocked - recovery is contextual."," If the user previously blocked the site, clicking ",[15,16797,16522],{}," shows UI to re-enable it in place - no settings navigation required. The user is at the exact moment in the flow where they need location; the recovery prompt is relevant.",[11,16800,16801,16804],{},[118,16802,16803],{},"Permission granted - acts as a refresh."," If permission is already granted, clicking the element immediately fetches new coordinates without re-prompting. No stale position from page load.",[11,16806,16807,16810],{},[118,16808,16809],{},"No surprise prompts."," The element can only request permission in response to a user click. No script can call it early or on a timer.",[40,16812,16814],{"id":16813},"origin-trial-numbers","Origin trial numbers",[11,16816,16817,16818,16825],{},"Before shipping, the concept ran as an origin trial (Chrome 126-143) under the name ",[51,16819,16822],{"href":16820,"rel":16821},"https://developer.chrome.com/docs/capabilities/web-apis/permission-element",[55],[15,16823,16824],{},"\u003Cpermission>",". Three companies published production data:",[59,16827,16828,16840],{},[62,16829,16830],{},[65,16831,16832,16835,16837],{},[68,16833,16834],{},"Company",[68,16836,2468],{},[68,16838,16839],{},"Result",[78,16841,16842,16853,16864],{},[65,16843,16844,16847,16850],{},[83,16845,16846],{},"Zoom",[83,16848,16849],{},"Location capture errors",[83,16851,16852],{},"-46.9%",[65,16854,16855,16858,16861],{},[83,16856,16857],{},"Immobiliare.it",[83,16859,16860],{},"Successful geolocation flows",[83,16862,16863],{},"+20%",[65,16865,16866,16869,16872],{},[83,16867,16868],{},"ZapImóveis",[83,16870,16871],{},"Permission recovery success rate",[83,16873,16874],{},"+54.4%",[11,16876,16877],{},"Users who had previously blocked location were recovering that permission at a 54.4% higher rate. The recovery UI - shown at the moment the user is actively trying to use a location feature - is far more effective than asking them to go find browser settings.",[40,16879,16881],{"id":16880},"attributes","Attributes",[11,16883,16884],{},"Four attributes control behavior:",[11,16886,16887,16892],{},[118,16888,16889],{},[15,16890,16891],{},"autolocate"," - if permission is already granted, the element attempts to retrieve location on load, without waiting for a click. Useful when you're confident the user wants their location (e.g., on a map page they explicitly navigated to).",[11,16894,16895,16900,16901,1335,16904,16907,16908,16911],{},[118,16896,16897],{},[15,16898,16899],{},"accuracymode"," - ",[15,16902,16903],{},"\"precise\"",[15,16905,16906],{},"\"approximate\"",". Maps to ",[15,16909,16910],{},"enableHighAccuracy"," in the JS API. Approximate uses cell tower and WiFi data; precise uses GPS when available, which is slower and drains battery.",[11,16913,16914,16918,16919,16922,16923,16925],{},[118,16915,16916],{},[15,16917,3912],{}," - continuous updates, equivalent to ",[15,16920,16921],{},"watchPosition()",". Fires the ",[15,16924,16579],{}," event each time the position changes.",[255,16927,16929],{"className":10399,"code":16928,"language":10400,"meta":260,"style":260},"\u003C!-- One-time high-accuracy request -->\n\u003Cgeolocation accuracymode=\"precise\" onlocation=\"onLocation(event)\">\u003C/geolocation>\n\n\u003C!-- Continuous tracking -->\n\u003Cgeolocation watch onlocation=\"onMove(event)\">\u003C/geolocation>\n\n\u003C!-- Auto-fetch if already permitted, else wait for click -->\n\u003Cgeolocation autolocate onlocation=\"onLocation(event)\">\u003C/geolocation>\n",[15,16930,16931,16936,16971,16975,16980,17006,17010,17015],{"__ignoreMap":260},[129,16932,16933],{"class":265,"line":266},[129,16934,16935],{"class":376},"\u003C!-- One-time high-accuracy request -->\n",[129,16937,16938,16940,16942,16945,16947,16949,16951,16953,16956,16958,16960,16963,16965,16967,16969],{"class":265,"line":297},[129,16939,3945],{"class":277},[129,16941,16504],{"class":1376},[129,16943,16944],{"class":269}," accuracymode",[129,16946,278],{"class":277},[129,16948,2258],{"class":277},[129,16950,16618],{"class":427},[129,16952,2258],{"class":277},[129,16954,16955],{"class":269}," onlocation",[129,16957,278],{"class":277},[129,16959,2258],{"class":277},[129,16961,16962],{"class":427},"onLocation(event)",[129,16964,2258],{"class":277},[129,16966,10517],{"class":277},[129,16968,16504],{"class":1376},[129,16970,4676],{"class":277},[129,16972,16973],{"class":265,"line":315},[129,16974,336],{"emptyLinePlaceholder":335},[129,16976,16977],{"class":265,"line":332},[129,16978,16979],{"class":376},"\u003C!-- Continuous tracking -->\n",[129,16981,16982,16984,16986,16989,16991,16993,16995,16998,17000,17002,17004],{"class":265,"line":339},[129,16983,3945],{"class":277},[129,16985,16504],{"class":1376},[129,16987,16988],{"class":269}," watch",[129,16990,16955],{"class":269},[129,16992,278],{"class":277},[129,16994,2258],{"class":277},[129,16996,16997],{"class":427},"onMove(event)",[129,16999,2258],{"class":277},[129,17001,10517],{"class":277},[129,17003,16504],{"class":1376},[129,17005,4676],{"class":277},[129,17007,17008],{"class":265,"line":356},[129,17009,336],{"emptyLinePlaceholder":335},[129,17011,17012],{"class":265,"line":651},[129,17013,17014],{"class":376},"\u003C!-- Auto-fetch if already permitted, else wait for click -->\n",[129,17016,17017,17019,17021,17024,17026,17028,17030,17032,17034,17036,17038],{"class":265,"line":657},[129,17018,3945],{"class":277},[129,17020,16504],{"class":1376},[129,17022,17023],{"class":269}," autolocate",[129,17025,16955],{"class":269},[129,17027,278],{"class":277},[129,17029,2258],{"class":277},[129,17031,16962],{"class":427},[129,17033,2258],{"class":277},[129,17035,10517],{"class":277},[129,17037,16504],{"class":1376},[129,17039,4676],{"class":277},[40,17041,17043],{"id":17042},"styling-constraints","Styling constraints",[11,17045,17046],{},"The element is browser-rendered. The browser enforces visual constraints to prevent deceptive UIs:",[1822,17048,17049,17055,17061,17067,17073],{},[1825,17050,17051,17054],{},[118,17052,17053],{},"Contrast",": button text must meet a 3:1 minimum contrast ratio",[1825,17056,17057,17060],{},[118,17058,17059],{},"Opacity",": must be 1 - can't make it transparent to overlay on other elements",[1825,17062,17063,17066],{},[118,17064,17065],{},"Size bounds",": minimum and maximum width, height, and font size are enforced",[1825,17068,17069,17072],{},[118,17070,17071],{},"Transforms",": limited to 2D translations and proportional scaling - can't rotate or flip it",[1825,17074,17075,17078],{},[118,17076,17077],{},"Negative margins",": disabled",[11,17080,17081,17082,17085],{},"These constraints eliminate a specific ",[118,17083,17084],{},"clickjacking"," attack: overlaying the element transparently over something else, so a click on \"Play video\" actually triggers a location permission request. The styling enforcement makes that attack surface disappear at the platform level.",[11,17087,17088,17089,17092],{},"You can still theme it. The element supports ",[15,17090,17091],{},":granted"," for state-aware styling:",[255,17094,17096],{"className":10730,"code":17095,"language":10731,"meta":260,"style":260},"geolocation {\n  background: #1e1e26;\n  border: 1px solid #2a2a36;\n  color: #c8cdd6;\n  border-radius: 8px;\n  padding: 8px 16px;\n}\n\ngeolocation:granted {\n  color: #4ade80;\n}\n",[15,17097,17098,17105,17121,17142,17156,17168,17182,17186,17190,17197,17210],{"__ignoreMap":260},[129,17099,17100,17103],{"class":265,"line":266},[129,17101,17102],{"class":273},"geolocation ",[129,17104,6455],{"class":277},[129,17106,17107,17110,17112,17115,17118],{"class":265,"line":297},[129,17108,17109],{"class":10761},"  background",[129,17111,1380],{"class":277},[129,17113,17114],{"class":277}," #",[129,17116,17117],{"class":273},"1e1e26",[129,17119,17120],{"class":277},";\n",[129,17122,17123,17126,17128,17131,17134,17137,17140],{"class":265,"line":315},[129,17124,17125],{"class":10761},"  border",[129,17127,1380],{"class":277},[129,17129,17130],{"class":290}," 1px",[129,17132,17133],{"class":273}," solid ",[129,17135,17136],{"class":277},"#",[129,17138,17139],{"class":273},"2a2a36",[129,17141,17120],{"class":277},[129,17143,17144,17147,17149,17151,17154],{"class":265,"line":332},[129,17145,17146],{"class":10761},"  color",[129,17148,1380],{"class":277},[129,17150,17114],{"class":277},[129,17152,17153],{"class":273},"c8cdd6",[129,17155,17120],{"class":277},[129,17157,17158,17161,17163,17166],{"class":265,"line":339},[129,17159,17160],{"class":10761},"  border-radius",[129,17162,1380],{"class":277},[129,17164,17165],{"class":290}," 8px",[129,17167,17120],{"class":277},[129,17169,17170,17173,17175,17177,17180],{"class":265,"line":356},[129,17171,17172],{"class":10761},"  padding",[129,17174,1380],{"class":277},[129,17176,17165],{"class":290},[129,17178,17179],{"class":290}," 16px",[129,17181,17120],{"class":277},[129,17183,17184],{"class":265,"line":651},[129,17185,1530],{"class":277},[129,17187,17188],{"class":265,"line":657},[129,17189,336],{"emptyLinePlaceholder":335},[129,17191,17192,17195],{"class":265,"line":669},[129,17193,17194],{"class":273},"geolocation:granted ",[129,17196,6455],{"class":277},[129,17198,17199,17201,17203,17205,17208],{"class":265,"line":693},[129,17200,17146],{"class":10761},[129,17202,1380],{"class":277},[129,17204,17114],{"class":277},[129,17206,17207],{"class":273},"4ade80",[129,17209,17120],{"class":277},[129,17211,17212],{"class":265,"line":712},[129,17213,1530],{"class":277},[40,17215,17217],{"id":17216},"progressive-enhancement","Progressive enhancement",[11,17219,17220,17222],{},[15,17221,16522],{}," is Chrome 144+ only. For every other browser, the fallback is clean because unknown HTML elements render their children without breaking:",[255,17224,17226],{"className":10399,"code":17225,"language":10400,"meta":260,"style":260},"\u003Cgeolocation onlocation=\"handleLocation(event)\">\n  \u003C!-- visible only in browsers that don't support \u003Cgeolocation> -->\n  \u003Cbutton onclick=\"navigator.geolocation.getCurrentPosition(handleLocation)\">\n    Use my location\n  \u003C/button>\n\u003C/geolocation>\n",[15,17227,17228,17246,17251,17285,17290,17299],{"__ignoreMap":260},[129,17229,17230,17232,17234,17236,17238,17240,17242,17244],{"class":265,"line":266},[129,17231,3945],{"class":277},[129,17233,16504],{"class":1376},[129,17235,16955],{"class":269},[129,17237,278],{"class":277},[129,17239,2258],{"class":277},[129,17241,16604],{"class":427},[129,17243,2258],{"class":277},[129,17245,4676],{"class":277},[129,17247,17248],{"class":265,"line":297},[129,17249,17250],{"class":376},"  \u003C!-- visible only in browsers that don't support \u003Cgeolocation> -->\n",[129,17252,17253,17255,17257,17260,17262,17264,17266,17268,17270,17272,17274,17276,17279,17281,17283],{"class":265,"line":315},[129,17254,12979],{"class":277},[129,17256,16072],{"class":1376},[129,17258,17259],{"class":269}," onclick",[129,17261,278],{"class":277},[129,17263,2258],{"class":277},[129,17265,15977],{"class":273},[129,17267,362],{"class":277},[129,17269,16504],{"class":273},[129,17271,362],{"class":277},[129,17273,16509],{"class":284},[129,17275,147],{"class":427},[129,17277,17278],{"class":273},"handleLocation",[129,17280,160],{"class":427},[129,17282,2258],{"class":277},[129,17284,4676],{"class":277},[129,17286,17287],{"class":265,"line":332},[129,17288,17289],{"class":273},"    Use my location\n",[129,17291,17292,17295,17297],{"class":265,"line":339},[129,17293,17294],{"class":277},"  \u003C/",[129,17296,16072],{"class":1376},[129,17298,4676],{"class":277},[129,17300,17301,17303,17305],{"class":265,"line":356},[129,17302,12609],{"class":277},[129,17304,16504],{"class":1376},[129,17306,4676],{"class":277},[11,17308,17309,17310,17312,17313,17316,17317,17319],{},"In Chrome 144+, the ",[15,17311,16522],{}," element renders as a browser button and the child ",[15,17314,17315],{},"\u003Cbutton>"," is hidden. In every other browser, the ",[15,17318,16522],{}," tag is invisible (unknown elements have no default display) and the fallback button shows.",[11,17321,17322],{},"Feature detection:",[255,17324,17326],{"className":16633,"code":17325,"language":16635,"meta":260,"style":260},"const supportsGeoElement = 'HTMLGeolocationElement' in window\n",[15,17327,17328],{"__ignoreMap":260},[129,17329,17330,17332,17335,17337,17339,17342,17344,17347],{"class":265,"line":266},[129,17331,270],{"class":269},[129,17333,17334],{"class":273}," supportsGeoElement ",[129,17336,278],{"class":277},[129,17338,4261],{"class":277},[129,17340,17341],{"class":427},"HTMLGeolocationElement",[129,17343,424],{"class":277},[129,17345,17346],{"class":277}," in",[129,17348,17349],{"class":273}," window\n",[11,17351,17352,17353,17358,17359,17362],{},"There's also a ",[51,17354,17357],{"href":17355,"rel":17356},"https://www.npmjs.com/package/geolocation-element",[55],"polyfill on npm"," that swaps unsupported instances with a custom element (",[15,17360,17361],{},"\u003Cgeo-location>",") backed by the standard JS API.",[17364,17365],"geolocation-demo",{},[40,17367,17369],{"id":17368},"in-a-nuxtvue-context","In a Nuxt/Vue context",[11,17371,17372,17374,17375,968,17377,17379,17380,17383],{},[15,17373,16522],{}," works in Vue templates since Vue passes through unknown elements as-is. One thing to handle: Vue doesn't know the ",[15,17376,16669],{},[15,17378,10921],{}," properties exist on the element, so access them via a template ref rather than ",[15,17381,17382],{},"v-model"," or binding:",[255,17385,17389],{"className":17386,"code":17387,"language":17388,"meta":260,"style":260},"language-vue shiki shiki-themes material-theme-lighter material-theme material-theme-palenight","\u003Cscript setup>\nconst geoEl = useTemplateRef('geoEl')\nconst coords = ref(null)\nconst geoError = ref(null)\n\nfunction onLocation() {\n  if (geoEl.value?.position) {\n    coords.value = geoEl.value.position.coords\n  } else {\n    geoError.value = geoEl.value?.error?.message\n  }\n}\n\u003C/script>\n\n\u003Ctemplate>\n  \u003Cgeolocation\n    ref=\"geoEl\"\n    @location=\"onLocation\"\n    accuracymode=\"precise\"\n  />\n\u003C/template>\n","vue",[15,17390,17391,17402,17425,17442,17459,17463,17474,17494,17520,17528,17554,17558,17562,17570,17574,17583,17589,17602,17616,17629,17634],{"__ignoreMap":260},[129,17392,17393,17395,17397,17400],{"class":265,"line":266},[129,17394,3945],{"class":277},[129,17396,10436],{"class":1376},[129,17398,17399],{"class":269}," setup",[129,17401,4676],{"class":277},[129,17403,17404,17406,17409,17411,17414,17416,17418,17421,17423],{"class":265,"line":297},[129,17405,270],{"class":269},[129,17407,17408],{"class":273}," geoEl ",[129,17410,278],{"class":277},[129,17412,17413],{"class":284}," useTemplateRef",[129,17415,147],{"class":273},[129,17417,424],{"class":277},[129,17419,17420],{"class":427},"geoEl",[129,17422,424],{"class":277},[129,17424,294],{"class":273},[129,17426,17427,17429,17432,17434,17436,17438,17440],{"class":265,"line":315},[129,17428,270],{"class":269},[129,17430,17431],{"class":273}," coords ",[129,17433,278],{"class":277},[129,17435,3894],{"class":284},[129,17437,147],{"class":273},[129,17439,3961],{"class":277},[129,17441,294],{"class":273},[129,17443,17444,17446,17449,17451,17453,17455,17457],{"class":265,"line":332},[129,17445,270],{"class":269},[129,17447,17448],{"class":273}," geoError ",[129,17450,278],{"class":277},[129,17452,3894],{"class":284},[129,17454,147],{"class":273},[129,17456,3961],{"class":277},[129,17458,294],{"class":273},[129,17460,17461],{"class":265,"line":339},[129,17462,336],{"emptyLinePlaceholder":335},[129,17464,17465,17467,17470,17472],{"class":265,"line":356},[129,17466,10102],{"class":269},[129,17468,17469],{"class":284}," onLocation",[129,17471,4140],{"class":277},[129,17473,1371],{"class":277},[129,17475,17476,17478,17480,17482,17484,17486,17488,17490,17492],{"class":265,"line":651},[129,17477,3998],{"class":2139},[129,17479,3984],{"class":1376},[129,17481,17420],{"class":273},[129,17483,362],{"class":277},[129,17485,8389],{"class":273},[129,17487,6059],{"class":277},[129,17489,16669],{"class":273},[129,17491,4005],{"class":1376},[129,17493,6455],{"class":277},[129,17495,17496,17499,17501,17503,17505,17508,17510,17512,17514,17516,17518],{"class":265,"line":657},[129,17497,17498],{"class":273},"    coords",[129,17500,362],{"class":277},[129,17502,8389],{"class":273},[129,17504,4745],{"class":277},[129,17506,17507],{"class":273}," geoEl",[129,17509,362],{"class":277},[129,17511,8389],{"class":273},[129,17513,362],{"class":277},[129,17515,16669],{"class":273},[129,17517,362],{"class":277},[129,17519,16707],{"class":273},[129,17521,17522,17524,17526],{"class":265,"line":669},[129,17523,4182],{"class":277},[129,17525,16719],{"class":2139},[129,17527,1371],{"class":277},[129,17529,17530,17533,17535,17537,17539,17541,17543,17545,17547,17549,17551],{"class":265,"line":693},[129,17531,17532],{"class":273},"    geoError",[129,17534,362],{"class":277},[129,17536,8389],{"class":273},[129,17538,4745],{"class":277},[129,17540,17507],{"class":273},[129,17542,362],{"class":277},[129,17544,8389],{"class":273},[129,17546,6059],{"class":277},[129,17548,10921],{"class":273},[129,17550,6059],{"class":277},[129,17552,17553],{"class":273},"message\n",[129,17555,17556],{"class":265,"line":712},[129,17557,1524],{"class":277},[129,17559,17560],{"class":265,"line":1521},[129,17561,1530],{"class":277},[129,17563,17564,17566,17568],{"class":265,"line":1527},[129,17565,12609],{"class":277},[129,17567,10436],{"class":1376},[129,17569,4676],{"class":277},[129,17571,17572],{"class":265,"line":2295},[129,17573,336],{"emptyLinePlaceholder":335},[129,17575,17576,17578,17581],{"class":265,"line":2300},[129,17577,3945],{"class":277},[129,17579,17580],{"class":1376},"template",[129,17582,4676],{"class":277},[129,17584,17585,17587],{"class":265,"line":2305},[129,17586,12979],{"class":277},[129,17588,16592],{"class":1376},[129,17590,17591,17594,17596,17598,17600],{"class":265,"line":2311},[129,17592,17593],{"class":269},"    ref",[129,17595,278],{"class":277},[129,17597,2258],{"class":277},[129,17599,17420],{"class":427},[129,17601,2292],{"class":277},[129,17603,17604,17607,17609,17611,17614],{"class":265,"line":2329},[129,17605,17606],{"class":269},"    @location",[129,17608,278],{"class":277},[129,17610,2258],{"class":277},[129,17612,17613],{"class":427},"onLocation",[129,17615,2292],{"class":277},[129,17617,17618,17621,17623,17625,17627],{"class":265,"line":2351},[129,17619,17620],{"class":269},"    accuracymode",[129,17622,278],{"class":277},[129,17624,2258],{"class":277},[129,17626,16618],{"class":427},[129,17628,2292],{"class":277},[129,17630,17631],{"class":265,"line":2387},[129,17632,17633],{"class":277},"  />\n",[129,17635,17636,17638,17640],{"class":265,"line":2392},[129,17637,12609],{"class":277},[129,17639,17580],{"class":1376},[129,17641,4676],{"class":277},[3576,17643,17644,17651],{},[11,17645,17646,17647,17650],{},"Add this to ",[15,17648,17649],{},"nuxt.config.ts"," to suppress the \"unknown component\" warning at build time without affecting anything else:",[255,17652,17654],{"className":3922,"code":17653,"language":3924,"meta":260,"style":260},"vue: {\n  compilerOptions: {\n    isCustomElement: tag => tag === 'geolocation'\n  }\n}\n",[15,17655,17656,17664,17673,17694,17698],{"__ignoreMap":260},[129,17657,17658,17660,17662],{"class":265,"line":266},[129,17659,17388],{"class":2161},[129,17661,1380],{"class":277},[129,17663,1371],{"class":277},[129,17665,17666,17669,17671],{"class":265,"line":297},[129,17667,17668],{"class":2161},"  compilerOptions",[129,17670,1380],{"class":277},[129,17672,1371],{"class":277},[129,17674,17675,17678,17680,17682,17684,17686,17688,17690,17692],{"class":265,"line":315},[129,17676,17677],{"class":2161},"    isCustomElement",[129,17679,1380],{"class":277},[129,17681,5674],{"class":452},[129,17683,456],{"class":269},[129,17685,5674],{"class":273},[129,17687,5116],{"class":277},[129,17689,4261],{"class":277},[129,17691,16504],{"class":427},[129,17693,4267],{"class":277},[129,17695,17696],{"class":265,"line":332},[129,17697,1524],{"class":277},[129,17699,17700],{"class":265,"line":339},[129,17701,1530],{"class":277},[11,17703,17704,17705,17707,17708,362],{},"For SSR: the element is a browser-only control and will only render on the client. Wrap it in ",[15,17706,8718],{}," if you're hitting hydration mismatches, or guard it with ",[15,17709,17710,17712,17714,17716,17718],{"className":257,"language":259,"style":260},[129,17711,2140],{"class":2139},[129,17713,362],{"class":277},[129,17715,15994],{"class":273},[129,17717,362],{"class":277},[129,17719,7465],{"class":273},[40,17721,8603,17723,17726],{"id":17722},"the-usermedia-companion",[15,17724,17725],{},"\u003Cusermedia>"," companion",[11,17728,17729,17730,17736],{},"Chrome 144 also launched an origin trial for ",[51,17731,17734],{"href":17732,"rel":17733},"https://developer.chrome.com/docs/capabilities/web-apis/usermedia-element",[55],[15,17735,17725],{}," - the same concept applied to camera and microphone access. Same user-initiated model, same declarative approach, same styling constraints. The pattern is consistent: permissions that require demonstrated user intent get their own HTML element instead of a JS API that sites call whenever they feel like it.",[2001,17738],{},[11,17740,17741],{},"The JS Geolocation API isn't going away but the origin trial data is hard to argue with: when the user initiates the permission instead of a script doing it on their behalf, they actually grant it more often and recover from blocks more often. The fix was always behavioral, not technical.",[2026,17743,17744],{},"html pre.shiki code .sTEyZ, html code.shiki .sTEyZ{--shiki-light:#90A4AE;--shiki-default:#EEFFFF;--shiki-dark:#BABED8}html pre.shiki code .sMK4o, html code.shiki .sMK4o{--shiki-light:#39ADB5;--shiki-default:#89DDFF;--shiki-dark:#89DDFF}html pre.shiki code .s2Zo4, html code.shiki .s2Zo4{--shiki-light:#6182B8;--shiki-default:#82AAFF;--shiki-dark:#82AAFF}html .light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html.light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .swJcz, html code.shiki .swJcz{--shiki-light:#E53935;--shiki-default:#F07178;--shiki-dark:#F07178}html pre.shiki code .spNyl, html code.shiki .spNyl{--shiki-light:#9C3EDA;--shiki-default:#C792EA;--shiki-dark:#C792EA}html pre.shiki code .sfazB, html code.shiki .sfazB{--shiki-light:#91B859;--shiki-default:#C3E88D;--shiki-dark:#C3E88D}html pre.shiki code .sHdIc, html code.shiki .sHdIc{--shiki-light:#90A4AE;--shiki-light-font-style:italic;--shiki-default:#EEFFFF;--shiki-default-font-style:italic;--shiki-dark:#BABED8;--shiki-dark-font-style:italic}html pre.shiki code .s7zQu, html code.shiki .s7zQu{--shiki-light:#39ADB5;--shiki-light-font-style:italic;--shiki-default:#89DDFF;--shiki-default-font-style:italic;--shiki-dark:#89DDFF;--shiki-dark-font-style:italic}html pre.shiki code .sHwdD, html code.shiki .sHwdD{--shiki-light:#90A4AE;--shiki-light-font-style:italic;--shiki-default:#546E7A;--shiki-default-font-style:italic;--shiki-dark:#676E95;--shiki-dark-font-style:italic}html pre.shiki code .sqsOY, html code.shiki .sqsOY{--shiki-light:#8796B0;--shiki-default:#B2CCD6;--shiki-dark:#B2CCD6}html pre.shiki code .sbssI, html code.shiki .sbssI{--shiki-light:#F76D47;--shiki-default:#F78C6C;--shiki-dark:#F78C6C}html pre.shiki code .sBMFI, html code.shiki .sBMFI{--shiki-light:#E2931D;--shiki-default:#FFCB6B;--shiki-dark:#FFCB6B}",{"title":260,"searchDepth":297,"depth":297,"links":17746},[17747,17749,17751,17752,17753,17754,17755,17756],{"id":16526,"depth":297,"text":17748},"The actual problem with getCurrentPosition()",{"id":16569,"depth":297,"text":17750},"What \u003Cgeolocation> actually does",{"id":16813,"depth":297,"text":16814},{"id":16880,"depth":297,"text":16881},{"id":17042,"depth":297,"text":17043},{"id":17216,"depth":297,"text":17217},{"id":17368,"depth":297,"text":17369},{"id":17722,"depth":297,"text":17757},"The \u003Cusermedia> companion","Chrome 144 ships a declarative HTML element for location access. Instead of a script firing a permission prompt, the user clicks a browser-controlled button. The conversion numbers from the origin trial are hard to ignore.",{},"/blog/geolocation-element",{"title":16490,"description":17758},"blog/geolocation-element",[16485,17764,17765,12632,17766],"HTML","UX","Web","JT5TjQrOwE_DtQ1QTm9ftsEMonFwbKH-1biqHtQejoI",{"id":17769,"title":17770,"body":17771,"cover":2042,"date":8623,"description":21447,"extension":2045,"meta":21448,"navigation":335,"path":21449,"readingTime":712,"seo":21450,"stem":21451,"tags":21452,"__hash__":21454},"blog/blog/nuxt-system-patterns.md","System patterns in Nuxt",{"type":8,"value":17772,"toc":21439},[17773,17781,17784,17787,17804,17807,17810,17814,17817,17945,17952,17958,17967,18329,18338,18779,18782,18786,18789,18795,19003,19006,19106,19113,19239,19242,19319,19325,19397,19407,19411,19414,19425,19528,19531,19925,19928,19957,19969,20065,20069,20072,20079,20493,20496,20499,20613,20628,20632,20635,20641,20853,20856,21345,21360,21363,21367,21370,21421,21424,21431,21433,21436],[17774,17775],"x-card",{"url":17776,"authorName":17777,"handle":17778,"text":17779,"dateText":17780},"https://x.com/404G_/status/2027267741225533882","NX","notfoundXG","Want to think like a senior engineer?\n\nStop building apps.\n\nStart building systems:\n• Queue system\n• Caching strategy\n• Retry mechanism\n• Rate limiting\n• Feature flags\n\nApps impress beginners.\nSystems impress engineers.\n\nBookmark this for reference.","February 27, 2026",[11,17782,17783],{},"That tweet is half right. The half it gets right: most developers design exclusively for the happy path. User clicks button, server responds, data renders. Done. The half it gets wrong: you're always building a system - you're just deciding whether to design it intentionally or discover it on fire at 2am.",[11,17785,17786],{},"The difference between an \"app\" and a \"system\" it's whether you've answered the failure questions:",[1822,17788,17789,17792,17795,17798,17801],{},[1825,17790,17791],{},"What happens when the email service is down for 20 minutes?",[1825,17793,17794],{},"What happens when your database is crushed by N+1 queries at peak traffic?",[1825,17796,17797],{},"What happens when a third-party API returns a 503?",[1825,17799,17800],{},"What happens when a bot decides to hit your signup endpoint 500 times per minute?",[1825,17802,17803],{},"What happens when you need to change behavior for 5% of users without deploying?",[11,17805,17806],{},"Scheduled maintenance windows, traffic spikes, flaky dependencies, scrapers, changing product requirements - every app with real users hits all of these at some point.",[11,17808,17809],{},"You can handle all of it in Nuxt. Nitro (Nuxt's server engine) ships with the primitives you need: a storage layer, server tasks, event hooks, and a middleware system. For scale, you plug in Redis and BullMQ. But you can start without them.",[40,17811,17813],{"id":17812},"queue-system","Queue system",[11,17815,17816],{},"The naive pattern for sending a welcome email:",[255,17818,17820],{"className":3922,"code":17819,"filename":8014,"language":3924,"meta":260,"style":260},"export default defineEventHandler(async (event) => {\n  const { email, name } = await readBody(event)\n  const user = await db.users.create({ email, name })\n  await emailService.sendWelcome(user) // blocks the response\n  return { success: true }\n})\n",[15,17821,17822,17844,17870,17904,17925,17939],{"__ignoreMap":260},[129,17823,17824,17826,17828,17830,17832,17834,17836,17838,17840,17842],{"class":265,"line":266},[129,17825,4050],{"class":2139},[129,17827,4053],{"class":2139},[129,17829,4503],{"class":284},[129,17831,147],{"class":273},[129,17833,4508],{"class":269},[129,17835,3984],{"class":277},[129,17837,4100],{"class":452},[129,17839,160],{"class":277},[129,17841,456],{"class":269},[129,17843,1371],{"class":277},[129,17845,17846,17848,17850,17852,17854,17856,17858,17860,17862,17864,17866,17868],{"class":265,"line":297},[129,17847,5076],{"class":269},[129,17849,1416],{"class":277},[129,17851,8025],{"class":273},[129,17853,1015],{"class":277},[129,17855,5407],{"class":273},[129,17857,4255],{"class":277},[129,17859,4745],{"class":277},[129,17861,4779],{"class":2139},[129,17863,5264],{"class":284},[129,17865,147],{"class":1376},[129,17867,4100],{"class":273},[129,17869,294],{"class":1376},[129,17871,17872,17874,17876,17878,17880,17882,17884,17886,17888,17890,17892,17894,17896,17898,17900,17902],{"class":265,"line":315},[129,17873,5076],{"class":269},[129,17875,6462],{"class":273},[129,17877,4745],{"class":277},[129,17879,4779],{"class":2139},[129,17881,4479],{"class":273},[129,17883,362],{"class":277},[129,17885,4537],{"class":273},[129,17887,362],{"class":277},[129,17889,4791],{"class":284},[129,17891,147],{"class":1376},[129,17893,4796],{"class":277},[129,17895,8025],{"class":273},[129,17897,1015],{"class":277},[129,17899,5407],{"class":273},[129,17901,4255],{"class":277},[129,17903,294],{"class":1376},[129,17905,17906,17908,17911,17913,17916,17918,17920,17922],{"class":265,"line":332},[129,17907,8101],{"class":2139},[129,17909,17910],{"class":273}," emailService",[129,17912,362],{"class":277},[129,17914,17915],{"class":284},"sendWelcome",[129,17917,147],{"class":1376},[129,17919,6335],{"class":273},[129,17921,4005],{"class":1376},[129,17923,17924],{"class":376},"// blocks the response\n",[129,17926,17927,17929,17931,17933,17935,17937],{"class":265,"line":339},[129,17928,4520],{"class":2139},[129,17930,1416],{"class":277},[129,17932,8188],{"class":1376},[129,17934,1380],{"class":277},[129,17936,4823],{"class":4822},[129,17938,1476],{"class":277},[129,17940,17941,17943],{"class":265,"line":356},[129,17942,4028],{"class":277},[129,17944,294],{"class":273},[11,17946,17947,17948,17951],{},"If ",[15,17949,17950],{},"emailService.sendWelcome"," takes 800ms (it will), your API response takes 800ms. If the email service is down, registration fails. If it times out, the user never gets the email and you have no record of the failure.",[11,17953,561,17954,17957],{},[118,17955,17956],{},"job queue"," decouples the trigger (user registered) from the work (send email). The API responds immediately; the worker processes asynchronously.",[11,17959,17960,17961,17966],{},"For simple cases - scheduled jobs, lightweight background work - Nitro's built-in ",[51,17962,17965],{"href":17963,"rel":17964},"https://nitro.build/guide/tasks",[55],"server tasks"," are enough:",[17968,17969,17970,18128],"code-group",{},[255,17971,17974],{"className":3922,"code":17972,"filename":17973,"language":3924,"meta":260,"style":260},"export default defineTask({\n  meta: {\n    name: 'email:welcome',\n    description: 'Send welcome email to new user',\n  },\n  async run({ payload }: { payload: { userId: string } }) {\n    const user = await db.users.findById(payload.userId)\n    await emailService.sendWelcome(user)\n  },\n})\n","server/tasks/email/welcome.ts",[15,17975,17976,17989,17998,18014,18030,18034,18069,18102,18118,18122],{"__ignoreMap":260},[129,17977,17978,17980,17982,17985,17987],{"class":265,"line":266},[129,17979,4050],{"class":2139},[129,17981,4053],{"class":2139},[129,17983,17984],{"class":284}," defineTask",[129,17986,147],{"class":273},[129,17988,6455],{"class":277},[129,17990,17991,17994,17996],{"class":265,"line":297},[129,17992,17993],{"class":1376},"  meta",[129,17995,1380],{"class":277},[129,17997,1371],{"class":277},[129,17999,18000,18003,18005,18007,18010,18012],{"class":265,"line":315},[129,18001,18002],{"class":1376},"    name",[129,18004,1380],{"class":277},[129,18006,4261],{"class":277},[129,18008,18009],{"class":427},"email:welcome",[129,18011,424],{"class":277},[129,18013,1386],{"class":277},[129,18015,18016,18019,18021,18023,18026,18028],{"class":265,"line":332},[129,18017,18018],{"class":1376},"    description",[129,18020,1380],{"class":277},[129,18022,4261],{"class":277},[129,18024,18025],{"class":427},"Send welcome email to new user",[129,18027,424],{"class":277},[129,18029,1386],{"class":277},[129,18031,18032],{"class":265,"line":339},[129,18033,1481],{"class":277},[129,18035,18036,18038,18041,18043,18046,18048,18050,18052,18054,18056,18059,18061,18063,18065,18067],{"class":265,"line":356},[129,18037,4703],{"class":269},[129,18039,18040],{"class":1376}," run",[129,18042,7507],{"class":277},[129,18044,18045],{"class":452}," payload",[129,18047,7520],{"class":277},[129,18049,1416],{"class":277},[129,18051,18045],{"class":1376},[129,18053,1380],{"class":277},[129,18055,1416],{"class":277},[129,18057,18058],{"class":1376}," userId",[129,18060,1380],{"class":277},[129,18062,4622],{"class":2161},[129,18064,4255],{"class":277},[129,18066,7547],{"class":277},[129,18068,1371],{"class":277},[129,18070,18071,18073,18075,18077,18079,18081,18083,18085,18087,18090,18092,18095,18097,18100],{"class":265,"line":651},[129,18072,4739],{"class":269},[129,18074,6462],{"class":273},[129,18076,4745],{"class":277},[129,18078,4779],{"class":2139},[129,18080,4479],{"class":273},[129,18082,362],{"class":277},[129,18084,4537],{"class":273},[129,18086,362],{"class":277},[129,18088,18089],{"class":284},"findById",[129,18091,147],{"class":1376},[129,18093,18094],{"class":273},"payload",[129,18096,362],{"class":277},[129,18098,18099],{"class":273},"userId",[129,18101,294],{"class":1376},[129,18103,18104,18106,18108,18110,18112,18114,18116],{"class":265,"line":657},[129,18105,4902],{"class":2139},[129,18107,17910],{"class":273},[129,18109,362],{"class":277},[129,18111,17915],{"class":284},[129,18113,147],{"class":1376},[129,18115,6335],{"class":273},[129,18117,294],{"class":1376},[129,18119,18120],{"class":265,"line":669},[129,18121,1481],{"class":277},[129,18123,18124,18126],{"class":265,"line":693},[129,18125,4028],{"class":277},[129,18127,294],{"class":273},[255,18129,18131],{"className":3922,"code":18130,"filename":8014,"language":3924,"meta":260,"style":260},"export default defineEventHandler(async (event) => {\n  const { email, name } = await readBody(event)\n  const user = await db.users.create({ email, name })\n\n  // Returns immediately - task runs in background\n  await $fetch('/api/_nitro/tasks/run', {\n    method: 'POST',\n    body: { task: 'email:welcome', payload: { userId: user.id } },\n  })\n\n  return { success: true }\n})\n",[15,18132,18133,18155,18181,18215,18219,18224,18243,18258,18299,18305,18309,18323],{"__ignoreMap":260},[129,18134,18135,18137,18139,18141,18143,18145,18147,18149,18151,18153],{"class":265,"line":266},[129,18136,4050],{"class":2139},[129,18138,4053],{"class":2139},[129,18140,4503],{"class":284},[129,18142,147],{"class":273},[129,18144,4508],{"class":269},[129,18146,3984],{"class":277},[129,18148,4100],{"class":452},[129,18150,160],{"class":277},[129,18152,456],{"class":269},[129,18154,1371],{"class":277},[129,18156,18157,18159,18161,18163,18165,18167,18169,18171,18173,18175,18177,18179],{"class":265,"line":297},[129,18158,5076],{"class":269},[129,18160,1416],{"class":277},[129,18162,8025],{"class":273},[129,18164,1015],{"class":277},[129,18166,5407],{"class":273},[129,18168,4255],{"class":277},[129,18170,4745],{"class":277},[129,18172,4779],{"class":2139},[129,18174,5264],{"class":284},[129,18176,147],{"class":1376},[129,18178,4100],{"class":273},[129,18180,294],{"class":1376},[129,18182,18183,18185,18187,18189,18191,18193,18195,18197,18199,18201,18203,18205,18207,18209,18211,18213],{"class":265,"line":315},[129,18184,5076],{"class":269},[129,18186,6462],{"class":273},[129,18188,4745],{"class":277},[129,18190,4779],{"class":2139},[129,18192,4479],{"class":273},[129,18194,362],{"class":277},[129,18196,4537],{"class":273},[129,18198,362],{"class":277},[129,18200,4791],{"class":284},[129,18202,147],{"class":1376},[129,18204,4796],{"class":277},[129,18206,8025],{"class":273},[129,18208,1015],{"class":277},[129,18210,5407],{"class":273},[129,18212,4255],{"class":277},[129,18214,294],{"class":1376},[129,18216,18217],{"class":265,"line":332},[129,18218,336],{"emptyLinePlaceholder":335},[129,18220,18221],{"class":265,"line":339},[129,18222,18223],{"class":376},"  // Returns immediately - task runs in background\n",[129,18225,18226,18228,18230,18232,18234,18237,18239,18241],{"class":265,"line":356},[129,18227,8101],{"class":2139},[129,18229,8288],{"class":284},[129,18231,147],{"class":1376},[129,18233,424],{"class":277},[129,18235,18236],{"class":427},"/api/_nitro/tasks/run",[129,18238,424],{"class":277},[129,18240,1015],{"class":277},[129,18242,1371],{"class":277},[129,18244,18245,18248,18250,18252,18254,18256],{"class":265,"line":651},[129,18246,18247],{"class":1376},"    method",[129,18249,1380],{"class":277},[129,18251,4261],{"class":277},[129,18253,3142],{"class":427},[129,18255,424],{"class":277},[129,18257,1386],{"class":277},[129,18259,18260,18263,18265,18267,18269,18271,18273,18275,18277,18279,18281,18283,18285,18287,18289,18291,18293,18295,18297],{"class":265,"line":657},[129,18261,18262],{"class":1376},"    body",[129,18264,1380],{"class":277},[129,18266,1416],{"class":277},[129,18268,15677],{"class":1376},[129,18270,1380],{"class":277},[129,18272,4261],{"class":277},[129,18274,18009],{"class":427},[129,18276,424],{"class":277},[129,18278,1015],{"class":277},[129,18280,18045],{"class":1376},[129,18282,1380],{"class":277},[129,18284,1416],{"class":277},[129,18286,18058],{"class":1376},[129,18288,1380],{"class":277},[129,18290,6462],{"class":273},[129,18292,362],{"class":277},[129,18294,3190],{"class":273},[129,18296,4255],{"class":277},[129,18298,1444],{"class":277},[129,18300,18301,18303],{"class":265,"line":669},[129,18302,4182],{"class":277},[129,18304,294],{"class":1376},[129,18306,18307],{"class":265,"line":693},[129,18308,336],{"emptyLinePlaceholder":335},[129,18310,18311,18313,18315,18317,18319,18321],{"class":265,"line":712},[129,18312,4520],{"class":2139},[129,18314,1416],{"class":277},[129,18316,8188],{"class":1376},[129,18318,1380],{"class":277},[129,18320,4823],{"class":4822},[129,18322,1476],{"class":277},[129,18324,18325,18327],{"class":265,"line":1521},[129,18326,4028],{"class":277},[129,18328,294],{"class":273},[11,18330,18331,18332,18337],{},"For production with real durability requirements - retries on failure, priority queues, concurrency control, dead letter queues - use ",[51,18333,18336],{"href":18334,"rel":18335},"https://docs.bullmq.io/",[55],"BullMQ"," with Redis:",[17968,18339,18340,18590],{},[255,18341,18344],{"className":3922,"code":18342,"filename":18343,"language":3924,"meta":260,"style":260},"import { Queue, Worker } from 'bullmq'\nimport { redis } from './redis'\n\nexport const emailQueue = new Queue('emails', { connection: redis })\n\n// Worker defined separately, can run in a different process\nnew Worker('emails', async (job) => {\n  if (job.name === 'welcome') {\n    await emailService.sendWelcome(job.data)\n  }\n}, {\n  connection: redis,\n  attempts: 3,           // retry 3 times before moving to dead letter\n  backoff: { type: 'exponential', delay: 1000 },\n})\n","server/lib/queue.ts",[15,18345,18346,18371,18391,18395,18434,18438,18443,18472,18497,18518,18522,18528,18539,18553,18584],{"__ignoreMap":260},[129,18347,18348,18350,18352,18355,18357,18360,18362,18364,18366,18369],{"class":265,"line":266},[129,18349,2140],{"class":2139},[129,18351,1416],{"class":277},[129,18353,18354],{"class":273}," Queue",[129,18356,1015],{"class":277},[129,18358,18359],{"class":273}," Worker",[129,18361,4255],{"class":277},[129,18363,4258],{"class":2139},[129,18365,4261],{"class":277},[129,18367,18368],{"class":427},"bullmq",[129,18370,4267],{"class":277},[129,18372,18373,18375,18377,18380,18382,18384,18386,18389],{"class":265,"line":297},[129,18374,2140],{"class":2139},[129,18376,1416],{"class":277},[129,18378,18379],{"class":273}," redis",[129,18381,4255],{"class":277},[129,18383,4258],{"class":2139},[129,18385,4261],{"class":277},[129,18387,18388],{"class":427},"./redis",[129,18390,4267],{"class":277},[129,18392,18393],{"class":265,"line":315},[129,18394,336],{"emptyLinePlaceholder":335},[129,18396,18397,18399,18401,18404,18406,18408,18410,18412,18414,18416,18418,18420,18422,18425,18427,18430,18432],{"class":265,"line":332},[129,18398,4050],{"class":2139},[129,18400,4456],{"class":269},[129,18402,18403],{"class":273}," emailQueue ",[129,18405,278],{"class":277},[129,18407,281],{"class":277},[129,18409,18354],{"class":284},[129,18411,147],{"class":273},[129,18413,424],{"class":277},[129,18415,7563],{"class":427},[129,18417,424],{"class":277},[129,18419,1015],{"class":277},[129,18421,1416],{"class":277},[129,18423,18424],{"class":1376}," connection",[129,18426,1380],{"class":277},[129,18428,18429],{"class":273}," redis ",[129,18431,4028],{"class":277},[129,18433,294],{"class":273},[129,18435,18436],{"class":265,"line":339},[129,18437,336],{"emptyLinePlaceholder":335},[129,18439,18440],{"class":265,"line":356},[129,18441,18442],{"class":376},"// Worker defined separately, can run in a different process\n",[129,18444,18445,18447,18449,18451,18453,18455,18457,18459,18461,18463,18466,18468,18470],{"class":265,"line":651},[129,18446,4134],{"class":277},[129,18448,18359],{"class":284},[129,18450,147],{"class":273},[129,18452,424],{"class":277},[129,18454,7563],{"class":427},[129,18456,424],{"class":277},[129,18458,1015],{"class":277},[129,18460,6020],{"class":269},[129,18462,3984],{"class":277},[129,18464,18465],{"class":452},"job",[129,18467,160],{"class":277},[129,18469,456],{"class":269},[129,18471,1371],{"class":277},[129,18473,18474,18476,18478,18480,18482,18484,18486,18488,18491,18493,18495],{"class":265,"line":657},[129,18475,3998],{"class":2139},[129,18477,3984],{"class":1376},[129,18479,18465],{"class":273},[129,18481,362],{"class":277},[129,18483,8164],{"class":273},[129,18485,5116],{"class":277},[129,18487,4261],{"class":277},[129,18489,18490],{"class":427},"welcome",[129,18492,424],{"class":277},[129,18494,4005],{"class":1376},[129,18496,6455],{"class":277},[129,18498,18499,18501,18503,18505,18507,18509,18511,18513,18516],{"class":265,"line":669},[129,18500,4902],{"class":2139},[129,18502,17910],{"class":273},[129,18504,362],{"class":277},[129,18506,17915],{"class":284},[129,18508,147],{"class":1376},[129,18510,18465],{"class":273},[129,18512,362],{"class":277},[129,18514,18515],{"class":273},"data",[129,18517,294],{"class":1376},[129,18519,18520],{"class":265,"line":693},[129,18521,1524],{"class":277},[129,18523,18524,18526],{"class":265,"line":712},[129,18525,6625],{"class":277},[129,18527,1371],{"class":277},[129,18529,18530,18533,18535,18537],{"class":265,"line":1521},[129,18531,18532],{"class":1376},"  connection",[129,18534,1380],{"class":277},[129,18536,18379],{"class":273},[129,18538,1386],{"class":277},[129,18540,18541,18544,18546,18548,18550],{"class":265,"line":1527},[129,18542,18543],{"class":1376},"  attempts",[129,18545,1380],{"class":277},[129,18547,1018],{"class":290},[129,18549,1015],{"class":277},[129,18551,18552],{"class":376},"           // retry 3 times before moving to dead letter\n",[129,18554,18555,18558,18560,18562,18564,18566,18568,18571,18573,18575,18578,18580,18582],{"class":265,"line":2295},[129,18556,18557],{"class":1376},"  backoff",[129,18559,1380],{"class":277},[129,18561,1416],{"class":277},[129,18563,11316],{"class":1376},[129,18565,1380],{"class":277},[129,18567,4261],{"class":277},[129,18569,18570],{"class":427},"exponential",[129,18572,424],{"class":277},[129,18574,1015],{"class":277},[129,18576,18577],{"class":1376}," delay",[129,18579,1380],{"class":277},[129,18581,9568],{"class":290},[129,18583,1444],{"class":277},[129,18585,18586,18588],{"class":265,"line":2300},[129,18587,4028],{"class":277},[129,18589,294],{"class":273},[255,18591,18593],{"className":3922,"code":18592,"filename":8014,"language":3924,"meta":260,"style":260},"export default defineEventHandler(async (event) => {\n  const { email, name } = await readBody(event)\n  const user = await db.users.create({ email, name })\n\n  // Non-blocking, durable - job survives server restarts\n  await emailQueue.add('welcome', { userId: user.id }, {\n    removeOnComplete: 100, // keep last 100 completed jobs for debugging\n    removeOnFail: 1000,\n  })\n\n  return { success: true }\n})\n",[15,18594,18595,18617,18643,18677,18681,18686,18723,18738,18749,18755,18759,18773],{"__ignoreMap":260},[129,18596,18597,18599,18601,18603,18605,18607,18609,18611,18613,18615],{"class":265,"line":266},[129,18598,4050],{"class":2139},[129,18600,4053],{"class":2139},[129,18602,4503],{"class":284},[129,18604,147],{"class":273},[129,18606,4508],{"class":269},[129,18608,3984],{"class":277},[129,18610,4100],{"class":452},[129,18612,160],{"class":277},[129,18614,456],{"class":269},[129,18616,1371],{"class":277},[129,18618,18619,18621,18623,18625,18627,18629,18631,18633,18635,18637,18639,18641],{"class":265,"line":297},[129,18620,5076],{"class":269},[129,18622,1416],{"class":277},[129,18624,8025],{"class":273},[129,18626,1015],{"class":277},[129,18628,5407],{"class":273},[129,18630,4255],{"class":277},[129,18632,4745],{"class":277},[129,18634,4779],{"class":2139},[129,18636,5264],{"class":284},[129,18638,147],{"class":1376},[129,18640,4100],{"class":273},[129,18642,294],{"class":1376},[129,18644,18645,18647,18649,18651,18653,18655,18657,18659,18661,18663,18665,18667,18669,18671,18673,18675],{"class":265,"line":315},[129,18646,5076],{"class":269},[129,18648,6462],{"class":273},[129,18650,4745],{"class":277},[129,18652,4779],{"class":2139},[129,18654,4479],{"class":273},[129,18656,362],{"class":277},[129,18658,4537],{"class":273},[129,18660,362],{"class":277},[129,18662,4791],{"class":284},[129,18664,147],{"class":1376},[129,18666,4796],{"class":277},[129,18668,8025],{"class":273},[129,18670,1015],{"class":277},[129,18672,5407],{"class":273},[129,18674,4255],{"class":277},[129,18676,294],{"class":1376},[129,18678,18679],{"class":265,"line":332},[129,18680,336],{"emptyLinePlaceholder":335},[129,18682,18683],{"class":265,"line":339},[129,18684,18685],{"class":376},"  // Non-blocking, durable - job survives server restarts\n",[129,18687,18688,18690,18693,18695,18697,18699,18701,18703,18705,18707,18709,18711,18713,18715,18717,18719,18721],{"class":265,"line":356},[129,18689,8101],{"class":2139},[129,18691,18692],{"class":273}," emailQueue",[129,18694,362],{"class":277},[129,18696,14141],{"class":284},[129,18698,147],{"class":1376},[129,18700,424],{"class":277},[129,18702,18490],{"class":427},[129,18704,424],{"class":277},[129,18706,1015],{"class":277},[129,18708,1416],{"class":277},[129,18710,18058],{"class":1376},[129,18712,1380],{"class":277},[129,18714,6462],{"class":273},[129,18716,362],{"class":277},[129,18718,3190],{"class":273},[129,18720,9058],{"class":277},[129,18722,1371],{"class":277},[129,18724,18725,18728,18730,18733,18735],{"class":265,"line":651},[129,18726,18727],{"class":1376},"    removeOnComplete",[129,18729,1380],{"class":277},[129,18731,18732],{"class":290}," 100",[129,18734,1015],{"class":277},[129,18736,18737],{"class":376}," // keep last 100 completed jobs for debugging\n",[129,18739,18740,18743,18745,18747],{"class":265,"line":657},[129,18741,18742],{"class":1376},"    removeOnFail",[129,18744,1380],{"class":277},[129,18746,9568],{"class":290},[129,18748,1386],{"class":277},[129,18750,18751,18753],{"class":265,"line":669},[129,18752,4182],{"class":277},[129,18754,294],{"class":1376},[129,18756,18757],{"class":265,"line":693},[129,18758,336],{"emptyLinePlaceholder":335},[129,18760,18761,18763,18765,18767,18769,18771],{"class":265,"line":712},[129,18762,4520],{"class":2139},[129,18764,1416],{"class":277},[129,18766,8188],{"class":1376},[129,18768,1380],{"class":277},[129,18770,4823],{"class":4822},[129,18772,1476],{"class":277},[129,18774,18775,18777],{"class":265,"line":1521},[129,18776,4028],{"class":277},[129,18778,294],{"class":273},[11,18780,18781],{},"BullMQ persists jobs to Redis. If your server crashes before processing, the job is still there when it restarts. That's the difference between \"I sent an email request\" and \"I know the email will be sent.\"",[40,18783,18785],{"id":18784},"caching-strategy","Caching strategy",[11,18787,18788],{},"Every database call that returns the same data twice is a candidate for a cache. Not because databases are slow, but because they have a finite capacity and your app's performance ceiling is that capacity divided by concurrent queries.",[11,18790,18791,18792,18794],{},"Nitro's ",[15,18793,3853],{}," wraps an API route with automatic cache-aside logic using whatever storage driver you configure:",[255,18796,18799],{"className":3922,"code":18797,"filename":18798,"language":3924,"meta":260,"style":260},"export default defineCachedEventHandler(async (event) => {\n  // This query only runs on cache miss\n  return await db.products.findMany({ where: { active: true } })\n}, {\n  maxAge: 60 * 5,    // 5 minutes\n  name: 'products',\n  // Cache key can vary by query params, user region, etc.\n  getKey: (event) => {\n    const query = getQuery(event)\n    return `products:${query.category ?? 'all'}:${query.page ?? 1}`\n  },\n})\n","server/api/products.get.ts",[15,18800,18801,18823,18828,18868,18874,18894,18909,18914,18931,18947,18993,18997],{"__ignoreMap":260},[129,18802,18803,18805,18807,18809,18811,18813,18815,18817,18819,18821],{"class":265,"line":266},[129,18804,4050],{"class":2139},[129,18806,4053],{"class":2139},[129,18808,6575],{"class":284},[129,18810,147],{"class":273},[129,18812,4508],{"class":269},[129,18814,3984],{"class":277},[129,18816,4100],{"class":452},[129,18818,160],{"class":277},[129,18820,456],{"class":269},[129,18822,1371],{"class":277},[129,18824,18825],{"class":265,"line":297},[129,18826,18827],{"class":376},"  // This query only runs on cache miss\n",[129,18829,18830,18832,18834,18836,18838,18840,18842,18845,18847,18849,18852,18854,18856,18858,18860,18862,18864,18866],{"class":265,"line":315},[129,18831,4520],{"class":2139},[129,18833,4779],{"class":2139},[129,18835,4479],{"class":273},[129,18837,362],{"class":277},[129,18839,6612],{"class":273},[129,18841,362],{"class":277},[129,18843,18844],{"class":284},"findMany",[129,18846,147],{"class":1376},[129,18848,4796],{"class":277},[129,18850,18851],{"class":1376}," where",[129,18853,1380],{"class":277},[129,18855,1416],{"class":277},[129,18857,16025],{"class":1376},[129,18859,1380],{"class":277},[129,18861,4823],{"class":4822},[129,18863,4255],{"class":277},[129,18865,4255],{"class":277},[129,18867,294],{"class":1376},[129,18869,18870,18872],{"class":265,"line":332},[129,18871,6625],{"class":277},[129,18873,1371],{"class":277},[129,18875,18876,18879,18881,18884,18887,18889,18891],{"class":265,"line":339},[129,18877,18878],{"class":1376},"  maxAge",[129,18880,1380],{"class":277},[129,18882,18883],{"class":290}," 60",[129,18885,18886],{"class":277}," *",[129,18888,1043],{"class":290},[129,18890,1015],{"class":277},[129,18892,18893],{"class":376},"    // 5 minutes\n",[129,18895,18896,18899,18901,18903,18905,18907],{"class":265,"line":356},[129,18897,18898],{"class":1376},"  name",[129,18900,1380],{"class":277},[129,18902,4261],{"class":277},[129,18904,6612],{"class":427},[129,18906,424],{"class":277},[129,18908,1386],{"class":277},[129,18910,18911],{"class":265,"line":651},[129,18912,18913],{"class":376},"  // Cache key can vary by query params, user region, etc.\n",[129,18915,18916,18919,18921,18923,18925,18927,18929],{"class":265,"line":657},[129,18917,18918],{"class":284},"  getKey",[129,18920,1380],{"class":277},[129,18922,3984],{"class":277},[129,18924,4100],{"class":452},[129,18926,160],{"class":277},[129,18928,456],{"class":269},[129,18930,1371],{"class":277},[129,18932,18933,18935,18937,18939,18941,18943,18945],{"class":265,"line":669},[129,18934,4739],{"class":269},[129,18936,5723],{"class":273},[129,18938,4745],{"class":277},[129,18940,5705],{"class":284},[129,18942,147],{"class":1376},[129,18944,4100],{"class":273},[129,18946,294],{"class":1376},[129,18948,18949,18951,18953,18956,18958,18960,18962,18965,18968,18970,18972,18975,18977,18979,18981,18983,18986,18988,18990],{"class":265,"line":693},[129,18950,4832],{"class":2139},[129,18952,5569],{"class":277},[129,18954,18955],{"class":427},"products:",[129,18957,4131],{"class":277},[129,18959,5781],{"class":273},[129,18961,362],{"class":277},[129,18963,18964],{"class":273},"category ",[129,18966,18967],{"class":277},"??",[129,18969,4261],{"class":277},[129,18971,4544],{"class":427},[129,18973,18974],{"class":277},"'}",[129,18976,1380],{"class":427},[129,18978,4131],{"class":277},[129,18980,5781],{"class":273},[129,18982,362],{"class":277},[129,18984,18985],{"class":273},"page ",[129,18987,18967],{"class":277},[129,18989,1383],{"class":290},[129,18991,18992],{"class":277},"}`\n",[129,18994,18995],{"class":265,"line":712},[129,18996,1481],{"class":277},[129,18998,18999,19001],{"class":265,"line":1521},[129,19000,4028],{"class":277},[129,19002,294],{"class":273},[11,19004,19005],{},"By default this uses in-memory storage (lost on restart). Switch to Redis with a single config change:",[255,19007,19009],{"className":3922,"code":19008,"filename":17649,"language":3924,"meta":260,"style":260},"export default defineNuxtConfig({\n  nitro: {\n    storage: {\n      cache: {\n        driver: 'redis',\n        url: process.env.REDIS_URL,\n      },\n    },\n  },\n})\n",[15,19010,19011,19024,19032,19041,19050,19066,19086,19091,19096,19100],{"__ignoreMap":260},[129,19012,19013,19015,19017,19020,19022],{"class":265,"line":266},[129,19014,4050],{"class":2139},[129,19016,4053],{"class":2139},[129,19018,19019],{"class":284}," defineNuxtConfig",[129,19021,147],{"class":273},[129,19023,6455],{"class":277},[129,19025,19026,19028,19030],{"class":265,"line":297},[129,19027,4074],{"class":1376},[129,19029,1380],{"class":277},[129,19031,1371],{"class":277},[129,19033,19034,19037,19039],{"class":265,"line":315},[129,19035,19036],{"class":1376},"    storage",[129,19038,1380],{"class":277},[129,19040,1371],{"class":277},[129,19042,19043,19046,19048],{"class":265,"line":332},[129,19044,19045],{"class":1376},"      cache",[129,19047,1380],{"class":277},[129,19049,1371],{"class":277},[129,19051,19052,19055,19057,19059,19062,19064],{"class":265,"line":339},[129,19053,19054],{"class":1376},"        driver",[129,19056,1380],{"class":277},[129,19058,4261],{"class":277},[129,19060,19061],{"class":427},"redis",[129,19063,424],{"class":277},[129,19065,1386],{"class":277},[129,19067,19068,19071,19073,19075,19077,19079,19081,19084],{"class":265,"line":356},[129,19069,19070],{"class":1376},"        url",[129,19072,1380],{"class":277},[129,19074,5084],{"class":273},[129,19076,362],{"class":277},[129,19078,4439],{"class":273},[129,19080,362],{"class":277},[129,19082,19083],{"class":273},"REDIS_URL",[129,19085,1386],{"class":277},[129,19087,19088],{"class":265,"line":651},[129,19089,19090],{"class":277},"      },\n",[129,19092,19093],{"class":265,"line":657},[129,19094,19095],{"class":277},"    },\n",[129,19097,19098],{"class":265,"line":669},[129,19099,1481],{"class":277},[129,19101,19102,19104],{"class":265,"line":693},[129,19103,4028],{"class":277},[129,19105,294],{"class":273},[11,19107,19108,19109,19112],{},"For function-level caching (not route-level), ",[15,19110,19111],{},"defineCachedFunction"," works the same way:",[255,19114,19117],{"className":3922,"code":19115,"filename":19116,"language":3924,"meta":260,"style":260},"export const getActiveProducts = defineCachedFunction(\n  async (category: string) => db.products.findMany({ where: { category, active: true } }),\n  { maxAge: 300, name: 'active-products', getKey: (category) => category }\n)\n","server/lib/cached-queries.ts",[15,19118,19119,19135,19191,19235],{"__ignoreMap":260},[129,19120,19121,19123,19125,19128,19130,19133],{"class":265,"line":266},[129,19122,4050],{"class":2139},[129,19124,4456],{"class":269},[129,19126,19127],{"class":273}," getActiveProducts ",[129,19129,278],{"class":277},[129,19131,19132],{"class":284}," defineCachedFunction",[129,19134,2241],{"class":273},[129,19136,19137,19139,19141,19144,19146,19148,19150,19152,19154,19156,19158,19160,19162,19164,19166,19168,19170,19172,19175,19177,19179,19181,19183,19185,19187,19189],{"class":265,"line":297},[129,19138,4703],{"class":269},[129,19140,3984],{"class":277},[129,19142,19143],{"class":452},"category",[129,19145,1380],{"class":277},[129,19147,4622],{"class":2161},[129,19149,160],{"class":277},[129,19151,456],{"class":269},[129,19153,4479],{"class":273},[129,19155,362],{"class":277},[129,19157,6612],{"class":273},[129,19159,362],{"class":277},[129,19161,18844],{"class":284},[129,19163,147],{"class":273},[129,19165,4796],{"class":277},[129,19167,18851],{"class":1376},[129,19169,1380],{"class":277},[129,19171,1416],{"class":277},[129,19173,19174],{"class":273}," category",[129,19176,1015],{"class":277},[129,19178,16025],{"class":1376},[129,19180,1380],{"class":277},[129,19182,4823],{"class":4822},[129,19184,4255],{"class":277},[129,19186,4255],{"class":277},[129,19188,160],{"class":273},[129,19190,1386],{"class":277},[129,19192,19193,19196,19198,19200,19202,19204,19206,19208,19210,19213,19215,19217,19220,19222,19224,19226,19228,19230,19233],{"class":265,"line":315},[129,19194,19195],{"class":277},"  {",[129,19197,6630],{"class":1376},[129,19199,1380],{"class":277},[129,19201,6635],{"class":290},[129,19203,1015],{"class":277},[129,19205,5407],{"class":1376},[129,19207,1380],{"class":277},[129,19209,4261],{"class":277},[129,19211,19212],{"class":427},"active-products",[129,19214,424],{"class":277},[129,19216,1015],{"class":277},[129,19218,19219],{"class":284}," getKey",[129,19221,1380],{"class":277},[129,19223,3984],{"class":277},[129,19225,19143],{"class":452},[129,19227,160],{"class":277},[129,19229,456],{"class":269},[129,19231,19232],{"class":273}," category ",[129,19234,1530],{"class":277},[129,19236,19237],{"class":265,"line":332},[129,19238,294],{"class":273},[11,19240,19241],{},"The layering matters for real traffic:",[59,19243,19244,19260],{},[62,19245,19246],{},[65,19247,19248,19251,19254,19257],{},[68,19249,19250],{},"Layer",[68,19252,19253],{},"Tool",[68,19255,19256],{},"Latency",[68,19258,19259],{},"When to use",[78,19261,19262,19276,19290,19304],{},[65,19263,19264,19267,19270,19273],{},[83,19265,19266],{},"Memory",[83,19268,19269],{},"Nitro in-process",[83,19271,19272],{},"\u003C1ms",[83,19274,19275],{},"Tiny, frequently-read, per-instance",[65,19277,19278,19281,19284,19287],{},[83,19279,19280],{},"Redis",[83,19282,19283],{},"Nitro + Redis driver",[83,19285,19286],{},"~1ms",[83,19288,19289],{},"Shared across instances, invalidatable",[65,19291,19292,19295,19298,19301],{},[83,19293,19294],{},"CDN",[83,19296,19297],{},"Cloudflare/Vercel cache",[83,19299,19300],{},"\u003C30ms globally",[83,19302,19303],{},"Public, rarely-changing responses",[65,19305,19306,19308,19313,19316],{},[83,19307,16485],{},[83,19309,19310,19312],{},[15,19311,12225],{}," + ETag",[83,19314,19315],{},"0ms on hit",[83,19317,19318],{},"Static assets, user-specific data",[11,19320,19321,19322,19324],{},"The CDN layer is the easiest win most teams miss. Nitro lets you set ",[15,19323,12225],{}," headers directly from route handlers:",[255,19326,19328],{"className":3922,"code":19327,"language":3924,"meta":260,"style":260},"export default defineEventHandler(async (event) => {\n  setResponseHeader(event, 'Cache-Control', 'public, max-age=300, s-maxage=3600')\n  return await getPublicData()\n})\n",[15,19329,19330,19352,19380,19391],{"__ignoreMap":260},[129,19331,19332,19334,19336,19338,19340,19342,19344,19346,19348,19350],{"class":265,"line":266},[129,19333,4050],{"class":2139},[129,19335,4053],{"class":2139},[129,19337,4503],{"class":284},[129,19339,147],{"class":273},[129,19341,4508],{"class":269},[129,19343,3984],{"class":277},[129,19345,4100],{"class":452},[129,19347,160],{"class":277},[129,19349,456],{"class":269},[129,19351,1371],{"class":277},[129,19353,19354,19357,19359,19361,19363,19365,19367,19369,19371,19373,19376,19378],{"class":265,"line":297},[129,19355,19356],{"class":284},"  setResponseHeader",[129,19358,147],{"class":1376},[129,19360,4100],{"class":273},[129,19362,1015],{"class":277},[129,19364,4261],{"class":277},[129,19366,12225],{"class":427},[129,19368,424],{"class":277},[129,19370,1015],{"class":277},[129,19372,4261],{"class":277},[129,19374,19375],{"class":427},"public, max-age=300, s-maxage=3600",[129,19377,424],{"class":277},[129,19379,294],{"class":1376},[129,19381,19382,19384,19386,19389],{"class":265,"line":315},[129,19383,4520],{"class":2139},[129,19385,4779],{"class":2139},[129,19387,19388],{"class":284}," getPublicData",[129,19390,2451],{"class":1376},[129,19392,19393,19395],{"class":265,"line":332},[129,19394,4028],{"class":277},[129,19396,294],{"class":273},[11,19398,19399,19402,19403,19406],{},[15,19400,19401],{},"max-age=300"," for browsers, ",[15,19404,19405],{},"s-maxage=3600"," for the CDN. The CDN serves the cached response globally; browsers revalidate at 5 minutes. Your server handles a fraction of the traffic it otherwise would.",[40,19408,19410],{"id":19409},"retry-mechanism","Retry mechanism",[11,19412,19413],{},"Third-party APIs fail. Not continuously - intermittently. A 502 here, a connection reset there. Without retries, these become user-facing errors. With retries, they're invisible.",[11,19415,19416,19418,19419,19424],{},[15,19417,14196],{}," (which Nuxt uses via ",[51,19420,19423],{"href":19421,"rel":19422},"https://github.com/unjs/ofetch",[55],"ofetch",") has built-in retry support:",[255,19426,19428],{"className":3922,"code":19427,"language":3924,"meta":260,"style":260},"// Retries automatically on network errors and 5xx responses\nconst data = await $fetch('/api/external', {\n  retry: 3,\n  retryDelay: 500,            // 500ms between attempts\n  retryStatusCodes: [429, 500, 502, 503, 504],\n})\n",[15,19429,19430,19435,19461,19472,19487,19522],{"__ignoreMap":260},[129,19431,19432],{"class":265,"line":266},[129,19433,19434],{"class":376},"// Retries automatically on network errors and 5xx responses\n",[129,19436,19437,19439,19442,19444,19446,19448,19450,19452,19455,19457,19459],{"class":265,"line":297},[129,19438,270],{"class":269},[129,19440,19441],{"class":273}," data ",[129,19443,278],{"class":277},[129,19445,4779],{"class":2139},[129,19447,8288],{"class":284},[129,19449,147],{"class":273},[129,19451,424],{"class":277},[129,19453,19454],{"class":427},"/api/external",[129,19456,424],{"class":277},[129,19458,1015],{"class":277},[129,19460,1371],{"class":277},[129,19462,19463,19466,19468,19470],{"class":265,"line":315},[129,19464,19465],{"class":1376},"  retry",[129,19467,1380],{"class":277},[129,19469,1018],{"class":290},[129,19471,1386],{"class":277},[129,19473,19474,19477,19479,19482,19484],{"class":265,"line":332},[129,19475,19476],{"class":1376},"  retryDelay",[129,19478,1380],{"class":277},[129,19480,19481],{"class":290}," 500",[129,19483,1015],{"class":277},[129,19485,19486],{"class":376},"            // 500ms between attempts\n",[129,19488,19489,19492,19494,19496,19499,19501,19503,19505,19508,19510,19513,19515,19518,19520],{"class":265,"line":339},[129,19490,19491],{"class":1376},"  retryStatusCodes",[129,19493,1380],{"class":277},[129,19495,1010],{"class":273},[129,19497,19498],{"class":290},"429",[129,19500,1015],{"class":277},[129,19502,19481],{"class":290},[129,19504,1015],{"class":277},[129,19506,19507],{"class":290}," 502",[129,19509,1015],{"class":277},[129,19511,19512],{"class":290}," 503",[129,19514,1015],{"class":277},[129,19516,19517],{"class":290}," 504",[129,19519,14170],{"class":273},[129,19521,1386],{"class":277},[129,19523,19524,19526],{"class":265,"line":356},[129,19525,4028],{"class":277},[129,19527,294],{"class":273},[11,19529,19530],{},"For exponential backoff - waiting progressively longer between attempts to avoid hammering a struggling service:",[17968,19532,19533,19812],{},[255,19534,19537],{"className":3922,"code":19535,"filename":19536,"language":3924,"meta":260,"style":260},"export async function withRetry\u003CT>(\n  fn: () => Promise\u003CT>,\n  { attempts = 3, baseDelay = 100 } = {}\n): Promise\u003CT> {\n  let lastError: Error\n\n  for (let i = 0; i \u003C attempts; i++) {\n    try {\n      return await fn()\n    } catch (err) {\n      lastError = err as Error\n      if (i \u003C attempts - 1) {\n        // 100ms, 200ms, 400ms - doubles each time\n        await new Promise(r => setTimeout(r, baseDelay * 2 ** i))\n      }\n    }\n  }\n\n  throw lastError!\n}\n","server/lib/retry.ts",[15,19538,19539,19558,19578,19605,19619,19631,19635,19670,19677,19689,19704,19719,19741,19746,19782,19787,19791,19795,19799,19808],{"__ignoreMap":260},[129,19540,19541,19543,19545,19547,19550,19552,19555],{"class":265,"line":266},[129,19542,4050],{"class":2139},[129,19544,6020],{"class":269},[129,19546,5060],{"class":269},[129,19548,19549],{"class":284}," withRetry",[129,19551,3945],{"class":277},[129,19553,19554],{"class":2161},"T",[129,19556,19557],{"class":277},">(\n",[129,19559,19560,19563,19565,19567,19569,19571,19573,19575],{"class":265,"line":297},[129,19561,19562],{"class":284},"  fn",[129,19564,1380],{"class":277},[129,19566,4511],{"class":277},[129,19568,456],{"class":269},[129,19570,4637],{"class":2161},[129,19572,3945],{"class":277},[129,19574,19554],{"class":2161},[129,19576,19577],{"class":277},">,\n",[129,19579,19580,19582,19585,19587,19589,19591,19594,19596,19598,19600,19602],{"class":265,"line":315},[129,19581,19195],{"class":277},[129,19583,19584],{"class":452}," attempts",[129,19586,4745],{"class":277},[129,19588,1018],{"class":290},[129,19590,1015],{"class":277},[129,19592,19593],{"class":452}," baseDelay",[129,19595,4745],{"class":277},[129,19597,18732],{"class":290},[129,19599,4255],{"class":277},[129,19601,4745],{"class":277},[129,19603,19604],{"class":277}," {}\n",[129,19606,19607,19609,19611,19613,19615,19617],{"class":265,"line":332},[129,19608,4634],{"class":277},[129,19610,4637],{"class":2161},[129,19612,3945],{"class":277},[129,19614,19554],{"class":2161},[129,19616,3956],{"class":277},[129,19618,1371],{"class":277},[129,19620,19621,19623,19626,19628],{"class":265,"line":339},[129,19622,5720],{"class":269},[129,19624,19625],{"class":273}," lastError",[129,19627,1380],{"class":277},[129,19629,19630],{"class":2161}," Error\n",[129,19632,19633],{"class":265,"line":356},[129,19634,336],{"emptyLinePlaceholder":335},[129,19636,19637,19639,19641,19644,19646,19648,19650,19652,19654,19657,19659,19661,19663,19666,19668],{"class":265,"line":651},[129,19638,6437],{"class":2139},[129,19640,3984],{"class":1376},[129,19642,19643],{"class":269},"let",[129,19645,10339],{"class":273},[129,19647,4745],{"class":277},[129,19649,5698],{"class":290},[129,19651,7376],{"class":277},[129,19653,10339],{"class":273},[129,19655,19656],{"class":277}," \u003C",[129,19658,19584],{"class":273},[129,19660,7376],{"class":277},[129,19662,10339],{"class":273},[129,19664,19665],{"class":277},"++",[129,19667,4005],{"class":1376},[129,19669,6455],{"class":277},[129,19671,19672,19675],{"class":265,"line":657},[129,19673,19674],{"class":2139},"    try",[129,19676,1371],{"class":277},[129,19678,19679,19682,19684,19687],{"class":265,"line":669},[129,19680,19681],{"class":2139},"      return",[129,19683,4779],{"class":2139},[129,19685,19686],{"class":284}," fn",[129,19688,2451],{"class":1376},[129,19690,19691,19693,19695,19697,19700,19702],{"class":265,"line":693},[129,19692,7619],{"class":277},[129,19694,13629],{"class":2139},[129,19696,3984],{"class":1376},[129,19698,19699],{"class":273},"err",[129,19701,4005],{"class":1376},[129,19703,6455],{"class":277},[129,19705,19706,19709,19711,19714,19717],{"class":265,"line":712},[129,19707,19708],{"class":273},"      lastError",[129,19710,4745],{"class":277},[129,19712,19713],{"class":273}," err",[129,19715,19716],{"class":2139}," as",[129,19718,19630],{"class":2161},[129,19720,19721,19724,19726,19728,19730,19732,19735,19737,19739],{"class":265,"line":1521},[129,19722,19723],{"class":2139},"      if",[129,19725,3984],{"class":1376},[129,19727,169],{"class":273},[129,19729,19656],{"class":277},[129,19731,19584],{"class":273},[129,19733,19734],{"class":277}," -",[129,19736,1383],{"class":290},[129,19738,4005],{"class":1376},[129,19740,6455],{"class":277},[129,19742,19743],{"class":265,"line":1527},[129,19744,19745],{"class":376},"        // 100ms, 200ms, 400ms - doubles each time\n",[129,19747,19748,19751,19753,19755,19757,19759,19761,19763,19765,19767,19769,19771,19773,19775,19778,19780],{"class":265,"line":2295},[129,19749,19750],{"class":2139},"        await",[129,19752,281],{"class":277},[129,19754,4637],{"class":2161},[129,19756,147],{"class":1376},[129,19758,15144],{"class":452},[129,19760,456],{"class":269},[129,19762,15149],{"class":284},[129,19764,147],{"class":1376},[129,19766,15144],{"class":273},[129,19768,1015],{"class":277},[129,19770,19593],{"class":273},[129,19772,18886],{"class":277},[129,19774,1023],{"class":290},[129,19776,19777],{"class":277}," **",[129,19779,10339],{"class":273},[129,19781,471],{"class":1376},[129,19783,19784],{"class":265,"line":2300},[129,19785,19786],{"class":277},"      }\n",[129,19788,19789],{"class":265,"line":2305},[129,19790,6516],{"class":277},[129,19792,19793],{"class":265,"line":2311},[129,19794,1524],{"class":277},[129,19796,19797],{"class":265,"line":2329},[129,19798,336],{"emptyLinePlaceholder":335},[129,19800,19801,19803,19805],{"class":265,"line":2351},[129,19802,5167],{"class":2139},[129,19804,19625],{"class":273},[129,19806,19807],{"class":277},"!\n",[129,19809,19810],{"class":265,"line":2387},[129,19811,1530],{"class":277},[255,19813,19816],{"className":3922,"code":19814,"filename":19815,"language":3924,"meta":260,"style":260},"export default defineEventHandler(async (event) => {\n  const { userId } = getQuery(event)\n  return await withRetry(\n    () => externalApi.fetchUserProfile(userId),\n    { attempts: 3, baseDelay: 200 }\n  )\n})\n","server/api/user.get.ts",[15,19817,19818,19840,19860,19870,19893,19914,19919],{"__ignoreMap":260},[129,19819,19820,19822,19824,19826,19828,19830,19832,19834,19836,19838],{"class":265,"line":266},[129,19821,4050],{"class":2139},[129,19823,4053],{"class":2139},[129,19825,4503],{"class":284},[129,19827,147],{"class":273},[129,19829,4508],{"class":269},[129,19831,3984],{"class":277},[129,19833,4100],{"class":452},[129,19835,160],{"class":277},[129,19837,456],{"class":269},[129,19839,1371],{"class":277},[129,19841,19842,19844,19846,19848,19850,19852,19854,19856,19858],{"class":265,"line":297},[129,19843,5076],{"class":269},[129,19845,1416],{"class":277},[129,19847,18058],{"class":273},[129,19849,4255],{"class":277},[129,19851,4745],{"class":277},[129,19853,5705],{"class":284},[129,19855,147],{"class":1376},[129,19857,4100],{"class":273},[129,19859,294],{"class":1376},[129,19861,19862,19864,19866,19868],{"class":265,"line":315},[129,19863,4520],{"class":2139},[129,19865,4779],{"class":2139},[129,19867,19549],{"class":284},[129,19869,2241],{"class":1376},[129,19871,19872,19875,19877,19880,19882,19885,19887,19889,19891],{"class":265,"line":332},[129,19873,19874],{"class":277},"    ()",[129,19876,456],{"class":269},[129,19878,19879],{"class":273}," externalApi",[129,19881,362],{"class":277},[129,19883,19884],{"class":284},"fetchUserProfile",[129,19886,147],{"class":1376},[129,19888,18099],{"class":273},[129,19890,160],{"class":1376},[129,19892,1386],{"class":277},[129,19894,19895,19897,19899,19901,19903,19905,19907,19909,19912],{"class":265,"line":339},[129,19896,13947],{"class":277},[129,19898,19584],{"class":1376},[129,19900,1380],{"class":277},[129,19902,1018],{"class":290},[129,19904,1015],{"class":277},[129,19906,19593],{"class":1376},[129,19908,1380],{"class":277},[129,19910,19911],{"class":290}," 200",[129,19913,1476],{"class":277},[129,19915,19916],{"class":265,"line":356},[129,19917,19918],{"class":1376},"  )\n",[129,19920,19921,19923],{"class":265,"line":651},[129,19922,4028],{"class":277},[129,19924,294],{"class":273},[11,19926,19927],{},"Two things to get right:",[11,19929,19930,19933,19934,19937,19938,19940,19941,500,19944,500,19947,500,19950,19953,19954,19956],{},[118,19931,19932],{},"Don't retry everything."," A ",[15,19935,19936],{},"404"," is not transient - retrying it wastes time and money. Retry only status codes that indicate temporary failure: ",[15,19939,19498],{}," (rate limited), ",[15,19942,19943],{},"500",[15,19945,19946],{},"502",[15,19948,19949],{},"503",[15,19951,19952],{},"504",". A ",[15,19955,2801],{}," means your request is wrong; retrying it won't help.",[11,19958,19959,19965,19966,19968],{},[118,19960,19961,19962,362],{},"Respect ",[15,19963,19964],{},"Retry-After"," When a 429 response includes a ",[15,19967,19964],{}," header, that's the server telling you exactly how long to wait. Ignoring it and retrying immediately just gets you rate-limited again faster.",[255,19970,19972],{"className":3922,"code":19971,"language":3924,"meta":260,"style":260},"const retryAfter = response.headers.get('Retry-After')\nconst delay = retryAfter ? parseInt(retryAfter) * 1000 : baseDelay * 2 ** attempt\nawait new Promise(r => setTimeout(r, delay))\n",[15,19973,19974,20004,20042],{"__ignoreMap":260},[129,19975,19976,19978,19981,19983,19985,19987,19990,19992,19994,19996,19998,20000,20002],{"class":265,"line":266},[129,19977,270],{"class":269},[129,19979,19980],{"class":273}," retryAfter ",[129,19982,278],{"class":277},[129,19984,14954],{"class":273},[129,19986,362],{"class":277},[129,19988,19989],{"class":273},"headers",[129,19991,362],{"class":277},[129,19993,6197],{"class":284},[129,19995,147],{"class":273},[129,19997,424],{"class":277},[129,19999,19964],{"class":427},[129,20001,424],{"class":277},[129,20003,294],{"class":273},[129,20005,20006,20008,20011,20013,20015,20018,20021,20024,20026,20028,20030,20033,20035,20037,20039],{"class":265,"line":297},[129,20007,270],{"class":269},[129,20009,20010],{"class":273}," delay ",[129,20012,278],{"class":277},[129,20014,19980],{"class":273},[129,20016,20017],{"class":277},"?",[129,20019,20020],{"class":284}," parseInt",[129,20022,20023],{"class":273},"(retryAfter) ",[129,20025,5501],{"class":277},[129,20027,9568],{"class":290},[129,20029,14414],{"class":277},[129,20031,20032],{"class":273}," baseDelay ",[129,20034,5501],{"class":277},[129,20036,1023],{"class":290},[129,20038,19777],{"class":277},[129,20040,20041],{"class":273}," attempt\n",[129,20043,20044,20046,20048,20050,20052,20054,20056,20058,20060,20062],{"class":265,"line":315},[129,20045,8083],{"class":2139},[129,20047,281],{"class":277},[129,20049,4637],{"class":2161},[129,20051,147],{"class":273},[129,20053,15144],{"class":452},[129,20055,456],{"class":269},[129,20057,15149],{"class":284},[129,20059,15152],{"class":273},[129,20061,1015],{"class":277},[129,20063,20064],{"class":273}," delay))\n",[40,20066,20068],{"id":20067},"rate-limiting","Rate limiting",[11,20070,20071],{},"Without rate limiting, your public API is an invitation for abuse. A form submission endpoint without limits gets spam. A search endpoint without limits gets scraped. An auth endpoint without limits gets brute-forced.",[11,20073,20074,20075,20078],{},"Nitro's server middleware runs before every request. Combined with ",[15,20076,20077],{},"useStorage"," (which uses the same storage you configured above), you get rate limiting with minimal code:",[255,20080,20083],{"className":3922,"code":20081,"filename":20082,"language":3924,"meta":260,"style":260},"export default defineEventHandler(async (event) => {\n  // Skip rate limiting for non-API routes\n  if (!event.path.startsWith('/api/')) return\n\n  const ip = getRequestIP(event, { xForwardedFor: true }) ?? 'unknown'\n  const key = `rate-limit:${ip}:${Math.floor(Date.now() / 60_000)}` // per-minute window\n\n  const storage = useStorage('cache')\n  const count = ((await storage.getItem\u003Cnumber>(key)) ?? 0) + 1\n\n  await storage.setItem(key, count, { ttl: 60 })\n\n  setResponseHeader(event, 'X-RateLimit-Limit', '100')\n  setResponseHeader(event, 'X-RateLimit-Remaining', String(Math.max(0, 100 - count)))\n\n  if (count > 100) {\n    throw createError({ statusCode: 429, message: 'Too many requests' })\n  }\n})\n","server/middleware/rate-limit.ts",[15,20084,20085,20107,20112,20145,20149,20189,20241,20245,20268,20312,20316,20350,20354,20382,20428,20432,20448,20483,20487],{"__ignoreMap":260},[129,20086,20087,20089,20091,20093,20095,20097,20099,20101,20103,20105],{"class":265,"line":266},[129,20088,4050],{"class":2139},[129,20090,4053],{"class":2139},[129,20092,4503],{"class":284},[129,20094,147],{"class":273},[129,20096,4508],{"class":269},[129,20098,3984],{"class":277},[129,20100,4100],{"class":452},[129,20102,160],{"class":277},[129,20104,456],{"class":269},[129,20106,1371],{"class":277},[129,20108,20109],{"class":265,"line":297},[129,20110,20111],{"class":376},"  // Skip rate limiting for non-API routes\n",[129,20113,20114,20116,20118,20120,20122,20124,20126,20128,20131,20133,20135,20138,20140,20142],{"class":265,"line":315},[129,20115,3998],{"class":2139},[129,20117,3984],{"class":1376},[129,20119,4447],{"class":277},[129,20121,4100],{"class":273},[129,20123,362],{"class":277},[129,20125,4172],{"class":273},[129,20127,362],{"class":277},[129,20129,20130],{"class":284},"startsWith",[129,20132,147],{"class":1376},[129,20134,424],{"class":277},[129,20136,20137],{"class":427},"/api/",[129,20139,424],{"class":277},[129,20141,13145],{"class":1376},[129,20143,20144],{"class":2139},"return\n",[129,20146,20147],{"class":265,"line":332},[129,20148,336],{"emptyLinePlaceholder":335},[129,20150,20151,20153,20156,20158,20161,20163,20165,20167,20169,20172,20174,20176,20178,20180,20182,20184,20187],{"class":265,"line":339},[129,20152,5076],{"class":269},[129,20154,20155],{"class":273}," ip",[129,20157,4745],{"class":277},[129,20159,20160],{"class":284}," getRequestIP",[129,20162,147],{"class":1376},[129,20164,4100],{"class":273},[129,20166,1015],{"class":277},[129,20168,1416],{"class":277},[129,20170,20171],{"class":1376}," xForwardedFor",[129,20173,1380],{"class":277},[129,20175,4823],{"class":4822},[129,20177,4255],{"class":277},[129,20179,4005],{"class":1376},[129,20181,18967],{"class":277},[129,20183,4261],{"class":277},[129,20185,20186],{"class":427},"unknown",[129,20188,4267],{"class":277},[129,20190,20191,20193,20195,20197,20199,20202,20204,20207,20209,20211,20213,20215,20217,20220,20223,20225,20227,20229,20231,20234,20236,20238],{"class":265,"line":356},[129,20192,5076],{"class":269},[129,20194,6243],{"class":273},[129,20196,4745],{"class":277},[129,20198,5569],{"class":277},[129,20200,20201],{"class":427},"rate-limit:",[129,20203,4131],{"class":277},[129,20205,20206],{"class":273},"ip",[129,20208,4028],{"class":277},[129,20210,1380],{"class":427},[129,20212,4131],{"class":277},[129,20214,10876],{"class":273},[129,20216,362],{"class":277},[129,20218,20219],{"class":284},"floor",[129,20221,20222],{"class":273},"(Date",[129,20224,362],{"class":277},[129,20226,3587],{"class":284},[129,20228,824],{"class":273},[129,20230,938],{"class":277},[129,20232,20233],{"class":290}," 60_000",[129,20235,160],{"class":273},[129,20237,4175],{"class":277},[129,20239,20240],{"class":376}," // per-minute window\n",[129,20242,20243],{"class":265,"line":651},[129,20244,336],{"emptyLinePlaceholder":335},[129,20246,20247,20249,20252,20254,20257,20259,20261,20264,20266],{"class":265,"line":657},[129,20248,5076],{"class":269},[129,20250,20251],{"class":273}," storage",[129,20253,4745],{"class":277},[129,20255,20256],{"class":284}," useStorage",[129,20258,147],{"class":1376},[129,20260,424],{"class":277},[129,20262,20263],{"class":427},"cache",[129,20265,424],{"class":277},[129,20267,294],{"class":1376},[129,20269,20270,20272,20274,20276,20279,20281,20283,20285,20288,20290,20293,20295,20297,20299,20301,20303,20305,20307,20309],{"class":265,"line":669},[129,20271,5076],{"class":269},[129,20273,9491],{"class":273},[129,20275,4745],{"class":277},[129,20277,20278],{"class":1376}," ((",[129,20280,8083],{"class":2139},[129,20282,20251],{"class":273},[129,20284,362],{"class":277},[129,20286,20287],{"class":284},"getItem",[129,20289,3945],{"class":277},[129,20291,20292],{"class":2161},"number",[129,20294,3956],{"class":277},[129,20296,147],{"class":1376},[129,20298,6273],{"class":273},[129,20300,13145],{"class":1376},[129,20302,18967],{"class":277},[129,20304,5698],{"class":290},[129,20306,4005],{"class":1376},[129,20308,219],{"class":277},[129,20310,20311],{"class":290}," 1\n",[129,20313,20314],{"class":265,"line":693},[129,20315,336],{"emptyLinePlaceholder":335},[129,20317,20318,20320,20322,20324,20327,20329,20331,20333,20335,20337,20339,20342,20344,20346,20348],{"class":265,"line":712},[129,20319,8101],{"class":2139},[129,20321,20251],{"class":273},[129,20323,362],{"class":277},[129,20325,20326],{"class":284},"setItem",[129,20328,147],{"class":1376},[129,20330,6273],{"class":273},[129,20332,1015],{"class":277},[129,20334,9491],{"class":273},[129,20336,1015],{"class":277},[129,20338,1416],{"class":277},[129,20340,20341],{"class":1376}," ttl",[129,20343,1380],{"class":277},[129,20345,18883],{"class":290},[129,20347,4255],{"class":277},[129,20349,294],{"class":1376},[129,20351,20352],{"class":265,"line":1521},[129,20353,336],{"emptyLinePlaceholder":335},[129,20355,20356,20358,20360,20362,20364,20366,20369,20371,20373,20375,20378,20380],{"class":265,"line":1527},[129,20357,19356],{"class":284},[129,20359,147],{"class":1376},[129,20361,4100],{"class":273},[129,20363,1015],{"class":277},[129,20365,4261],{"class":277},[129,20367,20368],{"class":427},"X-RateLimit-Limit",[129,20370,424],{"class":277},[129,20372,1015],{"class":277},[129,20374,4261],{"class":277},[129,20376,20377],{"class":427},"100",[129,20379,424],{"class":277},[129,20381,294],{"class":1376},[129,20383,20384,20386,20388,20390,20392,20394,20397,20399,20401,20404,20406,20408,20410,20413,20415,20417,20419,20421,20423,20425],{"class":265,"line":2295},[129,20385,19356],{"class":284},[129,20387,147],{"class":1376},[129,20389,4100],{"class":273},[129,20391,1015],{"class":277},[129,20393,4261],{"class":277},[129,20395,20396],{"class":427},"X-RateLimit-Remaining",[129,20398,424],{"class":277},[129,20400,1015],{"class":277},[129,20402,20403],{"class":284}," String",[129,20405,147],{"class":1376},[129,20407,10876],{"class":273},[129,20409,362],{"class":277},[129,20411,20412],{"class":284},"max",[129,20414,147],{"class":1376},[129,20416,345],{"class":290},[129,20418,1015],{"class":277},[129,20420,18732],{"class":290},[129,20422,19734],{"class":277},[129,20424,9491],{"class":273},[129,20426,20427],{"class":1376},")))\n",[129,20429,20430],{"class":265,"line":2300},[129,20431,336],{"emptyLinePlaceholder":335},[129,20433,20434,20436,20438,20440,20442,20444,20446],{"class":265,"line":2305},[129,20435,3998],{"class":2139},[129,20437,3984],{"class":1376},[129,20439,3904],{"class":273},[129,20441,2345],{"class":277},[129,20443,18732],{"class":290},[129,20445,4005],{"class":1376},[129,20447,6455],{"class":277},[129,20449,20450,20453,20455,20457,20459,20461,20463,20466,20468,20470,20472,20474,20477,20479,20481],{"class":265,"line":2311},[129,20451,20452],{"class":2139},"    throw",[129,20454,6916],{"class":284},[129,20456,147],{"class":1376},[129,20458,4796],{"class":277},[129,20460,6923],{"class":1376},[129,20462,1380],{"class":277},[129,20464,20465],{"class":290}," 429",[129,20467,1015],{"class":277},[129,20469,6933],{"class":1376},[129,20471,1380],{"class":277},[129,20473,4261],{"class":277},[129,20475,20476],{"class":427},"Too many requests",[129,20478,424],{"class":277},[129,20480,4255],{"class":277},[129,20482,294],{"class":1376},[129,20484,20485],{"class":265,"line":2329},[129,20486,1524],{"class":277},[129,20488,20489,20491],{"class":265,"line":2351},[129,20490,4028],{"class":277},[129,20492,294],{"class":273},[11,20494,20495],{},"This is fixed-window rate limiting: 100 requests per IP per minute. For production, sliding window is more accurate (it doesn't have the \"burst at window boundary\" problem), but this is enough to stop bots and casual abuse.",[11,20497,20498],{},"For per-route limits (auth stricter than search):",[255,20500,20502],{"className":3922,"code":20501,"language":3924,"meta":260,"style":260},"const LIMITS: Record\u003Cstring, number> = {\n  '/api/auth/login': 10,   // brute force protection\n  '/api/search': 300,      // liberal for search\n  default: 100,\n}\n\nconst limit = LIMITS[event.path] ?? LIMITS.default\n",[15,20503,20504,20531,20550,20568,20579,20583,20587],{"__ignoreMap":260},[129,20505,20506,20508,20511,20513,20516,20518,20521,20523,20525,20527,20529],{"class":265,"line":266},[129,20507,270],{"class":269},[129,20509,20510],{"class":273}," LIMITS",[129,20512,1380],{"class":277},[129,20514,20515],{"class":2161}," Record",[129,20517,3945],{"class":277},[129,20519,20520],{"class":2161},"string",[129,20522,1015],{"class":277},[129,20524,4612],{"class":2161},[129,20526,3956],{"class":277},[129,20528,4745],{"class":277},[129,20530,1371],{"class":277},[129,20532,20533,20536,20539,20541,20543,20545,20547],{"class":265,"line":297},[129,20534,20535],{"class":277},"  '",[129,20537,20538],{"class":1376},"/api/auth/login",[129,20540,424],{"class":277},[129,20542,1380],{"class":277},[129,20544,10264],{"class":290},[129,20546,1015],{"class":277},[129,20548,20549],{"class":376},"   // brute force protection\n",[129,20551,20552,20554,20557,20559,20561,20563,20565],{"class":265,"line":315},[129,20553,20535],{"class":277},[129,20555,20556],{"class":1376},"/api/search",[129,20558,424],{"class":277},[129,20560,1380],{"class":277},[129,20562,6635],{"class":290},[129,20564,1015],{"class":277},[129,20566,20567],{"class":376},"      // liberal for search\n",[129,20569,20570,20573,20575,20577],{"class":265,"line":332},[129,20571,20572],{"class":1376},"  default",[129,20574,1380],{"class":277},[129,20576,18732],{"class":290},[129,20578,1386],{"class":277},[129,20580,20581],{"class":265,"line":339},[129,20582,1530],{"class":277},[129,20584,20585],{"class":265,"line":356},[129,20586,336],{"emptyLinePlaceholder":335},[129,20588,20589,20591,20594,20596,20599,20601,20604,20606,20608,20610],{"class":265,"line":651},[129,20590,270],{"class":269},[129,20592,20593],{"class":273}," limit ",[129,20595,278],{"class":277},[129,20597,20598],{"class":273}," LIMITS[event",[129,20600,362],{"class":277},[129,20602,20603],{"class":273},"path] ",[129,20605,18967],{"class":277},[129,20607,20510],{"class":273},[129,20609,362],{"class":277},[129,20611,20612],{"class":273},"default\n",[3325,20614,20615],{},[11,20616,20617,20620,20621,20624,20625,20627],{},[15,20618,20619],{},"getRequestIP"," returns the IP from the connection or ",[15,20622,20623],{},"X-Forwarded-For"," header. ",[15,20626,20623],{}," is spoofable if your server is directly exposed - only trust it if requests go through a reverse proxy (Cloudflare, Nginx) that sets it. Check your deployment topology before relying on it for security-sensitive limits.",[40,20629,20631],{"id":20630},"feature-flags","Feature flags",[11,20633,20634],{},"Feature flags decouple deployment from release. You ship code to production with the new feature disabled, then enable it for 1% of users, then 10%, then everyone - or roll back without redeploying if something goes wrong.",[11,20636,20637,20638,362],{},"The simplest version: environment variables read via ",[15,20639,20640],{},"useRuntimeConfig",[17968,20642,20643,20761],{},[255,20644,20646],{"className":3922,"code":20645,"filename":17649,"language":3924,"meta":260,"style":260},"export default defineNuxtConfig({\n  runtimeConfig: {\n    public: {\n      features: {\n        newCheckout: process.env.FEATURE_NEW_CHECKOUT === 'true',\n        betaBadge: process.env.FEATURE_BETA_BADGE === 'true',\n      },\n    },\n  },\n})\n",[15,20647,20648,20660,20669,20678,20687,20715,20743,20747,20751,20755],{"__ignoreMap":260},[129,20649,20650,20652,20654,20656,20658],{"class":265,"line":266},[129,20651,4050],{"class":2139},[129,20653,4053],{"class":2139},[129,20655,19019],{"class":284},[129,20657,147],{"class":273},[129,20659,6455],{"class":277},[129,20661,20662,20665,20667],{"class":265,"line":297},[129,20663,20664],{"class":1376},"  runtimeConfig",[129,20666,1380],{"class":277},[129,20668,1371],{"class":277},[129,20670,20671,20674,20676],{"class":265,"line":315},[129,20672,20673],{"class":1376},"    public",[129,20675,1380],{"class":277},[129,20677,1371],{"class":277},[129,20679,20680,20683,20685],{"class":265,"line":332},[129,20681,20682],{"class":1376},"      features",[129,20684,1380],{"class":277},[129,20686,1371],{"class":277},[129,20688,20689,20692,20694,20696,20698,20700,20702,20705,20707,20709,20711,20713],{"class":265,"line":339},[129,20690,20691],{"class":1376},"        newCheckout",[129,20693,1380],{"class":277},[129,20695,5084],{"class":273},[129,20697,362],{"class":277},[129,20699,4439],{"class":273},[129,20701,362],{"class":277},[129,20703,20704],{"class":273},"FEATURE_NEW_CHECKOUT ",[129,20706,7981],{"class":277},[129,20708,4261],{"class":277},[129,20710,14996],{"class":427},[129,20712,424],{"class":277},[129,20714,1386],{"class":277},[129,20716,20717,20720,20722,20724,20726,20728,20730,20733,20735,20737,20739,20741],{"class":265,"line":356},[129,20718,20719],{"class":1376},"        betaBadge",[129,20721,1380],{"class":277},[129,20723,5084],{"class":273},[129,20725,362],{"class":277},[129,20727,4439],{"class":273},[129,20729,362],{"class":277},[129,20731,20732],{"class":273},"FEATURE_BETA_BADGE ",[129,20734,7981],{"class":277},[129,20736,4261],{"class":277},[129,20738,14996],{"class":427},[129,20740,424],{"class":277},[129,20742,1386],{"class":277},[129,20744,20745],{"class":265,"line":651},[129,20746,19090],{"class":277},[129,20748,20749],{"class":265,"line":657},[129,20750,19095],{"class":277},[129,20752,20753],{"class":265,"line":669},[129,20754,1481],{"class":277},[129,20756,20757,20759],{"class":265,"line":693},[129,20758,4028],{"class":277},[129,20760,294],{"class":273},[255,20762,20765],{"className":17386,"code":20763,"filename":20764,"language":17388,"meta":260,"style":260},"\u003Cscript setup>\nconst config = useRuntimeConfig()\n\u003C/script>\n\n\u003Ctemplate>\n  \u003CNewCheckout v-if=\"config.public.features.newCheckout\" />\n  \u003COldCheckout v-else />\n\u003C/template>\n","components/CheckoutView.vue",[15,20766,20767,20777,20791,20799,20803,20811,20833,20845],{"__ignoreMap":260},[129,20768,20769,20771,20773,20775],{"class":265,"line":266},[129,20770,3945],{"class":277},[129,20772,10436],{"class":1376},[129,20774,17399],{"class":269},[129,20776,4676],{"class":277},[129,20778,20779,20781,20784,20786,20789],{"class":265,"line":297},[129,20780,270],{"class":269},[129,20782,20783],{"class":273}," config ",[129,20785,278],{"class":277},[129,20787,20788],{"class":284}," useRuntimeConfig",[129,20790,2451],{"class":273},[129,20792,20793,20795,20797],{"class":265,"line":315},[129,20794,12609],{"class":277},[129,20796,10436],{"class":1376},[129,20798,4676],{"class":277},[129,20800,20801],{"class":265,"line":332},[129,20802,336],{"emptyLinePlaceholder":335},[129,20804,20805,20807,20809],{"class":265,"line":339},[129,20806,3945],{"class":277},[129,20808,17580],{"class":1376},[129,20810,4676],{"class":277},[129,20812,20813,20815,20818,20821,20823,20825,20828,20830],{"class":265,"line":356},[129,20814,12979],{"class":277},[129,20816,20817],{"class":1376},"NewCheckout",[129,20819,20820],{"class":269}," v-if",[129,20822,278],{"class":277},[129,20824,2258],{"class":277},[129,20826,20827],{"class":427},"config.public.features.newCheckout",[129,20829,2258],{"class":277},[129,20831,20832],{"class":277}," />\n",[129,20834,20835,20837,20840,20843],{"class":265,"line":651},[129,20836,12979],{"class":277},[129,20838,20839],{"class":1376},"OldCheckout",[129,20841,20842],{"class":269}," v-else",[129,20844,20832],{"class":277},[129,20846,20847,20849,20851],{"class":265,"line":657},[129,20848,12609],{"class":277},[129,20850,17580],{"class":1376},[129,20852,4676],{"class":277},[11,20854,20855],{},"The limitation: changing a flag requires redeploying. For truly dynamic flags (toggle without deploy, target specific users or percentages), you need runtime storage.",[17968,20857,20858,21041,21160,21251],{},[255,20859,20862],{"className":3922,"code":20860,"filename":20861,"language":3924,"meta":260,"style":260},"export default defineEventHandler(async (event) => {\n  // Add your own auth check here\n  const { flag, enabled } = await readBody(event)\n  const storage = useStorage('cache')\n  const flags = await storage.getItem\u003CRecord\u003Cstring, boolean>>('feature-flags') ?? {}\n  flags[flag] = enabled\n  await storage.setItem('feature-flags', flags)\n  return flags\n})\n","server/api/admin/flags.patch.ts",[15,20863,20864,20886,20891,20919,20939,20987,21004,21028,21035],{"__ignoreMap":260},[129,20865,20866,20868,20870,20872,20874,20876,20878,20880,20882,20884],{"class":265,"line":266},[129,20867,4050],{"class":2139},[129,20869,4053],{"class":2139},[129,20871,4503],{"class":284},[129,20873,147],{"class":273},[129,20875,4508],{"class":269},[129,20877,3984],{"class":277},[129,20879,4100],{"class":452},[129,20881,160],{"class":277},[129,20883,456],{"class":269},[129,20885,1371],{"class":277},[129,20887,20888],{"class":265,"line":297},[129,20889,20890],{"class":376},"  // Add your own auth check here\n",[129,20892,20893,20895,20897,20900,20902,20905,20907,20909,20911,20913,20915,20917],{"class":265,"line":315},[129,20894,5076],{"class":269},[129,20896,1416],{"class":277},[129,20898,20899],{"class":273}," flag",[129,20901,1015],{"class":277},[129,20903,20904],{"class":273}," enabled",[129,20906,4255],{"class":277},[129,20908,4745],{"class":277},[129,20910,4779],{"class":2139},[129,20912,5264],{"class":284},[129,20914,147],{"class":1376},[129,20916,4100],{"class":273},[129,20918,294],{"class":1376},[129,20920,20921,20923,20925,20927,20929,20931,20933,20935,20937],{"class":265,"line":332},[129,20922,5076],{"class":269},[129,20924,20251],{"class":273},[129,20926,4745],{"class":277},[129,20928,20256],{"class":284},[129,20930,147],{"class":1376},[129,20932,424],{"class":277},[129,20934,20263],{"class":427},[129,20936,424],{"class":277},[129,20938,294],{"class":1376},[129,20940,20941,20943,20946,20948,20950,20952,20954,20956,20958,20961,20963,20965,20967,20970,20973,20975,20977,20979,20981,20983,20985],{"class":265,"line":339},[129,20942,5076],{"class":269},[129,20944,20945],{"class":273}," flags",[129,20947,4745],{"class":277},[129,20949,4779],{"class":2139},[129,20951,20251],{"class":273},[129,20953,362],{"class":277},[129,20955,20287],{"class":284},[129,20957,3945],{"class":277},[129,20959,20960],{"class":2161},"Record",[129,20962,3945],{"class":277},[129,20964,20520],{"class":2161},[129,20966,1015],{"class":277},[129,20968,20969],{"class":2161}," boolean",[129,20971,20972],{"class":277},">>",[129,20974,147],{"class":1376},[129,20976,424],{"class":277},[129,20978,20630],{"class":427},[129,20980,424],{"class":277},[129,20982,4005],{"class":1376},[129,20984,18967],{"class":277},[129,20986,19604],{"class":277},[129,20988,20989,20992,20994,20997,20999,21001],{"class":265,"line":356},[129,20990,20991],{"class":273},"  flags",[129,20993,4128],{"class":1376},[129,20995,20996],{"class":273},"flag",[129,20998,348],{"class":1376},[129,21000,278],{"class":277},[129,21002,21003],{"class":273}," enabled\n",[129,21005,21006,21008,21010,21012,21014,21016,21018,21020,21022,21024,21026],{"class":265,"line":651},[129,21007,8101],{"class":2139},[129,21009,20251],{"class":273},[129,21011,362],{"class":277},[129,21013,20326],{"class":284},[129,21015,147],{"class":1376},[129,21017,424],{"class":277},[129,21019,20630],{"class":427},[129,21021,424],{"class":277},[129,21023,1015],{"class":277},[129,21025,20945],{"class":273},[129,21027,294],{"class":1376},[129,21029,21030,21032],{"class":265,"line":657},[129,21031,4520],{"class":2139},[129,21033,21034],{"class":273}," flags\n",[129,21036,21037,21039],{"class":265,"line":669},[129,21038,4028],{"class":277},[129,21040,294],{"class":273},[255,21042,21045],{"className":3922,"code":21043,"filename":21044,"language":3924,"meta":260,"style":260},"// 30s cache so changes propagate quickly\nexport default defineCachedEventHandler(async () => {\n  const storage = useStorage('cache')\n  return await storage.getItem\u003CRecord\u003Cstring, boolean>>('feature-flags') ?? {}\n}, { maxAge: 30, name: 'flags' })\n","server/api/flags.get.ts",[15,21046,21047,21052,21070,21090,21130],{"__ignoreMap":260},[129,21048,21049],{"class":265,"line":266},[129,21050,21051],{"class":376},"// 30s cache so changes propagate quickly\n",[129,21053,21054,21056,21058,21060,21062,21064,21066,21068],{"class":265,"line":297},[129,21055,4050],{"class":2139},[129,21057,4053],{"class":2139},[129,21059,6575],{"class":284},[129,21061,147],{"class":273},[129,21063,4508],{"class":269},[129,21065,4511],{"class":277},[129,21067,456],{"class":269},[129,21069,1371],{"class":277},[129,21071,21072,21074,21076,21078,21080,21082,21084,21086,21088],{"class":265,"line":315},[129,21073,5076],{"class":269},[129,21075,20251],{"class":273},[129,21077,4745],{"class":277},[129,21079,20256],{"class":284},[129,21081,147],{"class":1376},[129,21083,424],{"class":277},[129,21085,20263],{"class":427},[129,21087,424],{"class":277},[129,21089,294],{"class":1376},[129,21091,21092,21094,21096,21098,21100,21102,21104,21106,21108,21110,21112,21114,21116,21118,21120,21122,21124,21126,21128],{"class":265,"line":332},[129,21093,4520],{"class":2139},[129,21095,4779],{"class":2139},[129,21097,20251],{"class":273},[129,21099,362],{"class":277},[129,21101,20287],{"class":284},[129,21103,3945],{"class":277},[129,21105,20960],{"class":2161},[129,21107,3945],{"class":277},[129,21109,20520],{"class":2161},[129,21111,1015],{"class":277},[129,21113,20969],{"class":2161},[129,21115,20972],{"class":277},[129,21117,147],{"class":1376},[129,21119,424],{"class":277},[129,21121,20630],{"class":427},[129,21123,424],{"class":277},[129,21125,4005],{"class":1376},[129,21127,18967],{"class":277},[129,21129,19604],{"class":277},[129,21131,21132,21134,21136,21138,21140,21143,21145,21147,21149,21151,21154,21156,21158],{"class":265,"line":339},[129,21133,6625],{"class":277},[129,21135,1416],{"class":277},[129,21137,6630],{"class":1376},[129,21139,1380],{"class":277},[129,21141,21142],{"class":290}," 30",[129,21144,1015],{"class":277},[129,21146,5407],{"class":1376},[129,21148,1380],{"class":277},[129,21150,4261],{"class":277},[129,21152,21153],{"class":427},"flags",[129,21155,424],{"class":277},[129,21157,4255],{"class":277},[129,21159,294],{"class":273},[255,21161,21164],{"className":3922,"code":21162,"filename":21163,"language":3924,"meta":260,"style":260},"export function useFlag(flag: string) {\n  const { data } = useFetch('/api/flags')\n  return computed(() => data.value?.[flag] ?? false)\n}\n","composables/useFlag.ts",[15,21165,21166,21187,21213,21247],{"__ignoreMap":260},[129,21167,21168,21170,21172,21175,21177,21179,21181,21183,21185],{"class":265,"line":266},[129,21169,4050],{"class":2139},[129,21171,5060],{"class":269},[129,21173,21174],{"class":284}," useFlag",[129,21176,147],{"class":277},[129,21178,20996],{"class":452},[129,21180,1380],{"class":277},[129,21182,4622],{"class":2161},[129,21184,160],{"class":277},[129,21186,1371],{"class":277},[129,21188,21189,21191,21193,21195,21197,21199,21202,21204,21206,21209,21211],{"class":265,"line":297},[129,21190,5076],{"class":269},[129,21192,1416],{"class":277},[129,21194,11638],{"class":273},[129,21196,4255],{"class":277},[129,21198,4745],{"class":277},[129,21200,21201],{"class":284}," useFetch",[129,21203,147],{"class":1376},[129,21205,424],{"class":277},[129,21207,21208],{"class":427},"/api/flags",[129,21210,424],{"class":277},[129,21212,294],{"class":1376},[129,21214,21215,21217,21220,21222,21224,21226,21228,21230,21232,21234,21236,21238,21240,21242,21245],{"class":265,"line":315},[129,21216,4520],{"class":2139},[129,21218,21219],{"class":284}," computed",[129,21221,147],{"class":1376},[129,21223,4140],{"class":277},[129,21225,456],{"class":269},[129,21227,11638],{"class":273},[129,21229,362],{"class":277},[129,21231,8389],{"class":273},[129,21233,6059],{"class":277},[129,21235,4128],{"class":1376},[129,21237,20996],{"class":273},[129,21239,348],{"class":1376},[129,21241,18967],{"class":277},[129,21243,21244],{"class":4822}," false",[129,21246,294],{"class":1376},[129,21248,21249],{"class":265,"line":332},[129,21250,1530],{"class":277},[255,21252,21254],{"className":17386,"code":21253,"filename":20764,"language":17388,"meta":260,"style":260},"\u003Cscript setup>\nconst newCheckout = useFlag('new-checkout')\n\u003C/script>\n\n\u003Ctemplate>\n  \u003CNewCheckout v-if=\"newCheckout\" />\n  \u003COldCheckout v-else />\n\u003C/template>\n",[15,21255,21256,21266,21288,21296,21300,21308,21327,21337],{"__ignoreMap":260},[129,21257,21258,21260,21262,21264],{"class":265,"line":266},[129,21259,3945],{"class":277},[129,21261,10436],{"class":1376},[129,21263,17399],{"class":269},[129,21265,4676],{"class":277},[129,21267,21268,21270,21273,21275,21277,21279,21281,21284,21286],{"class":265,"line":297},[129,21269,270],{"class":269},[129,21271,21272],{"class":273}," newCheckout ",[129,21274,278],{"class":277},[129,21276,21174],{"class":284},[129,21278,147],{"class":273},[129,21280,424],{"class":277},[129,21282,21283],{"class":427},"new-checkout",[129,21285,424],{"class":277},[129,21287,294],{"class":273},[129,21289,21290,21292,21294],{"class":265,"line":315},[129,21291,12609],{"class":277},[129,21293,10436],{"class":1376},[129,21295,4676],{"class":277},[129,21297,21298],{"class":265,"line":332},[129,21299,336],{"emptyLinePlaceholder":335},[129,21301,21302,21304,21306],{"class":265,"line":339},[129,21303,3945],{"class":277},[129,21305,17580],{"class":1376},[129,21307,4676],{"class":277},[129,21309,21310,21312,21314,21316,21318,21320,21323,21325],{"class":265,"line":356},[129,21311,12979],{"class":277},[129,21313,20817],{"class":1376},[129,21315,20820],{"class":269},[129,21317,278],{"class":277},[129,21319,2258],{"class":277},[129,21321,21322],{"class":427},"newCheckout",[129,21324,2258],{"class":277},[129,21326,20832],{"class":277},[129,21328,21329,21331,21333,21335],{"class":265,"line":651},[129,21330,12979],{"class":277},[129,21332,20839],{"class":1376},[129,21334,20842],{"class":269},[129,21336,20832],{"class":277},[129,21338,21339,21341,21343],{"class":265,"line":657},[129,21340,12609],{"class":277},[129,21342,17580],{"class":1376},[129,21344,4676],{"class":277},[11,21346,21347,21348,21353,21354,21359],{},"For more sophisticated needs - percentage rollouts, user targeting, A/B testing with analytics - ",[51,21349,21352],{"href":21350,"rel":21351},"https://www.getunleash.io/",[55],"Unleash"," is open source and self-hostable. ",[51,21355,21358],{"href":21356,"rel":21357},"https://posthog.com/",[55],"PostHog"," bundles feature flags with session recording and analytics. Both have SDKs that work in a Nitro server context.",[21361,21362],"feature-flags-demo",{},[40,21364,21366],{"id":21365},"how-these-five-things-connect","How these five things connect",[11,21368,21369],{},"Each pattern answers one failure question:",[59,21371,21372,21381],{},[62,21373,21374],{},[65,21375,21376,21378],{},[68,21377,8468],{},[68,21379,21380],{},"Failure question it answers",[78,21382,21383,21390,21398,21406,21414],{},[65,21384,21385,21387],{},[83,21386,1610],{},[83,21388,21389],{},"What if the downstream service is slow or down?",[65,21391,21392,21395],{},[83,21393,21394],{},"Cache",[83,21396,21397],{},"What if the database can't handle this much read traffic?",[65,21399,21400,21403],{},[83,21401,21402],{},"Retry",[83,21404,21405],{},"What if this API call fails transiently?",[65,21407,21408,21411],{},[83,21409,21410],{},"Rate limit",[83,21412,21413],{},"What if someone abuses this endpoint?",[65,21415,21416,21418],{},[83,21417,20631],{},[83,21419,21420],{},"What if the new feature breaks in production?",[11,21422,21423],{},"Building these reactively - after the 2am incident - is more expensive than building them while the code is in front of you.",[11,21425,21426,21427,21430],{},"Nitro's storage abstraction is the thing that makes this accessible in Nuxt. The same ",[15,21428,21429],{},"useStorage('cache')"," call works in rate limiting, flag storage, and caching. Backed by memory locally, Redis in production - same API everywhere. You don't need to wire up five different clients.",[2001,21432],{},[11,21434,21435],{},"The tweet is right that most developers don't think about this until something breaks. Your checkout flow is already a system. Your auth is already a system.",[2026,21437,21438],{},"html pre.shiki code .s7zQu, html code.shiki .s7zQu{--shiki-light:#39ADB5;--shiki-light-font-style:italic;--shiki-default:#89DDFF;--shiki-default-font-style:italic;--shiki-dark:#89DDFF;--shiki-dark-font-style:italic}html pre.shiki code .s2Zo4, html code.shiki .s2Zo4{--shiki-light:#6182B8;--shiki-default:#82AAFF;--shiki-dark:#82AAFF}html pre.shiki code .sTEyZ, html code.shiki .sTEyZ{--shiki-light:#90A4AE;--shiki-default:#EEFFFF;--shiki-dark:#BABED8}html pre.shiki code .spNyl, html code.shiki .spNyl{--shiki-light:#9C3EDA;--shiki-default:#C792EA;--shiki-dark:#C792EA}html pre.shiki code .sMK4o, html code.shiki .sMK4o{--shiki-light:#39ADB5;--shiki-default:#89DDFF;--shiki-dark:#89DDFF}html pre.shiki code .sHdIc, html code.shiki .sHdIc{--shiki-light:#90A4AE;--shiki-light-font-style:italic;--shiki-default:#EEFFFF;--shiki-default-font-style:italic;--shiki-dark:#BABED8;--shiki-dark-font-style:italic}html pre.shiki code .swJcz, html code.shiki .swJcz{--shiki-light:#E53935;--shiki-default:#F07178;--shiki-dark:#F07178}html pre.shiki code .sHwdD, html code.shiki .sHwdD{--shiki-light:#90A4AE;--shiki-light-font-style:italic;--shiki-default:#546E7A;--shiki-default-font-style:italic;--shiki-dark:#676E95;--shiki-dark-font-style:italic}html pre.shiki code .sfNiH, html code.shiki .sfNiH{--shiki-light:#FF5370;--shiki-default:#FF9CAC;--shiki-dark:#FF9CAC}html .light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html.light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .sfazB, html code.shiki .sfazB{--shiki-light:#91B859;--shiki-default:#C3E88D;--shiki-dark:#C3E88D}html pre.shiki code .sBMFI, html code.shiki .sBMFI{--shiki-light:#E2931D;--shiki-default:#FFCB6B;--shiki-dark:#FFCB6B}html pre.shiki code .sbssI, html code.shiki .sbssI{--shiki-light:#F76D47;--shiki-default:#F78C6C;--shiki-dark:#F78C6C}",{"title":260,"searchDepth":297,"depth":297,"links":21440},[21441,21442,21443,21444,21445,21446],{"id":17812,"depth":297,"text":17813},{"id":18784,"depth":297,"text":18785},{"id":19409,"depth":297,"text":19410},{"id":20067,"depth":297,"text":20068},{"id":20630,"depth":297,"text":20631},{"id":21365,"depth":297,"text":21366},"Queues, caching, retries, rate limiting, feature flags - five infrastructure patterns every production Nuxt app eventually needs, and why Nitro makes them less painful.",{},"/blog/nuxt-system-patterns",{"title":17770,"description":21447},"blog/nuxt-system-patterns",[8631,2051,16484,21453,16486],"Backend","3SO1dOVFjbk6RwpvokEJB-PjK9KVh7Foa9Nk-c9PYrY",{"id":21456,"title":21457,"body":21458,"cover":2042,"date":8623,"description":25714,"extension":2045,"meta":25715,"navigation":335,"path":25716,"readingTime":2387,"seo":25717,"stem":25718,"tags":25719,"__hash__":25721},"blog/blog/system-design-fullstack.md","System design vs Nuxt fullstack developer",{"type":8,"value":21459,"toc":25678},[21460,21463,21466,21470,21474,21477,21480,21486,21622,21627,21713,21721,21752,21761,21765,21771,21774,21777,21864,21867,21871,21874,21954,21957,21960,21964,21967,21970,22248,22261,22267,22331,22342,22347,22349,22359,22361,22365,22369,22372,22465,22468,22493,22496,22500,22511,22514,22517,22664,22774,22863,22870,22874,22881,22884,22887,22896,22899,22903,22906,22926,22929,22949,22952,22955,22959,22962,22968,22974,22980,22986,22989,22991,22995,22999,23002,23008,23185,23191,23194,23198,23205,23673,23798,23810,23821,23825,23844,23848,23855,23859,23862,24229,24232,24238,24244,24254,24256,24260,24264,24270,24273,24276,24287,24290,24304,24310,24317,24323,24326,24330,24336,24342,24353,24497,24638,24784,24787,24791,24797,24800,24803,24823,24946,24952,24956,24961,24970,24976,25257,25260,25264,25267,25278,25281,25616,25625,25627,25631,25634,25672,25675],[11,21461,21462],{},"System design interviews talk about millions of users and terabytes of data. Most of us are building something used by thousands. But the vocabulary is still useful, and some of the patterns apply much earlier than you'd expect.",[11,21464,21465],{},"This covers 20 concepts from distributed systems through the lens of someone building Nuxt apps in production. Four categories. Where each one shows up and when it actually matters.",[40,21467,21469],{"id":21468},"performance-and-scaling","Performance and scaling",[2456,21471,21473],{"id":21472},"caching","Caching",[11,21475,21476],{},"The most impactful optimization most apps implement too late. Every read that could return the same data twice is a candidate - not because databases are slow, but because they have a ceiling, and your app's performance is bounded by that ceiling.",[11,21478,21479],{},"Nitro has three caching layers you can reach for in sequence:",[11,21481,21482,21485],{},[118,21483,21484],{},"In-process memory"," - fastest, lost on restart, not shared between instances:",[255,21487,21489],{"className":3922,"code":21488,"filename":18798,"language":3924,"meta":260,"style":260},"export default defineCachedEventHandler(async (event) => {\n  return db.select().from(products).where(eq(products.active, true))\n}, {\n  maxAge: 60 * 5,\n  getKey: (event) => `products:${getQuery(event).category ?? 'all'}`\n})\n",[15,21490,21491,21513,21557,21563,21577,21616],{"__ignoreMap":260},[129,21492,21493,21495,21497,21499,21501,21503,21505,21507,21509,21511],{"class":265,"line":266},[129,21494,4050],{"class":2139},[129,21496,4053],{"class":2139},[129,21498,6575],{"class":284},[129,21500,147],{"class":273},[129,21502,4508],{"class":269},[129,21504,3984],{"class":277},[129,21506,4100],{"class":452},[129,21508,160],{"class":277},[129,21510,456],{"class":269},[129,21512,1371],{"class":277},[129,21514,21515,21517,21519,21521,21523,21525,21527,21529,21531,21533,21535,21537,21539,21541,21543,21545,21547,21549,21551,21553,21555],{"class":265,"line":297},[129,21516,4520],{"class":2139},[129,21518,4479],{"class":273},[129,21520,362],{"class":277},[129,21522,2357],{"class":284},[129,21524,4140],{"class":1376},[129,21526,362],{"class":277},[129,21528,2589],{"class":284},[129,21530,147],{"class":1376},[129,21532,6612],{"class":273},[129,21534,160],{"class":1376},[129,21536,362],{"class":277},[129,21538,5436],{"class":284},[129,21540,147],{"class":1376},[129,21542,5441],{"class":284},[129,21544,147],{"class":1376},[129,21546,6612],{"class":273},[129,21548,362],{"class":277},[129,21550,5449],{"class":273},[129,21552,1015],{"class":277},[129,21554,4823],{"class":4822},[129,21556,471],{"class":1376},[129,21558,21559,21561],{"class":265,"line":315},[129,21560,6625],{"class":277},[129,21562,1371],{"class":277},[129,21564,21565,21567,21569,21571,21573,21575],{"class":265,"line":332},[129,21566,18878],{"class":1376},[129,21568,1380],{"class":277},[129,21570,18883],{"class":290},[129,21572,18886],{"class":277},[129,21574,1043],{"class":290},[129,21576,1386],{"class":277},[129,21578,21579,21581,21583,21585,21587,21589,21591,21593,21595,21597,21600,21603,21605,21607,21609,21611,21613],{"class":265,"line":339},[129,21580,18918],{"class":284},[129,21582,1380],{"class":277},[129,21584,3984],{"class":277},[129,21586,4100],{"class":452},[129,21588,160],{"class":277},[129,21590,456],{"class":269},[129,21592,5569],{"class":277},[129,21594,18955],{"class":427},[129,21596,4131],{"class":277},[129,21598,21599],{"class":284},"getQuery",[129,21601,21602],{"class":273},"(event)",[129,21604,362],{"class":277},[129,21606,18964],{"class":273},[129,21608,18967],{"class":277},[129,21610,4261],{"class":277},[129,21612,4544],{"class":427},[129,21614,21615],{"class":277},"'}`\n",[129,21617,21618,21620],{"class":265,"line":356},[129,21619,4028],{"class":277},[129,21621,294],{"class":273},[11,21623,21624,21626],{},[118,21625,19280],{}," - shared across instances, survives restarts, manually invalidatable. One config change:",[255,21628,21630],{"className":3922,"code":21629,"filename":17649,"language":3924,"meta":260,"style":260},"export default defineNuxtConfig({\n  nitro: {\n    storage: {\n      cache: { driver: 'redis', url: process.env.REDIS_URL }\n    }\n  }\n})\n",[15,21631,21632,21644,21652,21660,21699,21703,21707],{"__ignoreMap":260},[129,21633,21634,21636,21638,21640,21642],{"class":265,"line":266},[129,21635,4050],{"class":2139},[129,21637,4053],{"class":2139},[129,21639,19019],{"class":284},[129,21641,147],{"class":273},[129,21643,6455],{"class":277},[129,21645,21646,21648,21650],{"class":265,"line":297},[129,21647,4074],{"class":1376},[129,21649,1380],{"class":277},[129,21651,1371],{"class":277},[129,21653,21654,21656,21658],{"class":265,"line":315},[129,21655,19036],{"class":1376},[129,21657,1380],{"class":277},[129,21659,1371],{"class":277},[129,21661,21662,21664,21666,21668,21671,21673,21675,21677,21679,21681,21684,21686,21688,21690,21692,21694,21697],{"class":265,"line":332},[129,21663,19045],{"class":1376},[129,21665,1380],{"class":277},[129,21667,1416],{"class":277},[129,21669,21670],{"class":1376}," driver",[129,21672,1380],{"class":277},[129,21674,4261],{"class":277},[129,21676,19061],{"class":427},[129,21678,424],{"class":277},[129,21680,1015],{"class":277},[129,21682,21683],{"class":1376}," url",[129,21685,1380],{"class":277},[129,21687,5084],{"class":273},[129,21689,362],{"class":277},[129,21691,4439],{"class":273},[129,21693,362],{"class":277},[129,21695,21696],{"class":273},"REDIS_URL ",[129,21698,1530],{"class":277},[129,21700,21701],{"class":265,"line":339},[129,21702,6516],{"class":277},[129,21704,21705],{"class":265,"line":356},[129,21706,1524],{"class":277},[129,21708,21709,21711],{"class":265,"line":651},[129,21710,4028],{"class":277},[129,21712,294],{"class":273},[11,21714,21715,21717,21718,21720],{},[118,21716,19294],{}," - the layer most teams miss. Public endpoints with infrequently-changing data should set ",[15,21719,12225],{}," headers and let Cloudflare or Vercel serve them globally:",[255,21722,21724],{"className":3922,"code":21723,"language":3924,"meta":260,"style":260},"setResponseHeader(event, 'Cache-Control', 'public, max-age=300, s-maxage=3600')\n",[15,21725,21726],{"__ignoreMap":260},[129,21727,21728,21731,21734,21736,21738,21740,21742,21744,21746,21748,21750],{"class":265,"line":266},[129,21729,21730],{"class":284},"setResponseHeader",[129,21732,21733],{"class":273},"(event",[129,21735,1015],{"class":277},[129,21737,4261],{"class":277},[129,21739,12225],{"class":427},[129,21741,424],{"class":277},[129,21743,1015],{"class":277},[129,21745,4261],{"class":277},[129,21747,19375],{"class":427},[129,21749,424],{"class":277},[129,21751,294],{"class":273},[11,21753,21754,21756,21757,362],{},[15,21755,19405],{}," tells the CDN to cache for an hour. Your origin handles a fraction of the traffic it otherwise would. The full caching strategy and Nitro patterns are covered in the ",[51,21758,21760],{"href":21759},"/blog/nuxt-system-patterns#caching-strategy","system patterns article",[2456,21762,21764],{"id":21763},"load-balancing","Load balancing",[11,21766,21767,21768,362],{},"Load balancing distributes incoming requests across multiple server instances. You don't implement this - your infrastructure does (Nginx, Cloudflare, Kubernetes). What you do implement is the precondition: ",[118,21769,21770],{},"your app must be stateless",[11,21772,21773],{},"If your Nitro server stores anything in process memory that needs to survive across requests or be visible to other instances - session data, counters, locks - you have a problem the moment you scale to two servers. Server A handles the login, Server B handles the next request, Server B doesn't know who you are.",[11,21775,21776],{},"The fix is always the same: move shared state into a shared store.",[255,21778,21781],{"className":3922,"code":21779,"filename":21780,"language":3924,"meta":260,"style":260},"// Don't store sessions in Nitro's in-memory storage for multi-instance deploys\n// Redis is the same regardless of which instance handles the request\nconst storage = useStorage('cache') // backed by Redis in production\n\nawait storage.setItem(`session:${sessionId}`, userData, { ttl: 3600 })\n","server/utils/session.ts",[15,21782,21783,21788,21793,21817,21821],{"__ignoreMap":260},[129,21784,21785],{"class":265,"line":266},[129,21786,21787],{"class":376},"// Don't store sessions in Nitro's in-memory storage for multi-instance deploys\n",[129,21789,21790],{"class":265,"line":297},[129,21791,21792],{"class":376},"// Redis is the same regardless of which instance handles the request\n",[129,21794,21795,21797,21800,21802,21804,21806,21808,21810,21812,21814],{"class":265,"line":315},[129,21796,270],{"class":269},[129,21798,21799],{"class":273}," storage ",[129,21801,278],{"class":277},[129,21803,20256],{"class":284},[129,21805,147],{"class":273},[129,21807,424],{"class":277},[129,21809,20263],{"class":427},[129,21811,424],{"class":277},[129,21813,4005],{"class":273},[129,21815,21816],{"class":376},"// backed by Redis in production\n",[129,21818,21819],{"class":265,"line":332},[129,21820,336],{"emptyLinePlaceholder":335},[129,21822,21823,21825,21827,21829,21831,21833,21835,21838,21840,21842,21844,21846,21849,21851,21853,21855,21857,21860,21862],{"class":265,"line":339},[129,21824,8083],{"class":2139},[129,21826,20251],{"class":273},[129,21828,362],{"class":277},[129,21830,20326],{"class":284},[129,21832,147],{"class":273},[129,21834,4125],{"class":277},[129,21836,21837],{"class":427},"session:",[129,21839,4131],{"class":277},[129,21841,6179],{"class":273},[129,21843,4175],{"class":277},[129,21845,1015],{"class":277},[129,21847,21848],{"class":273}," userData",[129,21850,1015],{"class":277},[129,21852,1416],{"class":277},[129,21854,20341],{"class":1376},[129,21856,1380],{"class":277},[129,21858,21859],{"class":290}," 3600",[129,21861,4255],{"class":277},[129,21863,294],{"class":273},[11,21865,21866],{},"If your app is a single instance, load balancing is someone else's problem. When you add a second instance, memory state breaks silently.",[2456,21868,21870],{"id":21869},"horizontal-vs-vertical-scaling","Horizontal vs vertical scaling",[11,21872,21873],{},"Two ways to handle more traffic:",[59,21875,21876,21888],{},[62,21877,21878],{},[65,21879,21880,21882,21885],{},[68,21881],{},[68,21883,21884],{},"Vertical",[68,21886,21887],{},"Horizontal",[78,21889,21890,21903,21916,21928,21941],{},[65,21891,21892,21897,21900],{},[83,21893,21894],{},[118,21895,21896],{},"What",[83,21898,21899],{},"Bigger server (more CPU/RAM)",[83,21901,21902],{},"More servers",[65,21904,21905,21910,21913],{},[83,21906,21907],{},[118,21908,21909],{},"Ceiling",[83,21911,21912],{},"Hardware limit",[83,21914,21915],{},"Theoretically unlimited",[65,21917,21918,21923,21925],{},[83,21919,21920],{},[118,21921,21922],{},"Complexity",[83,21924,12692],{},[83,21926,21927],{},"Requires stateless app + shared state",[65,21929,21930,21935,21938],{},[83,21931,21932],{},[118,21933,21934],{},"Downtime",[83,21936,21937],{},"Restart required",[83,21939,21940],{},"Rolling deploys, zero downtime",[65,21942,21943,21948,21951],{},[83,21944,21945],{},[118,21946,21947],{},"Right choice when",[83,21949,21950],{},"First scaling decision",[83,21952,21953],{},"Vertical ceiling hit, or zero-downtime required",[11,21955,21956],{},"Vertical scaling is the right first answer. Double the RAM before you double the servers. It is simpler, cheaper at small scale, and defers the complexity of distributed state. A $400/month server handles significant load.",[11,21958,21959],{},"Horizontal scaling becomes necessary when you've hit the vertical ceiling or need zero-downtime deploys. Nuxt is designed for it - Nitro on Vercel, Netlify, or Cloudflare Workers is horizontal scaling by default. Each request can land on a different instance. This is why the Redis-for-state pattern matters even before you have multiple servers.",[2456,21961,21963],{"id":21962},"database-indexing","Database indexing",[11,21965,21966],{},"An unindexed query on a large table is a full table scan - every row read, every time. Indexes are the single highest-leverage database optimization available to most applications and the most commonly skipped.",[11,21968,21969],{},"With Drizzle ORM, define indexes alongside your schema:",[255,21971,21974],{"className":3922,"code":21972,"filename":21973,"language":3924,"meta":260,"style":260},"export const orders = pgTable('orders', {\n  id: uuid('id').primaryKey().default(sql`gen_random_uuid()`),\n  userId: uuid('user_id').notNull(),\n  status: text('status').notNull(),\n  createdAt: timestamp('created_at').defaultNow(),\n}, (t) => ({\n  // Single column - fast lookups by user\n  userIdx: index('orders_user_id_idx').on(t.userId),\n  // Composite - covers queries filtering by status AND sorting by date\n  statusCreatedIdx: index('orders_status_created_at_idx').on(t.status, t.createdAt),\n}))\n","server/db/schema.ts",[15,21975,21976,22003,22050,22079,22108,22138,22154,22159,22195,22200,22242],{"__ignoreMap":260},[129,21977,21978,21980,21982,21985,21987,21990,21992,21994,21997,21999,22001],{"class":265,"line":266},[129,21979,4050],{"class":2139},[129,21981,4456],{"class":269},[129,21983,21984],{"class":273}," orders ",[129,21986,278],{"class":277},[129,21988,21989],{"class":284}," pgTable",[129,21991,147],{"class":273},[129,21993,424],{"class":277},[129,21995,21996],{"class":427},"orders",[129,21998,424],{"class":277},[129,22000,1015],{"class":277},[129,22002,1371],{"class":277},[129,22004,22005,22008,22010,22013,22015,22017,22019,22021,22023,22025,22028,22030,22032,22034,22036,22039,22041,22044,22046,22048],{"class":265,"line":297},[129,22006,22007],{"class":1376},"  id",[129,22009,1380],{"class":277},[129,22011,22012],{"class":284}," uuid",[129,22014,147],{"class":273},[129,22016,424],{"class":277},[129,22018,3190],{"class":427},[129,22020,424],{"class":277},[129,22022,160],{"class":273},[129,22024,362],{"class":277},[129,22026,22027],{"class":284},"primaryKey",[129,22029,4140],{"class":273},[129,22031,362],{"class":277},[129,22033,12832],{"class":284},[129,22035,147],{"class":273},[129,22037,22038],{"class":284},"sql",[129,22040,4125],{"class":277},[129,22042,22043],{"class":427},"gen_random_uuid()",[129,22045,4125],{"class":277},[129,22047,160],{"class":273},[129,22049,1386],{"class":277},[129,22051,22052,22055,22057,22059,22061,22063,22066,22068,22070,22072,22075,22077],{"class":265,"line":315},[129,22053,22054],{"class":1376},"  userId",[129,22056,1380],{"class":277},[129,22058,22012],{"class":284},[129,22060,147],{"class":273},[129,22062,424],{"class":277},[129,22064,22065],{"class":427},"user_id",[129,22067,424],{"class":277},[129,22069,160],{"class":273},[129,22071,362],{"class":277},[129,22073,22074],{"class":284},"notNull",[129,22076,4140],{"class":273},[129,22078,1386],{"class":277},[129,22080,22081,22084,22086,22089,22091,22093,22096,22098,22100,22102,22104,22106],{"class":265,"line":332},[129,22082,22083],{"class":1376},"  status",[129,22085,1380],{"class":277},[129,22087,22088],{"class":284}," text",[129,22090,147],{"class":273},[129,22092,424],{"class":277},[129,22094,22095],{"class":427},"status",[129,22097,424],{"class":277},[129,22099,160],{"class":273},[129,22101,362],{"class":277},[129,22103,22074],{"class":284},[129,22105,4140],{"class":273},[129,22107,1386],{"class":277},[129,22109,22110,22113,22115,22118,22120,22122,22125,22127,22129,22131,22134,22136],{"class":265,"line":339},[129,22111,22112],{"class":1376},"  createdAt",[129,22114,1380],{"class":277},[129,22116,22117],{"class":284}," timestamp",[129,22119,147],{"class":273},[129,22121,424],{"class":277},[129,22123,22124],{"class":427},"created_at",[129,22126,424],{"class":277},[129,22128,160],{"class":273},[129,22130,362],{"class":277},[129,22132,22133],{"class":284},"defaultNow",[129,22135,4140],{"class":273},[129,22137,1386],{"class":277},[129,22139,22140,22142,22144,22146,22148,22150,22152],{"class":265,"line":356},[129,22141,6625],{"class":277},[129,22143,3984],{"class":277},[129,22145,205],{"class":452},[129,22147,160],{"class":277},[129,22149,456],{"class":269},[129,22151,3984],{"class":273},[129,22153,6455],{"class":277},[129,22155,22156],{"class":265,"line":651},[129,22157,22158],{"class":376},"  // Single column - fast lookups by user\n",[129,22160,22161,22164,22166,22169,22171,22173,22176,22178,22180,22182,22185,22188,22190,22193],{"class":265,"line":657},[129,22162,22163],{"class":1376},"  userIdx",[129,22165,1380],{"class":277},[129,22167,22168],{"class":284}," index",[129,22170,147],{"class":273},[129,22172,424],{"class":277},[129,22174,22175],{"class":427},"orders_user_id_idx",[129,22177,424],{"class":277},[129,22179,160],{"class":273},[129,22181,362],{"class":277},[129,22183,22184],{"class":284},"on",[129,22186,22187],{"class":273},"(t",[129,22189,362],{"class":277},[129,22191,22192],{"class":273},"userId)",[129,22194,1386],{"class":277},[129,22196,22197],{"class":265,"line":669},[129,22198,22199],{"class":376},"  // Composite - covers queries filtering by status AND sorting by date\n",[129,22201,22202,22205,22207,22209,22211,22213,22216,22218,22220,22222,22224,22226,22228,22230,22232,22235,22237,22240],{"class":265,"line":693},[129,22203,22204],{"class":1376},"  statusCreatedIdx",[129,22206,1380],{"class":277},[129,22208,22168],{"class":284},[129,22210,147],{"class":273},[129,22212,424],{"class":277},[129,22214,22215],{"class":427},"orders_status_created_at_idx",[129,22217,424],{"class":277},[129,22219,160],{"class":273},[129,22221,362],{"class":277},[129,22223,22184],{"class":284},[129,22225,22187],{"class":273},[129,22227,362],{"class":277},[129,22229,22095],{"class":273},[129,22231,1015],{"class":277},[129,22233,22234],{"class":273}," t",[129,22236,362],{"class":277},[129,22238,22239],{"class":273},"createdAt)",[129,22241,1386],{"class":277},[129,22243,22244,22246],{"class":265,"line":712},[129,22245,4028],{"class":277},[129,22247,471],{"class":273},[11,22249,22250,22251,22254,22255,22257,22258,22260],{},"Column order in composite indexes matters: ",[15,22252,22253],{},"(status, created_at)"," supports queries filtering by ",[15,22256,22095],{},", and queries filtering by both - but not queries filtering only by ",[15,22259,22124],{},". Put the most selective column first.",[11,22262,22263,22264,1380],{},"Verify indexes are being used with ",[15,22265,22266],{},"EXPLAIN ANALYZE",[255,22268,22271],{"className":22269,"code":22270,"language":22038,"meta":260,"style":260},"language-sql shiki shiki-themes material-theme-lighter material-theme material-theme-palenight","EXPLAIN ANALYZE\nSELECT * FROM orders WHERE user_id = '...' AND status = 'pending'\nORDER BY created_at DESC;\n",[15,22272,22273,22278,22319],{"__ignoreMap":260},[129,22274,22275],{"class":265,"line":266},[129,22276,22277],{"class":273},"EXPLAIN ANALYZE\n",[129,22279,22280,22283,22285,22288,22290,22293,22296,22298,22300,22302,22304,22307,22310,22312,22314,22317],{"class":265,"line":297},[129,22281,22282],{"class":290},"SELECT",[129,22284,18886],{"class":277},[129,22286,22287],{"class":290}," FROM",[129,22289,21984],{"class":273},[129,22291,22292],{"class":290},"WHERE",[129,22294,22295],{"class":273}," user_id ",[129,22297,278],{"class":277},[129,22299,4261],{"class":277},[129,22301,14218],{"class":427},[129,22303,424],{"class":277},[129,22305,22306],{"class":290}," AND",[129,22308,22309],{"class":290}," status",[129,22311,4745],{"class":277},[129,22313,4261],{"class":277},[129,22315,22316],{"class":427},"pending",[129,22318,4267],{"class":277},[129,22320,22321,22324,22327,22329],{"class":265,"line":315},[129,22322,22323],{"class":290},"ORDER BY",[129,22325,22326],{"class":273}," created_at ",[129,22328,5608],{"class":290},[129,22330,17120],{"class":273},[11,22332,22333,22334,22337,22338,22341],{},"If you see ",[15,22335,22336],{},"Seq Scan"," where you expect ",[15,22339,22340],{},"Index Scan",", either the index doesn't cover the query or the planner determined the table is small enough that a scan is faster (correct behavior - don't fight it).",[3325,22343,22344],{},[11,22345,22346],{},"Over-indexing is a real problem. Every index slows down writes - the index must be updated on every INSERT, UPDATE, and DELETE. Index columns that appear in WHERE clauses of your most frequent queries. Index foreign keys. Don't index everything.",[2456,22348,20068],{"id":20067},[11,22350,22351,22352,22355,22356,22358],{},"Covered in depth in the ",[51,22353,21760],{"href":22354},"/blog/nuxt-system-patterns#rate-limiting",". The short version: Nitro middleware plus ",[15,22357,20077],{}," gives you per-IP rate limiting with minimal code, backed by Redis in production. The important extension: different limits for different endpoints - auth endpoints need stricter limits (brute force protection) than read endpoints.",[2001,22360],{},[40,22362,22364],{"id":22363},"data-and-storage","Data and storage",[2456,22366,22368],{"id":22367},"sql-vs-nosql","SQL vs NoSQL",[11,22370,22371],{},"The default answer is PostgreSQL. Most applications - CRUD, transactional, relational data - are better served by a well-designed relational schema than a flexible document store. PostgreSQL's JSONB column type eliminates one of the few legitimate NoSQL advantages (schema flexibility).",[59,22373,22374,22386],{},[62,22375,22376],{},[65,22377,22378,22380,22383],{},[68,22379],{},[68,22381,22382],{},"SQL (PostgreSQL)",[68,22384,22385],{},"NoSQL (document/key-value/wide-column)",[78,22387,22388,22401,22414,22427,22440,22453],{},[65,22389,22390,22395,22398],{},[83,22391,22392],{},[118,22393,22394],{},"Data shape",[83,22396,22397],{},"Structured, defined schema",[83,22399,22400],{},"Flexible, schemaless",[65,22402,22403,22408,22411],{},[83,22404,22405],{},[118,22406,22407],{},"Relationships",[83,22409,22410],{},"Native (JOINs, foreign keys)",[83,22412,22413],{},"Application-level",[65,22415,22416,22421,22424],{},[83,22417,22418],{},[118,22419,22420],{},"Transactions",[83,22422,22423],{},"ACID, multi-table",[83,22425,22426],{},"Varies by engine",[65,22428,22429,22434,22437],{},[83,22430,22431],{},[118,22432,22433],{},"Query power",[83,22435,22436],{},"Full SQL",[83,22438,22439],{},"Limited or engine-specific",[65,22441,22442,22447,22450],{},[83,22443,22444],{},[118,22445,22446],{},"Scale pattern",[83,22448,22449],{},"Vertical, then read replicas",[83,22451,22452],{},"Designed for horizontal",[65,22454,22455,22459,22462],{},[83,22456,22457],{},[118,22458,12127],{},[83,22460,22461],{},"Most applications",[83,22463,22464],{},"Specific patterns listed below",[11,22466,22467],{},"NoSQL makes sense for specific roles:",[1822,22469,22470,22475,22481,22487],{},[1825,22471,22472,22474],{},[118,22473,19280],{},": caching, sessions, queues, pub/sub - it's a data structure server, use it for these",[1825,22476,22477,22480],{},[118,22478,22479],{},"Cassandra / DynamoDB",": write-heavy time-series data at very large scale",[1825,22482,22483,22486],{},[118,22484,22485],{},"MongoDB",": truly schemaless documents where record shape varies significantly",[1825,22488,22489,22492],{},[118,22490,22491],{},"Elasticsearch",": full-text search at scale",[11,22494,22495],{},"Most applications that reach for MongoDB early would be better off with PostgreSQL plus JSONB for the flexible parts. The query power of SQL is hard to replace once you need joins, aggregations, or complex filters.",[2456,22497,22499],{"id":22498},"replication","Replication",[11,22501,22502,22503,22506,22507,22510],{},"Replication copies data from a ",[118,22504,22505],{},"primary"," to one or more ",[118,22508,22509],{},"replicas"," asynchronously. The primary handles all writes; replicas can handle reads.",[11,22512,22513],{},"Two practical benefits: redundancy (if the primary fails, promote a replica) and read scaling (distribute read traffic).",[11,22515,22516],{},"In a Nuxt app, you'd use two database connections:",[255,22518,22520],{"className":3922,"code":22519,"filename":4371,"language":3924,"meta":260,"style":260},"import { drizzle } from 'drizzle-orm/node-postgres'\nimport { Pool } from 'pg'\n\n// All writes go here\nexport const writeDb = drizzle(new Pool({ connectionString: process.env.DATABASE_URL }))\n\n// Reads can go here - potentially stale by replication lag\nexport const readDb = drizzle(new Pool({ connectionString: process.env.DATABASE_READ_URL }))\n",[15,22521,22522,22541,22561,22565,22570,22613,22617,22622],{"__ignoreMap":260},[129,22523,22524,22526,22528,22530,22532,22534,22536,22539],{"class":265,"line":266},[129,22525,2140],{"class":2139},[129,22527,1416],{"class":277},[129,22529,4252],{"class":273},[129,22531,4255],{"class":277},[129,22533,4258],{"class":2139},[129,22535,4261],{"class":277},[129,22537,22538],{"class":427},"drizzle-orm/node-postgres",[129,22540,4267],{"class":277},[129,22542,22543,22545,22547,22550,22552,22554,22556,22559],{"class":265,"line":297},[129,22544,2140],{"class":2139},[129,22546,1416],{"class":277},[129,22548,22549],{"class":273}," Pool",[129,22551,4255],{"class":277},[129,22553,4258],{"class":2139},[129,22555,4261],{"class":277},[129,22557,22558],{"class":427},"pg",[129,22560,4267],{"class":277},[129,22562,22563],{"class":265,"line":315},[129,22564,336],{"emptyLinePlaceholder":335},[129,22566,22567],{"class":265,"line":332},[129,22568,22569],{"class":376},"// All writes go here\n",[129,22571,22572,22574,22576,22579,22581,22583,22585,22587,22589,22591,22593,22596,22598,22600,22602,22604,22606,22609,22611],{"class":265,"line":339},[129,22573,4050],{"class":2139},[129,22575,4456],{"class":269},[129,22577,22578],{"class":273}," writeDb ",[129,22580,278],{"class":277},[129,22582,4252],{"class":284},[129,22584,147],{"class":273},[129,22586,4134],{"class":277},[129,22588,22549],{"class":284},[129,22590,147],{"class":273},[129,22592,4796],{"class":277},[129,22594,22595],{"class":1376}," connectionString",[129,22597,1380],{"class":277},[129,22599,5084],{"class":273},[129,22601,362],{"class":277},[129,22603,4439],{"class":273},[129,22605,362],{"class":277},[129,22607,22608],{"class":273},"DATABASE_URL ",[129,22610,4028],{"class":277},[129,22612,471],{"class":273},[129,22614,22615],{"class":265,"line":356},[129,22616,336],{"emptyLinePlaceholder":335},[129,22618,22619],{"class":265,"line":651},[129,22620,22621],{"class":376},"// Reads can go here - potentially stale by replication lag\n",[129,22623,22624,22626,22628,22631,22633,22635,22637,22639,22641,22643,22645,22647,22649,22651,22653,22655,22657,22660,22662],{"class":265,"line":657},[129,22625,4050],{"class":2139},[129,22627,4456],{"class":269},[129,22629,22630],{"class":273}," readDb ",[129,22632,278],{"class":277},[129,22634,4252],{"class":284},[129,22636,147],{"class":273},[129,22638,4134],{"class":277},[129,22640,22549],{"class":284},[129,22642,147],{"class":273},[129,22644,4796],{"class":277},[129,22646,22595],{"class":1376},[129,22648,1380],{"class":277},[129,22650,5084],{"class":273},[129,22652,362],{"class":277},[129,22654,4439],{"class":273},[129,22656,362],{"class":277},[129,22658,22659],{"class":273},"DATABASE_READ_URL ",[129,22661,4028],{"class":277},[129,22663,471],{"class":273},[255,22665,22668],{"className":3922,"code":22666,"filename":22667,"language":3924,"meta":260,"style":260},"export default defineEventHandler(async (event) => {\n  const body = await readBody(event)\n  // Writes always to primary\n  const [order] = await writeDb.insert(orders).values(body).returning()\n  return order\n})\n","server/api/orders/create.post.ts",[15,22669,22670,22692,22710,22715,22761,22768],{"__ignoreMap":260},[129,22671,22672,22674,22676,22678,22680,22682,22684,22686,22688,22690],{"class":265,"line":266},[129,22673,4050],{"class":2139},[129,22675,4053],{"class":2139},[129,22677,4503],{"class":284},[129,22679,147],{"class":273},[129,22681,4508],{"class":269},[129,22683,3984],{"class":277},[129,22685,4100],{"class":452},[129,22687,160],{"class":277},[129,22689,456],{"class":269},[129,22691,1371],{"class":277},[129,22693,22694,22696,22698,22700,22702,22704,22706,22708],{"class":265,"line":297},[129,22695,5076],{"class":269},[129,22697,14357],{"class":273},[129,22699,4745],{"class":277},[129,22701,4779],{"class":2139},[129,22703,5264],{"class":284},[129,22705,147],{"class":1376},[129,22707,4100],{"class":273},[129,22709,294],{"class":1376},[129,22711,22712],{"class":265,"line":315},[129,22713,22714],{"class":376},"  // Writes always to primary\n",[129,22716,22717,22719,22721,22723,22725,22727,22729,22732,22734,22737,22739,22741,22743,22745,22748,22750,22752,22754,22756,22759],{"class":265,"line":332},[129,22718,5076],{"class":269},[129,22720,1010],{"class":277},[129,22722,5592],{"class":273},[129,22724,14170],{"class":277},[129,22726,4745],{"class":277},[129,22728,4779],{"class":2139},[129,22730,22731],{"class":273}," writeDb",[129,22733,362],{"class":277},[129,22735,22736],{"class":284},"insert",[129,22738,147],{"class":1376},[129,22740,21996],{"class":273},[129,22742,160],{"class":1376},[129,22744,362],{"class":277},[129,22746,22747],{"class":284},"values",[129,22749,147],{"class":1376},[129,22751,14959],{"class":273},[129,22753,160],{"class":1376},[129,22755,362],{"class":277},[129,22757,22758],{"class":284},"returning",[129,22760,2451],{"class":1376},[129,22762,22763,22765],{"class":265,"line":339},[129,22764,4520],{"class":2139},[129,22766,22767],{"class":273}," order\n",[129,22769,22770,22772],{"class":265,"line":356},[129,22771,4028],{"class":277},[129,22773,294],{"class":273},[255,22775,22777],{"className":3922,"code":22776,"filename":18798,"language":3924,"meta":260,"style":260},"export default defineCachedEventHandler(async () => {\n  // Stale by a few milliseconds is acceptable for a product catalog\n  return readDb.select().from(products).where(eq(products.active, true))\n}, { maxAge: 30 })\n",[15,22778,22779,22797,22802,22847],{"__ignoreMap":260},[129,22780,22781,22783,22785,22787,22789,22791,22793,22795],{"class":265,"line":266},[129,22782,4050],{"class":2139},[129,22784,4053],{"class":2139},[129,22786,6575],{"class":284},[129,22788,147],{"class":273},[129,22790,4508],{"class":269},[129,22792,4511],{"class":277},[129,22794,456],{"class":269},[129,22796,1371],{"class":277},[129,22798,22799],{"class":265,"line":297},[129,22800,22801],{"class":376},"  // Stale by a few milliseconds is acceptable for a product catalog\n",[129,22803,22804,22806,22809,22811,22813,22815,22817,22819,22821,22823,22825,22827,22829,22831,22833,22835,22837,22839,22841,22843,22845],{"class":265,"line":315},[129,22805,4520],{"class":2139},[129,22807,22808],{"class":273}," readDb",[129,22810,362],{"class":277},[129,22812,2357],{"class":284},[129,22814,4140],{"class":1376},[129,22816,362],{"class":277},[129,22818,2589],{"class":284},[129,22820,147],{"class":1376},[129,22822,6612],{"class":273},[129,22824,160],{"class":1376},[129,22826,362],{"class":277},[129,22828,5436],{"class":284},[129,22830,147],{"class":1376},[129,22832,5441],{"class":284},[129,22834,147],{"class":1376},[129,22836,6612],{"class":273},[129,22838,362],{"class":277},[129,22840,5449],{"class":273},[129,22842,1015],{"class":277},[129,22844,4823],{"class":4822},[129,22846,471],{"class":1376},[129,22848,22849,22851,22853,22855,22857,22859,22861],{"class":265,"line":332},[129,22850,6625],{"class":277},[129,22852,1416],{"class":277},[129,22854,6630],{"class":1376},[129,22856,1380],{"class":277},[129,22858,21142],{"class":290},[129,22860,4255],{"class":277},[129,22862,294],{"class":273},[11,22864,22865,22866,22869],{},"The catch is ",[118,22867,22868],{},"replication lag",". Replicas trail the primary by milliseconds to seconds. After a write, reading immediately from a replica might return stale data. For user-facing write-then-read flows (\"you just placed an order, here are your orders\"), read from the primary. For background loads and public data, replicas are fine.",[2456,22871,22873],{"id":22872},"sharding","Sharding",[11,22875,22876,22877,22880],{},"Sharding splits a single database into multiple ",[118,22878,22879],{},"shards",", each holding a subset of the data. A users table sharded by region means European users live in the EU shard, American users in the US shard.",[11,22882,22883],{},"This solves a problem you almost certainly don't have yet. Sharding makes sense when a single PostgreSQL instance can no longer handle your write volume - typically above 10,000 sustained writes per second - or when your dataset is large enough that even indexed queries are slow.",[11,22885,22886],{},"The operational cost is significant: cross-shard queries become application-level joins, transactions spanning shards require distributed transaction protocols, and resharding when your partition key was wrong is painful.",[11,22888,22889,22890,22895],{},"If you do need it: ",[51,22891,22894],{"href":22892,"rel":22893},"https://www.citusdata.com/",[55],"Citus"," extends PostgreSQL with transparent sharding. PlanetScale and CockroachDB handle it as managed services.",[11,22897,22898],{},"The practical Nuxt callout: if you're sharding by tenant in a multi-tenant SaaS app, you can do it at the connection level - route requests to a different database URL based on tenant ID. No special library needed, just a map of tenant IDs to connection strings.",[2456,22900,22902],{"id":22901},"cap-theorem","CAP theorem",[11,22904,22905],{},"Every distributed system makes a tradeoff between three properties:",[1822,22907,22908,22914,22920],{},[1825,22909,22910,22913],{},[118,22911,22912],{},"C","onsistency: every read receives the most recent write, or an error",[1825,22915,22916,22919],{},[118,22917,22918],{},"A","vailability: every request receives a response (not necessarily the latest data)",[1825,22921,22922,22925],{},[118,22923,22924],{},"P","artition tolerance: the system keeps operating when network communication between nodes fails",[11,22927,22928],{},"The theorem states that you can only guarantee two of the three simultaneously during a network partition. Partition tolerance is not optional in distributed systems - networks do fail - so the real choice is between consistency and availability.",[22930,22931,22934,22943],"callout",{"color":22932,"icon":22933},"neutral","i-lucide-split",[11,22935,22936,22939,22940],{},[118,22937,22938],{},"CP"," (consistency + partition tolerance): during a network split, nodes that can't confirm the latest data will return an error rather than stale data. PostgreSQL primary/replica with synchronous replication behaves this way. ",[22941,22942],"br",{},[11,22944,22945,22948],{},[118,22946,22947],{},"AP"," (availability + partition tolerance): during a split, nodes keep responding but might return stale data. DynamoDB and Cassandra default here. Responses are always fast; recency is not guaranteed.",[11,22950,22951],{},"For a Nuxt app with a single PostgreSQL database, CAP doesn't apply - it describes distributed systems with multiple nodes. Once you add read replicas or multiple database nodes, the tradeoffs become real.",[11,22953,22954],{},"In practice: most applications benefit from an AP posture for reads (slightly stale data is acceptable) and a CP posture for writes (must be consistent, better to fail than corrupt). That's roughly what \"read from replica, write to primary\" gives you.",[2456,22956,22958],{"id":22957},"consistency-models","Consistency models",[11,22960,22961],{},"Consistency models define what guarantees you have about the order and visibility of writes across distributed nodes. The four you'll encounter in practice:",[11,22963,22964,22967],{},[118,22965,22966],{},"Strong consistency",": every read sees the most recent write. Guaranteed on a single node, expensive on distributed systems. A single PostgreSQL instance gives you this.",[11,22969,22970,22973],{},[118,22971,22972],{},"Eventual consistency",": given no new writes, all nodes will eventually converge to the same value. Redis replication is eventually consistent - a write to the primary propagates to replicas with a small delay.",[11,22975,22976,22979],{},[118,22977,22978],{},"Read-your-writes consistency",": you always see your own writes, even if other users might temporarily see stale data. This is the minimum a user expects after submitting a form.",[11,22981,22982,22985],{},[118,22983,22984],{},"Monotonic reads",": you never see older data than data you've already seen. If you read version 5 of something, you won't subsequently read version 3.",[11,22987,22988],{},"In Nuxt, the relevant design question is: when using read replicas, which requests need to hit the primary? A reliable rule: always read from the primary immediately after a write within the same user session. Background loads, public pages, and analytics can use replicas.",[2001,22990],{},[40,22992,22994],{"id":22993},"reliability","Reliability",[2456,22996,22998],{"id":22997},"fault-tolerance","Fault tolerance",[11,23000,23001],{},"Fault tolerance is designing with the assumption that things will fail - not \"if the email service is down\" but \"when the email service is down.\" The question is whether that failure propagates to the user.",[11,23003,23004,23007],{},[118,23005,23006],{},"Graceful degradation",": the system keeps running with reduced functionality. If the recommendation engine is down, show a generic product list instead of returning a 500.",[255,23009,23012],{"className":3922,"code":23010,"filename":23011,"language":3924,"meta":260,"style":260},"export default defineEventHandler(async () => {\n  const products = await db.select().from(featured_products)\n\n  let recommendations: Product[] = []\n  try {\n    // External ML service - might be slow or down\n    recommendations = await $fetch('https://recommendations.internal/api/suggest', {\n      timeout: 500, // fail fast, don't let this block the page\n    })\n  } catch {\n    // Fail open - homepage loads without recommendations\n    // Important: log this, but don't surface to the user\n  }\n\n  return { products, recommendations }\n})\n","server/api/homepage.get.ts",[15,23013,23014,23032,23062,23066,23084,23090,23095,23119,23133,23139,23147,23152,23157,23161,23165,23179],{"__ignoreMap":260},[129,23015,23016,23018,23020,23022,23024,23026,23028,23030],{"class":265,"line":266},[129,23017,4050],{"class":2139},[129,23019,4053],{"class":2139},[129,23021,4503],{"class":284},[129,23023,147],{"class":273},[129,23025,4508],{"class":269},[129,23027,4511],{"class":277},[129,23029,456],{"class":269},[129,23031,1371],{"class":277},[129,23033,23034,23036,23039,23041,23043,23045,23047,23049,23051,23053,23055,23057,23060],{"class":265,"line":297},[129,23035,5076],{"class":269},[129,23037,23038],{"class":273}," products",[129,23040,4745],{"class":277},[129,23042,4779],{"class":2139},[129,23044,4479],{"class":273},[129,23046,362],{"class":277},[129,23048,2357],{"class":284},[129,23050,4140],{"class":1376},[129,23052,362],{"class":277},[129,23054,2589],{"class":284},[129,23056,147],{"class":1376},[129,23058,23059],{"class":273},"featured_products",[129,23061,294],{"class":1376},[129,23063,23064],{"class":265,"line":315},[129,23065,336],{"emptyLinePlaceholder":335},[129,23067,23068,23070,23073,23075,23078,23080,23082],{"class":265,"line":332},[129,23069,5720],{"class":269},[129,23071,23072],{"class":273}," recommendations",[129,23074,1380],{"class":277},[129,23076,23077],{"class":2161}," Product",[129,23079,15241],{"class":1376},[129,23081,278],{"class":277},[129,23083,587],{"class":1376},[129,23085,23086,23088],{"class":265,"line":339},[129,23087,13569],{"class":2139},[129,23089,1371],{"class":277},[129,23091,23092],{"class":265,"line":356},[129,23093,23094],{"class":376},"    // External ML service - might be slow or down\n",[129,23096,23097,23100,23102,23104,23106,23108,23110,23113,23115,23117],{"class":265,"line":651},[129,23098,23099],{"class":273},"    recommendations",[129,23101,4745],{"class":277},[129,23103,4779],{"class":2139},[129,23105,8288],{"class":284},[129,23107,147],{"class":1376},[129,23109,424],{"class":277},[129,23111,23112],{"class":427},"https://recommendations.internal/api/suggest",[129,23114,424],{"class":277},[129,23116,1015],{"class":277},[129,23118,1371],{"class":277},[129,23120,23121,23124,23126,23128,23130],{"class":265,"line":657},[129,23122,23123],{"class":1376},"      timeout",[129,23125,1380],{"class":277},[129,23127,19481],{"class":290},[129,23129,1015],{"class":277},[129,23131,23132],{"class":376}," // fail fast, don't let this block the page\n",[129,23134,23135,23137],{"class":265,"line":669},[129,23136,7619],{"class":277},[129,23138,294],{"class":1376},[129,23140,23141,23143,23145],{"class":265,"line":693},[129,23142,4182],{"class":277},[129,23144,13629],{"class":2139},[129,23146,1371],{"class":277},[129,23148,23149],{"class":265,"line":712},[129,23150,23151],{"class":376},"    // Fail open - homepage loads without recommendations\n",[129,23153,23154],{"class":265,"line":1521},[129,23155,23156],{"class":376},"    // Important: log this, but don't surface to the user\n",[129,23158,23159],{"class":265,"line":1527},[129,23160,1524],{"class":277},[129,23162,23163],{"class":265,"line":2295},[129,23164,336],{"emptyLinePlaceholder":335},[129,23166,23167,23169,23171,23173,23175,23177],{"class":265,"line":2300},[129,23168,4520],{"class":2139},[129,23170,1416],{"class":277},[129,23172,23038],{"class":273},[129,23174,1015],{"class":277},[129,23176,23072],{"class":273},[129,23178,1476],{"class":277},[129,23180,23181,23183],{"class":265,"line":2305},[129,23182,4028],{"class":277},[129,23184,294],{"class":273},[11,23186,23187,23190],{},[118,23188,23189],{},"Isolation",": failures in one component don't cascade. If image processing crashes, the API should not crash. This is achievable within a monolith by treating every external call as unreliable and wrapping it defensively.",[11,23192,23193],{},"Ask \"when this goes wrong, what should happen instead?\"",[2456,23195,23197],{"id":23196},"circuit-breaker","Circuit breaker",[11,23199,23200,23201,23204],{},"Retries are good. Retrying a service that is fully down, 200 times per second, is not. It hammers a struggling service and prevents recovery. A ",[118,23202,23203],{},"circuit breaker"," wraps external calls and tracks failure rate. When failures cross a threshold, it opens the circuit - subsequent calls fail immediately without attempting the real call, giving the downstream service time to recover.",[255,23206,23209],{"className":3922,"code":23207,"filename":23208,"language":3924,"meta":260,"style":260},"type State = 'CLOSED' | 'OPEN' | 'HALF_OPEN'\n\nexport class CircuitBreaker {\n  private state: State = 'CLOSED'\n  private failures = 0\n  private lastFailureTime = 0\n\n  constructor(\n    private readonly threshold = 5,       // failures before opening\n    private readonly resetTimeout = 30_000 // ms before trying again\n  ) {}\n\n  async call\u003CT>(fn: () => Promise\u003CT>): Promise\u003CT> {\n    if (this.state === 'OPEN') {\n      if (Date.now() - this.lastFailureTime \u003C this.resetTimeout) {\n        throw new Error('Circuit open - request rejected')\n      }\n      this.state = 'HALF_OPEN' // allow one probe request through\n    }\n\n    try {\n      const result = await fn()\n      this.failures = 0\n      this.state = 'CLOSED'\n      return result\n    } catch (err) {\n      this.failures++\n      this.lastFailureTime = Date.now()\n      if (this.failures >= this.threshold || this.state === 'HALF_OPEN') {\n        this.state = 'OPEN'\n      }\n      throw err\n    }\n  }\n}\n","server/lib/circuit-breaker.ts",[15,23210,23211,23245,23249,23260,23279,23291,23302,23306,23312,23332,23349,23356,23360,23402,23426,23458,23478,23482,23500,23504,23508,23514,23529,23540,23554,23560,23574,23582,23598,23635,23650,23654,23661,23665,23669],{"__ignoreMap":260},[129,23212,23213,23215,23218,23220,23222,23225,23227,23229,23231,23234,23236,23238,23240,23243],{"class":265,"line":266},[129,23214,5970],{"class":269},[129,23216,23217],{"class":2161}," State",[129,23219,4745],{"class":277},[129,23221,4261],{"class":277},[129,23223,23224],{"class":427},"CLOSED",[129,23226,424],{"class":277},[129,23228,3951],{"class":277},[129,23230,4261],{"class":277},[129,23232,23233],{"class":427},"OPEN",[129,23235,424],{"class":277},[129,23237,3951],{"class":277},[129,23239,4261],{"class":277},[129,23241,23242],{"class":427},"HALF_OPEN",[129,23244,4267],{"class":277},[129,23246,23247],{"class":265,"line":297},[129,23248,336],{"emptyLinePlaceholder":335},[129,23250,23251,23253,23255,23258],{"class":265,"line":315},[129,23252,4050],{"class":2139},[129,23254,7254],{"class":269},[129,23256,23257],{"class":2161}," CircuitBreaker",[129,23259,1371],{"class":277},[129,23261,23262,23264,23267,23269,23271,23273,23275,23277],{"class":265,"line":332},[129,23263,7436],{"class":269},[129,23265,23266],{"class":1376}," state",[129,23268,1380],{"class":277},[129,23270,23217],{"class":2161},[129,23272,4745],{"class":277},[129,23274,4261],{"class":277},[129,23276,23224],{"class":427},[129,23278,4267],{"class":277},[129,23280,23281,23283,23286,23288],{"class":265,"line":339},[129,23282,7436],{"class":269},[129,23284,23285],{"class":1376}," failures",[129,23287,4745],{"class":277},[129,23289,23290],{"class":290}," 0\n",[129,23292,23293,23295,23298,23300],{"class":265,"line":356},[129,23294,7436],{"class":269},[129,23296,23297],{"class":1376}," lastFailureTime",[129,23299,4745],{"class":277},[129,23301,23290],{"class":290},[129,23303,23304],{"class":265,"line":651},[129,23305,336],{"emptyLinePlaceholder":335},[129,23307,23308,23310],{"class":265,"line":657},[129,23309,7453],{"class":269},[129,23311,2241],{"class":277},[129,23313,23314,23317,23320,23323,23325,23327,23329],{"class":265,"line":669},[129,23315,23316],{"class":269},"    private",[129,23318,23319],{"class":269}," readonly",[129,23321,23322],{"class":452}," threshold",[129,23324,4745],{"class":277},[129,23326,1043],{"class":290},[129,23328,1015],{"class":277},[129,23330,23331],{"class":376},"       // failures before opening\n",[129,23333,23334,23336,23338,23341,23343,23346],{"class":265,"line":693},[129,23335,23316],{"class":269},[129,23337,23319],{"class":269},[129,23339,23340],{"class":452}," resetTimeout",[129,23342,4745],{"class":277},[129,23344,23345],{"class":290}," 30_000",[129,23347,23348],{"class":376}," // ms before trying again\n",[129,23350,23351,23354],{"class":265,"line":712},[129,23352,23353],{"class":277},"  )",[129,23355,19604],{"class":277},[129,23357,23358],{"class":265,"line":1521},[129,23359,336],{"emptyLinePlaceholder":335},[129,23361,23362,23364,23367,23369,23371,23374,23377,23379,23381,23383,23385,23387,23389,23392,23394,23396,23398,23400],{"class":265,"line":1527},[129,23363,4703],{"class":269},[129,23365,23366],{"class":1376}," call",[129,23368,3945],{"class":277},[129,23370,19554],{"class":2161},[129,23372,23373],{"class":277},">(",[129,23375,23376],{"class":284},"fn",[129,23378,1380],{"class":277},[129,23380,4511],{"class":277},[129,23382,456],{"class":269},[129,23384,4637],{"class":2161},[129,23386,3945],{"class":277},[129,23388,19554],{"class":2161},[129,23390,23391],{"class":277},">):",[129,23393,4637],{"class":2161},[129,23395,3945],{"class":277},[129,23397,19554],{"class":2161},[129,23399,3956],{"class":277},[129,23401,1371],{"class":277},[129,23403,23404,23406,23408,23411,23414,23416,23418,23420,23422,23424],{"class":265,"line":2295},[129,23405,6479],{"class":2139},[129,23407,3984],{"class":1376},[129,23409,23410],{"class":277},"this.",[129,23412,23413],{"class":273},"state",[129,23415,5116],{"class":277},[129,23417,4261],{"class":277},[129,23419,23233],{"class":427},[129,23421,424],{"class":277},[129,23423,4005],{"class":1376},[129,23425,6455],{"class":277},[129,23427,23428,23430,23432,23434,23436,23438,23440,23442,23444,23447,23449,23451,23454,23456],{"class":265,"line":2300},[129,23429,19723],{"class":2139},[129,23431,3984],{"class":1376},[129,23433,9433],{"class":273},[129,23435,362],{"class":277},[129,23437,3587],{"class":284},[129,23439,824],{"class":1376},[129,23441,6768],{"class":277},[129,23443,7556],{"class":277},[129,23445,23446],{"class":273},"lastFailureTime",[129,23448,19656],{"class":277},[129,23450,7556],{"class":277},[129,23452,23453],{"class":273},"resetTimeout",[129,23455,4005],{"class":1376},[129,23457,6455],{"class":277},[129,23459,23460,23463,23465,23467,23469,23471,23474,23476],{"class":265,"line":2305},[129,23461,23462],{"class":2139},"        throw",[129,23464,281],{"class":277},[129,23466,5172],{"class":284},[129,23468,147],{"class":1376},[129,23470,424],{"class":277},[129,23472,23473],{"class":427},"Circuit open - request rejected",[129,23475,424],{"class":277},[129,23477,294],{"class":1376},[129,23479,23480],{"class":265,"line":2311},[129,23481,19786],{"class":277},[129,23483,23484,23487,23489,23491,23493,23495,23497],{"class":265,"line":2329},[129,23485,23486],{"class":277},"      this.",[129,23488,23413],{"class":273},[129,23490,4745],{"class":277},[129,23492,4261],{"class":277},[129,23494,23242],{"class":427},[129,23496,424],{"class":277},[129,23498,23499],{"class":376}," // allow one probe request through\n",[129,23501,23502],{"class":265,"line":2351},[129,23503,6516],{"class":277},[129,23505,23506],{"class":265,"line":2387},[129,23507,336],{"emptyLinePlaceholder":335},[129,23509,23510,23512],{"class":265,"line":2392},[129,23511,19674],{"class":2139},[129,23513,1371],{"class":277},[129,23515,23516,23519,23521,23523,23525,23527],{"class":265,"line":2398},[129,23517,23518],{"class":269},"      const",[129,23520,6735],{"class":273},[129,23522,4745],{"class":277},[129,23524,4779],{"class":2139},[129,23526,19686],{"class":284},[129,23528,2451],{"class":1376},[129,23530,23531,23533,23536,23538],{"class":265,"line":2441},[129,23532,23486],{"class":277},[129,23534,23535],{"class":273},"failures",[129,23537,4745],{"class":277},[129,23539,23290],{"class":290},[129,23541,23542,23544,23546,23548,23550,23552],{"class":265,"line":3246},[129,23543,23486],{"class":277},[129,23545,23413],{"class":273},[129,23547,4745],{"class":277},[129,23549,4261],{"class":277},[129,23551,23224],{"class":427},[129,23553,4267],{"class":277},[129,23555,23556,23558],{"class":265,"line":3251},[129,23557,19681],{"class":2139},[129,23559,6824],{"class":273},[129,23561,23562,23564,23566,23568,23570,23572],{"class":265,"line":3263},[129,23563,7619],{"class":277},[129,23565,13629],{"class":2139},[129,23567,3984],{"class":1376},[129,23569,19699],{"class":273},[129,23571,4005],{"class":1376},[129,23573,6455],{"class":277},[129,23575,23576,23578,23580],{"class":265,"line":5055},[129,23577,23486],{"class":277},[129,23579,23535],{"class":273},[129,23581,9542],{"class":277},[129,23583,23584,23586,23588,23590,23592,23594,23596],{"class":265,"line":5073},[129,23585,23486],{"class":277},[129,23587,23446],{"class":273},[129,23589,4745],{"class":277},[129,23591,4137],{"class":273},[129,23593,362],{"class":277},[129,23595,3587],{"class":284},[129,23597,2451],{"class":1376},[129,23599,23600,23602,23604,23606,23608,23611,23613,23616,23619,23621,23623,23625,23627,23629,23631,23633],{"class":265,"line":5106},[129,23601,19723],{"class":2139},[129,23603,3984],{"class":1376},[129,23605,23410],{"class":277},[129,23607,23535],{"class":273},[129,23609,23610],{"class":277}," >=",[129,23612,7556],{"class":277},[129,23614,23615],{"class":273},"threshold",[129,23617,23618],{"class":277}," ||",[129,23620,7556],{"class":277},[129,23622,23413],{"class":273},[129,23624,5116],{"class":277},[129,23626,4261],{"class":277},[129,23628,23242],{"class":427},[129,23630,424],{"class":277},[129,23632,4005],{"class":1376},[129,23634,6455],{"class":277},[129,23636,23637,23640,23642,23644,23646,23648],{"class":265,"line":5136},[129,23638,23639],{"class":277},"        this.",[129,23641,23413],{"class":273},[129,23643,4745],{"class":277},[129,23645,4261],{"class":277},[129,23647,23233],{"class":427},[129,23649,4267],{"class":277},[129,23651,23652],{"class":265,"line":5164},[129,23653,19786],{"class":277},[129,23655,23656,23658],{"class":265,"line":5190},[129,23657,6913],{"class":2139},[129,23659,23660],{"class":273}," err\n",[129,23662,23663],{"class":265,"line":7751},[129,23664,6516],{"class":277},[129,23666,23667],{"class":265,"line":7796},[129,23668,1524],{"class":277},[129,23670,23671],{"class":265,"line":7803},[129,23672,1530],{"class":277},[255,23674,23677],{"className":3922,"code":23675,"filename":23676,"language":3924,"meta":260,"style":260},"const paymentBreaker = new CircuitBreaker(5, 30_000)\n\nexport const chargeCard = (token: string, amount: number) =>\n  paymentBreaker.call(() =>\n    stripe.paymentIntents.create({ amount, payment_method: token, confirm: true })\n  )\n","server/lib/payment-client.ts",[15,23678,23679,23703,23707,23739,23755,23794],{"__ignoreMap":260},[129,23680,23681,23683,23686,23688,23690,23692,23694,23697,23699,23701],{"class":265,"line":266},[129,23682,270],{"class":269},[129,23684,23685],{"class":273}," paymentBreaker ",[129,23687,278],{"class":277},[129,23689,281],{"class":277},[129,23691,23257],{"class":284},[129,23693,147],{"class":273},[129,23695,23696],{"class":290},"5",[129,23698,1015],{"class":277},[129,23700,23345],{"class":290},[129,23702,294],{"class":273},[129,23704,23705],{"class":265,"line":297},[129,23706,336],{"emptyLinePlaceholder":335},[129,23708,23709,23711,23713,23716,23718,23720,23722,23724,23726,23728,23730,23732,23734,23736],{"class":265,"line":315},[129,23710,4050],{"class":2139},[129,23712,4456],{"class":269},[129,23714,23715],{"class":273}," chargeCard ",[129,23717,278],{"class":277},[129,23719,3984],{"class":277},[129,23721,6089],{"class":452},[129,23723,1380],{"class":277},[129,23725,4622],{"class":2161},[129,23727,1015],{"class":277},[129,23729,4799],{"class":452},[129,23731,1380],{"class":277},[129,23733,4612],{"class":2161},[129,23735,160],{"class":277},[129,23737,23738],{"class":269}," =>\n",[129,23740,23741,23744,23746,23749,23751,23753],{"class":265,"line":332},[129,23742,23743],{"class":273},"  paymentBreaker",[129,23745,362],{"class":277},[129,23747,23748],{"class":284},"call",[129,23750,147],{"class":273},[129,23752,4140],{"class":277},[129,23754,23738],{"class":269},[129,23756,23757,23760,23762,23764,23766,23768,23770,23772,23774,23776,23778,23780,23782,23784,23786,23788,23790,23792],{"class":265,"line":339},[129,23758,23759],{"class":273},"    stripe",[129,23761,362],{"class":277},[129,23763,4786],{"class":273},[129,23765,362],{"class":277},[129,23767,4791],{"class":284},[129,23769,147],{"class":273},[129,23771,4796],{"class":277},[129,23773,4799],{"class":273},[129,23775,1015],{"class":277},[129,23777,4808],{"class":1376},[129,23779,1380],{"class":277},[129,23781,4627],{"class":273},[129,23783,1015],{"class":277},[129,23785,4817],{"class":1376},[129,23787,1380],{"class":277},[129,23789,4823],{"class":4822},[129,23791,4255],{"class":277},[129,23793,294],{"class":273},[129,23795,23796],{"class":265,"line":356},[129,23797,19918],{"class":273},[11,23799,23800,23801,23803,23804,23806,23807,23809],{},"Three states: ",[118,23802,23224],{}," (normal, calls go through), ",[118,23805,23233],{}," (failing fast, no calls attempted), ",[118,23808,23242],{}," (one probe call - success closes the circuit, failure keeps it open).",[3576,23811,23812],{},[11,23813,23814,23815,23820],{},"For production you might use ",[51,23816,23819],{"href":23817,"rel":23818},"https://nodeshift.dev/opossum/",[55],"opossum"," is a mature Node.js circuit breaker with timeout handling, event emitters, and Prometheus metrics integration. The implementation above illustrates the concept; opossum handles the edge cases.",[2456,23822,23824],{"id":23823},"retries-with-backoff","Retries with backoff",[11,23826,23827,23828,11299,23831,23833,23834,968,23837,23840,23841,23843],{},"Covered in the ",[51,23829,21760],{"href":23830},"/blog/nuxt-system-patterns#retry-mechanism",[15,23832,14196],{}," has ",[15,23835,23836],{},"retry",[15,23838,23839],{},"retryDelay"," built in. For server-to-server calls, use exponential backoff. Do not retry 4xx errors - they indicate your request is wrong, not the server. Respect ",[15,23842,19964],{}," headers on 429 responses.",[2456,23845,23847],{"id":23846},"idempotency","Idempotency",[11,23849,23827,23850,23854],{},[51,23851,23853],{"href":23852},"/blog/ticket-booking-concurrency#idempotency-keys-the-payment-double-charge-problem","ticket booking article",". Idempotency keys ensure that retrying a failed request doesn't process it twice. Required for any operation with side effects a user might retry: payments, email sends, form submissions over unreliable connections.",[2456,23856,23858],{"id":23857},"health-checks","Health checks",[11,23860,23861],{},"A health check endpoint lets your load balancer, uptime monitor, and deployment system verify the app is ready to serve traffic. Without it, a failed deploy is silent until users report it.",[255,23863,23866],{"className":3922,"code":23864,"filename":23865,"language":3924,"meta":260,"style":260},"export default defineEventHandler(async (event) => {\n  const checks = await Promise.allSettled([\n    db.execute(sql`SELECT 1`),                       // database reachable\n    useStorage('cache').setItem('_ping', '1'),        // redis writable\n  ])\n\n  const [dbCheck, redisCheck] = checks.map(r => r.status === 'fulfilled' ? 'ok' : 'error')\n  const healthy = checks.every(r => r.status === 'fulfilled')\n\n  setResponseStatus(event, healthy ? 200 : 503)\n  return {\n    status: healthy ? 'healthy' : 'degraded',\n    checks: { db: dbCheck, redis: redisCheck },\n    uptime: process.uptime(),\n    timestamp: new Date().toISOString(),\n  }\n})\n","server/api/health.get.ts",[15,23867,23868,23890,23911,23939,23982,23987,23991,24056,24093,24097,24120,24126,24155,24181,24199,24219,24223],{"__ignoreMap":260},[129,23869,23870,23872,23874,23876,23878,23880,23882,23884,23886,23888],{"class":265,"line":266},[129,23871,4050],{"class":2139},[129,23873,4053],{"class":2139},[129,23875,4503],{"class":284},[129,23877,147],{"class":273},[129,23879,4508],{"class":269},[129,23881,3984],{"class":277},[129,23883,4100],{"class":452},[129,23885,160],{"class":277},[129,23887,456],{"class":269},[129,23889,1371],{"class":277},[129,23891,23892,23894,23897,23899,23901,23903,23905,23908],{"class":265,"line":297},[129,23893,5076],{"class":269},[129,23895,23896],{"class":273}," checks",[129,23898,4745],{"class":277},[129,23900,4779],{"class":2139},[129,23902,4637],{"class":2161},[129,23904,362],{"class":277},[129,23906,23907],{"class":284},"allSettled",[129,23909,23910],{"class":1376},"([\n",[129,23912,23913,23916,23918,23921,23923,23925,23927,23930,23932,23934,23936],{"class":265,"line":315},[129,23914,23915],{"class":273},"    db",[129,23917,362],{"class":277},[129,23919,23920],{"class":284},"execute",[129,23922,147],{"class":1376},[129,23924,22038],{"class":284},[129,23926,4125],{"class":277},[129,23928,23929],{"class":427},"SELECT 1",[129,23931,4125],{"class":277},[129,23933,160],{"class":1376},[129,23935,1015],{"class":277},[129,23937,23938],{"class":376},"                       // database reachable\n",[129,23940,23941,23944,23946,23948,23950,23952,23954,23956,23958,23960,23962,23965,23967,23969,23971,23973,23975,23977,23979],{"class":265,"line":332},[129,23942,23943],{"class":284},"    useStorage",[129,23945,147],{"class":1376},[129,23947,424],{"class":277},[129,23949,20263],{"class":427},[129,23951,424],{"class":277},[129,23953,160],{"class":1376},[129,23955,362],{"class":277},[129,23957,20326],{"class":284},[129,23959,147],{"class":1376},[129,23961,424],{"class":277},[129,23963,23964],{"class":427},"_ping",[129,23966,424],{"class":277},[129,23968,1015],{"class":277},[129,23970,4261],{"class":277},[129,23972,154],{"class":427},[129,23974,424],{"class":277},[129,23976,160],{"class":1376},[129,23978,1015],{"class":277},[129,23980,23981],{"class":376},"        // redis writable\n",[129,23983,23984],{"class":265,"line":339},[129,23985,23986],{"class":1376},"  ])\n",[129,23988,23989],{"class":265,"line":356},[129,23990,336],{"emptyLinePlaceholder":335},[129,23992,23993,23995,23997,24000,24002,24005,24007,24009,24011,24013,24015,24017,24019,24021,24024,24026,24028,24030,24032,24035,24037,24039,24041,24044,24046,24048,24050,24052,24054],{"class":265,"line":651},[129,23994,5076],{"class":269},[129,23996,1010],{"class":277},[129,23998,23999],{"class":273},"dbCheck",[129,24001,1015],{"class":277},[129,24003,24004],{"class":273}," redisCheck",[129,24006,14170],{"class":277},[129,24008,4745],{"class":277},[129,24010,23896],{"class":273},[129,24012,362],{"class":277},[129,24014,447],{"class":284},[129,24016,147],{"class":1376},[129,24018,15144],{"class":452},[129,24020,456],{"class":269},[129,24022,24023],{"class":273}," r",[129,24025,362],{"class":277},[129,24027,22095],{"class":273},[129,24029,5116],{"class":277},[129,24031,4261],{"class":277},[129,24033,24034],{"class":427},"fulfilled",[129,24036,424],{"class":277},[129,24038,14409],{"class":277},[129,24040,4261],{"class":277},[129,24042,24043],{"class":427},"ok",[129,24045,424],{"class":277},[129,24047,14414],{"class":277},[129,24049,4261],{"class":277},[129,24051,10921],{"class":427},[129,24053,424],{"class":277},[129,24055,294],{"class":1376},[129,24057,24058,24060,24063,24065,24067,24069,24071,24073,24075,24077,24079,24081,24083,24085,24087,24089,24091],{"class":265,"line":657},[129,24059,5076],{"class":269},[129,24061,24062],{"class":273}," healthy",[129,24064,4745],{"class":277},[129,24066,23896],{"class":273},[129,24068,362],{"class":277},[129,24070,12541],{"class":284},[129,24072,147],{"class":1376},[129,24074,15144],{"class":452},[129,24076,456],{"class":269},[129,24078,24023],{"class":273},[129,24080,362],{"class":277},[129,24082,22095],{"class":273},[129,24084,5116],{"class":277},[129,24086,4261],{"class":277},[129,24088,24034],{"class":427},[129,24090,424],{"class":277},[129,24092,294],{"class":1376},[129,24094,24095],{"class":265,"line":669},[129,24096,336],{"emptyLinePlaceholder":335},[129,24098,24099,24102,24104,24106,24108,24110,24112,24114,24116,24118],{"class":265,"line":693},[129,24100,24101],{"class":284},"  setResponseStatus",[129,24103,147],{"class":1376},[129,24105,4100],{"class":273},[129,24107,1015],{"class":277},[129,24109,24062],{"class":273},[129,24111,14409],{"class":277},[129,24113,19911],{"class":290},[129,24115,14414],{"class":277},[129,24117,19512],{"class":290},[129,24119,294],{"class":1376},[129,24121,24122,24124],{"class":265,"line":712},[129,24123,4520],{"class":2139},[129,24125,1371],{"class":277},[129,24127,24128,24131,24133,24135,24137,24139,24142,24144,24146,24148,24151,24153],{"class":265,"line":1521},[129,24129,24130],{"class":1376},"    status",[129,24132,1380],{"class":277},[129,24134,24062],{"class":273},[129,24136,14409],{"class":277},[129,24138,4261],{"class":277},[129,24140,24141],{"class":427},"healthy",[129,24143,424],{"class":277},[129,24145,14414],{"class":277},[129,24147,4261],{"class":277},[129,24149,24150],{"class":427},"degraded",[129,24152,424],{"class":277},[129,24154,1386],{"class":277},[129,24156,24157,24160,24162,24164,24166,24168,24171,24173,24175,24177,24179],{"class":265,"line":1527},[129,24158,24159],{"class":1376},"    checks",[129,24161,1380],{"class":277},[129,24163,1416],{"class":277},[129,24165,4479],{"class":1376},[129,24167,1380],{"class":277},[129,24169,24170],{"class":273}," dbCheck",[129,24172,1015],{"class":277},[129,24174,18379],{"class":1376},[129,24176,1380],{"class":277},[129,24178,24004],{"class":273},[129,24180,1444],{"class":277},[129,24182,24183,24186,24188,24190,24192,24195,24197],{"class":265,"line":2295},[129,24184,24185],{"class":1376},"    uptime",[129,24187,1380],{"class":277},[129,24189,5084],{"class":273},[129,24191,362],{"class":277},[129,24193,24194],{"class":284},"uptime",[129,24196,4140],{"class":1376},[129,24198,1386],{"class":277},[129,24200,24201,24203,24205,24207,24209,24211,24213,24215,24217],{"class":265,"line":2300},[129,24202,2200],{"class":1376},[129,24204,1380],{"class":277},[129,24206,281],{"class":277},[129,24208,4137],{"class":284},[129,24210,4140],{"class":1376},[129,24212,362],{"class":277},[129,24214,4145],{"class":284},[129,24216,4140],{"class":1376},[129,24218,1386],{"class":277},[129,24220,24221],{"class":265,"line":2305},[129,24222,1524],{"class":277},[129,24224,24225,24227],{"class":265,"line":2311},[129,24226,4028],{"class":277},[129,24228,294],{"class":273},[11,24230,24231],{},"Two variants worth knowing:",[11,24233,24234,24237],{},[118,24235,24236],{},"Liveness",": is the process alive? A simple 200. If this fails, restart the container.",[11,24239,24240,24243],{},[118,24241,24242],{},"Readiness",": is the process ready to handle traffic? Checks dependencies. If this fails, stop sending traffic but don't restart - the process is alive but dependencies aren't ready.",[11,24245,24246,24247,968,24250,24253],{},"In Kubernetes, these are separate endpoints (",[15,24248,24249],{},"/health/live",[15,24251,24252],{},"/health/ready","). In simpler deployments, one endpoint that returns 503 when dependencies are unavailable is enough.",[2001,24255],{},[40,24257,24259],{"id":24258},"architecture-patterns","Architecture patterns",[2456,24261,24263],{"id":24262},"microservices-vs-monolith","Microservices vs monolith",[11,24265,24266,24267,362],{},"For most teams building new products: ",[118,24268,24269],{},"start with a monolith",[11,24271,24272],{},"Microservices solve problems of scale and organizational independence that most applications don't have yet. They introduce distributed systems complexity - network calls, distributed transactions, independent deployments, service discovery, distributed tracing - that is expensive to manage and adds no user value at small scale.",[11,24274,24275],{},"The case for starting with a monolith:",[1822,24277,24278,24281,24284],{},[1825,24279,24280],{},"Nuxt is a monolith by default and it's excellent at it",[1825,24282,24283],{},"You can modularize internally without paying the distributed tax",[1825,24285,24286],{},"You can extract services later with evidence, not speculation",[11,24288,24289],{},"The case for microservices:",[1822,24291,24292,24295,24298,24301],{},[1825,24293,24294],{},"Different services need different runtime environments",[1825,24296,24297],{},"Independent deployment: billing team deploys without affecting checkout",[1825,24299,24300],{},"Genuinely different scaling requirements (image processing vs API)",[1825,24302,24303],{},"Organizational scale: 50+ engineers on one codebase becomes a coordination problem",[11,24305,561,24306,24309],{},[118,24307,24308],{},"modular monolith"," is the pragmatic middle ground: a single deployable with clean internal boundaries. When you eventually need to extract a service, the boundary is already there.",[11,24311,24312,24313,24316],{},"Nuxt's ",[15,24314,24315],{},"server/"," directory naturally supports this. Group by feature domain, not by technical layer:",[255,24318,24321],{"className":24319,"code":24320,"language":3237},[12199],"server/\n  auth/\n    api/login.post.ts\n    api/register.post.ts\n    lib/session.ts\n  products/\n    api/[id].get.ts\n    api/index.get.ts\n    lib/catalog.ts\n  orders/\n    api/create.post.ts\n    tasks/process.ts\n    lib/payment.ts\n",[15,24322,24320],{"__ignoreMap":260},[11,24324,24325],{},"Each domain owns its routes, its business logic, and its data access. Clean seam. No distributed systems overhead.",[2456,24327,24329],{"id":24328},"event-driven-architecture","Event-driven architecture",[11,24331,24332,24335],{},[118,24333,24334],{},"Request-driven",": service A calls service B and waits for a response. Simple, traceable, the default.",[11,24337,24338,24341],{},[118,24339,24340],{},"Event-driven",": service A emits an event (\"order placed\"). Any number of subscribers handle it asynchronously. Service A doesn't know or care who handles the event.",[11,24343,24344,24345,24348,24349,24352],{},"When you place an order, several things need to happen: confirmation email, inventory update, fulfillment trigger, warehouse notification. In a request-driven system, the ",[15,24346,24347],{},"POST /orders"," handler calls all of these and waits. In an event-driven system, it emits ",[15,24350,24351],{},"order.created"," and returns immediately - each subscriber handles it independently.",[255,24354,24357],{"className":3922,"code":24355,"filename":24356,"language":3924,"meta":260,"style":260},"import { Queue, Worker } from 'bullmq'\nimport { redis } from './redis'\n\nexport const events = new Queue('events', { connection: redis })\n\n// Emit and forget - fire the event, don't wait for handlers\nexport const emit = (name: string, data: unknown) => events.add(name, data)\n","server/lib/events.ts",[15,24358,24359,24381,24399,24403,24441,24445,24450],{"__ignoreMap":260},[129,24360,24361,24363,24365,24367,24369,24371,24373,24375,24377,24379],{"class":265,"line":266},[129,24362,2140],{"class":2139},[129,24364,1416],{"class":277},[129,24366,18354],{"class":273},[129,24368,1015],{"class":277},[129,24370,18359],{"class":273},[129,24372,4255],{"class":277},[129,24374,4258],{"class":2139},[129,24376,4261],{"class":277},[129,24378,18368],{"class":427},[129,24380,4267],{"class":277},[129,24382,24383,24385,24387,24389,24391,24393,24395,24397],{"class":265,"line":297},[129,24384,2140],{"class":2139},[129,24386,1416],{"class":277},[129,24388,18379],{"class":273},[129,24390,4255],{"class":277},[129,24392,4258],{"class":2139},[129,24394,4261],{"class":277},[129,24396,18388],{"class":427},[129,24398,4267],{"class":277},[129,24400,24401],{"class":265,"line":315},[129,24402,336],{"emptyLinePlaceholder":335},[129,24404,24405,24407,24409,24412,24414,24416,24418,24420,24422,24425,24427,24429,24431,24433,24435,24437,24439],{"class":265,"line":332},[129,24406,4050],{"class":2139},[129,24408,4456],{"class":269},[129,24410,24411],{"class":273}," events ",[129,24413,278],{"class":277},[129,24415,281],{"class":277},[129,24417,18354],{"class":284},[129,24419,147],{"class":273},[129,24421,424],{"class":277},[129,24423,24424],{"class":427},"events",[129,24426,424],{"class":277},[129,24428,1015],{"class":277},[129,24430,1416],{"class":277},[129,24432,18424],{"class":1376},[129,24434,1380],{"class":277},[129,24436,18429],{"class":273},[129,24438,4028],{"class":277},[129,24440,294],{"class":273},[129,24442,24443],{"class":265,"line":339},[129,24444,336],{"emptyLinePlaceholder":335},[129,24446,24447],{"class":265,"line":356},[129,24448,24449],{"class":376},"// Emit and forget - fire the event, don't wait for handlers\n",[129,24451,24452,24454,24456,24459,24461,24463,24465,24467,24469,24471,24473,24475,24478,24480,24482,24485,24487,24489,24492,24494],{"class":265,"line":651},[129,24453,4050],{"class":2139},[129,24455,4456],{"class":269},[129,24457,24458],{"class":273}," emit ",[129,24460,278],{"class":277},[129,24462,3984],{"class":277},[129,24464,8164],{"class":452},[129,24466,1380],{"class":277},[129,24468,4622],{"class":2161},[129,24470,1015],{"class":277},[129,24472,11638],{"class":452},[129,24474,1380],{"class":277},[129,24476,24477],{"class":2161}," unknown",[129,24479,160],{"class":277},[129,24481,456],{"class":269},[129,24483,24484],{"class":273}," events",[129,24486,362],{"class":277},[129,24488,14141],{"class":284},[129,24490,24491],{"class":273},"(name",[129,24493,1015],{"class":277},[129,24495,24496],{"class":273}," data)\n",[255,24498,24500],{"className":3922,"code":24499,"filename":22667,"language":3924,"meta":260,"style":260},"export default defineEventHandler(async (event) => {\n  const [order] = await db.insert(orders).values(body).returning()\n\n  // Response is already fast - downstream processing is someone else's problem\n  await emit('order.created', { orderId: order.id, userId: order.userId })\n\n  return order\n})\n",[15,24501,24502,24524,24566,24570,24575,24622,24626,24632],{"__ignoreMap":260},[129,24503,24504,24506,24508,24510,24512,24514,24516,24518,24520,24522],{"class":265,"line":266},[129,24505,4050],{"class":2139},[129,24507,4053],{"class":2139},[129,24509,4503],{"class":284},[129,24511,147],{"class":273},[129,24513,4508],{"class":269},[129,24515,3984],{"class":277},[129,24517,4100],{"class":452},[129,24519,160],{"class":277},[129,24521,456],{"class":269},[129,24523,1371],{"class":277},[129,24525,24526,24528,24530,24532,24534,24536,24538,24540,24542,24544,24546,24548,24550,24552,24554,24556,24558,24560,24562,24564],{"class":265,"line":297},[129,24527,5076],{"class":269},[129,24529,1010],{"class":277},[129,24531,5592],{"class":273},[129,24533,14170],{"class":277},[129,24535,4745],{"class":277},[129,24537,4779],{"class":2139},[129,24539,4479],{"class":273},[129,24541,362],{"class":277},[129,24543,22736],{"class":284},[129,24545,147],{"class":1376},[129,24547,21996],{"class":273},[129,24549,160],{"class":1376},[129,24551,362],{"class":277},[129,24553,22747],{"class":284},[129,24555,147],{"class":1376},[129,24557,14959],{"class":273},[129,24559,160],{"class":1376},[129,24561,362],{"class":277},[129,24563,22758],{"class":284},[129,24565,2451],{"class":1376},[129,24567,24568],{"class":265,"line":315},[129,24569,336],{"emptyLinePlaceholder":335},[129,24571,24572],{"class":265,"line":332},[129,24573,24574],{"class":376},"  // Response is already fast - downstream processing is someone else's problem\n",[129,24576,24577,24579,24582,24584,24586,24588,24590,24592,24594,24597,24599,24602,24604,24606,24608,24610,24612,24614,24616,24618,24620],{"class":265,"line":339},[129,24578,8101],{"class":2139},[129,24580,24581],{"class":284}," emit",[129,24583,147],{"class":1376},[129,24585,424],{"class":277},[129,24587,24351],{"class":427},[129,24589,424],{"class":277},[129,24591,1015],{"class":277},[129,24593,1416],{"class":277},[129,24595,24596],{"class":1376}," orderId",[129,24598,1380],{"class":277},[129,24600,24601],{"class":273}," order",[129,24603,362],{"class":277},[129,24605,3190],{"class":273},[129,24607,1015],{"class":277},[129,24609,18058],{"class":1376},[129,24611,1380],{"class":277},[129,24613,24601],{"class":273},[129,24615,362],{"class":277},[129,24617,18099],{"class":273},[129,24619,4255],{"class":277},[129,24621,294],{"class":1376},[129,24623,24624],{"class":265,"line":356},[129,24625,336],{"emptyLinePlaceholder":335},[129,24627,24628,24630],{"class":265,"line":651},[129,24629,4520],{"class":2139},[129,24631,22767],{"class":273},[129,24633,24634,24636],{"class":265,"line":657},[129,24635,4028],{"class":277},[129,24637,294],{"class":273},[255,24639,24642],{"className":3922,"code":24640,"filename":24641,"language":3924,"meta":260,"style":260},"new Worker('events', async (job) => {\n  if (job.name === 'order.created') {\n    await Promise.all([\n      sendOrderConfirmation(job.data),\n      updateInventory(job.data),\n      notifyFulfillment(job.data),\n    ])\n  }\n}, { connection: redis })\n","server/workers/order-events.ts",[15,24643,24644,24672,24696,24708,24725,24742,24759,24764,24768],{"__ignoreMap":260},[129,24645,24646,24648,24650,24652,24654,24656,24658,24660,24662,24664,24666,24668,24670],{"class":265,"line":266},[129,24647,4134],{"class":277},[129,24649,18359],{"class":284},[129,24651,147],{"class":273},[129,24653,424],{"class":277},[129,24655,24424],{"class":427},[129,24657,424],{"class":277},[129,24659,1015],{"class":277},[129,24661,6020],{"class":269},[129,24663,3984],{"class":277},[129,24665,18465],{"class":452},[129,24667,160],{"class":277},[129,24669,456],{"class":269},[129,24671,1371],{"class":277},[129,24673,24674,24676,24678,24680,24682,24684,24686,24688,24690,24692,24694],{"class":265,"line":297},[129,24675,3998],{"class":2139},[129,24677,3984],{"class":1376},[129,24679,18465],{"class":273},[129,24681,362],{"class":277},[129,24683,8164],{"class":273},[129,24685,5116],{"class":277},[129,24687,4261],{"class":277},[129,24689,24351],{"class":427},[129,24691,424],{"class":277},[129,24693,4005],{"class":1376},[129,24695,6455],{"class":277},[129,24697,24698,24700,24702,24704,24706],{"class":265,"line":315},[129,24699,4902],{"class":2139},[129,24701,4637],{"class":2161},[129,24703,362],{"class":277},[129,24705,4544],{"class":284},[129,24707,23910],{"class":1376},[129,24709,24710,24713,24715,24717,24719,24721,24723],{"class":265,"line":332},[129,24711,24712],{"class":284},"      sendOrderConfirmation",[129,24714,147],{"class":1376},[129,24716,18465],{"class":273},[129,24718,362],{"class":277},[129,24720,18515],{"class":273},[129,24722,160],{"class":1376},[129,24724,1386],{"class":277},[129,24726,24727,24730,24732,24734,24736,24738,24740],{"class":265,"line":339},[129,24728,24729],{"class":284},"      updateInventory",[129,24731,147],{"class":1376},[129,24733,18465],{"class":273},[129,24735,362],{"class":277},[129,24737,18515],{"class":273},[129,24739,160],{"class":1376},[129,24741,1386],{"class":277},[129,24743,24744,24747,24749,24751,24753,24755,24757],{"class":265,"line":356},[129,24745,24746],{"class":284},"      notifyFulfillment",[129,24748,147],{"class":1376},[129,24750,18465],{"class":273},[129,24752,362],{"class":277},[129,24754,18515],{"class":273},[129,24756,160],{"class":1376},[129,24758,1386],{"class":277},[129,24760,24761],{"class":265,"line":651},[129,24762,24763],{"class":1376},"    ])\n",[129,24765,24766],{"class":265,"line":657},[129,24767,1524],{"class":277},[129,24769,24770,24772,24774,24776,24778,24780,24782],{"class":265,"line":669},[129,24771,6625],{"class":277},[129,24773,1416],{"class":277},[129,24775,18424],{"class":1376},[129,24777,1380],{"class":277},[129,24779,18429],{"class":273},[129,24781,4028],{"class":277},[129,24783,294],{"class":273},[11,24785,24786],{},"The downside: event-driven systems are harder to trace (\"why didn't the email send?\") and harder to reason about causally. The tradeoffs make sense when you have multiple downstream consumers for an event, or when handlers need to be independently scalable.",[2456,24788,24790],{"id":24789},"message-queues","Message queues",[11,24792,561,24793,24796],{},[118,24794,24795],{},"message queue"," buffers between a producer and a consumer. The producer writes a message; the consumer reads it at its own pace. If the consumer is slow or temporarily down, messages accumulate in the queue rather than being lost.",[11,24798,24799],{},"Redis via BullMQ handles this well at typical Nuxt application scale. For higher throughput or more complex routing: RabbitMQ (topic exchanges, complex routing) or Kafka (millions of events/second, event log replay).",[11,24801,24802],{},"The key properties to understand:",[1822,24804,24805,24811,24817],{},[1825,24806,24807,24810],{},[118,24808,24809],{},"Durability",": messages survive consumer restarts, unlike in-memory event emitters",[1825,24812,24813,24816],{},[118,24814,24815],{},"Delivery guarantee",": at-least-once (message might be processed twice) vs exactly-once (harder, expensive - most systems settle for at-least-once with idempotent consumers)",[1825,24818,24819,24822],{},[118,24820,24821],{},"Dead letter queue",": messages that fail after N retries land here for investigation rather than being silently dropped",[255,24824,24826],{"className":3922,"code":24825,"filename":18343,"language":3924,"meta":260,"style":260},"export const emailQueue = new Queue('emails', {\n  connection: redis,\n  defaultJobOptions: {\n    attempts: 3,\n    backoff: { type: 'exponential', delay: 1000 },\n    removeOnComplete: 100,\n    removeOnFail: false, // keep failed jobs in the dead letter queue for inspection\n  },\n})\n",[15,24827,24828,24854,24864,24873,24884,24913,24923,24936,24940],{"__ignoreMap":260},[129,24829,24830,24832,24834,24836,24838,24840,24842,24844,24846,24848,24850,24852],{"class":265,"line":266},[129,24831,4050],{"class":2139},[129,24833,4456],{"class":269},[129,24835,18403],{"class":273},[129,24837,278],{"class":277},[129,24839,281],{"class":277},[129,24841,18354],{"class":284},[129,24843,147],{"class":273},[129,24845,424],{"class":277},[129,24847,7563],{"class":427},[129,24849,424],{"class":277},[129,24851,1015],{"class":277},[129,24853,1371],{"class":277},[129,24855,24856,24858,24860,24862],{"class":265,"line":297},[129,24857,18532],{"class":1376},[129,24859,1380],{"class":277},[129,24861,18379],{"class":273},[129,24863,1386],{"class":277},[129,24865,24866,24869,24871],{"class":265,"line":315},[129,24867,24868],{"class":1376},"  defaultJobOptions",[129,24870,1380],{"class":277},[129,24872,1371],{"class":277},[129,24874,24875,24878,24880,24882],{"class":265,"line":332},[129,24876,24877],{"class":1376},"    attempts",[129,24879,1380],{"class":277},[129,24881,1018],{"class":290},[129,24883,1386],{"class":277},[129,24885,24886,24889,24891,24893,24895,24897,24899,24901,24903,24905,24907,24909,24911],{"class":265,"line":339},[129,24887,24888],{"class":1376},"    backoff",[129,24890,1380],{"class":277},[129,24892,1416],{"class":277},[129,24894,11316],{"class":1376},[129,24896,1380],{"class":277},[129,24898,4261],{"class":277},[129,24900,18570],{"class":427},[129,24902,424],{"class":277},[129,24904,1015],{"class":277},[129,24906,18577],{"class":1376},[129,24908,1380],{"class":277},[129,24910,9568],{"class":290},[129,24912,1444],{"class":277},[129,24914,24915,24917,24919,24921],{"class":265,"line":356},[129,24916,18727],{"class":1376},[129,24918,1380],{"class":277},[129,24920,18732],{"class":290},[129,24922,1386],{"class":277},[129,24924,24925,24927,24929,24931,24933],{"class":265,"line":651},[129,24926,18742],{"class":1376},[129,24928,1380],{"class":277},[129,24930,21244],{"class":4822},[129,24932,1015],{"class":277},[129,24934,24935],{"class":376}," // keep failed jobs in the dead letter queue for inspection\n",[129,24937,24938],{"class":265,"line":657},[129,24939,1481],{"class":277},[129,24941,24942,24944],{"class":265,"line":669},[129,24943,4028],{"class":277},[129,24945,294],{"class":273},[11,24947,24948,24949,362],{},"Full implementation is in the ",[51,24950,21760],{"href":24951},"/blog/nuxt-system-patterns#queue-system",[2456,24953,24955],{"id":24954},"api-gateway","API gateway",[11,24957,16014,24958,24960],{},[118,24959,24955],{}," sits in front of backend services and handles cross-cutting concerns: routing, authentication, rate limiting, request transformation, SSL termination.",[11,24962,24963,24964,24966,24967,362],{},"For a Nuxt monolith, Nitro ",[24,24965,26],{}," the API gateway. Authentication middleware, rate limiting, logging - these run centrally in ",[15,24968,24969],{},"server/middleware/",[11,24971,24972,24973,1380],{},"For microservices, the gateway routes requests to the right service. Nitro can act as one using ",[15,24974,24975],{},"proxyRequest",[255,24977,24980],{"className":3922,"code":24978,"filename":24979,"language":3924,"meta":260,"style":260},"const SERVICE_MAP: Record\u003Cstring, string> = {\n  auth: process.env.AUTH_SERVICE_URL!,\n  catalog: process.env.CATALOG_SERVICE_URL!,\n  orders: process.env.ORDERS_SERVICE_URL!,\n}\n\nexport default defineEventHandler(async (event) => {\n  const service = event.context.params!.service\n  const target = SERVICE_MAP[service]\n\n  if (!target) {\n    throw createError({ statusCode: 404, message: `Unknown service: ${service}` })\n  }\n\n  // proxyRequest handles headers, body, method, and response streaming\n  return proxyRequest(event, `${target}/${event.context.params!.path}`)\n})\n","server/routes/services/[service]/[...path].ts",[15,24981,24982,25007,25028,25048,25068,25072,25076,25098,25124,25141,25145,25159,25197,25201,25205,25210,25251],{"__ignoreMap":260},[129,24983,24984,24986,24989,24991,24993,24995,24997,24999,25001,25003,25005],{"class":265,"line":266},[129,24985,270],{"class":269},[129,24987,24988],{"class":273}," SERVICE_MAP",[129,24990,1380],{"class":277},[129,24992,20515],{"class":2161},[129,24994,3945],{"class":277},[129,24996,20520],{"class":2161},[129,24998,1015],{"class":277},[129,25000,4622],{"class":2161},[129,25002,3956],{"class":277},[129,25004,4745],{"class":277},[129,25006,1371],{"class":277},[129,25008,25009,25012,25014,25016,25018,25020,25022,25025],{"class":265,"line":297},[129,25010,25011],{"class":1376},"  auth",[129,25013,1380],{"class":277},[129,25015,5084],{"class":273},[129,25017,362],{"class":277},[129,25019,4439],{"class":273},[129,25021,362],{"class":277},[129,25023,25024],{"class":273},"AUTH_SERVICE_URL",[129,25026,25027],{"class":277},"!,\n",[129,25029,25030,25033,25035,25037,25039,25041,25043,25046],{"class":265,"line":315},[129,25031,25032],{"class":1376},"  catalog",[129,25034,1380],{"class":277},[129,25036,5084],{"class":273},[129,25038,362],{"class":277},[129,25040,4439],{"class":273},[129,25042,362],{"class":277},[129,25044,25045],{"class":273},"CATALOG_SERVICE_URL",[129,25047,25027],{"class":277},[129,25049,25050,25053,25055,25057,25059,25061,25063,25066],{"class":265,"line":332},[129,25051,25052],{"class":1376},"  orders",[129,25054,1380],{"class":277},[129,25056,5084],{"class":273},[129,25058,362],{"class":277},[129,25060,4439],{"class":273},[129,25062,362],{"class":277},[129,25064,25065],{"class":273},"ORDERS_SERVICE_URL",[129,25067,25027],{"class":277},[129,25069,25070],{"class":265,"line":339},[129,25071,1530],{"class":277},[129,25073,25074],{"class":265,"line":356},[129,25075,336],{"emptyLinePlaceholder":335},[129,25077,25078,25080,25082,25084,25086,25088,25090,25092,25094,25096],{"class":265,"line":651},[129,25079,4050],{"class":2139},[129,25081,4053],{"class":2139},[129,25083,4503],{"class":284},[129,25085,147],{"class":273},[129,25087,4508],{"class":269},[129,25089,3984],{"class":277},[129,25091,4100],{"class":452},[129,25093,160],{"class":277},[129,25095,456],{"class":269},[129,25097,1371],{"class":277},[129,25099,25100,25102,25105,25107,25109,25111,25113,25115,25118,25121],{"class":265,"line":657},[129,25101,5076],{"class":269},[129,25103,25104],{"class":273}," service",[129,25106,4745],{"class":277},[129,25108,16694],{"class":273},[129,25110,362],{"class":277},[129,25112,6497],{"class":273},[129,25114,362],{"class":277},[129,25116,25117],{"class":273},"params",[129,25119,25120],{"class":277},"!.",[129,25122,25123],{"class":273},"service\n",[129,25125,25126,25128,25130,25132,25134,25136,25139],{"class":265,"line":669},[129,25127,5076],{"class":269},[129,25129,13211],{"class":273},[129,25131,4745],{"class":277},[129,25133,24988],{"class":273},[129,25135,4128],{"class":1376},[129,25137,25138],{"class":273},"service",[129,25140,1046],{"class":1376},[129,25142,25143],{"class":265,"line":693},[129,25144,336],{"emptyLinePlaceholder":335},[129,25146,25147,25149,25151,25153,25155,25157],{"class":265,"line":712},[129,25148,3998],{"class":2139},[129,25150,3984],{"class":1376},[129,25152,4447],{"class":277},[129,25154,13109],{"class":273},[129,25156,4005],{"class":1376},[129,25158,6455],{"class":277},[129,25160,25161,25163,25165,25167,25169,25171,25173,25176,25178,25180,25182,25184,25187,25189,25191,25193,25195],{"class":265,"line":1521},[129,25162,20452],{"class":2139},[129,25164,6916],{"class":284},[129,25166,147],{"class":1376},[129,25168,4796],{"class":277},[129,25170,6923],{"class":1376},[129,25172,1380],{"class":277},[129,25174,25175],{"class":290}," 404",[129,25177,1015],{"class":277},[129,25179,6933],{"class":1376},[129,25181,1380],{"class":277},[129,25183,5569],{"class":277},[129,25185,25186],{"class":427},"Unknown service: ",[129,25188,4131],{"class":277},[129,25190,25138],{"class":273},[129,25192,4175],{"class":277},[129,25194,4255],{"class":277},[129,25196,294],{"class":1376},[129,25198,25199],{"class":265,"line":1527},[129,25200,1524],{"class":277},[129,25202,25203],{"class":265,"line":2295},[129,25204,336],{"emptyLinePlaceholder":335},[129,25206,25207],{"class":265,"line":2300},[129,25208,25209],{"class":376},"  // proxyRequest handles headers, body, method, and response streaming\n",[129,25211,25212,25214,25217,25219,25221,25223,25225,25227,25229,25231,25233,25235,25237,25239,25241,25243,25245,25247,25249],{"class":265,"line":2305},[129,25213,4520],{"class":2139},[129,25215,25216],{"class":284}," proxyRequest",[129,25218,147],{"class":1376},[129,25220,4100],{"class":273},[129,25222,1015],{"class":277},[129,25224,6794],{"class":277},[129,25226,13109],{"class":273},[129,25228,4028],{"class":277},[129,25230,938],{"class":427},[129,25232,4131],{"class":277},[129,25234,4100],{"class":273},[129,25236,362],{"class":277},[129,25238,6497],{"class":273},[129,25240,362],{"class":277},[129,25242,25117],{"class":273},[129,25244,25120],{"class":277},[129,25246,4172],{"class":273},[129,25248,4175],{"class":277},[129,25250,294],{"class":1376},[129,25252,25253,25255],{"class":265,"line":2311},[129,25254,4028],{"class":277},[129,25256,294],{"class":273},[11,25258,25259],{},"In practice, most teams use a dedicated gateway - Kong, AWS API Gateway, Cloudflare - rather than building one. Nuxt's role is usually as one service behind the gateway, not the gateway itself. The exception: Nuxt as gateway makes sense for proxying internal APIs during development or in single-region setups where the added network hop isn't justified.",[2456,25261,25263],{"id":25262},"distributed-tracing","Distributed tracing",[11,25265,25266],{},"When a request touches multiple services, a stack trace is no longer sufficient. You need to know which service, which call, how long each step took, and which upstream request triggered which downstream calls.",[11,25268,25269,25271,25272,25277],{},[118,25270,25263],{}," gives each request a trace ID that flows through every service. ",[51,25273,25276],{"href":25274,"rel":25275},"https://opentelemetry.io/",[55],"OpenTelemetry"," standardizes the instrumentation; Jaeger, Zipkin, or Datadog collect and visualize the traces.",[11,25279,25280],{},"In Nitro, you can instrument via a plugin:",[255,25282,25285],{"className":3922,"code":25283,"filename":25284,"language":3924,"meta":260,"style":260},"import { trace, context, propagation } from '@opentelemetry/api'\n\nexport default defineNitroPlugin((nitroApp) => {\n  nitroApp.hooks.hook('request', (event) => {\n    // Extract trace context from incoming headers (set by upstream service or load balancer)\n    const parentCtx = propagation.extract(context.active(), getHeaders(event))\n    const tracer = trace.getTracer('nitro')\n    const span = tracer.startSpan(`${event.method} ${event.path}`, undefined, parentCtx)\n\n    event.context.span = span\n    event.context.traceId = span.spanContext().traceId\n  })\n\n  nitroApp.hooks.hook('afterResponse', (event) => {\n    event.context.span?.end()\n  })\n})\n","server/plugins/tracing.ts",[15,25286,25287,25317,25321,25342,25375,25380,25417,25443,25491,25495,25513,25542,25548,25552,25585,25604,25610],{"__ignoreMap":260},[129,25288,25289,25291,25293,25296,25298,25301,25303,25306,25308,25310,25312,25315],{"class":265,"line":266},[129,25290,2140],{"class":2139},[129,25292,1416],{"class":277},[129,25294,25295],{"class":273}," trace",[129,25297,1015],{"class":277},[129,25299,25300],{"class":273}," context",[129,25302,1015],{"class":277},[129,25304,25305],{"class":273}," propagation",[129,25307,4255],{"class":277},[129,25309,4258],{"class":2139},[129,25311,4261],{"class":277},[129,25313,25314],{"class":427},"@opentelemetry/api",[129,25316,4267],{"class":277},[129,25318,25319],{"class":265,"line":297},[129,25320,336],{"emptyLinePlaceholder":335},[129,25322,25323,25325,25327,25329,25331,25333,25336,25338,25340],{"class":265,"line":315},[129,25324,4050],{"class":2139},[129,25326,4053],{"class":2139},[129,25328,4056],{"class":284},[129,25330,147],{"class":273},[129,25332,147],{"class":277},[129,25334,25335],{"class":452},"nitroApp",[129,25337,160],{"class":277},[129,25339,456],{"class":269},[129,25341,1371],{"class":277},[129,25343,25344,25347,25349,25351,25353,25355,25357,25359,25361,25363,25365,25367,25369,25371,25373],{"class":265,"line":332},[129,25345,25346],{"class":273},"  nitroApp",[129,25348,362],{"class":277},[129,25350,4079],{"class":273},[129,25352,362],{"class":277},[129,25354,4084],{"class":284},[129,25356,147],{"class":1376},[129,25358,424],{"class":277},[129,25360,4091],{"class":427},[129,25362,424],{"class":277},[129,25364,1015],{"class":277},[129,25366,3984],{"class":277},[129,25368,4100],{"class":452},[129,25370,160],{"class":277},[129,25372,456],{"class":269},[129,25374,1371],{"class":277},[129,25376,25377],{"class":265,"line":339},[129,25378,25379],{"class":376},"    // Extract trace context from incoming headers (set by upstream service or load balancer)\n",[129,25381,25382,25384,25387,25389,25391,25393,25396,25398,25400,25402,25404,25406,25408,25411,25413,25415],{"class":265,"line":356},[129,25383,4739],{"class":269},[129,25385,25386],{"class":273}," parentCtx",[129,25388,4745],{"class":277},[129,25390,25305],{"class":273},[129,25392,362],{"class":277},[129,25394,25395],{"class":284},"extract",[129,25397,147],{"class":1376},[129,25399,6497],{"class":273},[129,25401,362],{"class":277},[129,25403,5449],{"class":284},[129,25405,4140],{"class":1376},[129,25407,1015],{"class":277},[129,25409,25410],{"class":284}," getHeaders",[129,25412,147],{"class":1376},[129,25414,4100],{"class":273},[129,25416,471],{"class":1376},[129,25418,25419,25421,25424,25426,25428,25430,25433,25435,25437,25439,25441],{"class":265,"line":651},[129,25420,4739],{"class":269},[129,25422,25423],{"class":273}," tracer",[129,25425,4745],{"class":277},[129,25427,25295],{"class":273},[129,25429,362],{"class":277},[129,25431,25432],{"class":284},"getTracer",[129,25434,147],{"class":1376},[129,25436,424],{"class":277},[129,25438,4063],{"class":427},[129,25440,424],{"class":277},[129,25442,294],{"class":1376},[129,25444,25445,25447,25450,25452,25454,25456,25459,25461,25464,25466,25468,25470,25472,25474,25476,25478,25480,25482,25484,25487,25489],{"class":265,"line":657},[129,25446,4739],{"class":269},[129,25448,25449],{"class":273}," span",[129,25451,4745],{"class":277},[129,25453,25423],{"class":273},[129,25455,362],{"class":277},[129,25457,25458],{"class":284},"startSpan",[129,25460,147],{"class":1376},[129,25462,25463],{"class":277},"`${",[129,25465,4100],{"class":273},[129,25467,362],{"class":277},[129,25469,4160],{"class":273},[129,25471,4028],{"class":277},[129,25473,4165],{"class":277},[129,25475,4100],{"class":273},[129,25477,362],{"class":277},[129,25479,4172],{"class":273},[129,25481,4175],{"class":277},[129,25483,1015],{"class":277},[129,25485,25486],{"class":277}," undefined,",[129,25488,25386],{"class":273},[129,25490,294],{"class":1376},[129,25492,25493],{"class":265,"line":669},[129,25494,336],{"emptyLinePlaceholder":335},[129,25496,25497,25500,25502,25504,25506,25508,25510],{"class":265,"line":693},[129,25498,25499],{"class":273},"    event",[129,25501,362],{"class":277},[129,25503,6497],{"class":273},[129,25505,362],{"class":277},[129,25507,129],{"class":273},[129,25509,4745],{"class":277},[129,25511,25512],{"class":273}," span\n",[129,25514,25515,25517,25519,25521,25523,25526,25528,25530,25532,25535,25537,25539],{"class":265,"line":712},[129,25516,25499],{"class":273},[129,25518,362],{"class":277},[129,25520,6497],{"class":273},[129,25522,362],{"class":277},[129,25524,25525],{"class":273},"traceId",[129,25527,4745],{"class":277},[129,25529,25449],{"class":273},[129,25531,362],{"class":277},[129,25533,25534],{"class":284},"spanContext",[129,25536,4140],{"class":1376},[129,25538,362],{"class":277},[129,25540,25541],{"class":273},"traceId\n",[129,25543,25544,25546],{"class":265,"line":1521},[129,25545,4182],{"class":277},[129,25547,294],{"class":1376},[129,25549,25550],{"class":265,"line":1527},[129,25551,336],{"emptyLinePlaceholder":335},[129,25553,25554,25556,25558,25560,25562,25564,25566,25568,25571,25573,25575,25577,25579,25581,25583],{"class":265,"line":2295},[129,25555,25346],{"class":273},[129,25557,362],{"class":277},[129,25559,4079],{"class":273},[129,25561,362],{"class":277},[129,25563,4084],{"class":284},[129,25565,147],{"class":1376},[129,25567,424],{"class":277},[129,25569,25570],{"class":427},"afterResponse",[129,25572,424],{"class":277},[129,25574,1015],{"class":277},[129,25576,3984],{"class":277},[129,25578,4100],{"class":452},[129,25580,160],{"class":277},[129,25582,456],{"class":269},[129,25584,1371],{"class":277},[129,25586,25587,25589,25591,25593,25595,25597,25599,25602],{"class":265,"line":2300},[129,25588,25499],{"class":273},[129,25590,362],{"class":277},[129,25592,6497],{"class":273},[129,25594,362],{"class":277},[129,25596,129],{"class":273},[129,25598,6059],{"class":277},[129,25600,25601],{"class":284},"end",[129,25603,2451],{"class":1376},[129,25605,25606,25608],{"class":265,"line":2305},[129,25607,4182],{"class":277},[129,25609,294],{"class":1376},[129,25611,25612,25614],{"class":265,"line":2311},[129,25613,4028],{"class":277},[129,25615,294],{"class":273},[3576,25617,25618],{},[11,25619,25620,25621,25624],{},"For most teams, start with ",[118,25622,25623],{},"structured logging"," before distributed tracing. Logging with a request ID and shipping to a centralized tool (Loki, Datadog, BetterStack) solves 80% of debugging problems at a fraction of the operational cost. Distributed tracing becomes necessary when: a request touches more than 3 services, async processing makes timing hard to reason about, or you need to diagnose latency across service boundaries.",[2001,25626],{},[40,25628,25630],{"id":25629},"where-to-start","Where to start",[11,25632,25633],{},"Not all of this applies at day one. A rough priority order as a Nuxt app moves from \"it works\" to \"it works in production\":",[25635,25636,25637,25641,25651,25655,25658,25662,25665,25669],"steps",{},[2456,25638,25640],{"id":25639},"before-launch","Before launch",[11,25642,25643,25644,25647,25648,25650],{},"Database indexes on columns you query. Rate limiting on public endpoints. A ",[15,25645,25646],{},"/api/health"," endpoint. Basic caching with ",[15,25649,3853],{}," on expensive reads.",[2456,25652,25654],{"id":25653},"at-early-traffic","At early traffic",[11,25656,25657],{},"Redis for caching and sessions - this enables horizontal scaling. Retries with backoff on external API calls. Idempotency keys for any payment or write operation users might retry.",[2456,25659,25661],{"id":25660},"when-dependencies-become-flaky","When dependencies become flaky",[11,25663,25664],{},"Circuit breakers on external services. Graceful degradation for non-critical features. Read replicas with write/read DB split for high-read workloads.",[2456,25666,25668],{"id":25667},"at-organizational-scale","At organizational scale",[11,25670,25671],{},"Message queues for background work that should decouple from the request cycle. Microservice extraction - only when teams genuinely need independent deployment. Distributed tracing - only when services multiply and logs become insufficient.",[11,25673,25674],{},"The order matters. Indexes and rate limiting are cheap to add and high value. Sharding and microservices are expensive to operate and only justified at scale. Get the sequencing wrong and you're dealing with distributed systems complexity before you have distributed systems traffic.",[2026,25676,25677],{},"html pre.shiki code .s7zQu, html code.shiki .s7zQu{--shiki-light:#39ADB5;--shiki-light-font-style:italic;--shiki-default:#89DDFF;--shiki-default-font-style:italic;--shiki-dark:#89DDFF;--shiki-dark-font-style:italic}html pre.shiki code .s2Zo4, html code.shiki .s2Zo4{--shiki-light:#6182B8;--shiki-default:#82AAFF;--shiki-dark:#82AAFF}html pre.shiki code .sTEyZ, html code.shiki .sTEyZ{--shiki-light:#90A4AE;--shiki-default:#EEFFFF;--shiki-dark:#BABED8}html pre.shiki code .spNyl, html code.shiki .spNyl{--shiki-light:#9C3EDA;--shiki-default:#C792EA;--shiki-dark:#C792EA}html pre.shiki code .sMK4o, html code.shiki .sMK4o{--shiki-light:#39ADB5;--shiki-default:#89DDFF;--shiki-dark:#89DDFF}html pre.shiki code .sHdIc, html code.shiki .sHdIc{--shiki-light:#90A4AE;--shiki-light-font-style:italic;--shiki-default:#EEFFFF;--shiki-default-font-style:italic;--shiki-dark:#BABED8;--shiki-dark-font-style:italic}html pre.shiki code .swJcz, html code.shiki .swJcz{--shiki-light:#E53935;--shiki-default:#F07178;--shiki-dark:#F07178}html pre.shiki code .sfNiH, html code.shiki .sfNiH{--shiki-light:#FF5370;--shiki-default:#FF9CAC;--shiki-dark:#FF9CAC}html pre.shiki code .sbssI, html code.shiki .sbssI{--shiki-light:#F76D47;--shiki-default:#F78C6C;--shiki-dark:#F78C6C}html pre.shiki code .sfazB, html code.shiki .sfazB{--shiki-light:#91B859;--shiki-default:#C3E88D;--shiki-dark:#C3E88D}html .light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html.light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .sHwdD, html code.shiki .sHwdD{--shiki-light:#90A4AE;--shiki-light-font-style:italic;--shiki-default:#546E7A;--shiki-default-font-style:italic;--shiki-dark:#676E95;--shiki-dark-font-style:italic}html pre.shiki code .sBMFI, html code.shiki .sBMFI{--shiki-light:#E2931D;--shiki-default:#FFCB6B;--shiki-dark:#FFCB6B}",{"title":260,"searchDepth":297,"depth":297,"links":25679},[25680,25687,25694,25701,25708],{"id":21468,"depth":297,"text":21469,"children":25681},[25682,25683,25684,25685,25686],{"id":21472,"depth":315,"text":21473},{"id":21763,"depth":315,"text":21764},{"id":21869,"depth":315,"text":21870},{"id":21962,"depth":315,"text":21963},{"id":20067,"depth":315,"text":20068},{"id":22363,"depth":297,"text":22364,"children":25688},[25689,25690,25691,25692,25693],{"id":22367,"depth":315,"text":22368},{"id":22498,"depth":315,"text":22499},{"id":22872,"depth":315,"text":22873},{"id":22901,"depth":315,"text":22902},{"id":22957,"depth":315,"text":22958},{"id":22993,"depth":297,"text":22994,"children":25695},[25696,25697,25698,25699,25700],{"id":22997,"depth":315,"text":22998},{"id":23196,"depth":315,"text":23197},{"id":23823,"depth":315,"text":23824},{"id":23846,"depth":315,"text":23847},{"id":23857,"depth":315,"text":23858},{"id":24258,"depth":297,"text":24259,"children":25702},[25703,25704,25705,25706,25707],{"id":24262,"depth":315,"text":24263},{"id":24328,"depth":315,"text":24329},{"id":24789,"depth":315,"text":24790},{"id":24954,"depth":315,"text":24955},{"id":25262,"depth":315,"text":25263},{"id":25629,"depth":297,"text":25630,"children":25709},[25710,25711,25712,25713],{"id":25639,"depth":315,"text":25640},{"id":25653,"depth":315,"text":25654},{"id":25660,"depth":315,"text":25661},{"id":25667,"depth":315,"text":25668},"20 patterns from distributed systems - caching, circuit breakers, CAP theorem, sharding, event-driven architecture - mapped to real implementations in Nuxt and Nitro.",{},"/blog/system-design-fullstack",{"title":21457,"description":25714},"blog/system-design-fullstack",[8631,2051,16484,21453,25720],"System Design","axgs3ubyKTclvCtZ1It21gCHLZfiXSYk5KOy1gShskk",{"id":25723,"title":25724,"body":25725,"cover":2042,"date":8623,"description":31208,"extension":2045,"meta":31209,"navigation":335,"path":31210,"readingTime":2305,"seo":31211,"stem":31212,"tags":31213,"__hash__":31216},"blog/blog/ticket-booking-concurrency.md","Building a ticket booking system in Nuxt that doesn't double-book under load",{"type":8,"value":25726,"toc":31185},[25727,25734,25737,25740,25744,25747,25753,25760,25762,25766,25769,26088,26091,26101,26111,26114,26807,26809,26813,26816,26825,26829,26832,27301,27312,27315,27319,27322,27673,27690,27699,27702,27706,27714,27717,27719,27723,27726,27738,27741,28027,28030,28130,28133,28401,28405,28413,28423,28426,28435,28438,28769,28772,28774,28778,28781,28787,28794,29704,29707,29952,29958,29960,29964,29967,30185,30188,30196,30199,30201,30205,30208,30216,30522,30528,30531,30794,30992,30995,30997,31001,31004,31055,31057,31061,31141,31144,31166,31168,31182],[17774,25728],{"url":25729,"authorName":25730,"handle":25731,"text":25732,"dateText":25733},"https://x.com/mysticwillz/status/2026241841922769045","THE CODE SCIENTIST","mysticwillz","You're in a system design interview. \n\nThey ask: \"Design a ticket booking system that prevents double-booking under high concurrency.\"\n\nHere's is a good approach:","February 24, 2026",[11,25735,25736],{},"Engineering interview might land on \"design a ticket booking system\". The expected answer - transactions, row locking, TTL, idempotency - is mostly correct. But there's a gap between knowing the answer and knowing why the naive version silently fails under load.",[11,25738,25739],{},"Here's the full implementation in Nuxt and Nitro, starting with the failure mode.",[40,25741,25743],{"id":25742},"what-actually-breaks-without-this","What actually breaks without this",[11,25745,25746],{},"Two users try to book seat 14B at the same time. Your naive implementation:",[255,25748,25751],{"className":25749,"code":25750,"language":3237},[12199],"User A: SELECT * FROM seats WHERE seat_number = '14B' → status: 'available' ✓\nUser B: SELECT * FROM seats WHERE seat_number = '14B' → status: 'available' ✓\nUser A: UPDATE seats SET status = 'reserved' WHERE seat_number = '14B'\nUser B: UPDATE seats SET status = 'reserved' WHERE seat_number = '14B'\nUser A: Booking confirmed. 🎉\nUser B: Booking confirmed. 🎉\n",[15,25752,25750],{"__ignoreMap":260},[11,25754,25755,25756,25759],{},"Both users have a confirmed booking for the same seat. Under normal traffic this is rare. Under a ticket sale for anything popular - a concert, a flight opening - it's a certainty. This is called ",[118,25757,25758],{},"TOCTOU (Time of Check to Time of Use)",": you check a condition, then act on it, but the condition can change between the check and the action.",[2001,25761],{},[40,25763,25765],{"id":25764},"the-schema-first","The schema first",[11,25767,25768],{},"Before any code, the schema. The database is the source of truth and must enforce consistency even if application code is buggy:",[255,25770,25772],{"className":22269,"code":25771,"language":22038,"meta":260,"style":260},"CREATE TABLE seats (\n  id          SERIAL PRIMARY KEY,\n  event_id    UUID NOT NULL REFERENCES events(id),\n  seat_number VARCHAR(10) NOT NULL,\n  status      TEXT NOT NULL DEFAULT 'available'\n              CHECK (status IN ('available', 'reserved', 'sold')),\n  reserved_by UUID,\n  reserved_until TIMESTAMPTZ,\n\n  -- This is your last line of defense\n  UNIQUE (event_id, seat_number)\n);\n\nCREATE TABLE bookings (\n  id              UUID PRIMARY KEY DEFAULT gen_random_uuid(),\n  event_id        UUID NOT NULL REFERENCES events(id),\n  seat_id         INTEGER NOT NULL REFERENCES seats(id),\n  user_id         UUID NOT NULL,\n  idempotency_key TEXT UNIQUE, -- prevents double-charge\n  status          TEXT NOT NULL DEFAULT 'pending'\n                  CHECK (status IN ('pending', 'confirmed', 'cancelled')),\n  created_at      TIMESTAMPTZ DEFAULT NOW()\n);\n",[15,25773,25774,25788,25801,25815,25833,25853,25894,25899,25909,25913,25918,25926,25931,25935,25946,25963,25974,25989,25998,26014,26031,26070,26084],{"__ignoreMap":260},[129,25775,25776,25779,25782,25785],{"class":265,"line":266},[129,25777,25778],{"class":290},"CREATE",[129,25780,25781],{"class":290}," TABLE",[129,25783,25784],{"class":284}," seats",[129,25786,25787],{"class":273}," (\n",[129,25789,25790,25793,25796,25799],{"class":265,"line":297},[129,25791,25792],{"class":273},"  id          ",[129,25794,25795],{"class":269},"SERIAL",[129,25797,25798],{"class":269}," PRIMARY KEY",[129,25800,1386],{"class":273},[129,25802,25803,25806,25809,25812],{"class":265,"line":315},[129,25804,25805],{"class":273},"  event_id    UUID ",[129,25807,25808],{"class":290},"NOT NULL",[129,25810,25811],{"class":269}," REFERENCES",[129,25813,25814],{"class":273}," events(id),\n",[129,25816,25817,25820,25823,25825,25827,25829,25831],{"class":265,"line":332},[129,25818,25819],{"class":273},"  seat_number ",[129,25821,25822],{"class":269},"VARCHAR",[129,25824,147],{"class":273},[129,25826,5623],{"class":290},[129,25828,4005],{"class":273},[129,25830,25808],{"class":290},[129,25832,1386],{"class":273},[129,25834,25835,25837,25840,25843,25846,25848,25851],{"class":265,"line":339},[129,25836,22083],{"class":290},[129,25838,25839],{"class":269},"      TEXT",[129,25841,25842],{"class":290}," NOT NULL",[129,25844,25845],{"class":269}," DEFAULT",[129,25847,4261],{"class":277},[129,25849,25850],{"class":427},"available",[129,25852,4267],{"class":277},[129,25854,25855,25858,25860,25862,25865,25867,25869,25871,25873,25875,25877,25880,25882,25884,25886,25889,25891],{"class":265,"line":356},[129,25856,25857],{"class":269},"              CHECK",[129,25859,3984],{"class":273},[129,25861,22095],{"class":290},[129,25863,25864],{"class":290}," IN",[129,25866,3984],{"class":273},[129,25868,424],{"class":277},[129,25870,25850],{"class":427},[129,25872,424],{"class":277},[129,25874,500],{"class":273},[129,25876,424],{"class":277},[129,25878,25879],{"class":427},"reserved",[129,25881,424],{"class":277},[129,25883,500],{"class":273},[129,25885,424],{"class":277},[129,25887,25888],{"class":427},"sold",[129,25890,424],{"class":277},[129,25892,25893],{"class":273},")),\n",[129,25895,25896],{"class":265,"line":651},[129,25897,25898],{"class":273},"  reserved_by UUID,\n",[129,25900,25901,25904,25907],{"class":265,"line":657},[129,25902,25903],{"class":273},"  reserved_until ",[129,25905,25906],{"class":269},"TIMESTAMPTZ",[129,25908,1386],{"class":273},[129,25910,25911],{"class":265,"line":669},[129,25912,336],{"emptyLinePlaceholder":335},[129,25914,25915],{"class":265,"line":693},[129,25916,25917],{"class":376},"  -- This is your last line of defense\n",[129,25919,25920,25923],{"class":265,"line":712},[129,25921,25922],{"class":290},"  UNIQUE",[129,25924,25925],{"class":273}," (event_id, seat_number)\n",[129,25927,25928],{"class":265,"line":1521},[129,25929,25930],{"class":273},");\n",[129,25932,25933],{"class":265,"line":1527},[129,25934,336],{"emptyLinePlaceholder":335},[129,25936,25937,25939,25941,25944],{"class":265,"line":2295},[129,25938,25778],{"class":290},[129,25940,25781],{"class":290},[129,25942,25943],{"class":284}," bookings",[129,25945,25787],{"class":273},[129,25947,25948,25951,25954,25956,25959,25961],{"class":265,"line":2300},[129,25949,25950],{"class":273},"  id              UUID ",[129,25952,25953],{"class":269},"PRIMARY KEY",[129,25955,25845],{"class":269},[129,25957,25958],{"class":273}," gen_random_uuid",[129,25960,4140],{"class":277},[129,25962,1386],{"class":273},[129,25964,25965,25968,25970,25972],{"class":265,"line":2305},[129,25966,25967],{"class":273},"  event_id        UUID ",[129,25969,25808],{"class":290},[129,25971,25811],{"class":269},[129,25973,25814],{"class":273},[129,25975,25976,25979,25982,25984,25986],{"class":265,"line":2311},[129,25977,25978],{"class":273},"  seat_id         ",[129,25980,25981],{"class":269},"INTEGER",[129,25983,25842],{"class":290},[129,25985,25811],{"class":269},[129,25987,25988],{"class":273}," seats(id),\n",[129,25990,25991,25994,25996],{"class":265,"line":2329},[129,25992,25993],{"class":273},"  user_id         UUID ",[129,25995,25808],{"class":290},[129,25997,1386],{"class":273},[129,25999,26000,26003,26006,26009,26011],{"class":265,"line":2351},[129,26001,26002],{"class":273},"  idempotency_key ",[129,26004,26005],{"class":269},"TEXT",[129,26007,26008],{"class":290}," UNIQUE",[129,26010,500],{"class":273},[129,26012,26013],{"class":376},"-- prevents double-charge\n",[129,26015,26016,26018,26021,26023,26025,26027,26029],{"class":265,"line":2387},[129,26017,22083],{"class":290},[129,26019,26020],{"class":269},"          TEXT",[129,26022,25842],{"class":290},[129,26024,25845],{"class":269},[129,26026,4261],{"class":277},[129,26028,22316],{"class":427},[129,26030,4267],{"class":277},[129,26032,26033,26036,26038,26040,26042,26044,26046,26048,26050,26052,26054,26057,26059,26061,26063,26066,26068],{"class":265,"line":2392},[129,26034,26035],{"class":269},"                  CHECK",[129,26037,3984],{"class":273},[129,26039,22095],{"class":290},[129,26041,25864],{"class":290},[129,26043,3984],{"class":273},[129,26045,424],{"class":277},[129,26047,22316],{"class":427},[129,26049,424],{"class":277},[129,26051,500],{"class":273},[129,26053,424],{"class":277},[129,26055,26056],{"class":427},"confirmed",[129,26058,424],{"class":277},[129,26060,500],{"class":273},[129,26062,424],{"class":277},[129,26064,26065],{"class":427},"cancelled",[129,26067,424],{"class":277},[129,26069,25893],{"class":273},[129,26071,26072,26075,26077,26079,26082],{"class":265,"line":2398},[129,26073,26074],{"class":273},"  created_at      ",[129,26076,25906],{"class":269},[129,26078,25845],{"class":269},[129,26080,26081],{"class":290}," NOW",[129,26083,2451],{"class":277},[129,26085,26086],{"class":265,"line":2441},[129,26087,25930],{"class":273},[11,26089,26090],{},"Two things worth noting:",[11,26092,26093,26096,26097,26100],{},[15,26094,26095],{},"UNIQUE (event_id, seat_number)"," is the schema-level guarantee. If your application code has a bug and two transactions somehow both reach the ",[15,26098,26099],{},"INSERT"," stage for the same seat, the database rejects the second one with a unique constraint violation. Application logic prevents the problem. The constraint catches the impossible case.",[11,26102,26103,26106,26107,26110],{},[15,26104,26105],{},"CHECK (status IN (...))"," means the database rejects invalid state transitions. You can't accidentally set status to ",[15,26108,26109],{},"'booked'"," because a developer mistyped it.",[11,26112,26113],{},"With Drizzle ORM:",[255,26115,26117],{"className":3922,"code":26116,"filename":21973,"language":3924,"meta":260,"style":260},"import { pgTable, serial, uuid, varchar, text, timestamp, unique, check } from 'drizzle-orm/pg-core'\nimport { sql } from 'drizzle-orm'\n\nexport const seats = pgTable('seats', {\n  id: serial('id').primaryKey(),\n  eventId: uuid('event_id').notNull().references(() => events.id),\n  seatNumber: varchar('seat_number', { length: 10 }).notNull(),\n  status: text('status').notNull().default('available'),\n  reservedBy: uuid('reserved_by'),\n  reservedUntil: timestamp('reserved_until', { withTimezone: true }),\n}, (t) => ({\n  uniqueSeat: unique().on(t.eventId, t.seatNumber),\n  statusCheck: check('status_check', sql`${t.status} IN ('available', 'reserved', 'sold')`),\n}))\n\nexport const bookings = pgTable('bookings', {\n  id: uuid('id').primaryKey().default(sql`gen_random_uuid()`),\n  eventId: uuid('event_id').notNull(),\n  seatId: serial('seat_id').notNull().references(() => seats.id),\n  userId: uuid('user_id').notNull(),\n  idempotencyKey: text('idempotency_key').unique(),\n  status: text('status').notNull().default('pending'),\n  createdAt: timestamp('created_at', { withTimezone: true }).defaultNow(),\n})\n",[15,26118,26119,26170,26190,26194,26220,26246,26292,26333,26373,26395,26430,26446,26479,26520,26526,26530,26556,26598,26624,26668,26694,26723,26763,26801],{"__ignoreMap":260},[129,26120,26121,26123,26125,26127,26129,26132,26134,26136,26138,26141,26143,26145,26147,26149,26151,26154,26156,26159,26161,26163,26165,26168],{"class":265,"line":266},[129,26122,2140],{"class":2139},[129,26124,1416],{"class":277},[129,26126,21989],{"class":273},[129,26128,1015],{"class":277},[129,26130,26131],{"class":273}," serial",[129,26133,1015],{"class":277},[129,26135,22012],{"class":273},[129,26137,1015],{"class":277},[129,26139,26140],{"class":273}," varchar",[129,26142,1015],{"class":277},[129,26144,22088],{"class":273},[129,26146,1015],{"class":277},[129,26148,22117],{"class":273},[129,26150,1015],{"class":277},[129,26152,26153],{"class":273}," unique",[129,26155,1015],{"class":277},[129,26157,26158],{"class":273}," check",[129,26160,4255],{"class":277},[129,26162,4258],{"class":2139},[129,26164,4261],{"class":277},[129,26166,26167],{"class":427},"drizzle-orm/pg-core",[129,26169,4267],{"class":277},[129,26171,26172,26174,26176,26179,26181,26183,26185,26188],{"class":265,"line":297},[129,26173,2140],{"class":2139},[129,26175,1416],{"class":277},[129,26177,26178],{"class":273}," sql",[129,26180,4255],{"class":277},[129,26182,4258],{"class":2139},[129,26184,4261],{"class":277},[129,26186,26187],{"class":427},"drizzle-orm",[129,26189,4267],{"class":277},[129,26191,26192],{"class":265,"line":315},[129,26193,336],{"emptyLinePlaceholder":335},[129,26195,26196,26198,26200,26203,26205,26207,26209,26211,26214,26216,26218],{"class":265,"line":332},[129,26197,4050],{"class":2139},[129,26199,4456],{"class":269},[129,26201,26202],{"class":273}," seats ",[129,26204,278],{"class":277},[129,26206,21989],{"class":284},[129,26208,147],{"class":273},[129,26210,424],{"class":277},[129,26212,26213],{"class":427},"seats",[129,26215,424],{"class":277},[129,26217,1015],{"class":277},[129,26219,1371],{"class":277},[129,26221,26222,26224,26226,26228,26230,26232,26234,26236,26238,26240,26242,26244],{"class":265,"line":339},[129,26223,22007],{"class":1376},[129,26225,1380],{"class":277},[129,26227,26131],{"class":284},[129,26229,147],{"class":273},[129,26231,424],{"class":277},[129,26233,3190],{"class":427},[129,26235,424],{"class":277},[129,26237,160],{"class":273},[129,26239,362],{"class":277},[129,26241,22027],{"class":284},[129,26243,4140],{"class":273},[129,26245,1386],{"class":277},[129,26247,26248,26251,26253,26255,26257,26259,26262,26264,26266,26268,26270,26272,26274,26277,26279,26281,26283,26285,26287,26290],{"class":265,"line":356},[129,26249,26250],{"class":1376},"  eventId",[129,26252,1380],{"class":277},[129,26254,22012],{"class":284},[129,26256,147],{"class":273},[129,26258,424],{"class":277},[129,26260,26261],{"class":427},"event_id",[129,26263,424],{"class":277},[129,26265,160],{"class":273},[129,26267,362],{"class":277},[129,26269,22074],{"class":284},[129,26271,4140],{"class":273},[129,26273,362],{"class":277},[129,26275,26276],{"class":284},"references",[129,26278,147],{"class":273},[129,26280,4140],{"class":277},[129,26282,456],{"class":269},[129,26284,24484],{"class":273},[129,26286,362],{"class":277},[129,26288,26289],{"class":273},"id)",[129,26291,1386],{"class":277},[129,26293,26294,26297,26299,26301,26303,26305,26308,26310,26312,26314,26317,26319,26321,26323,26325,26327,26329,26331],{"class":265,"line":651},[129,26295,26296],{"class":1376},"  seatNumber",[129,26298,1380],{"class":277},[129,26300,26140],{"class":284},[129,26302,147],{"class":273},[129,26304,424],{"class":277},[129,26306,26307],{"class":427},"seat_number",[129,26309,424],{"class":277},[129,26311,1015],{"class":277},[129,26313,1416],{"class":277},[129,26315,26316],{"class":1376}," length",[129,26318,1380],{"class":277},[129,26320,10264],{"class":290},[129,26322,4255],{"class":277},[129,26324,160],{"class":273},[129,26326,362],{"class":277},[129,26328,22074],{"class":284},[129,26330,4140],{"class":273},[129,26332,1386],{"class":277},[129,26334,26335,26337,26339,26341,26343,26345,26347,26349,26351,26353,26355,26357,26359,26361,26363,26365,26367,26369,26371],{"class":265,"line":657},[129,26336,22083],{"class":1376},[129,26338,1380],{"class":277},[129,26340,22088],{"class":284},[129,26342,147],{"class":273},[129,26344,424],{"class":277},[129,26346,22095],{"class":427},[129,26348,424],{"class":277},[129,26350,160],{"class":273},[129,26352,362],{"class":277},[129,26354,22074],{"class":284},[129,26356,4140],{"class":273},[129,26358,362],{"class":277},[129,26360,12832],{"class":284},[129,26362,147],{"class":273},[129,26364,424],{"class":277},[129,26366,25850],{"class":427},[129,26368,424],{"class":277},[129,26370,160],{"class":273},[129,26372,1386],{"class":277},[129,26374,26375,26378,26380,26382,26384,26386,26389,26391,26393],{"class":265,"line":669},[129,26376,26377],{"class":1376},"  reservedBy",[129,26379,1380],{"class":277},[129,26381,22012],{"class":284},[129,26383,147],{"class":273},[129,26385,424],{"class":277},[129,26387,26388],{"class":427},"reserved_by",[129,26390,424],{"class":277},[129,26392,160],{"class":273},[129,26394,1386],{"class":277},[129,26396,26397,26400,26402,26404,26406,26408,26411,26413,26415,26417,26420,26422,26424,26426,26428],{"class":265,"line":693},[129,26398,26399],{"class":1376},"  reservedUntil",[129,26401,1380],{"class":277},[129,26403,22117],{"class":284},[129,26405,147],{"class":273},[129,26407,424],{"class":277},[129,26409,26410],{"class":427},"reserved_until",[129,26412,424],{"class":277},[129,26414,1015],{"class":277},[129,26416,1416],{"class":277},[129,26418,26419],{"class":1376}," withTimezone",[129,26421,1380],{"class":277},[129,26423,4823],{"class":4822},[129,26425,4255],{"class":277},[129,26427,160],{"class":273},[129,26429,1386],{"class":277},[129,26431,26432,26434,26436,26438,26440,26442,26444],{"class":265,"line":712},[129,26433,6625],{"class":277},[129,26435,3984],{"class":277},[129,26437,205],{"class":452},[129,26439,160],{"class":277},[129,26441,456],{"class":269},[129,26443,3984],{"class":273},[129,26445,6455],{"class":277},[129,26447,26448,26451,26453,26455,26457,26459,26461,26463,26465,26468,26470,26472,26474,26477],{"class":265,"line":1521},[129,26449,26450],{"class":1376},"  uniqueSeat",[129,26452,1380],{"class":277},[129,26454,26153],{"class":284},[129,26456,4140],{"class":273},[129,26458,362],{"class":277},[129,26460,22184],{"class":284},[129,26462,22187],{"class":273},[129,26464,362],{"class":277},[129,26466,26467],{"class":273},"eventId",[129,26469,1015],{"class":277},[129,26471,22234],{"class":273},[129,26473,362],{"class":277},[129,26475,26476],{"class":273},"seatNumber)",[129,26478,1386],{"class":277},[129,26480,26481,26484,26486,26488,26490,26492,26495,26497,26499,26501,26503,26505,26507,26509,26511,26514,26516,26518],{"class":265,"line":1527},[129,26482,26483],{"class":1376},"  statusCheck",[129,26485,1380],{"class":277},[129,26487,26158],{"class":284},[129,26489,147],{"class":273},[129,26491,424],{"class":277},[129,26493,26494],{"class":427},"status_check",[129,26496,424],{"class":277},[129,26498,1015],{"class":277},[129,26500,26178],{"class":284},[129,26502,25463],{"class":277},[129,26504,205],{"class":273},[129,26506,362],{"class":277},[129,26508,22095],{"class":273},[129,26510,4028],{"class":277},[129,26512,26513],{"class":427}," IN ('available', 'reserved', 'sold')",[129,26515,4125],{"class":277},[129,26517,160],{"class":273},[129,26519,1386],{"class":277},[129,26521,26522,26524],{"class":265,"line":2295},[129,26523,4028],{"class":277},[129,26525,471],{"class":273},[129,26527,26528],{"class":265,"line":2300},[129,26529,336],{"emptyLinePlaceholder":335},[129,26531,26532,26534,26536,26539,26541,26543,26545,26547,26550,26552,26554],{"class":265,"line":2305},[129,26533,4050],{"class":2139},[129,26535,4456],{"class":269},[129,26537,26538],{"class":273}," bookings ",[129,26540,278],{"class":277},[129,26542,21989],{"class":284},[129,26544,147],{"class":273},[129,26546,424],{"class":277},[129,26548,26549],{"class":427},"bookings",[129,26551,424],{"class":277},[129,26553,1015],{"class":277},[129,26555,1371],{"class":277},[129,26557,26558,26560,26562,26564,26566,26568,26570,26572,26574,26576,26578,26580,26582,26584,26586,26588,26590,26592,26594,26596],{"class":265,"line":2311},[129,26559,22007],{"class":1376},[129,26561,1380],{"class":277},[129,26563,22012],{"class":284},[129,26565,147],{"class":273},[129,26567,424],{"class":277},[129,26569,3190],{"class":427},[129,26571,424],{"class":277},[129,26573,160],{"class":273},[129,26575,362],{"class":277},[129,26577,22027],{"class":284},[129,26579,4140],{"class":273},[129,26581,362],{"class":277},[129,26583,12832],{"class":284},[129,26585,147],{"class":273},[129,26587,22038],{"class":284},[129,26589,4125],{"class":277},[129,26591,22043],{"class":427},[129,26593,4125],{"class":277},[129,26595,160],{"class":273},[129,26597,1386],{"class":277},[129,26599,26600,26602,26604,26606,26608,26610,26612,26614,26616,26618,26620,26622],{"class":265,"line":2329},[129,26601,26250],{"class":1376},[129,26603,1380],{"class":277},[129,26605,22012],{"class":284},[129,26607,147],{"class":273},[129,26609,424],{"class":277},[129,26611,26261],{"class":427},[129,26613,424],{"class":277},[129,26615,160],{"class":273},[129,26617,362],{"class":277},[129,26619,22074],{"class":284},[129,26621,4140],{"class":273},[129,26623,1386],{"class":277},[129,26625,26626,26629,26631,26633,26635,26637,26640,26642,26644,26646,26648,26650,26652,26654,26656,26658,26660,26662,26664,26666],{"class":265,"line":2351},[129,26627,26628],{"class":1376},"  seatId",[129,26630,1380],{"class":277},[129,26632,26131],{"class":284},[129,26634,147],{"class":273},[129,26636,424],{"class":277},[129,26638,26639],{"class":427},"seat_id",[129,26641,424],{"class":277},[129,26643,160],{"class":273},[129,26645,362],{"class":277},[129,26647,22074],{"class":284},[129,26649,4140],{"class":273},[129,26651,362],{"class":277},[129,26653,26276],{"class":284},[129,26655,147],{"class":273},[129,26657,4140],{"class":277},[129,26659,456],{"class":269},[129,26661,25784],{"class":273},[129,26663,362],{"class":277},[129,26665,26289],{"class":273},[129,26667,1386],{"class":277},[129,26669,26670,26672,26674,26676,26678,26680,26682,26684,26686,26688,26690,26692],{"class":265,"line":2387},[129,26671,22054],{"class":1376},[129,26673,1380],{"class":277},[129,26675,22012],{"class":284},[129,26677,147],{"class":273},[129,26679,424],{"class":277},[129,26681,22065],{"class":427},[129,26683,424],{"class":277},[129,26685,160],{"class":273},[129,26687,362],{"class":277},[129,26689,22074],{"class":284},[129,26691,4140],{"class":273},[129,26693,1386],{"class":277},[129,26695,26696,26699,26701,26703,26705,26707,26710,26712,26714,26716,26719,26721],{"class":265,"line":2392},[129,26697,26698],{"class":1376},"  idempotencyKey",[129,26700,1380],{"class":277},[129,26702,22088],{"class":284},[129,26704,147],{"class":273},[129,26706,424],{"class":277},[129,26708,26709],{"class":427},"idempotency_key",[129,26711,424],{"class":277},[129,26713,160],{"class":273},[129,26715,362],{"class":277},[129,26717,26718],{"class":284},"unique",[129,26720,4140],{"class":273},[129,26722,1386],{"class":277},[129,26724,26725,26727,26729,26731,26733,26735,26737,26739,26741,26743,26745,26747,26749,26751,26753,26755,26757,26759,26761],{"class":265,"line":2398},[129,26726,22083],{"class":1376},[129,26728,1380],{"class":277},[129,26730,22088],{"class":284},[129,26732,147],{"class":273},[129,26734,424],{"class":277},[129,26736,22095],{"class":427},[129,26738,424],{"class":277},[129,26740,160],{"class":273},[129,26742,362],{"class":277},[129,26744,22074],{"class":284},[129,26746,4140],{"class":273},[129,26748,362],{"class":277},[129,26750,12832],{"class":284},[129,26752,147],{"class":273},[129,26754,424],{"class":277},[129,26756,22316],{"class":427},[129,26758,424],{"class":277},[129,26760,160],{"class":273},[129,26762,1386],{"class":277},[129,26764,26765,26767,26769,26771,26773,26775,26777,26779,26781,26783,26785,26787,26789,26791,26793,26795,26797,26799],{"class":265,"line":2441},[129,26766,22112],{"class":1376},[129,26768,1380],{"class":277},[129,26770,22117],{"class":284},[129,26772,147],{"class":273},[129,26774,424],{"class":277},[129,26776,22124],{"class":427},[129,26778,424],{"class":277},[129,26780,1015],{"class":277},[129,26782,1416],{"class":277},[129,26784,26419],{"class":1376},[129,26786,1380],{"class":277},[129,26788,4823],{"class":4822},[129,26790,4255],{"class":277},[129,26792,160],{"class":273},[129,26794,362],{"class":277},[129,26796,22133],{"class":284},[129,26798,4140],{"class":273},[129,26800,1386],{"class":277},[129,26802,26803,26805],{"class":265,"line":3246},[129,26804,4028],{"class":277},[129,26806,294],{"class":273},[2001,26808],{},[40,26810,26812],{"id":26811},"the-core-problem-checking-then-acting-is-always-wrong","The core problem: checking then acting is always wrong",[11,26814,26815],{},"The 6-point interview answer says \"use a transaction with SELECT FOR UPDATE.\" That's correct - but it's not the only solution, and it's not always the best one.",[11,26817,26818,26819,968,26822,362],{},"There are two approaches to this problem: ",[118,26820,26821],{},"pessimistic locking",[118,26823,26824],{},"optimistic locking",[2456,26826,26828],{"id":26827},"pessimistic-locking-select-for-update","Pessimistic locking: SELECT FOR UPDATE",[11,26830,26831],{},"Lock the row before reading it. Nobody else can touch it until your transaction commits.",[255,26833,26836],{"className":3922,"code":26834,"filename":26835,"language":3924,"meta":260,"style":260},"export default defineEventHandler(async (event) => {\n  const { eventId, seatId, userId } = await readBody(event)\n\n  const seat = await db.transaction(async (tx) => {\n    // Lock the row - anyone else waiting on this seat blocks here\n    const [row] = await tx\n      .select()\n      .from(seats)\n      .where(and(eq(seats.id, seatId), eq(seats.eventId, eventId)))\n      .for('update') // SELECT ... FOR UPDATE\n\n    if (!row || row.status !== 'available') {\n      throw createError({ statusCode: 409, message: 'Seat no longer available' })\n    }\n\n    const [updated] = await tx\n      .update(seats)\n      .set({\n        status: 'reserved',\n        reservedBy: userId,\n        reservedUntil: new Date(Date.now() + 10 * 60 * 1000), // 10 minutes\n      })\n      .where(eq(seats.id, seatId))\n      .returning()\n\n    return updated\n  })\n\n  return { seat }\n})\n","server/api/bookings/reserve.post.ts",[15,26837,26838,26860,26892,26896,26929,26934,26952,26961,26973,27021,27042,27046,27077,27111,27115,27119,27136,27148,27159,27174,27185,27225,27232,27256,27264,27268,27275,27281,27285,27295],{"__ignoreMap":260},[129,26839,26840,26842,26844,26846,26848,26850,26852,26854,26856,26858],{"class":265,"line":266},[129,26841,4050],{"class":2139},[129,26843,4053],{"class":2139},[129,26845,4503],{"class":284},[129,26847,147],{"class":273},[129,26849,4508],{"class":269},[129,26851,3984],{"class":277},[129,26853,4100],{"class":452},[129,26855,160],{"class":277},[129,26857,456],{"class":269},[129,26859,1371],{"class":277},[129,26861,26862,26864,26866,26869,26871,26874,26876,26878,26880,26882,26884,26886,26888,26890],{"class":265,"line":297},[129,26863,5076],{"class":269},[129,26865,1416],{"class":277},[129,26867,26868],{"class":273}," eventId",[129,26870,1015],{"class":277},[129,26872,26873],{"class":273}," seatId",[129,26875,1015],{"class":277},[129,26877,18058],{"class":273},[129,26879,4255],{"class":277},[129,26881,4745],{"class":277},[129,26883,4779],{"class":2139},[129,26885,5264],{"class":284},[129,26887,147],{"class":1376},[129,26889,4100],{"class":273},[129,26891,294],{"class":1376},[129,26893,26894],{"class":265,"line":315},[129,26895,336],{"emptyLinePlaceholder":335},[129,26897,26898,26900,26903,26905,26907,26909,26911,26914,26916,26918,26920,26923,26925,26927],{"class":265,"line":332},[129,26899,5076],{"class":269},[129,26901,26902],{"class":273}," seat",[129,26904,4745],{"class":277},[129,26906,4779],{"class":2139},[129,26908,4479],{"class":273},[129,26910,362],{"class":277},[129,26912,26913],{"class":284},"transaction",[129,26915,147],{"class":1376},[129,26917,4508],{"class":269},[129,26919,3984],{"class":277},[129,26921,26922],{"class":452},"tx",[129,26924,160],{"class":277},[129,26926,456],{"class":269},[129,26928,1371],{"class":277},[129,26930,26931],{"class":265,"line":339},[129,26932,26933],{"class":376},"    // Lock the row - anyone else waiting on this seat blocks here\n",[129,26935,26936,26938,26940,26943,26945,26947,26949],{"class":265,"line":356},[129,26937,4739],{"class":269},[129,26939,1010],{"class":277},[129,26941,26942],{"class":273},"row",[129,26944,14170],{"class":277},[129,26946,4745],{"class":277},[129,26948,4779],{"class":2139},[129,26950,26951],{"class":273}," tx\n",[129,26953,26954,26957,26959],{"class":265,"line":651},[129,26955,26956],{"class":277},"      .",[129,26958,2357],{"class":284},[129,26960,2451],{"class":1376},[129,26962,26963,26965,26967,26969,26971],{"class":265,"line":657},[129,26964,26956],{"class":277},[129,26966,2589],{"class":284},[129,26968,147],{"class":1376},[129,26970,26213],{"class":273},[129,26972,294],{"class":1376},[129,26974,26975,26977,26979,26981,26984,26986,26988,26990,26992,26994,26996,26998,27000,27002,27004,27007,27009,27011,27013,27015,27017,27019],{"class":265,"line":669},[129,26976,26956],{"class":277},[129,26978,5436],{"class":284},[129,26980,147],{"class":1376},[129,26982,26983],{"class":284},"and",[129,26985,147],{"class":1376},[129,26987,5441],{"class":284},[129,26989,147],{"class":1376},[129,26991,26213],{"class":273},[129,26993,362],{"class":277},[129,26995,3190],{"class":273},[129,26997,1015],{"class":277},[129,26999,26873],{"class":273},[129,27001,160],{"class":1376},[129,27003,1015],{"class":277},[129,27005,27006],{"class":284}," eq",[129,27008,147],{"class":1376},[129,27010,26213],{"class":273},[129,27012,362],{"class":277},[129,27014,26467],{"class":273},[129,27016,1015],{"class":277},[129,27018,26868],{"class":273},[129,27020,20427],{"class":1376},[129,27022,27023,27025,27028,27030,27032,27035,27037,27039],{"class":265,"line":693},[129,27024,26956],{"class":277},[129,27026,27027],{"class":284},"for",[129,27029,147],{"class":1376},[129,27031,424],{"class":277},[129,27033,27034],{"class":427},"update",[129,27036,424],{"class":277},[129,27038,4005],{"class":1376},[129,27040,27041],{"class":376},"// SELECT ... FOR UPDATE\n",[129,27043,27044],{"class":265,"line":712},[129,27045,336],{"emptyLinePlaceholder":335},[129,27047,27048,27050,27052,27054,27056,27058,27061,27063,27065,27067,27069,27071,27073,27075],{"class":265,"line":1521},[129,27049,6479],{"class":2139},[129,27051,3984],{"class":1376},[129,27053,4447],{"class":277},[129,27055,26942],{"class":273},[129,27057,23618],{"class":277},[129,27059,27060],{"class":273}," row",[129,27062,362],{"class":277},[129,27064,22095],{"class":273},[129,27066,13652],{"class":277},[129,27068,4261],{"class":277},[129,27070,25850],{"class":427},[129,27072,424],{"class":277},[129,27074,4005],{"class":1376},[129,27076,6455],{"class":277},[129,27078,27079,27081,27083,27085,27087,27089,27091,27094,27096,27098,27100,27102,27105,27107,27109],{"class":265,"line":1527},[129,27080,6913],{"class":2139},[129,27082,6916],{"class":284},[129,27084,147],{"class":1376},[129,27086,4796],{"class":277},[129,27088,6923],{"class":1376},[129,27090,1380],{"class":277},[129,27092,27093],{"class":290}," 409",[129,27095,1015],{"class":277},[129,27097,6933],{"class":1376},[129,27099,1380],{"class":277},[129,27101,4261],{"class":277},[129,27103,27104],{"class":427},"Seat no longer available",[129,27106,424],{"class":277},[129,27108,4255],{"class":277},[129,27110,294],{"class":1376},[129,27112,27113],{"class":265,"line":2295},[129,27114,6516],{"class":277},[129,27116,27117],{"class":265,"line":2300},[129,27118,336],{"emptyLinePlaceholder":335},[129,27120,27121,27123,27125,27128,27130,27132,27134],{"class":265,"line":2305},[129,27122,4739],{"class":269},[129,27124,1010],{"class":277},[129,27126,27127],{"class":273},"updated",[129,27129,14170],{"class":277},[129,27131,4745],{"class":277},[129,27133,4779],{"class":2139},[129,27135,26951],{"class":273},[129,27137,27138,27140,27142,27144,27146],{"class":265,"line":2311},[129,27139,26956],{"class":277},[129,27141,27034],{"class":284},[129,27143,147],{"class":1376},[129,27145,26213],{"class":273},[129,27147,294],{"class":1376},[129,27149,27150,27152,27155,27157],{"class":265,"line":2329},[129,27151,26956],{"class":277},[129,27153,27154],{"class":284},"set",[129,27156,147],{"class":1376},[129,27158,6455],{"class":277},[129,27160,27161,27164,27166,27168,27170,27172],{"class":265,"line":2351},[129,27162,27163],{"class":1376},"        status",[129,27165,1380],{"class":277},[129,27167,4261],{"class":277},[129,27169,25879],{"class":427},[129,27171,424],{"class":277},[129,27173,1386],{"class":277},[129,27175,27176,27179,27181,27183],{"class":265,"line":2387},[129,27177,27178],{"class":1376},"        reservedBy",[129,27180,1380],{"class":277},[129,27182,18058],{"class":273},[129,27184,1386],{"class":277},[129,27186,27187,27190,27192,27194,27196,27198,27200,27202,27204,27206,27208,27210,27212,27214,27216,27218,27220,27222],{"class":265,"line":2392},[129,27188,27189],{"class":1376},"        reservedUntil",[129,27191,1380],{"class":277},[129,27193,281],{"class":277},[129,27195,4137],{"class":284},[129,27197,147],{"class":1376},[129,27199,9433],{"class":273},[129,27201,362],{"class":277},[129,27203,3587],{"class":284},[129,27205,824],{"class":1376},[129,27207,219],{"class":277},[129,27209,10264],{"class":290},[129,27211,18886],{"class":277},[129,27213,18883],{"class":290},[129,27215,18886],{"class":277},[129,27217,9568],{"class":290},[129,27219,160],{"class":1376},[129,27221,1015],{"class":277},[129,27223,27224],{"class":376}," // 10 minutes\n",[129,27226,27227,27230],{"class":265,"line":2398},[129,27228,27229],{"class":277},"      }",[129,27231,294],{"class":1376},[129,27233,27234,27236,27238,27240,27242,27244,27246,27248,27250,27252,27254],{"class":265,"line":2441},[129,27235,26956],{"class":277},[129,27237,5436],{"class":284},[129,27239,147],{"class":1376},[129,27241,5441],{"class":284},[129,27243,147],{"class":1376},[129,27245,26213],{"class":273},[129,27247,362],{"class":277},[129,27249,3190],{"class":273},[129,27251,1015],{"class":277},[129,27253,26873],{"class":273},[129,27255,471],{"class":1376},[129,27257,27258,27260,27262],{"class":265,"line":3246},[129,27259,26956],{"class":277},[129,27261,22758],{"class":284},[129,27263,2451],{"class":1376},[129,27265,27266],{"class":265,"line":3251},[129,27267,336],{"emptyLinePlaceholder":335},[129,27269,27270,27272],{"class":265,"line":3263},[129,27271,4832],{"class":2139},[129,27273,27274],{"class":273}," updated\n",[129,27276,27277,27279],{"class":265,"line":5055},[129,27278,4182],{"class":277},[129,27280,294],{"class":1376},[129,27282,27283],{"class":265,"line":5073},[129,27284,336],{"emptyLinePlaceholder":335},[129,27286,27287,27289,27291,27293],{"class":265,"line":5106},[129,27288,4520],{"class":2139},[129,27290,1416],{"class":277},[129,27292,26902],{"class":273},[129,27294,1476],{"class":277},[129,27296,27297,27299],{"class":265,"line":5136},[129,27298,4028],{"class":277},[129,27300,294],{"class":273},[11,27302,27303,27304,27307,27308,27311],{},"This works. Under concurrent requests, the second transaction that reaches the ",[15,27305,27306],{},"SELECT ... FOR UPDATE"," line will wait until the first transaction commits. It then reads the updated row (",[15,27309,27310],{},"status: 'reserved'",") and throws the 409.",[11,27313,27314],{},"The tradeoff: rows are locked for the duration of the transaction. Under high concurrency, this creates a queue of waiting database connections.",[2456,27316,27318],{"id":27317},"optimistic-locking-atomic-update","Optimistic locking: atomic UPDATE",[11,27320,27321],{},"Don't lock at all. Instead, express the condition and the mutation as a single atomic statement:",[255,27323,27325],{"className":3922,"code":27324,"filename":26835,"language":3924,"meta":260,"style":260},"export default defineEventHandler(async (event) => {\n  const { eventId, seatId, userId } = await readBody(event)\n\n  // Single atomic statement: update the seat ONLY IF it's still available\n  // If someone else got it first, this affects 0 rows\n  const [reserved] = await db\n    .update(seats)\n    .set({\n      status: 'reserved',\n      reservedBy: userId,\n      reservedUntil: new Date(Date.now() + 10 * 60 * 1000),\n    })\n    .where(and(\n      eq(seats.id, seatId),\n      eq(seats.eventId, eventId),\n      eq(seats.status, 'available'), // atomic check - if someone else took it, 0 rows updated\n    ))\n    .returning()\n\n  if (!reserved) {\n    throw createError({ statusCode: 409, message: 'Seat no longer available' })\n  }\n\n  return { seat: reserved }\n})\n",[15,27326,27327,27349,27379,27383,27388,27393,27409,27422,27432,27447,27458,27495,27501,27513,27534,27554,27581,27586,27594,27598,27612,27644,27648,27652,27667],{"__ignoreMap":260},[129,27328,27329,27331,27333,27335,27337,27339,27341,27343,27345,27347],{"class":265,"line":266},[129,27330,4050],{"class":2139},[129,27332,4053],{"class":2139},[129,27334,4503],{"class":284},[129,27336,147],{"class":273},[129,27338,4508],{"class":269},[129,27340,3984],{"class":277},[129,27342,4100],{"class":452},[129,27344,160],{"class":277},[129,27346,456],{"class":269},[129,27348,1371],{"class":277},[129,27350,27351,27353,27355,27357,27359,27361,27363,27365,27367,27369,27371,27373,27375,27377],{"class":265,"line":297},[129,27352,5076],{"class":269},[129,27354,1416],{"class":277},[129,27356,26868],{"class":273},[129,27358,1015],{"class":277},[129,27360,26873],{"class":273},[129,27362,1015],{"class":277},[129,27364,18058],{"class":273},[129,27366,4255],{"class":277},[129,27368,4745],{"class":277},[129,27370,4779],{"class":2139},[129,27372,5264],{"class":284},[129,27374,147],{"class":1376},[129,27376,4100],{"class":273},[129,27378,294],{"class":1376},[129,27380,27381],{"class":265,"line":315},[129,27382,336],{"emptyLinePlaceholder":335},[129,27384,27385],{"class":265,"line":332},[129,27386,27387],{"class":376},"  // Single atomic statement: update the seat ONLY IF it's still available\n",[129,27389,27390],{"class":265,"line":339},[129,27391,27392],{"class":376},"  // If someone else got it first, this affects 0 rows\n",[129,27394,27395,27397,27399,27401,27403,27405,27407],{"class":265,"line":356},[129,27396,5076],{"class":269},[129,27398,1010],{"class":277},[129,27400,25879],{"class":273},[129,27402,14170],{"class":277},[129,27404,4745],{"class":277},[129,27406,4779],{"class":2139},[129,27408,5380],{"class":273},[129,27410,27411,27414,27416,27418,27420],{"class":265,"line":651},[129,27412,27413],{"class":277},"    .",[129,27415,27034],{"class":284},[129,27417,147],{"class":1376},[129,27419,26213],{"class":273},[129,27421,294],{"class":1376},[129,27423,27424,27426,27428,27430],{"class":265,"line":657},[129,27425,27413],{"class":277},[129,27427,27154],{"class":284},[129,27429,147],{"class":1376},[129,27431,6455],{"class":277},[129,27433,27434,27437,27439,27441,27443,27445],{"class":265,"line":669},[129,27435,27436],{"class":1376},"      status",[129,27438,1380],{"class":277},[129,27440,4261],{"class":277},[129,27442,25879],{"class":427},[129,27444,424],{"class":277},[129,27446,1386],{"class":277},[129,27448,27449,27452,27454,27456],{"class":265,"line":693},[129,27450,27451],{"class":1376},"      reservedBy",[129,27453,1380],{"class":277},[129,27455,18058],{"class":273},[129,27457,1386],{"class":277},[129,27459,27460,27463,27465,27467,27469,27471,27473,27475,27477,27479,27481,27483,27485,27487,27489,27491,27493],{"class":265,"line":712},[129,27461,27462],{"class":1376},"      reservedUntil",[129,27464,1380],{"class":277},[129,27466,281],{"class":277},[129,27468,4137],{"class":284},[129,27470,147],{"class":1376},[129,27472,9433],{"class":273},[129,27474,362],{"class":277},[129,27476,3587],{"class":284},[129,27478,824],{"class":1376},[129,27480,219],{"class":277},[129,27482,10264],{"class":290},[129,27484,18886],{"class":277},[129,27486,18883],{"class":290},[129,27488,18886],{"class":277},[129,27490,9568],{"class":290},[129,27492,160],{"class":1376},[129,27494,1386],{"class":277},[129,27496,27497,27499],{"class":265,"line":1521},[129,27498,7619],{"class":277},[129,27500,294],{"class":1376},[129,27502,27503,27505,27507,27509,27511],{"class":265,"line":1527},[129,27504,27413],{"class":277},[129,27506,5436],{"class":284},[129,27508,147],{"class":1376},[129,27510,26983],{"class":284},[129,27512,2241],{"class":1376},[129,27514,27515,27518,27520,27522,27524,27526,27528,27530,27532],{"class":265,"line":2295},[129,27516,27517],{"class":284},"      eq",[129,27519,147],{"class":1376},[129,27521,26213],{"class":273},[129,27523,362],{"class":277},[129,27525,3190],{"class":273},[129,27527,1015],{"class":277},[129,27529,26873],{"class":273},[129,27531,160],{"class":1376},[129,27533,1386],{"class":277},[129,27535,27536,27538,27540,27542,27544,27546,27548,27550,27552],{"class":265,"line":2300},[129,27537,27517],{"class":284},[129,27539,147],{"class":1376},[129,27541,26213],{"class":273},[129,27543,362],{"class":277},[129,27545,26467],{"class":273},[129,27547,1015],{"class":277},[129,27549,26868],{"class":273},[129,27551,160],{"class":1376},[129,27553,1386],{"class":277},[129,27555,27556,27558,27560,27562,27564,27566,27568,27570,27572,27574,27576,27578],{"class":265,"line":2305},[129,27557,27517],{"class":284},[129,27559,147],{"class":1376},[129,27561,26213],{"class":273},[129,27563,362],{"class":277},[129,27565,22095],{"class":273},[129,27567,1015],{"class":277},[129,27569,4261],{"class":277},[129,27571,25850],{"class":427},[129,27573,424],{"class":277},[129,27575,160],{"class":1376},[129,27577,1015],{"class":277},[129,27579,27580],{"class":376}," // atomic check - if someone else took it, 0 rows updated\n",[129,27582,27583],{"class":265,"line":2311},[129,27584,27585],{"class":1376},"    ))\n",[129,27587,27588,27590,27592],{"class":265,"line":2329},[129,27589,27413],{"class":277},[129,27591,22758],{"class":284},[129,27593,2451],{"class":1376},[129,27595,27596],{"class":265,"line":2351},[129,27597,336],{"emptyLinePlaceholder":335},[129,27599,27600,27602,27604,27606,27608,27610],{"class":265,"line":2387},[129,27601,3998],{"class":2139},[129,27603,3984],{"class":1376},[129,27605,4447],{"class":277},[129,27607,25879],{"class":273},[129,27609,4005],{"class":1376},[129,27611,6455],{"class":277},[129,27613,27614,27616,27618,27620,27622,27624,27626,27628,27630,27632,27634,27636,27638,27640,27642],{"class":265,"line":2392},[129,27615,20452],{"class":2139},[129,27617,6916],{"class":284},[129,27619,147],{"class":1376},[129,27621,4796],{"class":277},[129,27623,6923],{"class":1376},[129,27625,1380],{"class":277},[129,27627,27093],{"class":290},[129,27629,1015],{"class":277},[129,27631,6933],{"class":1376},[129,27633,1380],{"class":277},[129,27635,4261],{"class":277},[129,27637,27104],{"class":427},[129,27639,424],{"class":277},[129,27641,4255],{"class":277},[129,27643,294],{"class":1376},[129,27645,27646],{"class":265,"line":2398},[129,27647,1524],{"class":277},[129,27649,27650],{"class":265,"line":2441},[129,27651,336],{"emptyLinePlaceholder":335},[129,27653,27654,27656,27658,27660,27662,27665],{"class":265,"line":3246},[129,27655,4520],{"class":2139},[129,27657,1416],{"class":277},[129,27659,26902],{"class":1376},[129,27661,1380],{"class":277},[129,27663,27664],{"class":273}," reserved",[129,27666,1476],{"class":277},[129,27668,27669,27671],{"class":265,"line":3251},[129,27670,4028],{"class":277},[129,27672,294],{"class":273},[11,27674,27675,27676,27678,27679,27682,27683,27685,27686,27689],{},"This is a single SQL statement. The database evaluates the ",[15,27677,22292],{}," clause and the ",[15,27680,27681],{},"UPDATE"," atomically - there's no gap between checking and acting. If ",[15,27684,22095],{}," changed between your application reading it and this statement executing, the ",[15,27687,27688],{},"WHERE eq(seats.status, 'available')"," clause matches zero rows. You get an empty array back. No lock ever held.",[3576,27691,27692],{},[11,27693,27694,27695,27698],{},"Optimistic locking is generally preferable under low-to-medium contention. It holds no locks, creates no queues, and scales better. Use ",[15,27696,27697],{},"SELECT FOR UPDATE"," (pessimistic) when you need to read the row's current state as part of complex transaction logic before deciding what to write.",[11,27700,27701],{},"For the seat reservation case - where the only decision is \"is it available?\" - the atomic UPDATE is cleaner and faster.",[2456,27703,27705],{"id":27704},"what-about-sqlite","What about SQLite?",[11,27707,27708,27710,27711,27713],{},[15,27709,27306],{}," is not supported in SQLite. If you're using ",[15,27712,4281],{},", pessimistic locking isn't an option. The atomic UPDATE approach works because SQLite serializes writes at the database level anyway (one writer at a time in WAL mode).",[11,27715,27716],{},"That said - for a real ticket booking system with production concurrency, use PostgreSQL. SQLite's write serialization becomes a bottleneck when you have thousands of concurrent booking attempts.",[2001,27718],{},[40,27720,27722],{"id":27721},"reservation-ttl-holding-seats-without-losing-them-forever","Reservation TTL: holding seats without losing them forever",[11,27724,27725],{},"The flow is: user reserves seat → goes to payment → payment takes 2 minutes → succeeds or fails. If it fails (card declined, user closes tab), the seat must be released. Without this, a seat can be \"reserved\" forever by a user who never completed payment.",[11,27727,8603,27728,27730,27731,968,27734,27737],{},[15,27729,26410],{}," column handles this. Seats with ",[15,27732,27733],{},"status = 'reserved'",[15,27735,27736],{},"reserved_until \u003C NOW()"," are logically available - you just need to clean them up periodically.",[11,27739,27740],{},"Nitro tasks are the right tool:",[255,27742,27745],{"className":3922,"code":27743,"filename":27744,"language":3924,"meta":260,"style":260},"export default defineTask({\n  meta: {\n    name: 'seats:release-expired',\n    description: 'Release seats where reservation has expired',\n  },\n  async run() {\n    const released = await db\n      .update(seats)\n      .set({\n        status: 'available',\n        reservedBy: null,\n        reservedUntil: null,\n      })\n      .where(and(\n        eq(seats.status, 'reserved'),\n        lt(seats.reservedUntil, new Date()),\n      ))\n      .returning({ id: seats.id, seatNumber: seats.seatNumber })\n\n    return { released: released.length, seats: released }\n  },\n})\n","server/tasks/seats/release-expired.ts",[15,27746,27747,27759,27767,27782,27797,27801,27811,27824,27836,27846,27860,27868,27876,27882,27894,27919,27943,27948,27986,27990,28017,28021],{"__ignoreMap":260},[129,27748,27749,27751,27753,27755,27757],{"class":265,"line":266},[129,27750,4050],{"class":2139},[129,27752,4053],{"class":2139},[129,27754,17984],{"class":284},[129,27756,147],{"class":273},[129,27758,6455],{"class":277},[129,27760,27761,27763,27765],{"class":265,"line":297},[129,27762,17993],{"class":1376},[129,27764,1380],{"class":277},[129,27766,1371],{"class":277},[129,27768,27769,27771,27773,27775,27778,27780],{"class":265,"line":315},[129,27770,18002],{"class":1376},[129,27772,1380],{"class":277},[129,27774,4261],{"class":277},[129,27776,27777],{"class":427},"seats:release-expired",[129,27779,424],{"class":277},[129,27781,1386],{"class":277},[129,27783,27784,27786,27788,27790,27793,27795],{"class":265,"line":332},[129,27785,18018],{"class":1376},[129,27787,1380],{"class":277},[129,27789,4261],{"class":277},[129,27791,27792],{"class":427},"Release seats where reservation has expired",[129,27794,424],{"class":277},[129,27796,1386],{"class":277},[129,27798,27799],{"class":265,"line":339},[129,27800,1481],{"class":277},[129,27802,27803,27805,27807,27809],{"class":265,"line":356},[129,27804,4703],{"class":269},[129,27806,18040],{"class":1376},[129,27808,4140],{"class":277},[129,27810,1371],{"class":277},[129,27812,27813,27815,27818,27820,27822],{"class":265,"line":651},[129,27814,4739],{"class":269},[129,27816,27817],{"class":273}," released",[129,27819,4745],{"class":277},[129,27821,4779],{"class":2139},[129,27823,5380],{"class":273},[129,27825,27826,27828,27830,27832,27834],{"class":265,"line":657},[129,27827,26956],{"class":277},[129,27829,27034],{"class":284},[129,27831,147],{"class":1376},[129,27833,26213],{"class":273},[129,27835,294],{"class":1376},[129,27837,27838,27840,27842,27844],{"class":265,"line":669},[129,27839,26956],{"class":277},[129,27841,27154],{"class":284},[129,27843,147],{"class":1376},[129,27845,6455],{"class":277},[129,27847,27848,27850,27852,27854,27856,27858],{"class":265,"line":693},[129,27849,27163],{"class":1376},[129,27851,1380],{"class":277},[129,27853,4261],{"class":277},[129,27855,25850],{"class":427},[129,27857,424],{"class":277},[129,27859,1386],{"class":277},[129,27861,27862,27864,27866],{"class":265,"line":712},[129,27863,27178],{"class":1376},[129,27865,1380],{"class":277},[129,27867,1509],{"class":277},[129,27869,27870,27872,27874],{"class":265,"line":1521},[129,27871,27189],{"class":1376},[129,27873,1380],{"class":277},[129,27875,1509],{"class":277},[129,27877,27878,27880],{"class":265,"line":1527},[129,27879,27229],{"class":277},[129,27881,294],{"class":1376},[129,27883,27884,27886,27888,27890,27892],{"class":265,"line":2295},[129,27885,26956],{"class":277},[129,27887,5436],{"class":284},[129,27889,147],{"class":1376},[129,27891,26983],{"class":284},[129,27893,2241],{"class":1376},[129,27895,27896,27899,27901,27903,27905,27907,27909,27911,27913,27915,27917],{"class":265,"line":2300},[129,27897,27898],{"class":284},"        eq",[129,27900,147],{"class":1376},[129,27902,26213],{"class":273},[129,27904,362],{"class":277},[129,27906,22095],{"class":273},[129,27908,1015],{"class":277},[129,27910,4261],{"class":277},[129,27912,25879],{"class":427},[129,27914,424],{"class":277},[129,27916,160],{"class":1376},[129,27918,1386],{"class":277},[129,27920,27921,27924,27926,27928,27930,27933,27935,27937,27939,27941],{"class":265,"line":2305},[129,27922,27923],{"class":284},"        lt",[129,27925,147],{"class":1376},[129,27927,26213],{"class":273},[129,27929,362],{"class":277},[129,27931,27932],{"class":273},"reservedUntil",[129,27934,1015],{"class":277},[129,27936,281],{"class":277},[129,27938,4137],{"class":284},[129,27940,15415],{"class":1376},[129,27942,1386],{"class":277},[129,27944,27945],{"class":265,"line":2311},[129,27946,27947],{"class":1376},"      ))\n",[129,27949,27950,27952,27954,27956,27958,27960,27962,27964,27966,27968,27970,27973,27975,27977,27979,27982,27984],{"class":265,"line":2329},[129,27951,26956],{"class":277},[129,27953,22758],{"class":284},[129,27955,147],{"class":1376},[129,27957,4796],{"class":277},[129,27959,4643],{"class":1376},[129,27961,1380],{"class":277},[129,27963,25784],{"class":273},[129,27965,362],{"class":277},[129,27967,3190],{"class":273},[129,27969,1015],{"class":277},[129,27971,27972],{"class":1376}," seatNumber",[129,27974,1380],{"class":277},[129,27976,25784],{"class":273},[129,27978,362],{"class":277},[129,27980,27981],{"class":273},"seatNumber",[129,27983,4255],{"class":277},[129,27985,294],{"class":1376},[129,27987,27988],{"class":265,"line":2351},[129,27989,336],{"emptyLinePlaceholder":335},[129,27991,27992,27994,27996,27998,28000,28002,28004,28007,28009,28011,28013,28015],{"class":265,"line":2387},[129,27993,4832],{"class":2139},[129,27995,1416],{"class":277},[129,27997,27817],{"class":1376},[129,27999,1380],{"class":277},[129,28001,27817],{"class":273},[129,28003,362],{"class":277},[129,28005,28006],{"class":273},"length",[129,28008,1015],{"class":277},[129,28010,25784],{"class":1376},[129,28012,1380],{"class":277},[129,28014,27817],{"class":273},[129,28016,1476],{"class":277},[129,28018,28019],{"class":265,"line":2392},[129,28020,1481],{"class":277},[129,28022,28023,28025],{"class":265,"line":2398},[129,28024,4028],{"class":277},[129,28026,294],{"class":273},[11,28028,28029],{},"Schedule it in Nitro config:",[255,28031,28034],{"className":3922,"code":28032,"filename":17649,"highlights":28033,"language":3924,"meta":260,"style":260},"export default defineNuxtConfig({\n  nitro: {\n    experimental: { tasks: true },\n    scheduledTasks: {\n      // Run every minute\n      '* * * * *': ['seats:release-expired'],\n    },\n  },\n})\n",[339,356,651],[15,28035,28036,28048,28056,28074,28083,28090,28115,28120,28124],{"__ignoreMap":260},[129,28037,28038,28040,28042,28044,28046],{"class":265,"line":266},[129,28039,4050],{"class":2139},[129,28041,4053],{"class":2139},[129,28043,19019],{"class":284},[129,28045,147],{"class":273},[129,28047,6455],{"class":277},[129,28049,28050,28052,28054],{"class":265,"line":297},[129,28051,4074],{"class":1376},[129,28053,1380],{"class":277},[129,28055,1371],{"class":277},[129,28057,28058,28061,28063,28065,28068,28070,28072],{"class":265,"line":315},[129,28059,28060],{"class":1376},"    experimental",[129,28062,1380],{"class":277},[129,28064,1416],{"class":277},[129,28066,28067],{"class":1376}," tasks",[129,28069,1380],{"class":277},[129,28071,4823],{"class":4822},[129,28073,1444],{"class":277},[129,28075,28076,28079,28081],{"class":265,"line":332},[129,28077,28078],{"class":1376},"    scheduledTasks",[129,28080,1380],{"class":277},[129,28082,1371],{"class":277},[129,28084,28087],{"class":28085,"line":339},[265,28086],"highlight",[129,28088,28089],{"class":376},"      // Run every minute\n",[129,28091,28093,28096,28099,28101,28103,28105,28107,28109,28111,28113],{"class":28092,"line":356},[265,28086],[129,28094,28095],{"class":277},"      '",[129,28097,28098],{"class":1376},"* * * * *",[129,28100,424],{"class":277},[129,28102,1380],{"class":277},[129,28104,1010],{"class":273},[129,28106,424],{"class":277},[129,28108,27777],{"class":427},[129,28110,424],{"class":277},[129,28112,14170],{"class":273},[129,28114,1386],{"class":277},[129,28116,28118],{"class":28117,"line":651},[265,28086],[129,28119,19095],{"class":277},[129,28121,28122],{"class":265,"line":657},[129,28123,1481],{"class":277},[129,28125,28126,28128],{"class":265,"line":669},[129,28127,4028],{"class":277},[129,28129,294],{"class":273},[11,28131,28132],{},"Also run it inline during availability checks - so a user viewing the seat map doesn't see stale \"reserved\" seats that have already expired:",[255,28134,28137],{"className":3922,"code":28135,"filename":28136,"language":3924,"meta":260,"style":260},"export default defineEventHandler(async (event) => {\n  // Release any expired reservations before returning availability\n  // This is cheap - the UPDATE WHERE clause is fast with an index on status + reserved_until\n  await db\n    .update(seats)\n    .set({ status: 'available', reservedBy: null, reservedUntil: null })\n    .where(and(eq(seats.status, 'reserved'), lt(seats.reservedUntil, new Date())))\n\n  return db.select({\n    id: seats.id,\n    seatNumber: seats.seatNumber,\n    status: seats.status,\n  }).from(seats).where(eq(seats.eventId, event.context.params!.id))\n})\n","server/api/events/[id]/seats.get.ts",[15,28138,28139,28161,28166,28171,28177,28189,28229,28283,28287,28301,28316,28331,28345,28395],{"__ignoreMap":260},[129,28140,28141,28143,28145,28147,28149,28151,28153,28155,28157,28159],{"class":265,"line":266},[129,28142,4050],{"class":2139},[129,28144,4053],{"class":2139},[129,28146,4503],{"class":284},[129,28148,147],{"class":273},[129,28150,4508],{"class":269},[129,28152,3984],{"class":277},[129,28154,4100],{"class":452},[129,28156,160],{"class":277},[129,28158,456],{"class":269},[129,28160,1371],{"class":277},[129,28162,28163],{"class":265,"line":297},[129,28164,28165],{"class":376},"  // Release any expired reservations before returning availability\n",[129,28167,28168],{"class":265,"line":315},[129,28169,28170],{"class":376},"  // This is cheap - the UPDATE WHERE clause is fast with an index on status + reserved_until\n",[129,28172,28173,28175],{"class":265,"line":332},[129,28174,8101],{"class":2139},[129,28176,5380],{"class":273},[129,28178,28179,28181,28183,28185,28187],{"class":265,"line":339},[129,28180,27413],{"class":277},[129,28182,27034],{"class":284},[129,28184,147],{"class":1376},[129,28186,26213],{"class":273},[129,28188,294],{"class":1376},[129,28190,28191,28193,28195,28197,28199,28201,28203,28205,28207,28209,28211,28214,28216,28218,28221,28223,28225,28227],{"class":265,"line":356},[129,28192,27413],{"class":277},[129,28194,27154],{"class":284},[129,28196,147],{"class":1376},[129,28198,4796],{"class":277},[129,28200,22309],{"class":1376},[129,28202,1380],{"class":277},[129,28204,4261],{"class":277},[129,28206,25850],{"class":427},[129,28208,424],{"class":277},[129,28210,1015],{"class":277},[129,28212,28213],{"class":1376}," reservedBy",[129,28215,1380],{"class":277},[129,28217,1433],{"class":277},[129,28219,28220],{"class":1376}," reservedUntil",[129,28222,1380],{"class":277},[129,28224,1441],{"class":277},[129,28226,4255],{"class":277},[129,28228,294],{"class":1376},[129,28230,28231,28233,28235,28237,28239,28241,28243,28245,28247,28249,28251,28253,28255,28257,28259,28261,28263,28266,28268,28270,28272,28274,28276,28278,28280],{"class":265,"line":651},[129,28232,27413],{"class":277},[129,28234,5436],{"class":284},[129,28236,147],{"class":1376},[129,28238,26983],{"class":284},[129,28240,147],{"class":1376},[129,28242,5441],{"class":284},[129,28244,147],{"class":1376},[129,28246,26213],{"class":273},[129,28248,362],{"class":277},[129,28250,22095],{"class":273},[129,28252,1015],{"class":277},[129,28254,4261],{"class":277},[129,28256,25879],{"class":427},[129,28258,424],{"class":277},[129,28260,160],{"class":1376},[129,28262,1015],{"class":277},[129,28264,28265],{"class":284}," lt",[129,28267,147],{"class":1376},[129,28269,26213],{"class":273},[129,28271,362],{"class":277},[129,28273,27932],{"class":273},[129,28275,1015],{"class":277},[129,28277,281],{"class":277},[129,28279,4137],{"class":284},[129,28281,28282],{"class":1376},"())))\n",[129,28284,28285],{"class":265,"line":657},[129,28286,336],{"emptyLinePlaceholder":335},[129,28288,28289,28291,28293,28295,28297,28299],{"class":265,"line":669},[129,28290,4520],{"class":2139},[129,28292,4479],{"class":273},[129,28294,362],{"class":277},[129,28296,2357],{"class":284},[129,28298,147],{"class":1376},[129,28300,6455],{"class":277},[129,28302,28303,28306,28308,28310,28312,28314],{"class":265,"line":693},[129,28304,28305],{"class":1376},"    id",[129,28307,1380],{"class":277},[129,28309,25784],{"class":273},[129,28311,362],{"class":277},[129,28313,3190],{"class":273},[129,28315,1386],{"class":277},[129,28317,28318,28321,28323,28325,28327,28329],{"class":265,"line":712},[129,28319,28320],{"class":1376},"    seatNumber",[129,28322,1380],{"class":277},[129,28324,25784],{"class":273},[129,28326,362],{"class":277},[129,28328,27981],{"class":273},[129,28330,1386],{"class":277},[129,28332,28333,28335,28337,28339,28341,28343],{"class":265,"line":1521},[129,28334,24130],{"class":1376},[129,28336,1380],{"class":277},[129,28338,25784],{"class":273},[129,28340,362],{"class":277},[129,28342,22095],{"class":273},[129,28344,1386],{"class":277},[129,28346,28347,28349,28351,28353,28355,28357,28359,28361,28363,28365,28367,28369,28371,28373,28375,28377,28379,28381,28383,28385,28387,28389,28391,28393],{"class":265,"line":1527},[129,28348,4182],{"class":277},[129,28350,160],{"class":1376},[129,28352,362],{"class":277},[129,28354,2589],{"class":284},[129,28356,147],{"class":1376},[129,28358,26213],{"class":273},[129,28360,160],{"class":1376},[129,28362,362],{"class":277},[129,28364,5436],{"class":284},[129,28366,147],{"class":1376},[129,28368,5441],{"class":284},[129,28370,147],{"class":1376},[129,28372,26213],{"class":273},[129,28374,362],{"class":277},[129,28376,26467],{"class":273},[129,28378,1015],{"class":277},[129,28380,16694],{"class":273},[129,28382,362],{"class":277},[129,28384,6497],{"class":273},[129,28386,362],{"class":277},[129,28388,25117],{"class":273},[129,28390,25120],{"class":277},[129,28392,3190],{"class":273},[129,28394,471],{"class":1376},[129,28396,28397,28399],{"class":265,"line":2295},[129,28398,4028],{"class":277},[129,28400,294],{"class":273},[2456,28402,28404],{"id":28403},"the-ttl-is-also-a-weapon","The TTL is also a weapon",[11,28406,8603,28407,28409,28410,362],{},[15,28408,26410],{}," window is exploitable - and on some airlines, it ",[118,28411,28412],{},"still works",[11,28414,28415,28416,28419,28420,28422],{},"The trick: you buy a ticket with online check-in included but no assigned seat. A few hours before the flight, you open a second account and start selecting every seat you ",[24,28417,28418],{},"don't"," want - middle seats, last row, seats next to the toilet. You don't complete payment on any of them. Each selection attempt triggers a reservation, so those seats appear as ",[15,28421,27310],{}," in the database for a few minutes while the system waits for payment confirmation.",[11,28424,28425],{},"With the undesirable seats temporarily locked out, you open check-in on your real account. The only \"available\" seats happen to be the window and aisle spots you wanted. Free upgrade, courtesy of the TTL.",[11,28427,28428,28429,28434],{},"This has been ",[51,28430,28433],{"href":28431,"rel":28432},"https://thepointsguy.com/guide/best-seat-on-the-plane/",[55],"documented in travel communities"," for years. Some airlines have patched it. Others haven't.",[11,28436,28437],{},"The fix from a systems perspective is straightforward: rate-limit reservation attempts per account per event. If one user initiates more than N reservations in a short window without confirming any of them, reject further attempts or flag the account.",[255,28439,28442],{"className":3922,"code":28440,"filename":28441,"language":3924,"meta":260,"style":260},"export default defineEventHandler(async (event) => {\n  if (event.method !== 'POST' || !event.path.startsWith('/api/bookings/reserve')) return\n\n  const userId = event.context.user?.id\n  if (!userId) return\n\n  const storage = useStorage('cache')\n  const key = `reserve-attempts:${userId}:${Math.floor(Date.now() / 300_000)}` // 5-minute window\n  const attempts = ((await storage.getItem\u003Cnumber>(key)) ?? 0) + 1\n  await storage.setItem(key, attempts, { ttl: 300 })\n\n  if (attempts > 5) {\n    // More than 5 unconfirmed reservation attempts in 5 minutes - likely abuse\n    throw createError({ statusCode: 429, message: 'Too many reservation attempts' })\n  }\n})\n","server/middleware/reservation-abuse.ts",[15,28443,28444,28466,28514,28518,28541,28555,28559,28579,28628,28668,28700,28704,28721,28726,28759,28763],{"__ignoreMap":260},[129,28445,28446,28448,28450,28452,28454,28456,28458,28460,28462,28464],{"class":265,"line":266},[129,28447,4050],{"class":2139},[129,28449,4053],{"class":2139},[129,28451,4503],{"class":284},[129,28453,147],{"class":273},[129,28455,4508],{"class":269},[129,28457,3984],{"class":277},[129,28459,4100],{"class":452},[129,28461,160],{"class":277},[129,28463,456],{"class":269},[129,28465,1371],{"class":277},[129,28467,28468,28470,28472,28474,28476,28478,28480,28482,28484,28486,28488,28491,28493,28495,28497,28499,28501,28503,28505,28508,28510,28512],{"class":265,"line":297},[129,28469,3998],{"class":2139},[129,28471,3984],{"class":1376},[129,28473,4100],{"class":273},[129,28475,362],{"class":277},[129,28477,4160],{"class":273},[129,28479,13652],{"class":277},[129,28481,4261],{"class":277},[129,28483,3142],{"class":427},[129,28485,424],{"class":277},[129,28487,23618],{"class":277},[129,28489,28490],{"class":277}," !",[129,28492,4100],{"class":273},[129,28494,362],{"class":277},[129,28496,4172],{"class":273},[129,28498,362],{"class":277},[129,28500,20130],{"class":284},[129,28502,147],{"class":1376},[129,28504,424],{"class":277},[129,28506,28507],{"class":427},"/api/bookings/reserve",[129,28509,424],{"class":277},[129,28511,13145],{"class":1376},[129,28513,20144],{"class":2139},[129,28515,28516],{"class":265,"line":315},[129,28517,336],{"emptyLinePlaceholder":335},[129,28519,28520,28522,28524,28526,28528,28530,28532,28534,28536,28538],{"class":265,"line":332},[129,28521,5076],{"class":269},[129,28523,18058],{"class":273},[129,28525,4745],{"class":277},[129,28527,16694],{"class":273},[129,28529,362],{"class":277},[129,28531,6497],{"class":273},[129,28533,362],{"class":277},[129,28535,6335],{"class":273},[129,28537,6059],{"class":277},[129,28539,28540],{"class":273},"id\n",[129,28542,28543,28545,28547,28549,28551,28553],{"class":265,"line":339},[129,28544,3998],{"class":2139},[129,28546,3984],{"class":1376},[129,28548,4447],{"class":277},[129,28550,18099],{"class":273},[129,28552,4005],{"class":1376},[129,28554,20144],{"class":2139},[129,28556,28557],{"class":265,"line":356},[129,28558,336],{"emptyLinePlaceholder":335},[129,28560,28561,28563,28565,28567,28569,28571,28573,28575,28577],{"class":265,"line":651},[129,28562,5076],{"class":269},[129,28564,20251],{"class":273},[129,28566,4745],{"class":277},[129,28568,20256],{"class":284},[129,28570,147],{"class":1376},[129,28572,424],{"class":277},[129,28574,20263],{"class":427},[129,28576,424],{"class":277},[129,28578,294],{"class":1376},[129,28580,28581,28583,28585,28587,28589,28592,28594,28596,28598,28600,28602,28604,28606,28608,28610,28612,28614,28616,28618,28621,28623,28625],{"class":265,"line":657},[129,28582,5076],{"class":269},[129,28584,6243],{"class":273},[129,28586,4745],{"class":277},[129,28588,5569],{"class":277},[129,28590,28591],{"class":427},"reserve-attempts:",[129,28593,4131],{"class":277},[129,28595,18099],{"class":273},[129,28597,4028],{"class":277},[129,28599,1380],{"class":427},[129,28601,4131],{"class":277},[129,28603,10876],{"class":273},[129,28605,362],{"class":277},[129,28607,20219],{"class":284},[129,28609,20222],{"class":273},[129,28611,362],{"class":277},[129,28613,3587],{"class":284},[129,28615,824],{"class":273},[129,28617,938],{"class":277},[129,28619,28620],{"class":290}," 300_000",[129,28622,160],{"class":273},[129,28624,4175],{"class":277},[129,28626,28627],{"class":376}," // 5-minute window\n",[129,28629,28630,28632,28634,28636,28638,28640,28642,28644,28646,28648,28650,28652,28654,28656,28658,28660,28662,28664,28666],{"class":265,"line":669},[129,28631,5076],{"class":269},[129,28633,19584],{"class":273},[129,28635,4745],{"class":277},[129,28637,20278],{"class":1376},[129,28639,8083],{"class":2139},[129,28641,20251],{"class":273},[129,28643,362],{"class":277},[129,28645,20287],{"class":284},[129,28647,3945],{"class":277},[129,28649,20292],{"class":2161},[129,28651,3956],{"class":277},[129,28653,147],{"class":1376},[129,28655,6273],{"class":273},[129,28657,13145],{"class":1376},[129,28659,18967],{"class":277},[129,28661,5698],{"class":290},[129,28663,4005],{"class":1376},[129,28665,219],{"class":277},[129,28667,20311],{"class":290},[129,28669,28670,28672,28674,28676,28678,28680,28682,28684,28686,28688,28690,28692,28694,28696,28698],{"class":265,"line":693},[129,28671,8101],{"class":2139},[129,28673,20251],{"class":273},[129,28675,362],{"class":277},[129,28677,20326],{"class":284},[129,28679,147],{"class":1376},[129,28681,6273],{"class":273},[129,28683,1015],{"class":277},[129,28685,19584],{"class":273},[129,28687,1015],{"class":277},[129,28689,1416],{"class":277},[129,28691,20341],{"class":1376},[129,28693,1380],{"class":277},[129,28695,6635],{"class":290},[129,28697,4255],{"class":277},[129,28699,294],{"class":1376},[129,28701,28702],{"class":265,"line":712},[129,28703,336],{"emptyLinePlaceholder":335},[129,28705,28706,28708,28710,28713,28715,28717,28719],{"class":265,"line":1521},[129,28707,3998],{"class":2139},[129,28709,3984],{"class":1376},[129,28711,28712],{"class":273},"attempts",[129,28714,2345],{"class":277},[129,28716,1043],{"class":290},[129,28718,4005],{"class":1376},[129,28720,6455],{"class":277},[129,28722,28723],{"class":265,"line":1527},[129,28724,28725],{"class":376},"    // More than 5 unconfirmed reservation attempts in 5 minutes - likely abuse\n",[129,28727,28728,28730,28732,28734,28736,28738,28740,28742,28744,28746,28748,28750,28753,28755,28757],{"class":265,"line":2295},[129,28729,20452],{"class":2139},[129,28731,6916],{"class":284},[129,28733,147],{"class":1376},[129,28735,4796],{"class":277},[129,28737,6923],{"class":1376},[129,28739,1380],{"class":277},[129,28741,20465],{"class":290},[129,28743,1015],{"class":277},[129,28745,6933],{"class":1376},[129,28747,1380],{"class":277},[129,28749,4261],{"class":277},[129,28751,28752],{"class":427},"Too many reservation attempts",[129,28754,424],{"class":277},[129,28756,4255],{"class":277},[129,28758,294],{"class":1376},[129,28760,28761],{"class":265,"line":2300},[129,28762,1524],{"class":277},[129,28764,28765,28767],{"class":265,"line":2305},[129,28766,4028],{"class":277},[129,28768,294],{"class":273},[11,28770,28771],{},"The TTL protects honest users from themselves (abandoned carts, failed payments). Rate limiting on reservation attempts protects you from users who've read this article.",[2001,28773],{},[40,28775,28777],{"id":28776},"idempotency-keys-the-payment-double-charge-problem","Idempotency keys: the payment double-charge problem",[11,28779,28780],{},"The booking flow:",[255,28782,28785],{"className":28783,"code":28784,"language":3237},[12199],"1. User clicks \"Pay\"\n2. Your server calls Stripe\n3. Stripe charges the card ✓\n4. Your server writes the booking to the database\n5. Network timeout - client never gets the response\n6. User clicks \"Pay\" again\n7. Your server calls Stripe again → double charge\n",[15,28786,28784],{"__ignoreMap":260},[11,28788,28789,28790,28793],{},"This happens more than you'd think. Slow connections, impatient users, automatic retries. ",[118,28791,28792],{},"Idempotency keys"," solve it: the client generates a unique key per booking attempt. The server caches its response for that key. If the same request arrives twice, it returns the cached response without reprocessing.",[255,28795,28798],{"className":3922,"code":28796,"filename":28797,"language":3924,"meta":260,"style":260},"export default defineEventHandler(async (event) => {\n  const idempotencyKey = getHeader(event, 'Idempotency-Key')\n\n  if (!idempotencyKey) {\n    throw createError({ statusCode: 400, message: 'Idempotency-Key header required' })\n  }\n\n  const storage = useStorage('cache')\n\n  // Already processed this request - return cached result\n  const cached = await storage.getItem\u003Cobject>(`idempotency:${idempotencyKey}`)\n  if (cached) return cached\n\n  const { seatId, paymentToken } = await readBody(event)\n  const userId = event.context.user!.id\n\n  // Run in transaction: verify reservation still valid + mark as sold + create booking\n  const booking = await db.transaction(async (tx) => {\n    const [seat] = await tx\n      .select()\n      .from(seats)\n      .where(and(\n        eq(seats.id, seatId),\n        eq(seats.reservedBy, userId),\n        eq(seats.status, 'reserved'),\n        gt(seats.reservedUntil, new Date()), // reservation hasn't expired\n      ))\n      .for('update') // lock here - we're doing multi-step logic\n\n    if (!seat) {\n      throw createError({ statusCode: 409, message: 'Reservation expired or invalid' })\n    }\n\n    // Charge the card\n    const charge = await stripe.paymentIntents.create({\n      amount: seat.price,\n      currency: 'eur',\n      payment_method: paymentToken,\n      confirm: true,\n      idempotency_key: idempotencyKey, // pass through to Stripe too\n    })\n\n    // Mark seat as sold\n    await tx.update(seats)\n      .set({ status: 'sold' })\n      .where(eq(seats.id, seatId))\n\n    // Create the booking record\n    const [created] = await tx.insert(bookings).values({\n      eventId: seat.eventId,\n      seatId: seat.id,\n      userId,\n      idempotencyKey,\n      status: 'confirmed',\n    }).returning()\n\n    return created\n  })\n\n  // Cache the response - 24h is enough, booking flows don't retry after days\n  await storage.setItem(`idempotency:${idempotencyKey}`, booking, { ttl: 86_400 })\n\n  return booking\n})\n","server/api/bookings/confirm.post.ts",[15,28799,28800,28822,28848,28852,28867,28901,28905,28909,28929,28933,28938,28976,28992,28996,29023,29045,29049,29054,29085,29102,29110,29122,29134,29154,29175,29199,29225,29229,29248,29252,29266,29299,29303,29307,29312,29336,29352,29367,29378,29389,29403,29410,29415,29421,29439,29464,29489,29494,29500,29536,29551,29567,29575,29583,29598,29611,29616,29624,29631,29636,29642,29684,29689,29697],{"__ignoreMap":260},[129,28801,28802,28804,28806,28808,28810,28812,28814,28816,28818,28820],{"class":265,"line":266},[129,28803,4050],{"class":2139},[129,28805,4053],{"class":2139},[129,28807,4503],{"class":284},[129,28809,147],{"class":273},[129,28811,4508],{"class":269},[129,28813,3984],{"class":277},[129,28815,4100],{"class":452},[129,28817,160],{"class":277},[129,28819,456],{"class":269},[129,28821,1371],{"class":277},[129,28823,28824,28826,28829,28831,28833,28835,28837,28839,28841,28844,28846],{"class":265,"line":297},[129,28825,5076],{"class":269},[129,28827,28828],{"class":273}," idempotencyKey",[129,28830,4745],{"class":277},[129,28832,6041],{"class":284},[129,28834,147],{"class":1376},[129,28836,4100],{"class":273},[129,28838,1015],{"class":277},[129,28840,4261],{"class":277},[129,28842,28843],{"class":427},"Idempotency-Key",[129,28845,424],{"class":277},[129,28847,294],{"class":1376},[129,28849,28850],{"class":265,"line":315},[129,28851,336],{"emptyLinePlaceholder":335},[129,28853,28854,28856,28858,28860,28863,28865],{"class":265,"line":332},[129,28855,3998],{"class":2139},[129,28857,3984],{"class":1376},[129,28859,4447],{"class":277},[129,28861,28862],{"class":273},"idempotencyKey",[129,28864,4005],{"class":1376},[129,28866,6455],{"class":277},[129,28868,28869,28871,28873,28875,28877,28879,28881,28884,28886,28888,28890,28892,28895,28897,28899],{"class":265,"line":339},[129,28870,20452],{"class":2139},[129,28872,6916],{"class":284},[129,28874,147],{"class":1376},[129,28876,4796],{"class":277},[129,28878,6923],{"class":1376},[129,28880,1380],{"class":277},[129,28882,28883],{"class":290}," 400",[129,28885,1015],{"class":277},[129,28887,6933],{"class":1376},[129,28889,1380],{"class":277},[129,28891,4261],{"class":277},[129,28893,28894],{"class":427},"Idempotency-Key header required",[129,28896,424],{"class":277},[129,28898,4255],{"class":277},[129,28900,294],{"class":1376},[129,28902,28903],{"class":265,"line":356},[129,28904,1524],{"class":277},[129,28906,28907],{"class":265,"line":651},[129,28908,336],{"emptyLinePlaceholder":335},[129,28910,28911,28913,28915,28917,28919,28921,28923,28925,28927],{"class":265,"line":657},[129,28912,5076],{"class":269},[129,28914,20251],{"class":273},[129,28916,4745],{"class":277},[129,28918,20256],{"class":284},[129,28920,147],{"class":1376},[129,28922,424],{"class":277},[129,28924,20263],{"class":427},[129,28926,424],{"class":277},[129,28928,294],{"class":1376},[129,28930,28931],{"class":265,"line":669},[129,28932,336],{"emptyLinePlaceholder":335},[129,28934,28935],{"class":265,"line":693},[129,28936,28937],{"class":376},"  // Already processed this request - return cached result\n",[129,28939,28940,28942,28945,28947,28949,28951,28953,28955,28957,28959,28961,28963,28965,28968,28970,28972,28974],{"class":265,"line":712},[129,28941,5076],{"class":269},[129,28943,28944],{"class":273}," cached",[129,28946,4745],{"class":277},[129,28948,4779],{"class":2139},[129,28950,20251],{"class":273},[129,28952,362],{"class":277},[129,28954,20287],{"class":284},[129,28956,3945],{"class":277},[129,28958,13171],{"class":2161},[129,28960,3956],{"class":277},[129,28962,147],{"class":1376},[129,28964,4125],{"class":277},[129,28966,28967],{"class":427},"idempotency:",[129,28969,4131],{"class":277},[129,28971,28862],{"class":273},[129,28973,4175],{"class":277},[129,28975,294],{"class":1376},[129,28977,28978,28980,28982,28985,28987,28989],{"class":265,"line":1521},[129,28979,3998],{"class":2139},[129,28981,3984],{"class":1376},[129,28983,28984],{"class":273},"cached",[129,28986,4005],{"class":1376},[129,28988,5127],{"class":2139},[129,28990,28991],{"class":273}," cached\n",[129,28993,28994],{"class":265,"line":1527},[129,28995,336],{"emptyLinePlaceholder":335},[129,28997,28998,29000,29002,29004,29006,29009,29011,29013,29015,29017,29019,29021],{"class":265,"line":2295},[129,28999,5076],{"class":269},[129,29001,1416],{"class":277},[129,29003,26873],{"class":273},[129,29005,1015],{"class":277},[129,29007,29008],{"class":273}," paymentToken",[129,29010,4255],{"class":277},[129,29012,4745],{"class":277},[129,29014,4779],{"class":2139},[129,29016,5264],{"class":284},[129,29018,147],{"class":1376},[129,29020,4100],{"class":273},[129,29022,294],{"class":1376},[129,29024,29025,29027,29029,29031,29033,29035,29037,29039,29041,29043],{"class":265,"line":2300},[129,29026,5076],{"class":269},[129,29028,18058],{"class":273},[129,29030,4745],{"class":277},[129,29032,16694],{"class":273},[129,29034,362],{"class":277},[129,29036,6497],{"class":273},[129,29038,362],{"class":277},[129,29040,6335],{"class":273},[129,29042,25120],{"class":277},[129,29044,28540],{"class":273},[129,29046,29047],{"class":265,"line":2305},[129,29048,336],{"emptyLinePlaceholder":335},[129,29050,29051],{"class":265,"line":2311},[129,29052,29053],{"class":376},"  // Run in transaction: verify reservation still valid + mark as sold + create booking\n",[129,29055,29056,29058,29061,29063,29065,29067,29069,29071,29073,29075,29077,29079,29081,29083],{"class":265,"line":2329},[129,29057,5076],{"class":269},[129,29059,29060],{"class":273}," booking",[129,29062,4745],{"class":277},[129,29064,4779],{"class":2139},[129,29066,4479],{"class":273},[129,29068,362],{"class":277},[129,29070,26913],{"class":284},[129,29072,147],{"class":1376},[129,29074,4508],{"class":269},[129,29076,3984],{"class":277},[129,29078,26922],{"class":452},[129,29080,160],{"class":277},[129,29082,456],{"class":269},[129,29084,1371],{"class":277},[129,29086,29087,29089,29091,29094,29096,29098,29100],{"class":265,"line":2351},[129,29088,4739],{"class":269},[129,29090,1010],{"class":277},[129,29092,29093],{"class":273},"seat",[129,29095,14170],{"class":277},[129,29097,4745],{"class":277},[129,29099,4779],{"class":2139},[129,29101,26951],{"class":273},[129,29103,29104,29106,29108],{"class":265,"line":2387},[129,29105,26956],{"class":277},[129,29107,2357],{"class":284},[129,29109,2451],{"class":1376},[129,29111,29112,29114,29116,29118,29120],{"class":265,"line":2392},[129,29113,26956],{"class":277},[129,29115,2589],{"class":284},[129,29117,147],{"class":1376},[129,29119,26213],{"class":273},[129,29121,294],{"class":1376},[129,29123,29124,29126,29128,29130,29132],{"class":265,"line":2398},[129,29125,26956],{"class":277},[129,29127,5436],{"class":284},[129,29129,147],{"class":1376},[129,29131,26983],{"class":284},[129,29133,2241],{"class":1376},[129,29135,29136,29138,29140,29142,29144,29146,29148,29150,29152],{"class":265,"line":2441},[129,29137,27898],{"class":284},[129,29139,147],{"class":1376},[129,29141,26213],{"class":273},[129,29143,362],{"class":277},[129,29145,3190],{"class":273},[129,29147,1015],{"class":277},[129,29149,26873],{"class":273},[129,29151,160],{"class":1376},[129,29153,1386],{"class":277},[129,29155,29156,29158,29160,29162,29164,29167,29169,29171,29173],{"class":265,"line":3246},[129,29157,27898],{"class":284},[129,29159,147],{"class":1376},[129,29161,26213],{"class":273},[129,29163,362],{"class":277},[129,29165,29166],{"class":273},"reservedBy",[129,29168,1015],{"class":277},[129,29170,18058],{"class":273},[129,29172,160],{"class":1376},[129,29174,1386],{"class":277},[129,29176,29177,29179,29181,29183,29185,29187,29189,29191,29193,29195,29197],{"class":265,"line":3251},[129,29178,27898],{"class":284},[129,29180,147],{"class":1376},[129,29182,26213],{"class":273},[129,29184,362],{"class":277},[129,29186,22095],{"class":273},[129,29188,1015],{"class":277},[129,29190,4261],{"class":277},[129,29192,25879],{"class":427},[129,29194,424],{"class":277},[129,29196,160],{"class":1376},[129,29198,1386],{"class":277},[129,29200,29201,29204,29206,29208,29210,29212,29214,29216,29218,29220,29222],{"class":265,"line":3263},[129,29202,29203],{"class":284},"        gt",[129,29205,147],{"class":1376},[129,29207,26213],{"class":273},[129,29209,362],{"class":277},[129,29211,27932],{"class":273},[129,29213,1015],{"class":277},[129,29215,281],{"class":277},[129,29217,4137],{"class":284},[129,29219,15415],{"class":1376},[129,29221,1015],{"class":277},[129,29223,29224],{"class":376}," // reservation hasn't expired\n",[129,29226,29227],{"class":265,"line":5055},[129,29228,27947],{"class":1376},[129,29230,29231,29233,29235,29237,29239,29241,29243,29245],{"class":265,"line":5073},[129,29232,26956],{"class":277},[129,29234,27027],{"class":284},[129,29236,147],{"class":1376},[129,29238,424],{"class":277},[129,29240,27034],{"class":427},[129,29242,424],{"class":277},[129,29244,4005],{"class":1376},[129,29246,29247],{"class":376},"// lock here - we're doing multi-step logic\n",[129,29249,29250],{"class":265,"line":5106},[129,29251,336],{"emptyLinePlaceholder":335},[129,29253,29254,29256,29258,29260,29262,29264],{"class":265,"line":5136},[129,29255,6479],{"class":2139},[129,29257,3984],{"class":1376},[129,29259,4447],{"class":277},[129,29261,29093],{"class":273},[129,29263,4005],{"class":1376},[129,29265,6455],{"class":277},[129,29267,29268,29270,29272,29274,29276,29278,29280,29282,29284,29286,29288,29290,29293,29295,29297],{"class":265,"line":5164},[129,29269,6913],{"class":2139},[129,29271,6916],{"class":284},[129,29273,147],{"class":1376},[129,29275,4796],{"class":277},[129,29277,6923],{"class":1376},[129,29279,1380],{"class":277},[129,29281,27093],{"class":290},[129,29283,1015],{"class":277},[129,29285,6933],{"class":1376},[129,29287,1380],{"class":277},[129,29289,4261],{"class":277},[129,29291,29292],{"class":427},"Reservation expired or invalid",[129,29294,424],{"class":277},[129,29296,4255],{"class":277},[129,29298,294],{"class":1376},[129,29300,29301],{"class":265,"line":5190},[129,29302,6516],{"class":277},[129,29304,29305],{"class":265,"line":7751},[129,29306,336],{"emptyLinePlaceholder":335},[129,29308,29309],{"class":265,"line":7796},[129,29310,29311],{"class":376},"    // Charge the card\n",[129,29313,29314,29316,29318,29320,29322,29324,29326,29328,29330,29332,29334],{"class":265,"line":7803},[129,29315,4739],{"class":269},[129,29317,4706],{"class":273},[129,29319,4745],{"class":277},[129,29321,4779],{"class":2139},[129,29323,4742],{"class":273},[129,29325,362],{"class":277},[129,29327,4786],{"class":273},[129,29329,362],{"class":277},[129,29331,4791],{"class":284},[129,29333,147],{"class":1376},[129,29335,6455],{"class":277},[129,29337,29338,29341,29343,29345,29347,29350],{"class":265,"line":7808},[129,29339,29340],{"class":1376},"      amount",[129,29342,1380],{"class":277},[129,29344,26902],{"class":273},[129,29346,362],{"class":277},[129,29348,29349],{"class":273},"price",[129,29351,1386],{"class":277},[129,29353,29354,29357,29359,29361,29363,29365],{"class":265,"line":7813},[129,29355,29356],{"class":1376},"      currency",[129,29358,1380],{"class":277},[129,29360,4261],{"class":277},[129,29362,5310],{"class":427},[129,29364,424],{"class":277},[129,29366,1386],{"class":277},[129,29368,29369,29372,29374,29376],{"class":265,"line":7862},[129,29370,29371],{"class":1376},"      payment_method",[129,29373,1380],{"class":277},[129,29375,29008],{"class":273},[129,29377,1386],{"class":277},[129,29379,29380,29383,29385,29387],{"class":265,"line":7911},[129,29381,29382],{"class":1376},"      confirm",[129,29384,1380],{"class":277},[129,29386,4823],{"class":4822},[129,29388,1386],{"class":277},[129,29390,29391,29394,29396,29398,29400],{"class":265,"line":7916},[129,29392,29393],{"class":1376},"      idempotency_key",[129,29395,1380],{"class":277},[129,29397,28828],{"class":273},[129,29399,1015],{"class":277},[129,29401,29402],{"class":376}," // pass through to Stripe too\n",[129,29404,29406,29408],{"class":265,"line":29405},41,[129,29407,7619],{"class":277},[129,29409,294],{"class":1376},[129,29411,29413],{"class":265,"line":29412},42,[129,29414,336],{"emptyLinePlaceholder":335},[129,29416,29418],{"class":265,"line":29417},43,[129,29419,29420],{"class":376},"    // Mark seat as sold\n",[129,29422,29424,29426,29429,29431,29433,29435,29437],{"class":265,"line":29423},44,[129,29425,4902],{"class":2139},[129,29427,29428],{"class":273}," tx",[129,29430,362],{"class":277},[129,29432,27034],{"class":284},[129,29434,147],{"class":1376},[129,29436,26213],{"class":273},[129,29438,294],{"class":1376},[129,29440,29442,29444,29446,29448,29450,29452,29454,29456,29458,29460,29462],{"class":265,"line":29441},45,[129,29443,26956],{"class":277},[129,29445,27154],{"class":284},[129,29447,147],{"class":1376},[129,29449,4796],{"class":277},[129,29451,22309],{"class":1376},[129,29453,1380],{"class":277},[129,29455,4261],{"class":277},[129,29457,25888],{"class":427},[129,29459,424],{"class":277},[129,29461,4255],{"class":277},[129,29463,294],{"class":1376},[129,29465,29467,29469,29471,29473,29475,29477,29479,29481,29483,29485,29487],{"class":265,"line":29466},46,[129,29468,26956],{"class":277},[129,29470,5436],{"class":284},[129,29472,147],{"class":1376},[129,29474,5441],{"class":284},[129,29476,147],{"class":1376},[129,29478,26213],{"class":273},[129,29480,362],{"class":277},[129,29482,3190],{"class":273},[129,29484,1015],{"class":277},[129,29486,26873],{"class":273},[129,29488,471],{"class":1376},[129,29490,29492],{"class":265,"line":29491},47,[129,29493,336],{"emptyLinePlaceholder":335},[129,29495,29497],{"class":265,"line":29496},48,[129,29498,29499],{"class":376},"    // Create the booking record\n",[129,29501,29503,29505,29507,29510,29512,29514,29516,29518,29520,29522,29524,29526,29528,29530,29532,29534],{"class":265,"line":29502},49,[129,29504,4739],{"class":269},[129,29506,1010],{"class":277},[129,29508,29509],{"class":273},"created",[129,29511,14170],{"class":277},[129,29513,4745],{"class":277},[129,29515,4779],{"class":2139},[129,29517,29428],{"class":273},[129,29519,362],{"class":277},[129,29521,22736],{"class":284},[129,29523,147],{"class":1376},[129,29525,26549],{"class":273},[129,29527,160],{"class":1376},[129,29529,362],{"class":277},[129,29531,22747],{"class":284},[129,29533,147],{"class":1376},[129,29535,6455],{"class":277},[129,29537,29538,29541,29543,29545,29547,29549],{"class":265,"line":16480},[129,29539,29540],{"class":1376},"      eventId",[129,29542,1380],{"class":277},[129,29544,26902],{"class":273},[129,29546,362],{"class":277},[129,29548,26467],{"class":273},[129,29550,1386],{"class":277},[129,29552,29554,29557,29559,29561,29563,29565],{"class":265,"line":29553},51,[129,29555,29556],{"class":1376},"      seatId",[129,29558,1380],{"class":277},[129,29560,26902],{"class":273},[129,29562,362],{"class":277},[129,29564,3190],{"class":273},[129,29566,1386],{"class":277},[129,29568,29570,29573],{"class":265,"line":29569},52,[129,29571,29572],{"class":273},"      userId",[129,29574,1386],{"class":277},[129,29576,29578,29581],{"class":265,"line":29577},53,[129,29579,29580],{"class":273},"      idempotencyKey",[129,29582,1386],{"class":277},[129,29584,29586,29588,29590,29592,29594,29596],{"class":265,"line":29585},54,[129,29587,27436],{"class":1376},[129,29589,1380],{"class":277},[129,29591,4261],{"class":277},[129,29593,26056],{"class":427},[129,29595,424],{"class":277},[129,29597,1386],{"class":277},[129,29599,29601,29603,29605,29607,29609],{"class":265,"line":29600},55,[129,29602,7619],{"class":277},[129,29604,160],{"class":1376},[129,29606,362],{"class":277},[129,29608,22758],{"class":284},[129,29610,2451],{"class":1376},[129,29612,29614],{"class":265,"line":29613},56,[129,29615,336],{"emptyLinePlaceholder":335},[129,29617,29619,29621],{"class":265,"line":29618},57,[129,29620,4832],{"class":2139},[129,29622,29623],{"class":273}," created\n",[129,29625,29627,29629],{"class":265,"line":29626},58,[129,29628,4182],{"class":277},[129,29630,294],{"class":1376},[129,29632,29634],{"class":265,"line":29633},59,[129,29635,336],{"emptyLinePlaceholder":335},[129,29637,29639],{"class":265,"line":29638},60,[129,29640,29641],{"class":376},"  // Cache the response - 24h is enough, booking flows don't retry after days\n",[129,29643,29645,29647,29649,29651,29653,29655,29657,29659,29661,29663,29665,29667,29669,29671,29673,29675,29677,29680,29682],{"class":265,"line":29644},61,[129,29646,8101],{"class":2139},[129,29648,20251],{"class":273},[129,29650,362],{"class":277},[129,29652,20326],{"class":284},[129,29654,147],{"class":1376},[129,29656,4125],{"class":277},[129,29658,28967],{"class":427},[129,29660,4131],{"class":277},[129,29662,28862],{"class":273},[129,29664,4175],{"class":277},[129,29666,1015],{"class":277},[129,29668,29060],{"class":273},[129,29670,1015],{"class":277},[129,29672,1416],{"class":277},[129,29674,20341],{"class":1376},[129,29676,1380],{"class":277},[129,29678,29679],{"class":290}," 86_400",[129,29681,4255],{"class":277},[129,29683,294],{"class":1376},[129,29685,29687],{"class":265,"line":29686},62,[129,29688,336],{"emptyLinePlaceholder":335},[129,29690,29692,29694],{"class":265,"line":29691},63,[129,29693,4520],{"class":2139},[129,29695,29696],{"class":273}," booking\n",[129,29698,29700,29702],{"class":265,"line":29699},64,[129,29701,4028],{"class":277},[129,29703,294],{"class":273},[11,29705,29706],{},"The client side - generate the key once per booking attempt and keep it across retries:",[255,29708,29711],{"className":3922,"code":29709,"filename":29710,"language":3924,"meta":260,"style":260},"export function useBooking() {\n  // Generated once per checkout session\n  const idempotencyKey = ref(crypto.randomUUID())\n\n  const confirmBooking = async (seatId: number, paymentToken: string) => {\n    return $fetch('/api/bookings/confirm', {\n      method: 'POST',\n      headers: { 'Idempotency-Key': idempotencyKey.value },\n      body: { seatId, paymentToken },\n      // On network failure, retry - same key means same result\n      retry: 2,\n      retryDelay: 500,\n    })\n  }\n\n  return { confirmBooking, resetKey: () => { idempotencyKey.value = crypto.randomUUID() } }\n}\n","composables/useBooking.ts",[15,29712,29713,29726,29731,29752,29756,29790,29809,29824,29848,29865,29870,29881,29892,29898,29902,29906,29948],{"__ignoreMap":260},[129,29714,29715,29717,29719,29722,29724],{"class":265,"line":266},[129,29716,4050],{"class":2139},[129,29718,5060],{"class":269},[129,29720,29721],{"class":284}," useBooking",[129,29723,4140],{"class":277},[129,29725,1371],{"class":277},[129,29727,29728],{"class":265,"line":297},[129,29729,29730],{"class":376},"  // Generated once per checkout session\n",[129,29732,29733,29735,29737,29739,29741,29743,29745,29747,29750],{"class":265,"line":315},[129,29734,5076],{"class":269},[129,29736,28828],{"class":273},[129,29738,4745],{"class":277},[129,29740,3894],{"class":284},[129,29742,147],{"class":1376},[129,29744,8913],{"class":273},[129,29746,362],{"class":277},[129,29748,29749],{"class":284},"randomUUID",[129,29751,13560],{"class":1376},[129,29753,29754],{"class":265,"line":332},[129,29755,336],{"emptyLinePlaceholder":335},[129,29757,29758,29760,29763,29765,29767,29769,29772,29774,29776,29778,29780,29782,29784,29786,29788],{"class":265,"line":339},[129,29759,5076],{"class":269},[129,29761,29762],{"class":273}," confirmBooking",[129,29764,4745],{"class":277},[129,29766,6020],{"class":269},[129,29768,3984],{"class":277},[129,29770,29771],{"class":452},"seatId",[129,29773,1380],{"class":277},[129,29775,4612],{"class":2161},[129,29777,1015],{"class":277},[129,29779,29008],{"class":452},[129,29781,1380],{"class":277},[129,29783,4622],{"class":2161},[129,29785,160],{"class":277},[129,29787,456],{"class":269},[129,29789,1371],{"class":277},[129,29791,29792,29794,29796,29798,29800,29803,29805,29807],{"class":265,"line":356},[129,29793,4832],{"class":2139},[129,29795,8288],{"class":284},[129,29797,147],{"class":1376},[129,29799,424],{"class":277},[129,29801,29802],{"class":427},"/api/bookings/confirm",[129,29804,424],{"class":277},[129,29806,1015],{"class":277},[129,29808,1371],{"class":277},[129,29810,29811,29814,29816,29818,29820,29822],{"class":265,"line":651},[129,29812,29813],{"class":1376},"      method",[129,29815,1380],{"class":277},[129,29817,4261],{"class":277},[129,29819,3142],{"class":427},[129,29821,424],{"class":277},[129,29823,1386],{"class":277},[129,29825,29826,29828,29830,29832,29834,29836,29838,29840,29842,29844,29846],{"class":265,"line":657},[129,29827,8315],{"class":1376},[129,29829,1380],{"class":277},[129,29831,1416],{"class":277},[129,29833,4261],{"class":277},[129,29835,28843],{"class":1376},[129,29837,424],{"class":277},[129,29839,1380],{"class":277},[129,29841,28828],{"class":273},[129,29843,362],{"class":277},[129,29845,8389],{"class":273},[129,29847,1444],{"class":277},[129,29849,29850,29853,29855,29857,29859,29861,29863],{"class":265,"line":669},[129,29851,29852],{"class":1376},"      body",[129,29854,1380],{"class":277},[129,29856,1416],{"class":277},[129,29858,26873],{"class":273},[129,29860,1015],{"class":277},[129,29862,29008],{"class":273},[129,29864,1444],{"class":277},[129,29866,29867],{"class":265,"line":693},[129,29868,29869],{"class":376},"      // On network failure, retry - same key means same result\n",[129,29871,29872,29875,29877,29879],{"class":265,"line":712},[129,29873,29874],{"class":1376},"      retry",[129,29876,1380],{"class":277},[129,29878,1023],{"class":290},[129,29880,1386],{"class":277},[129,29882,29883,29886,29888,29890],{"class":265,"line":1521},[129,29884,29885],{"class":1376},"      retryDelay",[129,29887,1380],{"class":277},[129,29889,19481],{"class":290},[129,29891,1386],{"class":277},[129,29893,29894,29896],{"class":265,"line":1527},[129,29895,7619],{"class":277},[129,29897,294],{"class":1376},[129,29899,29900],{"class":265,"line":2295},[129,29901,1524],{"class":277},[129,29903,29904],{"class":265,"line":2300},[129,29905,336],{"emptyLinePlaceholder":335},[129,29907,29908,29910,29912,29914,29916,29919,29921,29923,29925,29927,29929,29931,29933,29935,29938,29940,29942,29944,29946],{"class":265,"line":2305},[129,29909,4520],{"class":2139},[129,29911,1416],{"class":277},[129,29913,29762],{"class":273},[129,29915,1015],{"class":277},[129,29917,29918],{"class":284}," resetKey",[129,29920,1380],{"class":277},[129,29922,4511],{"class":277},[129,29924,456],{"class":269},[129,29926,1416],{"class":277},[129,29928,28828],{"class":273},[129,29930,362],{"class":277},[129,29932,8389],{"class":273},[129,29934,4745],{"class":277},[129,29936,29937],{"class":273}," crypto",[129,29939,362],{"class":277},[129,29941,29749],{"class":284},[129,29943,824],{"class":1376},[129,29945,4028],{"class":277},[129,29947,1476],{"class":277},[129,29949,29950],{"class":265,"line":2311},[129,29951,1530],{"class":277},[11,29953,29954,29957],{},[15,29955,29956],{},"resetKey()"," creates a new key for the next booking attempt (after a failed payment, for example).",[2001,29959],{},[40,29961,29963],{"id":29962},"cache-for-reads-database-for-writes","Cache for reads, database for writes",[11,29965,29966],{},"The availability endpoint gets hit constantly - users refresh the seat map, share links, browsers preload. These reads don't need to touch the primary database on every request:",[255,29968,29971],{"className":3922,"code":29969,"filename":29970,"language":3924,"meta":260,"style":260},"export default defineCachedEventHandler(async (event) => {\n  const eventId = event.context.params!.id\n\n  return db.select({\n    id: seats.id,\n    seatNumber: seats.seatNumber,\n    status: seats.status,\n  }).from(seats).where(eq(seats.eventId, eventId))\n}, {\n  maxAge: 5, // 5-second cache - balance freshness vs DB load\n  name: 'seat-availability',\n  getKey: (event) => event.context.params!.id,\n})\n","server/api/events/[id]/seats-cached.get.ts",[15,29972,29973,29995,30017,30021,30035,30049,30063,30077,30115,30121,30134,30149,30179],{"__ignoreMap":260},[129,29974,29975,29977,29979,29981,29983,29985,29987,29989,29991,29993],{"class":265,"line":266},[129,29976,4050],{"class":2139},[129,29978,4053],{"class":2139},[129,29980,6575],{"class":284},[129,29982,147],{"class":273},[129,29984,4508],{"class":269},[129,29986,3984],{"class":277},[129,29988,4100],{"class":452},[129,29990,160],{"class":277},[129,29992,456],{"class":269},[129,29994,1371],{"class":277},[129,29996,29997,29999,30001,30003,30005,30007,30009,30011,30013,30015],{"class":265,"line":297},[129,29998,5076],{"class":269},[129,30000,26868],{"class":273},[129,30002,4745],{"class":277},[129,30004,16694],{"class":273},[129,30006,362],{"class":277},[129,30008,6497],{"class":273},[129,30010,362],{"class":277},[129,30012,25117],{"class":273},[129,30014,25120],{"class":277},[129,30016,28540],{"class":273},[129,30018,30019],{"class":265,"line":315},[129,30020,336],{"emptyLinePlaceholder":335},[129,30022,30023,30025,30027,30029,30031,30033],{"class":265,"line":332},[129,30024,4520],{"class":2139},[129,30026,4479],{"class":273},[129,30028,362],{"class":277},[129,30030,2357],{"class":284},[129,30032,147],{"class":1376},[129,30034,6455],{"class":277},[129,30036,30037,30039,30041,30043,30045,30047],{"class":265,"line":339},[129,30038,28305],{"class":1376},[129,30040,1380],{"class":277},[129,30042,25784],{"class":273},[129,30044,362],{"class":277},[129,30046,3190],{"class":273},[129,30048,1386],{"class":277},[129,30050,30051,30053,30055,30057,30059,30061],{"class":265,"line":356},[129,30052,28320],{"class":1376},[129,30054,1380],{"class":277},[129,30056,25784],{"class":273},[129,30058,362],{"class":277},[129,30060,27981],{"class":273},[129,30062,1386],{"class":277},[129,30064,30065,30067,30069,30071,30073,30075],{"class":265,"line":651},[129,30066,24130],{"class":1376},[129,30068,1380],{"class":277},[129,30070,25784],{"class":273},[129,30072,362],{"class":277},[129,30074,22095],{"class":273},[129,30076,1386],{"class":277},[129,30078,30079,30081,30083,30085,30087,30089,30091,30093,30095,30097,30099,30101,30103,30105,30107,30109,30111,30113],{"class":265,"line":657},[129,30080,4182],{"class":277},[129,30082,160],{"class":1376},[129,30084,362],{"class":277},[129,30086,2589],{"class":284},[129,30088,147],{"class":1376},[129,30090,26213],{"class":273},[129,30092,160],{"class":1376},[129,30094,362],{"class":277},[129,30096,5436],{"class":284},[129,30098,147],{"class":1376},[129,30100,5441],{"class":284},[129,30102,147],{"class":1376},[129,30104,26213],{"class":273},[129,30106,362],{"class":277},[129,30108,26467],{"class":273},[129,30110,1015],{"class":277},[129,30112,26868],{"class":273},[129,30114,471],{"class":1376},[129,30116,30117,30119],{"class":265,"line":669},[129,30118,6625],{"class":277},[129,30120,1371],{"class":277},[129,30122,30123,30125,30127,30129,30131],{"class":265,"line":693},[129,30124,18878],{"class":1376},[129,30126,1380],{"class":277},[129,30128,1043],{"class":290},[129,30130,1015],{"class":277},[129,30132,30133],{"class":376}," // 5-second cache - balance freshness vs DB load\n",[129,30135,30136,30138,30140,30142,30145,30147],{"class":265,"line":712},[129,30137,18898],{"class":1376},[129,30139,1380],{"class":277},[129,30141,4261],{"class":277},[129,30143,30144],{"class":427},"seat-availability",[129,30146,424],{"class":277},[129,30148,1386],{"class":277},[129,30150,30151,30153,30155,30157,30159,30161,30163,30165,30167,30169,30171,30173,30175,30177],{"class":265,"line":1521},[129,30152,18918],{"class":284},[129,30154,1380],{"class":277},[129,30156,3984],{"class":277},[129,30158,4100],{"class":452},[129,30160,160],{"class":277},[129,30162,456],{"class":269},[129,30164,16694],{"class":273},[129,30166,362],{"class":277},[129,30168,6497],{"class":273},[129,30170,362],{"class":277},[129,30172,25117],{"class":273},[129,30174,25120],{"class":277},[129,30176,3190],{"class":273},[129,30178,1386],{"class":277},[129,30180,30181,30183],{"class":265,"line":1527},[129,30182,4028],{"class":277},[129,30184,294],{"class":273},[11,30186,30187],{},"5 seconds is intentional. For a concert going on sale, the seat map will show slightly stale data - a seat might appear available for up to 5 seconds after being taken. That's acceptable. Users will see the conflict when they attempt to reserve (the atomic UPDATE returns 0 rows), get a clear error, and can pick a different seat.",[3325,30189,30190],{},[11,30191,30192,30195],{},[118,30193,30194],{},"The important rule",": reads go through cache, writes always go to the primary database. Never write through cache. Never check the cache before a reservation - always hit the database for the mutation.",[11,30197,30198],{},"What the interview answer gets slightly wrong here: it implies the cache needs to be invalidated on every booking. At 5-second TTL, you don't need manual invalidation - staleness is bounded and acceptable. Aggressive cache invalidation on every booking creates more complexity than it solves.",[2001,30200],{},[40,30202,30204],{"id":30203},"handling-spikes","Handling spikes",[11,30206,30207],{},"The interview answer mentions a \"virtual queue or rate limiter.\" This is vague. In practice, two separate things:",[11,30209,30210,30212,30213,30215],{},[118,30211,20068],{}," protects the server. From the ",[51,30214,21760],{"href":22354},", Nitro middleware handles this:",[255,30217,30220],{"className":3922,"code":30218,"filename":30219,"language":3924,"meta":260,"style":260},"export default defineEventHandler(async (event) => {\n  if (!event.path.startsWith('/api/bookings')) return\n\n  const ip = getRequestIP(event, { xForwardedFor: true }) ?? 'unknown'\n  const key = `rate-limit:booking:${ip}:${Math.floor(Date.now() / 60_000)}`\n  const storage = useStorage('cache')\n\n  const count = ((await storage.getItem\u003Cnumber>(key)) ?? 0) + 1\n  await storage.setItem(key, count, { ttl: 60 })\n\n  if (count > 10) { // max 10 booking attempts per minute per IP\n    throw createError({ statusCode: 429, message: 'Too many booking attempts' })\n  }\n})\n","server/middleware/booking-rate-limit.ts",[15,30221,30222,30244,30275,30279,30315,30360,30380,30384,30424,30456,30460,30479,30512,30516],{"__ignoreMap":260},[129,30223,30224,30226,30228,30230,30232,30234,30236,30238,30240,30242],{"class":265,"line":266},[129,30225,4050],{"class":2139},[129,30227,4053],{"class":2139},[129,30229,4503],{"class":284},[129,30231,147],{"class":273},[129,30233,4508],{"class":269},[129,30235,3984],{"class":277},[129,30237,4100],{"class":452},[129,30239,160],{"class":277},[129,30241,456],{"class":269},[129,30243,1371],{"class":277},[129,30245,30246,30248,30250,30252,30254,30256,30258,30260,30262,30264,30266,30269,30271,30273],{"class":265,"line":297},[129,30247,3998],{"class":2139},[129,30249,3984],{"class":1376},[129,30251,4447],{"class":277},[129,30253,4100],{"class":273},[129,30255,362],{"class":277},[129,30257,4172],{"class":273},[129,30259,362],{"class":277},[129,30261,20130],{"class":284},[129,30263,147],{"class":1376},[129,30265,424],{"class":277},[129,30267,30268],{"class":427},"/api/bookings",[129,30270,424],{"class":277},[129,30272,13145],{"class":1376},[129,30274,20144],{"class":2139},[129,30276,30277],{"class":265,"line":315},[129,30278,336],{"emptyLinePlaceholder":335},[129,30280,30281,30283,30285,30287,30289,30291,30293,30295,30297,30299,30301,30303,30305,30307,30309,30311,30313],{"class":265,"line":332},[129,30282,5076],{"class":269},[129,30284,20155],{"class":273},[129,30286,4745],{"class":277},[129,30288,20160],{"class":284},[129,30290,147],{"class":1376},[129,30292,4100],{"class":273},[129,30294,1015],{"class":277},[129,30296,1416],{"class":277},[129,30298,20171],{"class":1376},[129,30300,1380],{"class":277},[129,30302,4823],{"class":4822},[129,30304,4255],{"class":277},[129,30306,4005],{"class":1376},[129,30308,18967],{"class":277},[129,30310,4261],{"class":277},[129,30312,20186],{"class":427},[129,30314,4267],{"class":277},[129,30316,30317,30319,30321,30323,30325,30328,30330,30332,30334,30336,30338,30340,30342,30344,30346,30348,30350,30352,30354,30356,30358],{"class":265,"line":339},[129,30318,5076],{"class":269},[129,30320,6243],{"class":273},[129,30322,4745],{"class":277},[129,30324,5569],{"class":277},[129,30326,30327],{"class":427},"rate-limit:booking:",[129,30329,4131],{"class":277},[129,30331,20206],{"class":273},[129,30333,4028],{"class":277},[129,30335,1380],{"class":427},[129,30337,4131],{"class":277},[129,30339,10876],{"class":273},[129,30341,362],{"class":277},[129,30343,20219],{"class":284},[129,30345,20222],{"class":273},[129,30347,362],{"class":277},[129,30349,3587],{"class":284},[129,30351,824],{"class":273},[129,30353,938],{"class":277},[129,30355,20233],{"class":290},[129,30357,160],{"class":273},[129,30359,18992],{"class":277},[129,30361,30362,30364,30366,30368,30370,30372,30374,30376,30378],{"class":265,"line":356},[129,30363,5076],{"class":269},[129,30365,20251],{"class":273},[129,30367,4745],{"class":277},[129,30369,20256],{"class":284},[129,30371,147],{"class":1376},[129,30373,424],{"class":277},[129,30375,20263],{"class":427},[129,30377,424],{"class":277},[129,30379,294],{"class":1376},[129,30381,30382],{"class":265,"line":651},[129,30383,336],{"emptyLinePlaceholder":335},[129,30385,30386,30388,30390,30392,30394,30396,30398,30400,30402,30404,30406,30408,30410,30412,30414,30416,30418,30420,30422],{"class":265,"line":657},[129,30387,5076],{"class":269},[129,30389,9491],{"class":273},[129,30391,4745],{"class":277},[129,30393,20278],{"class":1376},[129,30395,8083],{"class":2139},[129,30397,20251],{"class":273},[129,30399,362],{"class":277},[129,30401,20287],{"class":284},[129,30403,3945],{"class":277},[129,30405,20292],{"class":2161},[129,30407,3956],{"class":277},[129,30409,147],{"class":1376},[129,30411,6273],{"class":273},[129,30413,13145],{"class":1376},[129,30415,18967],{"class":277},[129,30417,5698],{"class":290},[129,30419,4005],{"class":1376},[129,30421,219],{"class":277},[129,30423,20311],{"class":290},[129,30425,30426,30428,30430,30432,30434,30436,30438,30440,30442,30444,30446,30448,30450,30452,30454],{"class":265,"line":669},[129,30427,8101],{"class":2139},[129,30429,20251],{"class":273},[129,30431,362],{"class":277},[129,30433,20326],{"class":284},[129,30435,147],{"class":1376},[129,30437,6273],{"class":273},[129,30439,1015],{"class":277},[129,30441,9491],{"class":273},[129,30443,1015],{"class":277},[129,30445,1416],{"class":277},[129,30447,20341],{"class":1376},[129,30449,1380],{"class":277},[129,30451,18883],{"class":290},[129,30453,4255],{"class":277},[129,30455,294],{"class":1376},[129,30457,30458],{"class":265,"line":693},[129,30459,336],{"emptyLinePlaceholder":335},[129,30461,30462,30464,30466,30468,30470,30472,30474,30476],{"class":265,"line":712},[129,30463,3998],{"class":2139},[129,30465,3984],{"class":1376},[129,30467,3904],{"class":273},[129,30469,2345],{"class":277},[129,30471,10264],{"class":290},[129,30473,4005],{"class":1376},[129,30475,4796],{"class":277},[129,30477,30478],{"class":376}," // max 10 booking attempts per minute per IP\n",[129,30480,30481,30483,30485,30487,30489,30491,30493,30495,30497,30499,30501,30503,30506,30508,30510],{"class":265,"line":1521},[129,30482,20452],{"class":2139},[129,30484,6916],{"class":284},[129,30486,147],{"class":1376},[129,30488,4796],{"class":277},[129,30490,6923],{"class":1376},[129,30492,1380],{"class":277},[129,30494,20465],{"class":290},[129,30496,1015],{"class":277},[129,30498,6933],{"class":1376},[129,30500,1380],{"class":277},[129,30502,4261],{"class":277},[129,30504,30505],{"class":427},"Too many booking attempts",[129,30507,424],{"class":277},[129,30509,4255],{"class":277},[129,30511,294],{"class":1376},[129,30513,30514],{"class":265,"line":1527},[129,30515,1524],{"class":277},[129,30517,30518,30520],{"class":265,"line":2295},[129,30519,4028],{"class":277},[129,30521,294],{"class":273},[11,30523,30524,30527],{},[118,30525,30526],{},"A virtual queue"," is different - it's what Ticketmaster's \"waiting room\" is. Instead of processing all requests simultaneously, users enter a queue and are admitted in order. This prevents the database from being hammered by 50,000 concurrent users all trying to book at 10:00am exactly.",[11,30529,30530],{},"The Nuxt implementation uses BullMQ:",[255,30532,30535],{"className":3922,"code":30533,"filename":30534,"language":3924,"meta":260,"style":260},"import { Queue, Worker } from 'bullmq'\nimport { redis } from './redis'\n\nexport const bookingQueue = new Queue('bookings', {\n  connection: redis,\n  defaultJobOptions: {\n    attempts: 1, // no retries for bookings - idempotency handles client-side retries\n    removeOnComplete: 500,\n    removeOnFail: 1000,\n  },\n})\n\n// Worker processes one booking at a time per seat\n// concurrency: 50 means up to 50 seats can be processed simultaneously\nnew Worker('bookings', async (job) => {\n  const { seatId, userId, idempotencyKey } = job.data\n  return processReservation(seatId, userId, idempotencyKey)\n}, {\n  connection: redis,\n  concurrency: 50,\n})\n","server/lib/booking-queue.ts",[15,30536,30537,30559,30577,30581,30608,30618,30626,30639,30649,30659,30663,30669,30673,30678,30683,30711,30739,30760,30766,30776,30788],{"__ignoreMap":260},[129,30538,30539,30541,30543,30545,30547,30549,30551,30553,30555,30557],{"class":265,"line":266},[129,30540,2140],{"class":2139},[129,30542,1416],{"class":277},[129,30544,18354],{"class":273},[129,30546,1015],{"class":277},[129,30548,18359],{"class":273},[129,30550,4255],{"class":277},[129,30552,4258],{"class":2139},[129,30554,4261],{"class":277},[129,30556,18368],{"class":427},[129,30558,4267],{"class":277},[129,30560,30561,30563,30565,30567,30569,30571,30573,30575],{"class":265,"line":297},[129,30562,2140],{"class":2139},[129,30564,1416],{"class":277},[129,30566,18379],{"class":273},[129,30568,4255],{"class":277},[129,30570,4258],{"class":2139},[129,30572,4261],{"class":277},[129,30574,18388],{"class":427},[129,30576,4267],{"class":277},[129,30578,30579],{"class":265,"line":315},[129,30580,336],{"emptyLinePlaceholder":335},[129,30582,30583,30585,30587,30590,30592,30594,30596,30598,30600,30602,30604,30606],{"class":265,"line":332},[129,30584,4050],{"class":2139},[129,30586,4456],{"class":269},[129,30588,30589],{"class":273}," bookingQueue ",[129,30591,278],{"class":277},[129,30593,281],{"class":277},[129,30595,18354],{"class":284},[129,30597,147],{"class":273},[129,30599,424],{"class":277},[129,30601,26549],{"class":427},[129,30603,424],{"class":277},[129,30605,1015],{"class":277},[129,30607,1371],{"class":277},[129,30609,30610,30612,30614,30616],{"class":265,"line":339},[129,30611,18532],{"class":1376},[129,30613,1380],{"class":277},[129,30615,18379],{"class":273},[129,30617,1386],{"class":277},[129,30619,30620,30622,30624],{"class":265,"line":356},[129,30621,24868],{"class":1376},[129,30623,1380],{"class":277},[129,30625,1371],{"class":277},[129,30627,30628,30630,30632,30634,30636],{"class":265,"line":651},[129,30629,24877],{"class":1376},[129,30631,1380],{"class":277},[129,30633,1383],{"class":290},[129,30635,1015],{"class":277},[129,30637,30638],{"class":376}," // no retries for bookings - idempotency handles client-side retries\n",[129,30640,30641,30643,30645,30647],{"class":265,"line":657},[129,30642,18727],{"class":1376},[129,30644,1380],{"class":277},[129,30646,19481],{"class":290},[129,30648,1386],{"class":277},[129,30650,30651,30653,30655,30657],{"class":265,"line":669},[129,30652,18742],{"class":1376},[129,30654,1380],{"class":277},[129,30656,9568],{"class":290},[129,30658,1386],{"class":277},[129,30660,30661],{"class":265,"line":693},[129,30662,1481],{"class":277},[129,30664,30665,30667],{"class":265,"line":712},[129,30666,4028],{"class":277},[129,30668,294],{"class":273},[129,30670,30671],{"class":265,"line":1521},[129,30672,336],{"emptyLinePlaceholder":335},[129,30674,30675],{"class":265,"line":1527},[129,30676,30677],{"class":376},"// Worker processes one booking at a time per seat\n",[129,30679,30680],{"class":265,"line":2295},[129,30681,30682],{"class":376},"// concurrency: 50 means up to 50 seats can be processed simultaneously\n",[129,30684,30685,30687,30689,30691,30693,30695,30697,30699,30701,30703,30705,30707,30709],{"class":265,"line":2300},[129,30686,4134],{"class":277},[129,30688,18359],{"class":284},[129,30690,147],{"class":273},[129,30692,424],{"class":277},[129,30694,26549],{"class":427},[129,30696,424],{"class":277},[129,30698,1015],{"class":277},[129,30700,6020],{"class":269},[129,30702,3984],{"class":277},[129,30704,18465],{"class":452},[129,30706,160],{"class":277},[129,30708,456],{"class":269},[129,30710,1371],{"class":277},[129,30712,30713,30715,30717,30719,30721,30723,30725,30727,30729,30731,30734,30736],{"class":265,"line":2305},[129,30714,5076],{"class":269},[129,30716,1416],{"class":277},[129,30718,26873],{"class":273},[129,30720,1015],{"class":277},[129,30722,18058],{"class":273},[129,30724,1015],{"class":277},[129,30726,28828],{"class":273},[129,30728,4255],{"class":277},[129,30730,4745],{"class":277},[129,30732,30733],{"class":273}," job",[129,30735,362],{"class":277},[129,30737,30738],{"class":273},"data\n",[129,30740,30741,30743,30746,30748,30750,30752,30754,30756,30758],{"class":265,"line":2311},[129,30742,4520],{"class":2139},[129,30744,30745],{"class":284}," processReservation",[129,30747,147],{"class":1376},[129,30749,29771],{"class":273},[129,30751,1015],{"class":277},[129,30753,18058],{"class":273},[129,30755,1015],{"class":277},[129,30757,28828],{"class":273},[129,30759,294],{"class":1376},[129,30761,30762,30764],{"class":265,"line":2329},[129,30763,6625],{"class":277},[129,30765,1371],{"class":277},[129,30767,30768,30770,30772,30774],{"class":265,"line":2351},[129,30769,18532],{"class":1376},[129,30771,1380],{"class":277},[129,30773,18379],{"class":273},[129,30775,1386],{"class":277},[129,30777,30778,30781,30783,30786],{"class":265,"line":2387},[129,30779,30780],{"class":1376},"  concurrency",[129,30782,1380],{"class":277},[129,30784,30785],{"class":290}," 50",[129,30787,1386],{"class":277},[129,30789,30790,30792],{"class":265,"line":2392},[129,30791,4028],{"class":277},[129,30793,294],{"class":273},[255,30795,30798],{"className":3922,"code":30796,"filename":30797,"language":3924,"meta":260,"style":260},"export default defineEventHandler(async (event) => {\n  const { seatId } = await readBody(event)\n  const userId = event.context.user!.id\n  const idempotencyKey = getHeader(event, 'Idempotency-Key')!\n\n  // Add to queue - returns immediately\n  const job = await bookingQueue.add('reserve', { seatId, userId, idempotencyKey })\n\n  // Client polls this job ID for the result\n  return { jobId: job.id, status: 'queued' }\n})\n","server/api/bookings/queue-reserve.post.ts",[15,30799,30800,30822,30844,30866,30892,30896,30901,30945,30949,30954,30986],{"__ignoreMap":260},[129,30801,30802,30804,30806,30808,30810,30812,30814,30816,30818,30820],{"class":265,"line":266},[129,30803,4050],{"class":2139},[129,30805,4053],{"class":2139},[129,30807,4503],{"class":284},[129,30809,147],{"class":273},[129,30811,4508],{"class":269},[129,30813,3984],{"class":277},[129,30815,4100],{"class":452},[129,30817,160],{"class":277},[129,30819,456],{"class":269},[129,30821,1371],{"class":277},[129,30823,30824,30826,30828,30830,30832,30834,30836,30838,30840,30842],{"class":265,"line":297},[129,30825,5076],{"class":269},[129,30827,1416],{"class":277},[129,30829,26873],{"class":273},[129,30831,4255],{"class":277},[129,30833,4745],{"class":277},[129,30835,4779],{"class":2139},[129,30837,5264],{"class":284},[129,30839,147],{"class":1376},[129,30841,4100],{"class":273},[129,30843,294],{"class":1376},[129,30845,30846,30848,30850,30852,30854,30856,30858,30860,30862,30864],{"class":265,"line":315},[129,30847,5076],{"class":269},[129,30849,18058],{"class":273},[129,30851,4745],{"class":277},[129,30853,16694],{"class":273},[129,30855,362],{"class":277},[129,30857,6497],{"class":273},[129,30859,362],{"class":277},[129,30861,6335],{"class":273},[129,30863,25120],{"class":277},[129,30865,28540],{"class":273},[129,30867,30868,30870,30872,30874,30876,30878,30880,30882,30884,30886,30888,30890],{"class":265,"line":332},[129,30869,5076],{"class":269},[129,30871,28828],{"class":273},[129,30873,4745],{"class":277},[129,30875,6041],{"class":284},[129,30877,147],{"class":1376},[129,30879,4100],{"class":273},[129,30881,1015],{"class":277},[129,30883,4261],{"class":277},[129,30885,28843],{"class":427},[129,30887,424],{"class":277},[129,30889,160],{"class":1376},[129,30891,19807],{"class":277},[129,30893,30894],{"class":265,"line":339},[129,30895,336],{"emptyLinePlaceholder":335},[129,30897,30898],{"class":265,"line":356},[129,30899,30900],{"class":376},"  // Add to queue - returns immediately\n",[129,30902,30903,30905,30907,30909,30911,30914,30916,30918,30920,30922,30925,30927,30929,30931,30933,30935,30937,30939,30941,30943],{"class":265,"line":651},[129,30904,5076],{"class":269},[129,30906,30733],{"class":273},[129,30908,4745],{"class":277},[129,30910,4779],{"class":2139},[129,30912,30913],{"class":273}," bookingQueue",[129,30915,362],{"class":277},[129,30917,14141],{"class":284},[129,30919,147],{"class":1376},[129,30921,424],{"class":277},[129,30923,30924],{"class":427},"reserve",[129,30926,424],{"class":277},[129,30928,1015],{"class":277},[129,30930,1416],{"class":277},[129,30932,26873],{"class":273},[129,30934,1015],{"class":277},[129,30936,18058],{"class":273},[129,30938,1015],{"class":277},[129,30940,28828],{"class":273},[129,30942,4255],{"class":277},[129,30944,294],{"class":1376},[129,30946,30947],{"class":265,"line":657},[129,30948,336],{"emptyLinePlaceholder":335},[129,30950,30951],{"class":265,"line":669},[129,30952,30953],{"class":376},"  // Client polls this job ID for the result\n",[129,30955,30956,30958,30960,30963,30965,30967,30969,30971,30973,30975,30977,30979,30982,30984],{"class":265,"line":693},[129,30957,4520],{"class":2139},[129,30959,1416],{"class":277},[129,30961,30962],{"class":1376}," jobId",[129,30964,1380],{"class":277},[129,30966,30733],{"class":273},[129,30968,362],{"class":277},[129,30970,3190],{"class":273},[129,30972,1015],{"class":277},[129,30974,22309],{"class":1376},[129,30976,1380],{"class":277},[129,30978,4261],{"class":277},[129,30980,30981],{"class":427},"queued",[129,30983,424],{"class":277},[129,30985,1476],{"class":277},[129,30987,30988,30990],{"class":265,"line":712},[129,30989,4028],{"class":277},[129,30991,294],{"class":273},[11,30993,30994],{},"For most applications, rate limiting is sufficient. A virtual queue adds significant complexity (you need SSE or WebSocket for job status updates, a waiting room UI, queue management). Worth it for a Taylor Swift sale. Not worth it for booking appointments at a small business.",[2001,30996],{},[40,30998,31000],{"id":30999},"the-complete-flow","The complete flow",[11,31002,31003],{},"Putting it together:",[25635,31005,31006,31010,31016,31020,31026,31030,31035,31039,31045,31049],{},[2456,31007,31009],{"id":31008},"user-views-seat-map","User views seat map",[11,31011,31012,31015],{},[15,31013,31014],{},"GET /api/events/:id/seats-cached"," - served from 5-second cache. Stale is acceptable.",[2456,31017,31019],{"id":31018},"user-selects-seat-14b","User selects seat 14B",[11,31021,31022,31025],{},[15,31023,31024],{},"POST /api/bookings/reserve"," - atomic UPDATE WHERE status = 'available'. Returns 200 or 409. No lock held.",[2456,31027,31029],{"id":31028},"countdown-timer-starts-10-minutes","Countdown timer starts (10 minutes)",[11,31031,31032,31034],{},[15,31033,26410],{}," set. Background task runs every minute to release expired reservations.",[2456,31036,31038],{"id":31037},"user-completes-payment","User completes payment",[11,31040,31041,31044],{},[15,31042,31043],{},"POST /api/bookings/confirm"," with Idempotency-Key header. Transaction: verify reservation valid + Stripe charge + mark sold + create booking record.",[2456,31046,31048],{"id":31047},"if-payment-fails-or-user-abandons","If payment fails or user abandons",[11,31050,31051,31052,31054],{},"After 10 minutes, ",[15,31053,27777],{}," task runs. Seat returns to available.",[2001,31056],{},[40,31058,31060],{"id":31059},"verdict-on-the-6-point-answer","Verdict on the 6-point answer",[59,31062,31063,31075],{},[62,31064,31065],{},[65,31066,31067,31070,31073],{},[68,31068,31069],{},"Point",[68,31071,31072],{},"Correct?",[68,31074,12656],{},[78,31076,31077,31087,31097,31109,31120,31130],{},[65,31078,31079,31082,31084],{},[83,31080,31081],{},"Single source of truth",[83,31083,3437],{},[83,31085,31086],{},"The unique constraint is essential. This is the last line of defense.",[65,31088,31089,31092,31094],{},[83,31090,31091],{},"Atomic reservation",[83,31093,3437],{},[83,31095,31096],{},"But: optimistic locking (atomic UPDATE) is usually better than SELECT FOR UPDATE. Both work.",[65,31098,31099,31102,31104],{},[83,31100,31101],{},"Short reservation window",[83,31103,3437],{},[83,31105,31106,31107,362],{},"10 minutes is more realistic than 5 for payment flows. Implemented via ",[15,31108,26410],{},[65,31110,31111,31114,31117],{},[83,31112,31113],{},"Queue spikes",[83,31115,31116],{},"Partial",[83,31118,31119],{},"Rate limiting is always needed. Virtual queue only for very high-traffic sales.",[65,31121,31122,31125,31127],{},[83,31123,31124],{},"Idempotent booking API",[83,31126,3437],{},[83,31128,31129],{},"This one is undersold in the answer - it's what prevents the double-charge scenario.",[65,31131,31132,31135,31138],{},[83,31133,31134],{},"Cache for reads, DB for writes",[83,31136,31137],{},"Mostly",[83,31139,31140],{},"5-second TTL beats manual invalidation. The rule is correct; the implementation matters.",[11,31142,31143],{},"The answer is solid for an interview. The gap between \"interview answer\" and \"working code\" is mainly:",[2086,31145,31146,31152,31158],{},[1825,31147,31148,31151],{},[118,31149,31150],{},"You need to handle the payment-booking atomicity"," - marking the seat as sold and creating the booking record must happen in the same transaction as the Stripe charge confirmation. If the DB write fails after the charge, you have a charged user with no booking.",[1825,31153,31154,31157],{},[118,31155,31156],{},"The idempotency key must be passed to Stripe too"," - Stripe supports idempotency keys natively. Using the same key means Stripe won't charge twice even if you call their API twice.",[1825,31159,31160,16900,31163,31165],{},[118,31161,31162],{},"Optimistic locking scales better",[15,31164,27697],{}," is fine but creates lock queues under high concurrency. The atomic UPDATE approach avoids this entirely.",[2001,31167],{},[11,31169,31170,31171,31174,31175,31177,31178,31181],{},"The fundamental requirement: ",[118,31172,31173],{},"make the check and the write a single operation",". Whether that's ",[15,31176,27697],{}," inside a transaction or an atomic ",[15,31179,31180],{},"UPDATE WHERE"," - you need to eliminate the gap between \"is this available?\" and \"mark it as mine.\" TTL, idempotency, caching - all of that protects against the failure modes around that one core operation.",[2026,31183,31184],{},"html pre.shiki code .sbssI, html code.shiki .sbssI{--shiki-light:#F76D47;--shiki-default:#F78C6C;--shiki-dark:#F78C6C}html pre.shiki code .s2Zo4, html code.shiki .s2Zo4{--shiki-light:#6182B8;--shiki-default:#82AAFF;--shiki-dark:#82AAFF}html pre.shiki code .sTEyZ, html code.shiki .sTEyZ{--shiki-light:#90A4AE;--shiki-default:#EEFFFF;--shiki-dark:#BABED8}html pre.shiki code .spNyl, html code.shiki .spNyl{--shiki-light:#9C3EDA;--shiki-default:#C792EA;--shiki-dark:#C792EA}html pre.shiki code .sMK4o, html code.shiki .sMK4o{--shiki-light:#39ADB5;--shiki-default:#89DDFF;--shiki-dark:#89DDFF}html pre.shiki code .sfazB, html code.shiki .sfazB{--shiki-light:#91B859;--shiki-default:#C3E88D;--shiki-dark:#C3E88D}html pre.shiki code .sHwdD, html code.shiki .sHwdD{--shiki-light:#90A4AE;--shiki-light-font-style:italic;--shiki-default:#546E7A;--shiki-default-font-style:italic;--shiki-dark:#676E95;--shiki-dark-font-style:italic}html .light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html.light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .s7zQu, html code.shiki .s7zQu{--shiki-light:#39ADB5;--shiki-light-font-style:italic;--shiki-default:#89DDFF;--shiki-default-font-style:italic;--shiki-dark:#89DDFF;--shiki-dark-font-style:italic}html pre.shiki code .swJcz, html code.shiki .swJcz{--shiki-light:#E53935;--shiki-default:#F07178;--shiki-dark:#F07178}html pre.shiki code .sfNiH, html code.shiki .sfNiH{--shiki-light:#FF5370;--shiki-default:#FF9CAC;--shiki-dark:#FF9CAC}html pre.shiki code .sHdIc, html code.shiki .sHdIc{--shiki-light:#90A4AE;--shiki-light-font-style:italic;--shiki-default:#EEFFFF;--shiki-default-font-style:italic;--shiki-dark:#BABED8;--shiki-dark-font-style:italic}html pre.shiki code .sBMFI, html code.shiki .sBMFI{--shiki-light:#E2931D;--shiki-default:#FFCB6B;--shiki-dark:#FFCB6B}",{"title":260,"searchDepth":297,"depth":297,"links":31186},[31187,31188,31189,31194,31197,31198,31199,31200,31207],{"id":25742,"depth":297,"text":25743},{"id":25764,"depth":297,"text":25765},{"id":26811,"depth":297,"text":26812,"children":31190},[31191,31192,31193],{"id":26827,"depth":315,"text":26828},{"id":27317,"depth":315,"text":27318},{"id":27704,"depth":315,"text":27705},{"id":27721,"depth":297,"text":27722,"children":31195},[31196],{"id":28403,"depth":315,"text":28404},{"id":28776,"depth":297,"text":28777},{"id":29962,"depth":297,"text":29963},{"id":30203,"depth":297,"text":30204},{"id":30999,"depth":297,"text":31000,"children":31201},[31202,31203,31204,31205,31206],{"id":31008,"depth":315,"text":31009},{"id":31018,"depth":315,"text":31019},{"id":31028,"depth":315,"text":31029},{"id":31037,"depth":315,"text":31038},{"id":31047,"depth":315,"text":31048},{"id":31059,"depth":297,"text":31060},"The classic interview question with real code. SELECT FOR UPDATE vs atomic UPDATE, reservation TTL with Nitro tasks, idempotency keys",{},"/blog/ticket-booking-concurrency",{"title":25724,"description":31208},"blog/ticket-booking-concurrency",[8631,2051,21453,31214,31215],"PostgreSQL","Nitro","HJ6IarCH-pL8Gp8QrTO8mMXle2t-ZzwwylIlCyfo4zo",{"id":31218,"title":31219,"body":31220,"cover":2042,"date":33101,"description":33102,"extension":2045,"meta":33103,"navigation":335,"path":33104,"readingTime":693,"seo":33105,"stem":33106,"tags":33107,"__hash__":33110},"blog/blog/bionic-reading.md","Fast reading: Bionic, The Science, and How to Build It",{"type":8,"value":31221,"toc":33075},[31222,31226,31237,31244,31246,31250,31260,31271,31274,31277,31295,31299,31306,31314,31317,31321,31327,31338,31341,31355,31360,31386,31390,31394,31397,31431,31435,31438,31508,31512,31515,31534,31541,31543,31547,31550,31553,31556,31558,31562,31565,31571,31581,31584,31588,31591,31597,31600,31606,31609,31612,31615,31619,31622,31858,31926,31929,31967,32015,32019,32026,32030,32036,32040,32304,32307,32510,32513,32918,32921,32949,32968,32970,32974,33057,33064,33066,33069,33072],[40,31223,31225],{"id":31224},"what-is-bionic-reading","What is Bionic Reading?",[11,31227,31228,31229,31232,31233,31236],{},"In 2022, Swiss typographer ",[118,31230,31231],{},"Renato Casutt"," released a technique called ",[24,31234,31235],{},"Bionic Reading",". The premise is elegant: bold the first portion of each word - the \"fixation point\" - so your eyes skip to it automatically and your brain fills in the rest. Less eye movement, faster comprehension, less cognitive load.",[11,31238,31239,31240,31243],{},"The original Bionic Reading is a ",[118,31241,31242],{},"registered trademark"," with a paid API. That's precisely why the open-source ecosystem spawned a dozen clones within weeks - and why understanding the algorithm yourself is more useful than paying for the license.",[2001,31245],{},[40,31247,31249],{"id":31248},"does-it-actually-work","Does it actually work?",[11,31251,561,31252,31255,31256,31259],{},[118,31253,31254],{},"2022 study by the Nielsen Norman Group"," tested reading speed and comprehension with and without Bionic Reading on a sample of participants. The result: ",[118,31257,31258],{},"no statistically significant improvement"," in reading speed for neurotypical readers. In some cases, the visual noise actually slowed people down slightly.",[11,31261,31262,31263,31266,31267,31270],{},"A follow-up crowd-sourced experiment on Reddit's ",[15,31264,31265],{},"r/slatestarcodex"," reached similar conclusions - participants who ",[24,31268,31269],{},"believed"," they were reading faster showed the same WPM as the control group.",[11,31272,31273],{},"So why do so many people swear by it?",[11,31275,31276],{},"Two likely explanations:",[2086,31278,31279,31289],{},[1825,31280,31281,31284,31285,31288],{},[118,31282,31283],{},"The placebo effect"," - if you believe you're reading faster, the experience ",[24,31286,31287],{},"feels"," faster. Perception ≠ measurement.",[1825,31290,31291,31294],{},[118,31292,31293],{},"Neurodivergent benefit"," - users with ADHD and dyslexia consistently report genuine improvement. The bold anchors give scattered attention something to lock onto. This is anecdotally strong, even if RCT data is thin.",[2456,31296,31298],{"id":31297},"what-about-vowel-bolding","What about vowel-bolding?",[11,31300,31301,31302,31305],{},"An alternative approach bolds ",[118,31303,31304],{},"all vowels"," instead of the first letters.",[3283,31307,31308],{},[11,31309,31310,31313],{},[118,31311,31312],{},"The theory",": vowels carry more phonetic information, so highlighting them aids subvocalization-free reading. Independent testing suggests similar results to fixation-point bolding - marginal gains for most, potentially meaningful for some.",[11,31315,31316],{},"For neurotypical readers, it probably doesn't speed things up. For readers with ADHD or dyslexia, worth trying.",[40,31318,31320],{"id":31319},"the-variable-nobody-talks-about-font","The Variable Nobody Talks About: Font",[11,31322,31323,31324],{},"Here's something the Bionic Reading discourse almost entirely ignores: ",[118,31325,31326],{},"the font you're reading in may matter more than any typographic trick you apply on top of it.",[11,31328,31329,31330,31333,31334,31337],{},"A 2022 ACM study (",[24,31331,31332],{},"Towards Individuated Reading Experiences",") tested 16 fonts on 352 participants. The headline finding: switching a reader to their optimal font produced an average ",[118,31335,31336],{},"35% change in reading speed"," - with stable comprehension. No bionic bolding, no special algorithm. Just the right typeface.",[11,31339,31340],{},"The serif vs. sans-serif debate turned out to be mostly noise. What mattered more was individual fit. For the same reader, Arial might beat Georgia by 40% - or the reverse. There's no universal winner.",[11,31342,31343,31344,31347,31348,968,31351,31354],{},"That said, on a ",[118,31345,31346],{},"group average",", passages in ",[118,31349,31350],{},"Roboto",[118,31352,31353],{},"Arial"," were read significantly faster than other tested fonts. Both are grotesque sans-serifs with open apertures and high x-height - properties that consistently correlate with screen legibility. The Nielsen Norman Group's research echoes this: no single best font exists, but a handful of well-designed sans-serifs (Roboto, Inter, Arial, Helvetica) tend to cluster near the top.",[11,31356,31357],{},[118,31358,31359],{},"What the research agrees on:",[1822,31361,31362,31368,31374,31380],{},[1825,31363,31364,31367],{},[118,31365,31366],{},"x-height"," - taller lowercase letters (relative to caps) improve legibility at small sizes",[1825,31369,31370,31373],{},[118,31371,31372],{},"Letter spacing"," - slightly looser than default aids readers with dyslexia significantly",[1825,31375,31376,31379],{},[118,31377,31378],{},"Line height"," - 1.5–1.7× font size is the sweet spot for body text on screen",[1825,31381,31382,31385],{},[118,31383,31384],{},"Line length"," - 65–75 characters per line. Longer lines increase saccade cost; shorter lines break rhythm.",[40,31387,31389],{"id":31388},"the-algorithm-three-approaches","The Algorithm - Three Approaches",[2456,31391,31393],{"id":31392},"_1-fixed-ratio-40","1. Fixed Ratio (40%)",[11,31395,31396],{},"Bold the first 40% of each word's letters. Simple, language-agnostic, and what most open-source tools use.",[255,31398,31400],{"className":16633,"code":31399,"language":16635,"meta":260,"style":260},"function bionicWord(word) {\n    const letters = word.replace(/[^a-zA-Z]/g, '')\n    if (letters.length \u003C= 1) return word\n    const boldLen = Math.ceil(letters.length * 0.4)\n    return `\u003Cb>${word.slice(0, boldLen)}\u003C/b>${word.slice(boldLen)}`\n}\n",[15,31401,31402,31407,31412,31417,31422,31427],{"__ignoreMap":260},[129,31403,31404],{"class":265,"line":266},[129,31405,31406],{},"function bionicWord(word) {\n",[129,31408,31409],{"class":265,"line":297},[129,31410,31411],{},"    const letters = word.replace(/[^a-zA-Z]/g, '')\n",[129,31413,31414],{"class":265,"line":315},[129,31415,31416],{},"    if (letters.length \u003C= 1) return word\n",[129,31418,31419],{"class":265,"line":332},[129,31420,31421],{},"    const boldLen = Math.ceil(letters.length * 0.4)\n",[129,31423,31424],{"class":265,"line":339},[129,31425,31426],{},"    return `\u003Cb>${word.slice(0, boldLen)}\u003C/b>${word.slice(boldLen)}`\n",[129,31428,31429],{"class":265,"line":356},[129,31430,1530],{},[2456,31432,31434],{"id":31433},"_2-length-based-steps","2. Length-Based Steps",[11,31436,31437],{},"This is closer to Casutt's original patent - the bold length jumps at discrete word-length thresholds rather than scaling continuously. Slightly more \"natural\" reading rhythm.",[255,31439,31441],{"className":16633,"code":31440,"language":16635,"meta":260,"style":260},"function boldLength(wordLen) {\n    if (wordLen \u003C= 2)  return 1\n    if (wordLen \u003C= 5)  return 2\n    if (wordLen \u003C= 9)  return 3\n    if (wordLen \u003C= 12) return 4\n    return 5\n}\n\nfunction bionicWord(word) {\n    const letters = word.replace(/[^a-zA-Z]/g, '')\n    if (letters.length === 0) return word\n    const n = boldLength(letters.length)\n    return `\u003Cb>${word.slice(0, n)}\u003C/b>${word.slice(n)}`\n}\n",[15,31442,31443,31448,31453,31458,31463,31468,31473,31477,31481,31485,31489,31494,31499,31504],{"__ignoreMap":260},[129,31444,31445],{"class":265,"line":266},[129,31446,31447],{},"function boldLength(wordLen) {\n",[129,31449,31450],{"class":265,"line":297},[129,31451,31452],{},"    if (wordLen \u003C= 2)  return 1\n",[129,31454,31455],{"class":265,"line":315},[129,31456,31457],{},"    if (wordLen \u003C= 5)  return 2\n",[129,31459,31460],{"class":265,"line":332},[129,31461,31462],{},"    if (wordLen \u003C= 9)  return 3\n",[129,31464,31465],{"class":265,"line":339},[129,31466,31467],{},"    if (wordLen \u003C= 12) return 4\n",[129,31469,31470],{"class":265,"line":356},[129,31471,31472],{},"    return 5\n",[129,31474,31475],{"class":265,"line":651},[129,31476,1530],{},[129,31478,31479],{"class":265,"line":657},[129,31480,336],{"emptyLinePlaceholder":335},[129,31482,31483],{"class":265,"line":669},[129,31484,31406],{},[129,31486,31487],{"class":265,"line":693},[129,31488,31411],{},[129,31490,31491],{"class":265,"line":712},[129,31492,31493],{},"    if (letters.length === 0) return word\n",[129,31495,31496],{"class":265,"line":1521},[129,31497,31498],{},"    const n = boldLength(letters.length)\n",[129,31500,31501],{"class":265,"line":1527},[129,31502,31503],{},"    return `\u003Cb>${word.slice(0, n)}\u003C/b>${word.slice(n)}`\n",[129,31505,31506],{"class":265,"line":2295},[129,31507,1530],{},[2456,31509,31511],{"id":31510},"_3-vowel-highlighting","3. Vowel Highlighting",[11,31513,31514],{},"Instead of position-based bolding, emphasize every vowel. Interesting alternative - works well for romance languages.",[255,31516,31518],{"className":16633,"code":31517,"language":16635,"meta":260,"style":260},"function bionicVowels(word) {\n    return word.replace(/[aeiouAEIOU]/g, '\u003Cspan class=\"bold\">$&\u003C/span>')\n}\n",[15,31519,31520,31525,31530],{"__ignoreMap":260},[129,31521,31522],{"class":265,"line":266},[129,31523,31524],{},"function bionicVowels(word) {\n",[129,31526,31527],{"class":265,"line":297},[129,31528,31529],{},"    return word.replace(/[aeiouAEIOU]/g, '\u003Cspan class=\"bold\">$&\u003C/span>')\n",[129,31531,31532],{"class":265,"line":315},[129,31533,1530],{},[11,31535,31536,31537,31540],{},"For the rest of this article, we'll use the ",[118,31538,31539],{},"fixed ratio"," approach - it's the most portable and easiest to reason about.",[2001,31542],{},[40,31544,31546],{"id":31545},"interactive-demo","Interactive Demo",[11,31548,31549],{},"Toggle the button to see bionic reading applied to a sample paragraph in real time.",[31551,31552],"bionic-demo",{},[11,31554,31555],{},"Notice how the bold anchors naturally pull your eye across each line.",[2001,31557],{},[40,31559,31561],{"id":31560},"rsvp-spritz-one-word-at-a-time","RSVP & Spritz: One Word at a Time",[11,31563,31564],{},"While Bionic Reading works within normal reading flow, a completely different family of techniques eliminates the flow itself.",[11,31566,31567,31570],{},[118,31568,31569],{},"RSVP (Rapid Serial Visual Presentation)"," shows words one at a time, in place, at a controlled rate. No scanning, no saccades. Your eyes stay fixed and the text comes to you.",[11,31572,31573,31576,31577,31580],{},[118,31574,31575],{},"Spritz"," (launched 2014, patented) refined RSVP with a key insight: the eye doesn't land at the center of a word - it targets the ",[118,31578,31579],{},"Optimal Recognition Point (ORP)",", roughly 30–35% from the word's start. In Spritz, the ORP character is always displayed at the exact same pixel position on screen (highlighted in red), and the word is positioned around it asymmetrically. The result: zero eye movement even between words of different lengths.",[11,31582,31583],{},"This is almost certainly what you've seen circulating on social media - one word flashing in the center of the screen, a single letter lit up in a different color.",[2456,31585,31587],{"id":31586},"does-rsvp-actually-work","Does RSVP actually work?",[11,31589,31590],{},"Better than Bionic Reading on raw speed metrics - but with a serious catch.",[11,31592,31593,31594,362],{},"A 2015 ScienceDirect study on Spritz found that ",[118,31595,31596],{},"literal comprehension degrades significantly above ~400 WPM",[11,31598,31599],{},"The reason: normal reading isn't purely linear. Readers constantly re-fixate on previous words (regressions) for disambiguation. RSVP makes regression impossible by design.",[11,31601,8603,31602,31605],{},[118,31603,31604],{},"sweet spot is 250–350 WPM"," - marginally faster than average reading speed (~200–250 WPM), but with retained comprehension. Above 400 WPM you're training a parlor trick, not building a reading skill.",[2456,31607,31546],{"id":31608},"interactive-demo-1",[11,31610,31611],{},"Hit play and drag the WPM slider. Notice how 300 WPM feels manageable, but 500+ WPM turns comprehension into a blur. The vertical red tick marks show where the ORP is always anchored - your eyes never need to move.",[31613,31614],"rsvp-demo",{},[40,31616,31618],{"id":31617},"adding-a-toggle-to-your-website","Adding a Toggle to Your Website",[11,31620,31621],{},"Here's a complete, dependency-free implementation you can drop into any HTML page.",[255,31623,31625],{"className":16633,"code":31624,"language":16635,"meta":260,"style":260},"function bionicWord(word) {\n    const letters = word.replace(/[^a-zA-Z]/g, '')\n    if (letters.length \u003C= 1) return word\n    const boldLen = Math.ceil(letters.length * 0.4)\n    return `\u003Cb>${word.slice(0, boldLen)}\u003C/b>${word.slice(boldLen)}`\n}\n\nfunction toBionic(text) {\n    return text.split(' ').map(bionicWord).join(' ')\n}\n\n// Walk the DOM and apply bionic reading to all text nodes\nfunction applyBionicToNode(node) {\n    const SKIP_TAGS = new Set(['SCRIPT', 'STYLE', 'NOSCRIPT', 'CODE', 'PRE', 'B', 'STRONG'])\n\n    if (node.nodeType === Node.TEXT_NODE) {\n        const parent = node.parentNode\n        if (!parent || SKIP_TAGS.has(parent.tagName)) return\n        if (!node.textContent.trim()) return\n\n        const span = document.createElement('span')\n        span.setAttribute('data-bionic', 'true')\n        span.innerHTML = toBionic(node.textContent)\n        parent.replaceChild(span, node)\n    } else {\n        // Clone childNodes list — live NodeList changes as we modify the DOM\n        Array.from(node.childNodes).forEach(applyBionicToNode)\n    }\n}\n\n// Reverse: remove all [data-bionic] spans and restore original text\nfunction removeBionic(root) {\n    root.querySelectorAll('[data-bionic]').forEach(span => {\n        span.replaceWith(document.createTextNode(span.textContent))\n    })\n}\n\n// Hook up a toggle button\nconst btn = document.getElementById('bionic-toggle')\nlet active = false\n\nbtn.addEventListener('click', () => {\n    active = !active\n    if (active) {\n        applyBionicToNode(document.querySelector('article'))\n    } else {\n        removeBionic(document.querySelector('article'))\n    }\n    btn.textContent = active ? 'Disable Bionic Reading' : 'Enable Bionic Reading'\n})\n",[15,31626,31627,31631,31635,31639,31643,31647,31651,31655,31660,31665,31669,31673,31678,31683,31688,31692,31697,31702,31707,31712,31716,31721,31726,31731,31736,31741,31746,31751,31755,31759,31763,31768,31773,31778,31783,31788,31792,31796,31801,31806,31811,31815,31820,31825,31830,31835,31839,31844,31848,31853],{"__ignoreMap":260},[129,31628,31629],{"class":265,"line":266},[129,31630,31406],{},[129,31632,31633],{"class":265,"line":297},[129,31634,31411],{},[129,31636,31637],{"class":265,"line":315},[129,31638,31416],{},[129,31640,31641],{"class":265,"line":332},[129,31642,31421],{},[129,31644,31645],{"class":265,"line":339},[129,31646,31426],{},[129,31648,31649],{"class":265,"line":356},[129,31650,1530],{},[129,31652,31653],{"class":265,"line":651},[129,31654,336],{"emptyLinePlaceholder":335},[129,31656,31657],{"class":265,"line":657},[129,31658,31659],{},"function toBionic(text) {\n",[129,31661,31662],{"class":265,"line":669},[129,31663,31664],{},"    return text.split(' ').map(bionicWord).join(' ')\n",[129,31666,31667],{"class":265,"line":693},[129,31668,1530],{},[129,31670,31671],{"class":265,"line":712},[129,31672,336],{"emptyLinePlaceholder":335},[129,31674,31675],{"class":265,"line":1521},[129,31676,31677],{},"// Walk the DOM and apply bionic reading to all text nodes\n",[129,31679,31680],{"class":265,"line":1527},[129,31681,31682],{},"function applyBionicToNode(node) {\n",[129,31684,31685],{"class":265,"line":2295},[129,31686,31687],{},"    const SKIP_TAGS = new Set(['SCRIPT', 'STYLE', 'NOSCRIPT', 'CODE', 'PRE', 'B', 'STRONG'])\n",[129,31689,31690],{"class":265,"line":2300},[129,31691,336],{"emptyLinePlaceholder":335},[129,31693,31694],{"class":265,"line":2305},[129,31695,31696],{},"    if (node.nodeType === Node.TEXT_NODE) {\n",[129,31698,31699],{"class":265,"line":2311},[129,31700,31701],{},"        const parent = node.parentNode\n",[129,31703,31704],{"class":265,"line":2329},[129,31705,31706],{},"        if (!parent || SKIP_TAGS.has(parent.tagName)) return\n",[129,31708,31709],{"class":265,"line":2351},[129,31710,31711],{},"        if (!node.textContent.trim()) return\n",[129,31713,31714],{"class":265,"line":2387},[129,31715,336],{"emptyLinePlaceholder":335},[129,31717,31718],{"class":265,"line":2392},[129,31719,31720],{},"        const span = document.createElement('span')\n",[129,31722,31723],{"class":265,"line":2398},[129,31724,31725],{},"        span.setAttribute('data-bionic', 'true')\n",[129,31727,31728],{"class":265,"line":2441},[129,31729,31730],{},"        span.innerHTML = toBionic(node.textContent)\n",[129,31732,31733],{"class":265,"line":3246},[129,31734,31735],{},"        parent.replaceChild(span, node)\n",[129,31737,31738],{"class":265,"line":3251},[129,31739,31740],{},"    } else {\n",[129,31742,31743],{"class":265,"line":3263},[129,31744,31745],{},"        // Clone childNodes list — live NodeList changes as we modify the DOM\n",[129,31747,31748],{"class":265,"line":5055},[129,31749,31750],{},"        Array.from(node.childNodes).forEach(applyBionicToNode)\n",[129,31752,31753],{"class":265,"line":5073},[129,31754,6516],{},[129,31756,31757],{"class":265,"line":5106},[129,31758,1530],{},[129,31760,31761],{"class":265,"line":5136},[129,31762,336],{"emptyLinePlaceholder":335},[129,31764,31765],{"class":265,"line":5164},[129,31766,31767],{},"// Reverse: remove all [data-bionic] spans and restore original text\n",[129,31769,31770],{"class":265,"line":5190},[129,31771,31772],{},"function removeBionic(root) {\n",[129,31774,31775],{"class":265,"line":7751},[129,31776,31777],{},"    root.querySelectorAll('[data-bionic]').forEach(span => {\n",[129,31779,31780],{"class":265,"line":7796},[129,31781,31782],{},"        span.replaceWith(document.createTextNode(span.textContent))\n",[129,31784,31785],{"class":265,"line":7803},[129,31786,31787],{},"    })\n",[129,31789,31790],{"class":265,"line":7808},[129,31791,1530],{},[129,31793,31794],{"class":265,"line":7813},[129,31795,336],{"emptyLinePlaceholder":335},[129,31797,31798],{"class":265,"line":7862},[129,31799,31800],{},"// Hook up a toggle button\n",[129,31802,31803],{"class":265,"line":7911},[129,31804,31805],{},"const btn = document.getElementById('bionic-toggle')\n",[129,31807,31808],{"class":265,"line":7916},[129,31809,31810],{},"let active = false\n",[129,31812,31813],{"class":265,"line":29405},[129,31814,336],{"emptyLinePlaceholder":335},[129,31816,31817],{"class":265,"line":29412},[129,31818,31819],{},"btn.addEventListener('click', () => {\n",[129,31821,31822],{"class":265,"line":29417},[129,31823,31824],{},"    active = !active\n",[129,31826,31827],{"class":265,"line":29423},[129,31828,31829],{},"    if (active) {\n",[129,31831,31832],{"class":265,"line":29441},[129,31833,31834],{},"        applyBionicToNode(document.querySelector('article'))\n",[129,31836,31837],{"class":265,"line":29466},[129,31838,31740],{},[129,31840,31841],{"class":265,"line":29491},[129,31842,31843],{},"        removeBionic(document.querySelector('article'))\n",[129,31845,31846],{"class":265,"line":29496},[129,31847,6516],{},[129,31849,31850],{"class":265,"line":29502},[129,31851,31852],{},"    btn.textContent = active ? 'Disable Bionic Reading' : 'Enable Bionic Reading'\n",[129,31854,31855],{"class":265,"line":16480},[129,31856,31857],{},"})\n",[255,31859,31861],{"className":10399,"code":31860,"language":10400,"meta":260,"style":260},"\u003Cbutton id=\"bionic-toggle\">Enable Bionic Reading\u003C/button>\n\u003Carticle>\n    \u003Cp>Your content lives here...\u003C/p>\n\u003C/article>\n",[15,31862,31863,31891,31900,31918],{"__ignoreMap":260},[129,31864,31865,31867,31869,31871,31873,31875,31878,31880,31882,31885,31887,31889],{"class":265,"line":266},[129,31866,3945],{"class":277},[129,31868,16072],{"class":1376},[129,31870,4643],{"class":269},[129,31872,278],{"class":277},[129,31874,2258],{"class":277},[129,31876,31877],{"class":427},"bionic-toggle",[129,31879,2258],{"class":277},[129,31881,3956],{"class":277},[129,31883,31884],{"class":273},"Enable Bionic Reading",[129,31886,12609],{"class":277},[129,31888,16072],{"class":1376},[129,31890,4676],{"class":277},[129,31892,31893,31895,31898],{"class":265,"line":297},[129,31894,3945],{"class":277},[129,31896,31897],{"class":1376},"article",[129,31899,4676],{"class":277},[129,31901,31902,31905,31907,31909,31912,31914,31916],{"class":265,"line":315},[129,31903,31904],{"class":277},"    \u003C",[129,31906,11],{"class":1376},[129,31908,3956],{"class":277},[129,31910,31911],{"class":273},"Your content lives here...",[129,31913,12609],{"class":277},[129,31915,11],{"class":1376},[129,31917,4676],{"class":277},[129,31919,31920,31922,31924],{"class":265,"line":332},[129,31921,12609],{"class":277},[129,31923,31897],{"class":1376},[129,31925,4676],{"class":277},[11,31927,31928],{},"A few things worth noting:",[1822,31930,31931,31946,31953],{},[1825,31932,31933,31934,1862,31936,500,31939,1653,31942,31945],{},"We ",[118,31935,5898],{},[15,31937,31938],{},"\u003Ccode>",[15,31940,31941],{},"\u003Cpre>",[15,31943,31944],{},"\u003Cscript>"," tags - you don't want bionic reading applied to code blocks.",[1825,31947,31948,31949,31952],{},"We store a ",[15,31950,31951],{},"data-bionic"," attribute so we can cleanly reverse the transformation without refreshing the page.",[1825,31954,31955,31956,31959,31960,31962,31963,31966],{},"We clone ",[15,31957,31958],{},"childNodes"," into an ",[15,31961,1145],{}," before iterating - modifying a live ",[15,31964,31965],{},"NodeList"," while walking it causes nodes to get skipped.",[3576,31968,31969],{},[11,31970,31971,31972,31980,31981,32000,32001,32009,32010],{},"Instead of ",[15,31973,31974,31976,31978],{"className":10399,"language":10400,"style":260},[129,31975,3945],{"class":277},[129,31977,625],{"class":1376},[129,31979,3956],{"class":277},", it's better to use ",[15,31982,31983,31985,31987,31989,31991,31993,31996,31998],{"className":10399,"language":10400,"style":260},[129,31984,3945],{"class":277},[129,31986,129],{"class":1376},[129,31988,7254],{"class":269},[129,31990,278],{"class":277},[129,31992,2258],{"class":277},[129,31994,31995],{"class":427},"bold",[129,31997,2258],{"class":277},[129,31999,3956],{"class":277}," or something similar to preserve semantics - the ",[15,32002,32003,32005,32007],{"className":10399,"language":10400,"style":260},[129,32004,3945],{"class":277},[129,32006,625],{"class":1376},[129,32008,3956],{"class":277}," element ",[51,32011,32014],{"href":32012,"rel":32013},"https://developer.mozilla.org/en-US/docs/Web/HTML/Element/b",[55],"isn't meant for bolding content",[40,32016,32018],{"id":32017},"building-a-chrome-extension","Building a Chrome Extension",[11,32020,32021,32022,32025],{},"A browser extension is the most powerful delivery mechanism - it works on ",[24,32023,32024],{},"any"," website without touching the site's source code.",[2456,32027,32029],{"id":32028},"file-structure","File structure",[255,32031,32034],{"className":32032,"code":32033,"language":3237},[12199],"bionic-reader/\n├── manifest.json\n├── content.js\n├── popup.html\n└── popup.js\n",[15,32035,32033],{"__ignoreMap":260},[2456,32037,32039],{"id":32038},"manifestjson","manifest.json",[255,32041,32043],{"className":10988,"code":32042,"language":2289,"meta":260,"style":260},"{\n    \"manifest_version\": 3,\n    \"name\": \"Bionic Reader\",\n    \"version\": \"1.0\",\n    \"description\": \"Apply bionic reading to any webpage with one click.\",\n    \"permissions\": [\"activeTab\", \"storage\"],\n    \"action\": {\n        \"default_popup\": \"popup.html\",\n        \"default_title\": \"Bionic Reader\"\n    },\n    \"content_scripts\": [\n        {\n            \"matches\": [\"\u003Call_urls>\"],\n            \"js\": [\"content.js\"],\n            \"run_at\": \"document_idle\"\n        }\n    ]\n}\n",[15,32044,32045,32049,32065,32084,32103,32123,32155,32167,32188,32205,32209,32223,32228,32251,32272,32290,32295,32300],{"__ignoreMap":260},[129,32046,32047],{"class":265,"line":266},[129,32048,6455],{"class":277},[129,32050,32051,32054,32057,32059,32061,32063],{"class":265,"line":297},[129,32052,32053],{"class":277},"    \"",[129,32055,32056],{"class":269},"manifest_version",[129,32058,2258],{"class":277},[129,32060,1380],{"class":277},[129,32062,1018],{"class":290},[129,32064,1386],{"class":277},[129,32066,32067,32069,32071,32073,32075,32077,32080,32082],{"class":265,"line":315},[129,32068,32053],{"class":277},[129,32070,8164],{"class":269},[129,32072,2258],{"class":277},[129,32074,1380],{"class":277},[129,32076,11021],{"class":277},[129,32078,32079],{"class":427},"Bionic Reader",[129,32081,2258],{"class":277},[129,32083,1386],{"class":277},[129,32085,32086,32088,32090,32092,32094,32096,32099,32101],{"class":265,"line":332},[129,32087,32053],{"class":277},[129,32089,11938],{"class":269},[129,32091,2258],{"class":277},[129,32093,1380],{"class":277},[129,32095,11021],{"class":277},[129,32097,32098],{"class":427},"1.0",[129,32100,2258],{"class":277},[129,32102,1386],{"class":277},[129,32104,32105,32107,32110,32112,32114,32116,32119,32121],{"class":265,"line":339},[129,32106,32053],{"class":277},[129,32108,32109],{"class":269},"description",[129,32111,2258],{"class":277},[129,32113,1380],{"class":277},[129,32115,11021],{"class":277},[129,32117,32118],{"class":427},"Apply bionic reading to any webpage with one click.",[129,32120,2258],{"class":277},[129,32122,1386],{"class":277},[129,32124,32125,32127,32130,32132,32134,32136,32138,32141,32143,32145,32147,32150,32152],{"class":265,"line":356},[129,32126,32053],{"class":277},[129,32128,32129],{"class":269},"permissions",[129,32131,2258],{"class":277},[129,32133,1380],{"class":277},[129,32135,1010],{"class":277},[129,32137,2258],{"class":277},[129,32139,32140],{"class":427},"activeTab",[129,32142,2258],{"class":277},[129,32144,1015],{"class":277},[129,32146,11021],{"class":277},[129,32148,32149],{"class":427},"storage",[129,32151,2258],{"class":277},[129,32153,32154],{"class":277},"],\n",[129,32156,32157,32159,32161,32163,32165],{"class":265,"line":651},[129,32158,32053],{"class":277},[129,32160,12990],{"class":269},[129,32162,2258],{"class":277},[129,32164,1380],{"class":277},[129,32166,1371],{"class":277},[129,32168,32169,32172,32175,32177,32179,32181,32184,32186],{"class":265,"line":657},[129,32170,32171],{"class":277},"        \"",[129,32173,32174],{"class":2161},"default_popup",[129,32176,2258],{"class":277},[129,32178,1380],{"class":277},[129,32180,11021],{"class":277},[129,32182,32183],{"class":427},"popup.html",[129,32185,2258],{"class":277},[129,32187,1386],{"class":277},[129,32189,32190,32192,32195,32197,32199,32201,32203],{"class":265,"line":669},[129,32191,32171],{"class":277},[129,32193,32194],{"class":2161},"default_title",[129,32196,2258],{"class":277},[129,32198,1380],{"class":277},[129,32200,11021],{"class":277},[129,32202,32079],{"class":427},[129,32204,2292],{"class":277},[129,32206,32207],{"class":265,"line":693},[129,32208,19095],{"class":277},[129,32210,32211,32213,32216,32218,32220],{"class":265,"line":712},[129,32212,32053],{"class":277},[129,32214,32215],{"class":269},"content_scripts",[129,32217,2258],{"class":277},[129,32219,1380],{"class":277},[129,32221,32222],{"class":277}," [\n",[129,32224,32225],{"class":265,"line":1521},[129,32226,32227],{"class":277},"        {\n",[129,32229,32230,32233,32236,32238,32240,32242,32244,32247,32249],{"class":265,"line":1527},[129,32231,32232],{"class":277},"            \"",[129,32234,32235],{"class":2161},"matches",[129,32237,2258],{"class":277},[129,32239,1380],{"class":277},[129,32241,1010],{"class":277},[129,32243,2258],{"class":277},[129,32245,32246],{"class":427},"\u003Call_urls>",[129,32248,2258],{"class":277},[129,32250,32154],{"class":277},[129,32252,32253,32255,32257,32259,32261,32263,32265,32268,32270],{"class":265,"line":2295},[129,32254,32232],{"class":277},[129,32256,16635],{"class":2161},[129,32258,2258],{"class":277},[129,32260,1380],{"class":277},[129,32262,1010],{"class":277},[129,32264,2258],{"class":277},[129,32266,32267],{"class":427},"content.js",[129,32269,2258],{"class":277},[129,32271,32154],{"class":277},[129,32273,32274,32276,32279,32281,32283,32285,32288],{"class":265,"line":2300},[129,32275,32232],{"class":277},[129,32277,32278],{"class":2161},"run_at",[129,32280,2258],{"class":277},[129,32282,1380],{"class":277},[129,32284,11021],{"class":277},[129,32286,32287],{"class":427},"document_idle",[129,32289,2292],{"class":277},[129,32291,32292],{"class":265,"line":2305},[129,32293,32294],{"class":277},"        }\n",[129,32296,32297],{"class":265,"line":2311},[129,32298,32299],{"class":277},"    ]\n",[129,32301,32302],{"class":265,"line":2329},[129,32303,1530],{"class":277},[2456,32305,32267],{"id":32306},"contentjs",[255,32308,32310],{"className":16633,"code":32309,"language":16635,"meta":260,"style":260},"let active = false\n\nfunction bionicWord(word) {\n    const letters = word.replace(/[^a-zA-Z]/g, '')\n    if (letters.length \u003C= 1) return word\n    const boldLen = Math.ceil(letters.length * 0.4)\n    return `\u003Cb>${word.slice(0, boldLen)}\u003C/b>${word.slice(boldLen)}`\n}\n\nconst SKIP_TAGS = new Set(['SCRIPT', 'STYLE', 'NOSCRIPT', 'CODE', 'PRE', 'B', 'STRONG', 'INPUT', 'TEXTAREA'])\n\nfunction applyBionicToNode(node) {\n    if (node.nodeType === Node.TEXT_NODE) {\n        const parent = node.parentNode\n        if (!parent || SKIP_TAGS.has(parent.tagName)) return\n        if (!node.textContent.trim()) return\n        const span = document.createElement('span')\n        span.setAttribute('data-bionic', 'true')\n        span.innerHTML = node.textContent.split(' ').map(bionicWord).join(' ')\n        parent.replaceChild(span, node)\n    } else {\n        Array.from(node.childNodes).forEach(applyBionicToNode)\n    }\n}\n\nfunction removeBionic() {\n    document.querySelectorAll('[data-bionic]').forEach(span => {\n        span.replaceWith(document.createTextNode(span.textContent))\n    })\n}\n\nchrome.runtime.onMessage.addListener((msg) => {\n    if (msg.action !== 'toggle') return\n    active = !active\n    active ? applyBionicToNode(document.body) : removeBionic()\n    // Persist state so new tabs remember user preference\n    chrome.storage.local.set({ bionicActive: active })\n})\n\n// Restore state on page load\nchrome.storage.local.get('bionicActive', ({ bionicActive }) => {\n    if (bionicActive) {\n        active = true\n        applyBionicToNode(document.body)\n    }\n})\n",[15,32311,32312,32316,32320,32324,32328,32332,32336,32340,32344,32348,32353,32357,32361,32365,32369,32373,32377,32381,32385,32390,32394,32398,32402,32406,32410,32414,32419,32424,32428,32432,32436,32440,32445,32450,32454,32459,32464,32469,32473,32477,32482,32487,32492,32497,32502,32506],{"__ignoreMap":260},[129,32313,32314],{"class":265,"line":266},[129,32315,31810],{},[129,32317,32318],{"class":265,"line":297},[129,32319,336],{"emptyLinePlaceholder":335},[129,32321,32322],{"class":265,"line":315},[129,32323,31406],{},[129,32325,32326],{"class":265,"line":332},[129,32327,31411],{},[129,32329,32330],{"class":265,"line":339},[129,32331,31416],{},[129,32333,32334],{"class":265,"line":356},[129,32335,31421],{},[129,32337,32338],{"class":265,"line":651},[129,32339,31426],{},[129,32341,32342],{"class":265,"line":657},[129,32343,1530],{},[129,32345,32346],{"class":265,"line":669},[129,32347,336],{"emptyLinePlaceholder":335},[129,32349,32350],{"class":265,"line":693},[129,32351,32352],{},"const SKIP_TAGS = new Set(['SCRIPT', 'STYLE', 'NOSCRIPT', 'CODE', 'PRE', 'B', 'STRONG', 'INPUT', 'TEXTAREA'])\n",[129,32354,32355],{"class":265,"line":712},[129,32356,336],{"emptyLinePlaceholder":335},[129,32358,32359],{"class":265,"line":1521},[129,32360,31682],{},[129,32362,32363],{"class":265,"line":1527},[129,32364,31696],{},[129,32366,32367],{"class":265,"line":2295},[129,32368,31701],{},[129,32370,32371],{"class":265,"line":2300},[129,32372,31706],{},[129,32374,32375],{"class":265,"line":2305},[129,32376,31711],{},[129,32378,32379],{"class":265,"line":2311},[129,32380,31720],{},[129,32382,32383],{"class":265,"line":2329},[129,32384,31725],{},[129,32386,32387],{"class":265,"line":2351},[129,32388,32389],{},"        span.innerHTML = node.textContent.split(' ').map(bionicWord).join(' ')\n",[129,32391,32392],{"class":265,"line":2387},[129,32393,31735],{},[129,32395,32396],{"class":265,"line":2392},[129,32397,31740],{},[129,32399,32400],{"class":265,"line":2398},[129,32401,31750],{},[129,32403,32404],{"class":265,"line":2441},[129,32405,6516],{},[129,32407,32408],{"class":265,"line":3246},[129,32409,1530],{},[129,32411,32412],{"class":265,"line":3251},[129,32413,336],{"emptyLinePlaceholder":335},[129,32415,32416],{"class":265,"line":3263},[129,32417,32418],{},"function removeBionic() {\n",[129,32420,32421],{"class":265,"line":5055},[129,32422,32423],{},"    document.querySelectorAll('[data-bionic]').forEach(span => {\n",[129,32425,32426],{"class":265,"line":5073},[129,32427,31782],{},[129,32429,32430],{"class":265,"line":5106},[129,32431,31787],{},[129,32433,32434],{"class":265,"line":5136},[129,32435,1530],{},[129,32437,32438],{"class":265,"line":5164},[129,32439,336],{"emptyLinePlaceholder":335},[129,32441,32442],{"class":265,"line":5190},[129,32443,32444],{},"chrome.runtime.onMessage.addListener((msg) => {\n",[129,32446,32447],{"class":265,"line":7751},[129,32448,32449],{},"    if (msg.action !== 'toggle') return\n",[129,32451,32452],{"class":265,"line":7796},[129,32453,31824],{},[129,32455,32456],{"class":265,"line":7803},[129,32457,32458],{},"    active ? applyBionicToNode(document.body) : removeBionic()\n",[129,32460,32461],{"class":265,"line":7808},[129,32462,32463],{},"    // Persist state so new tabs remember user preference\n",[129,32465,32466],{"class":265,"line":7813},[129,32467,32468],{},"    chrome.storage.local.set({ bionicActive: active })\n",[129,32470,32471],{"class":265,"line":7862},[129,32472,31857],{},[129,32474,32475],{"class":265,"line":7911},[129,32476,336],{"emptyLinePlaceholder":335},[129,32478,32479],{"class":265,"line":7916},[129,32480,32481],{},"// Restore state on page load\n",[129,32483,32484],{"class":265,"line":29405},[129,32485,32486],{},"chrome.storage.local.get('bionicActive', ({ bionicActive }) => {\n",[129,32488,32489],{"class":265,"line":29412},[129,32490,32491],{},"    if (bionicActive) {\n",[129,32493,32494],{"class":265,"line":29417},[129,32495,32496],{},"        active = true\n",[129,32498,32499],{"class":265,"line":29423},[129,32500,32501],{},"        applyBionicToNode(document.body)\n",[129,32503,32504],{"class":265,"line":29441},[129,32505,6516],{},[129,32507,32508],{"class":265,"line":29466},[129,32509,31857],{},[2456,32511,32183],{"id":32512},"popuphtml",[255,32514,32516],{"className":10399,"code":32515,"language":10400,"meta":260,"style":260},"\u003C!DOCTYPE html>\n\u003Chtml>\n\u003Chead>\n    \u003Cmeta charset=\"UTF-8\" />\n    \u003Cstyle>\n        body { width: 200px; padding: 16px; font-family: sans-serif; background: #0d1117; color: #c8cdd6; }\n        h2 { font-size: 14px; margin: 0 0 12px; color: #fff; }\n        button {\n            width: 100%;\n            padding: 8px;\n            background: #eb3c48;\n            color: white;\n            border: none;\n            border-radius: 6px;\n            font-weight: bold;\n            cursor: pointer;\n        }\n        button:hover { background: #ff6570; }\n    \u003C/style>\n\u003C/head>\n\u003Cbody>\n    \u003Ch2>Bionic Reader\u003C/h2>\n    \u003Cbutton id=\"toggle\">Toggle Bionic Reading\u003C/button>\n    \u003Cscript src=\"popup.js\">\u003C/script>\n\u003C/body>\n\u003C/html>\n",[15,32517,32518,32530,32538,32546,32566,32574,32634,32678,32685,32697,32708,32722,32734,32746,32758,32770,32782,32786,32810,32819,32827,32835,32851,32879,32902,32910],{"__ignoreMap":260},[129,32519,32520,32523,32526,32528],{"class":265,"line":266},[129,32521,32522],{"class":277},"\u003C!",[129,32524,32525],{"class":1376},"DOCTYPE",[129,32527,7388],{"class":269},[129,32529,4676],{"class":277},[129,32531,32532,32534,32536],{"class":265,"line":297},[129,32533,3945],{"class":277},[129,32535,10400],{"class":1376},[129,32537,4676],{"class":277},[129,32539,32540,32542,32544],{"class":265,"line":315},[129,32541,3945],{"class":277},[129,32543,10426],{"class":1376},[129,32545,4676],{"class":277},[129,32547,32548,32550,32552,32555,32557,32559,32562,32564],{"class":265,"line":332},[129,32549,31904],{"class":277},[129,32551,15994],{"class":1376},[129,32553,32554],{"class":269}," charset",[129,32556,278],{"class":277},[129,32558,2258],{"class":277},[129,32560,32561],{"class":427},"UTF-8",[129,32563,2258],{"class":277},[129,32565,20832],{"class":277},[129,32567,32568,32570,32572],{"class":265,"line":339},[129,32569,31904],{"class":277},[129,32571,2026],{"class":1376},[129,32573,4676],{"class":277},[129,32575,32576,32579,32581,32583,32585,32588,32590,32593,32595,32597,32599,32602,32604,32607,32609,32612,32614,32616,32619,32621,32624,32626,32628,32630,32632],{"class":265,"line":356},[129,32577,32578],{"class":2161},"        body",[129,32580,1416],{"class":277},[129,32582,10228],{"class":10761},[129,32584,1380],{"class":277},[129,32586,32587],{"class":290}," 200px",[129,32589,7376],{"class":277},[129,32591,32592],{"class":10761}," padding",[129,32594,1380],{"class":277},[129,32596,17179],{"class":290},[129,32598,7376],{"class":277},[129,32600,32601],{"class":10761}," font-family",[129,32603,1380],{"class":277},[129,32605,32606],{"class":273}," sans-serif",[129,32608,7376],{"class":277},[129,32610,32611],{"class":10761}," background",[129,32613,1380],{"class":277},[129,32615,17114],{"class":277},[129,32617,32618],{"class":273},"0d1117",[129,32620,7376],{"class":277},[129,32622,32623],{"class":10761}," color",[129,32625,1380],{"class":277},[129,32627,17114],{"class":277},[129,32629,17153],{"class":273},[129,32631,7376],{"class":277},[129,32633,1476],{"class":277},[129,32635,32636,32639,32641,32644,32646,32649,32651,32654,32656,32658,32660,32663,32665,32667,32669,32671,32674,32676],{"class":265,"line":651},[129,32637,32638],{"class":2161},"        h2",[129,32640,1416],{"class":277},[129,32642,32643],{"class":10761}," font-size",[129,32645,1380],{"class":277},[129,32647,32648],{"class":290}," 14px",[129,32650,7376],{"class":277},[129,32652,32653],{"class":10761}," margin",[129,32655,1380],{"class":277},[129,32657,5698],{"class":290},[129,32659,5698],{"class":290},[129,32661,32662],{"class":290}," 12px",[129,32664,7376],{"class":277},[129,32666,32623],{"class":10761},[129,32668,1380],{"class":277},[129,32670,17114],{"class":277},[129,32672,32673],{"class":273},"fff",[129,32675,7376],{"class":277},[129,32677,1476],{"class":277},[129,32679,32680,32683],{"class":265,"line":657},[129,32681,32682],{"class":2161},"        button",[129,32684,1371],{"class":277},[129,32686,32687,32690,32692,32695],{"class":265,"line":669},[129,32688,32689],{"class":10761},"            width",[129,32691,1380],{"class":277},[129,32693,32694],{"class":290}," 100%",[129,32696,17120],{"class":277},[129,32698,32699,32702,32704,32706],{"class":265,"line":693},[129,32700,32701],{"class":10761},"            padding",[129,32703,1380],{"class":277},[129,32705,17165],{"class":290},[129,32707,17120],{"class":277},[129,32709,32710,32713,32715,32717,32720],{"class":265,"line":712},[129,32711,32712],{"class":10761},"            background",[129,32714,1380],{"class":277},[129,32716,17114],{"class":277},[129,32718,32719],{"class":273},"eb3c48",[129,32721,17120],{"class":277},[129,32723,32724,32727,32729,32732],{"class":265,"line":1521},[129,32725,32726],{"class":10761},"            color",[129,32728,1380],{"class":277},[129,32730,32731],{"class":273}," white",[129,32733,17120],{"class":277},[129,32735,32736,32739,32741,32744],{"class":265,"line":1527},[129,32737,32738],{"class":10761},"            border",[129,32740,1380],{"class":277},[129,32742,32743],{"class":273}," none",[129,32745,17120],{"class":277},[129,32747,32748,32751,32753,32756],{"class":265,"line":2295},[129,32749,32750],{"class":10761},"            border-radius",[129,32752,1380],{"class":277},[129,32754,32755],{"class":290}," 6px",[129,32757,17120],{"class":277},[129,32759,32760,32763,32765,32768],{"class":265,"line":2300},[129,32761,32762],{"class":10761},"            font-weight",[129,32764,1380],{"class":277},[129,32766,32767],{"class":273}," bold",[129,32769,17120],{"class":277},[129,32771,32772,32775,32777,32780],{"class":265,"line":2305},[129,32773,32774],{"class":10761},"            cursor",[129,32776,1380],{"class":277},[129,32778,32779],{"class":273}," pointer",[129,32781,17120],{"class":277},[129,32783,32784],{"class":265,"line":2311},[129,32785,32294],{"class":277},[129,32787,32788,32790,32792,32795,32797,32799,32801,32803,32806,32808],{"class":265,"line":2329},[129,32789,32682],{"class":2161},[129,32791,1380],{"class":277},[129,32793,32794],{"class":269},"hover",[129,32796,1416],{"class":277},[129,32798,32611],{"class":10761},[129,32800,1380],{"class":277},[129,32802,17114],{"class":277},[129,32804,32805],{"class":273},"ff6570",[129,32807,7376],{"class":277},[129,32809,1476],{"class":277},[129,32811,32812,32815,32817],{"class":265,"line":2351},[129,32813,32814],{"class":277},"    \u003C/",[129,32816,2026],{"class":1376},[129,32818,4676],{"class":277},[129,32820,32821,32823,32825],{"class":265,"line":2387},[129,32822,12609],{"class":277},[129,32824,10426],{"class":1376},[129,32826,4676],{"class":277},[129,32828,32829,32831,32833],{"class":265,"line":2392},[129,32830,3945],{"class":277},[129,32832,14959],{"class":1376},[129,32834,4676],{"class":277},[129,32836,32837,32839,32841,32843,32845,32847,32849],{"class":265,"line":2398},[129,32838,31904],{"class":277},[129,32840,40],{"class":1376},[129,32842,3956],{"class":277},[129,32844,32079],{"class":273},[129,32846,12609],{"class":277},[129,32848,40],{"class":1376},[129,32850,4676],{"class":277},[129,32852,32853,32855,32857,32859,32861,32863,32866,32868,32870,32873,32875,32877],{"class":265,"line":2441},[129,32854,31904],{"class":277},[129,32856,16072],{"class":1376},[129,32858,4643],{"class":269},[129,32860,278],{"class":277},[129,32862,2258],{"class":277},[129,32864,32865],{"class":427},"toggle",[129,32867,2258],{"class":277},[129,32869,3956],{"class":277},[129,32871,32872],{"class":273},"Toggle Bionic Reading",[129,32874,12609],{"class":277},[129,32876,16072],{"class":1376},[129,32878,4676],{"class":277},[129,32880,32881,32883,32885,32887,32889,32891,32894,32896,32898,32900],{"class":265,"line":3246},[129,32882,31904],{"class":277},[129,32884,10436],{"class":1376},[129,32886,10505],{"class":269},[129,32888,278],{"class":277},[129,32890,2258],{"class":277},[129,32892,32893],{"class":427},"popup.js",[129,32895,2258],{"class":277},[129,32897,10517],{"class":277},[129,32899,10436],{"class":1376},[129,32901,4676],{"class":277},[129,32903,32904,32906,32908],{"class":265,"line":3251},[129,32905,12609],{"class":277},[129,32907,14959],{"class":1376},[129,32909,4676],{"class":277},[129,32911,32912,32914,32916],{"class":265,"line":3263},[129,32913,12609],{"class":277},[129,32915,10400],{"class":1376},[129,32917,4676],{"class":277},[2456,32919,32893],{"id":32920},"popupjs",[255,32922,32924],{"className":16633,"code":32923,"language":16635,"meta":260,"style":260},"document.getElementById('toggle').addEventListener('click', () => {\n    chrome.tabs.query({ active: true, currentWindow: true }, ([tab]) => {\n        chrome.tabs.sendMessage(tab.id, { action: 'toggle' })\n    })\n})\n",[15,32925,32926,32931,32936,32941,32945],{"__ignoreMap":260},[129,32927,32928],{"class":265,"line":266},[129,32929,32930],{},"document.getElementById('toggle').addEventListener('click', () => {\n",[129,32932,32933],{"class":265,"line":297},[129,32934,32935],{},"    chrome.tabs.query({ active: true, currentWindow: true }, ([tab]) => {\n",[129,32937,32938],{"class":265,"line":315},[129,32939,32940],{},"        chrome.tabs.sendMessage(tab.id, { action: 'toggle' })\n",[129,32942,32943],{"class":265,"line":332},[129,32944,31787],{},[129,32946,32947],{"class":265,"line":339},[129,32948,31857],{},[11,32950,32951,32952,32955,32956,32959,32960,32963,32964,32967],{},"To install locally: open ",[15,32953,32954],{},"chrome://extensions",", enable ",[118,32957,32958],{},"Developer Mode",", click ",[118,32961,32962],{},"Load unpacked",", and point it at your ",[15,32965,32966],{},"bionic-reader/"," folder. Done.",[2001,32969],{},[40,32971,32973],{"id":32972},"comparison-which-technique-is-the-best","Comparison: which technique is the best?",[59,32975,32976,32992],{},[62,32977,32978],{},[65,32979,32980,32983,32986,32989],{},[68,32981,32982],{},"Technique",[68,32984,32985],{},"Avg. Speed Gain",[68,32987,32988],{},"Comprehension",[68,32990,32991],{},"Best For",[78,32993,32994,33010,33025,33041],{},[65,32995,32996,33001,33004,33007],{},[83,32997,32998],{},[118,32999,33000],{},"Font optimization",[83,33002,33003],{},"0–35%",[83,33005,33006],{},"Stable",[83,33008,33009],{},"Everyone — fix this first",[65,33011,33012,33016,33019,33022],{},[83,33013,33014],{},[118,33015,31235],{},[83,33017,33018],{},"~0% neurotypical",[83,33020,33021],{},"Neutral/slight drop",[83,33023,33024],{},"ADHD, dyslexia",[65,33026,33027,33032,33035,33038],{},[83,33028,33029],{},[118,33030,33031],{},"RSVP / Spritz",[83,33033,33034],{},"20–50% at ≤350 WPM",[83,33036,33037],{},"Degrades above 400 WPM",[83,33039,33040],{},"Power users, dense material",[65,33042,33043,33048,33051,33054],{},[83,33044,33045],{},[118,33046,33047],{},"Vowel highlighting",[83,33049,33050],{},"~0%",[83,33052,33053],{},"Neutral",[83,33055,33056],{},"Interesting experiment",[11,33058,33059,33060,33063],{},"The pecking order is clear: ",[118,33061,33062],{},"font and typography first",", RSVP if you want measurable speed, Bionic Reading as an accessibility option rather than a universal speed hack.",[2001,33065],{},[11,33067,33068],{},"These techniques go from \"barely noticeable\" to \"completely reimagines reading.\" None of them is a universal fix.",[11,33070,33071],{},"Reading experience is highly individual - font preference alone accounts for a 35% swing in reading speed.",[2026,33073,33074],{},"html .light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html.light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .sMK4o, html code.shiki .sMK4o{--shiki-light:#39ADB5;--shiki-default:#89DDFF;--shiki-dark:#89DDFF}html pre.shiki code .swJcz, html code.shiki .swJcz{--shiki-light:#E53935;--shiki-default:#F07178;--shiki-dark:#F07178}html pre.shiki code .spNyl, html code.shiki .spNyl{--shiki-light:#9C3EDA;--shiki-default:#C792EA;--shiki-dark:#C792EA}html pre.shiki code .sfazB, html code.shiki .sfazB{--shiki-light:#91B859;--shiki-default:#C3E88D;--shiki-dark:#C3E88D}html pre.shiki code .sTEyZ, html code.shiki .sTEyZ{--shiki-light:#90A4AE;--shiki-default:#EEFFFF;--shiki-dark:#BABED8}html pre.shiki code .sbssI, html code.shiki .sbssI{--shiki-light:#F76D47;--shiki-default:#F78C6C;--shiki-dark:#F78C6C}html pre.shiki code .sBMFI, html code.shiki .sBMFI{--shiki-light:#E2931D;--shiki-default:#FFCB6B;--shiki-dark:#FFCB6B}html pre.shiki code .sqsOY, html code.shiki .sqsOY{--shiki-light:#8796B0;--shiki-default:#B2CCD6;--shiki-dark:#B2CCD6}",{"title":260,"searchDepth":297,"depth":297,"links":33076},[33077,33078,33081,33082,33087,33088,33092,33093,33100],{"id":31224,"depth":297,"text":31225},{"id":31248,"depth":297,"text":31249,"children":33079},[33080],{"id":31297,"depth":315,"text":31298},{"id":31319,"depth":297,"text":31320},{"id":31388,"depth":297,"text":31389,"children":33083},[33084,33085,33086],{"id":31392,"depth":315,"text":31393},{"id":31433,"depth":315,"text":31434},{"id":31510,"depth":315,"text":31511},{"id":31545,"depth":297,"text":31546},{"id":31560,"depth":297,"text":31561,"children":33089},[33090,33091],{"id":31586,"depth":315,"text":31587},{"id":31608,"depth":315,"text":31546},{"id":31617,"depth":297,"text":31618},{"id":32017,"depth":297,"text":32018,"children":33094},[33095,33096,33097,33098,33099],{"id":32028,"depth":315,"text":32029},{"id":32038,"depth":315,"text":32039},{"id":32306,"depth":315,"text":32267},{"id":32512,"depth":315,"text":32183},{"id":32920,"depth":315,"text":32893},{"id":32972,"depth":297,"text":32973},"2026-02-26","Bionic Reading promises to make you read faster by bolding the first letters of every word. But does the research hold up? And how to actually implement it?",{},"/blog/bionic-reading",{"title":31219,"description":33102},"blog/bionic-reading",[1716,17765,33108,33109],"Browser Extensions","Typography","HUu72fieEimFsIShCik81FLMRb63_4bTwMWgr_k61TI",{"id":33112,"title":33113,"body":33114,"cover":2042,"date":33101,"description":33297,"extension":2045,"meta":33298,"navigation":335,"path":33299,"readingTime":356,"seo":33300,"stem":33301,"tags":33302,"__hash__":33306},"blog/blog/claude-code-conductor.md","AI + Code: New Era",{"type":8,"value":33115,"toc":33285},[33116,33120,33123,33126,33130,33137,33142,33145,33148,33152,33155,33159,33166,33170,33177,33181,33194,33204,33207,33211,33214,33217,33220,33223,33227,33230,33233,33239,33242,33246,33249,33252,33259,33262,33265,33269,33272,33275,33278,33280],[40,33117,33119],{"id":33118},"the-conductor-era-why-the-future-programmer-wont-write-code","The conductor era: Why the future programmer won't write code",[11,33121,33122],{},"I was interested in AI before the whole boom, even before I started programming, so topics related to it are particularly close to me.",[11,33124,33125],{},"When the first ChatGPT appeared, I was genuinely impressed - this wasn't the old Cleverbot, but a tool that could actually be useful and revolutionary. True intelligence won't emerge from transformers, I'm certain of that, but as an advanced tool it opens a lot of doors. I never thought AI would be capable of displacing developers though.",[40,33127,33129],{"id":33128},"a-free-business-recipe-for-now","A free business recipe (for now)",[11,33131,33132,33133,33136],{},"I've been testing Claude Code on side projects - stuff I build \"for the drawer\" - and I have to admit, it's an excellent tool. It ",[118,33134,33135],{},"significantly speeds up the work",". It's not yet ready for truly complex projects, or commercial ones where far more context is needed, but I can say this:",[1546,33138,33139],{},[11,33140,33141],{},"A mid-level developer could already start going to companies and offering internal process automation - not even for a regular salary, but for a cut of the costs saved.",[11,33143,33144],{},"Right now, a decent developer using this tool well can build finished products that meaningfully reduce company costs, sometimes significantly.",[11,33146,33147],{},"Write this down as a hustle method. If you're a good salesperson, this is genuinely a solid business idea.",[40,33149,33151],{"id":33150},"less-writing","Less writing",[11,33153,33154],{},"I think developers will gradually stop being musicians and start being conductors, it will significantly multiply the output of a single developer. And I genuinely feel for juniors, who in my opinion are already somewhat redundant. The first available AI model can complete a junior's tasks in a matter of moments.",[40,33156,33158],{"id":33157},"why-ai-will-never-fully-replace-developers","Why AI will never fully replace developers",[11,33160,33161,33162,33165],{},"It's not really about complexity. At some point AI will get to a level where it can handle even the most intricate projects. But one question will always remain: ",[118,33163,33164],{},"responsibility",". Who is accountable for that code? A developer. One instead of a whole team - but there will always be room for that person.",[40,33167,33169],{"id":33168},"context-problem-time-limits","Context problem, time limits",[11,33171,33172,33173,33176],{},"There are already tools like the ",[118,33174,33175],{},"Ralph Loop"," that build applications by chaining Claude Code tasks, which work autonomously across a long session - while the human is out for a walk. Someone ran 27 separate tasks over 4.5 hours and came back to a complete, working application.",[40,33178,33180],{"id":33179},"code-written-by-a-machine-used-by-humans","Code written by a machine, used by humans",[11,33182,33183,33184,33189,33190,33193],{},"This week, ",[51,33185,33188],{"href":33186,"rel":33187},"https://borischerny.com/",[55],"Boris"," - one of the creators of Claude Code at Anthropic - released a new tool called ",[118,33191,33192],{},"Cork",". Think of it as an application-layer version of Claude Code, a more packaged experience for broader use. Around the same time, someone asked Boris on X what percentage of the code in that project had been written by Claude.",[11,33195,33196],{},[51,33197,33200,33201,362],{"href":33198,"rel":33199},"https://x.com/bcherny/status/2010813886052581538",[55],"The answer: ",[24,33202,33203],{},"all of it",[11,33205,33206],{},"I'm not particularly surprised. You don't have to literally write code anymore - but you do need to know how to write it, and what it should look like. \"All of it\" is a slick marketing shortcut. AI did write every line, sure - but that's a bit like saying a voice-to-text app wrote my application because I dictated it instead of typing. Technically true.",[40,33208,33210],{"id":33209},"claude-code-is-not-just-for-coding","Claude code is not just for coding",[11,33212,33213],{},"One of the biggest misconceptions is that the name \"Claude Code\" implies it's a developer-only tool. It isn't.",[11,33215,33216],{},"It's very good at writing, debugging, and architecting software. But it also handles files, APIs, documentation, and structured workflows pretty well. Marketing teams can use it. Business analysts can use it. Non-technical founders can use it to build internal tools they'd previously have needed a developer for.",[11,33218,33219],{},"Small internal utilities that previously needed to be scoped, estimated, and handed to a dev for a sprint - now buildable by someone with minimal technical background and a clear goal. A transcription tool, a reporting script, an internal dashboard. Without hiring, without waiting, etc.",[11,33221,33222],{},"Teams that figure this out early are going to have a real advantage.",[40,33224,33226],{"id":33225},"the-trust-problem","The trust problem",[11,33228,33229],{},"Claude Code and tools like it solve a lot of problems, but one always remains: hallucinations. Claude can write an application in 5 hours that you can actually use - but are you sure everything in it works the way it should?",[11,33231,33232],{},"Some people solve this by throwing another model at it. Use Gemini as a verifier.",[11,33234,33235,33236],{},"The workflow looks something like this: Claude Code gives you a solution or architectural suggestion. You take that output, paste it into Gemini (or another capable model), and prompt it as if you received advice from a developer colleague - ",[24,33237,33238],{},"\"a friend wrote this for me, but I'm not 100% sure they got it right. Can you review it, spot any errors or gaps?\"",[11,33240,33241],{},"If both models agree, your confidence goes up. If they diverge, you know to dig deeper. It's not bulletproof - nothing with current AI tools is - but it's a useful sanity check.",[40,33243,33245],{"id":33244},"ai-and-the-job-market","AI and the job market",[11,33247,33248],{},"The developer job market is splitting.",[11,33250,33251],{},"One segment - likely a significant portion of junior and mid-level roles focused on routine implementation work - is genuinely at risk. If your primary value is translating requirements into code, that's exactly what AI does well.",[11,33253,33254,33255,33258],{},"The other segment - architects, engineers who understand systems deeply, people who can ",[24,33256,33257],{},"direct"," AI effectively and catch its mistakes - will likely see their leverage increase dramatically. One senior developer with strong AI orchestration skills can now produce what previously required a small team.",[11,33260,33261],{},"\"Directing AI effectively\" requires genuine depth. You can't direct an AI through a complex codebase if you don't understand what it's doing. The conductor still needs to know music theory.",[11,33263,33264],{},"The most dangerous position right now is a junior developer who decides AI tools make learning fundamentals unnecessary. You need the foundations - not to write boilerplate manually, but to evaluate whether the AI's output is correct, where its reasoning breaks down, and when to override it.",[40,33266,33268],{"id":33267},"what-to-actually-focus-on","What to actually focus on",[11,33270,33271],{},"I don't think the takeaway here is to be scared. Learn to conduct.",[11,33273,33274],{},"The skills that actually matter: understanding systems well enough to direct AI at the right level of abstraction, knowing when the output is wrong even when it looks right, breaking goals into tasks an agent can execute reliably, and cross-checking with multiple models instead of trusting one blindly.",[11,33276,33277],{},"The developer role isn't going away. It's changing faster than most people expected.",[2001,33279],{},[11,33281,33282],{},[24,33283,33284],{},"I'm actively experimenting with Claude Code on real projects - this portfolio site included.",{"title":260,"searchDepth":297,"depth":297,"links":33286},[33287,33288,33289,33290,33291,33292,33293,33294,33295,33296],{"id":33118,"depth":297,"text":33119},{"id":33128,"depth":297,"text":33129},{"id":33150,"depth":297,"text":33151},{"id":33157,"depth":297,"text":33158},{"id":33168,"depth":297,"text":33169},{"id":33179,"depth":297,"text":33180},{"id":33209,"depth":297,"text":33210},{"id":33225,"depth":297,"text":33226},{"id":33244,"depth":297,"text":33245},{"id":33267,"depth":297,"text":33268},"Claude Code changed how I think about software development. I realized the role of a developer is fundamentally shifting - from musician to conductor.",{},"/blog/claude-code-conductor",{"title":33113,"description":33297},"blog/claude-code-conductor",[3839,33303,33304,33305],"Claude Code","Career","Dev Tools","bLDIlNhNmBAotE8ZW4wUyYPDZTMFlv2PGPBGZHx8PCo",{"id":33308,"title":33309,"body":33310,"cover":2042,"date":33101,"description":34087,"extension":2045,"meta":34088,"navigation":335,"path":34089,"readingTime":657,"seo":34090,"stem":34091,"tags":34092,"__hash__":34094},"blog/blog/seo-ai-era.md","SEO in the AI era: is it still worth it?",{"type":8,"value":33311,"toc":34074},[33312,33315,33318,33322,33329,33344,33351,33360,33363,33367,33373,33381,33398,33401,33405,33412,33481,33491,33494,33497,33503,33509,33515,33522,33537,33541,33559,33562,33595,33601,33834,33840,33844,33850,33853,33867,33870,33874,33881,33884,33910,33913,33929,33933,33936,33982,33991,33995,33998,34001,34006,34071],[11,33313,33314],{},"Every 1,000 Google searches in 2025 result in roughly 360 clicks to websites outside of Google. That number was 440 a year ago.",[11,33316,33317],{},"SEO isn't dead, but a lot of the tactics that worked in 2022 either don't work anymore or actively hurt. The rules shifted enough that most SEO advice from that era is now misleading.",[40,33319,33321],{"id":33320},"what-actually-happened","What actually happened",[11,33323,33324,33325,33328],{},"Google's ",[118,33326,33327],{},"AI Overviews"," (formerly Search Generative Experience / SGE) began broad rollout in 2024 and expanded significantly through 2025. It's now present on roughly 13% of all queries - heavily concentrated in informational, top-of-funnel searches.",[11,33330,33331,33332,33337,33338,33343],{},"The CTR impact is not subtle. ",[51,33333,33336],{"href":33334,"rel":33335},"https://ahrefs.com/blog/ai-overviews-reduce-clicks-update/",[55],"Ahrefs' analysis"," found a 58% reduction in click-through rate when an AI Overview is present. ",[51,33339,33342],{"href":33340,"rel":33341},"https://www.seerinteractive.com/insights/aio-impact-on-google-ctr-september-2025-update",[55],"Seer Interactive's September 2025 data"," puts it at 61% - from 1.76% CTR down to 0.61%. Paid results fell 68%.",[11,33345,33346,33347,33350],{},"This isn't evenly distributed. ",[118,33348,33349],{},"Definitional and informational content is getting hit hardest."," HubSpot reportedly lost 70-80% of organic traffic. CNN lost 27-38%. Chegg lost 49% of non-subscriber traffic between January 2024 and January 2025. Sites built on \"what is X\" and \"how to Y\" articles are facing structural collapse.",[11,33352,33353,33354,33359],{},"Meanwhile, AI-referred traffic from ChatGPT, Perplexity, and similar tools grew 527% in 2025 ",[51,33355,33358],{"href":33356,"rel":33357},"https://superprompt.com/blog/ai-traffic-up-527-percent-how-to-get-cited-by-chatgpt-claude-perplexity-2025",[55],"across 400+ sites tracked by Superprompt",". The conversion rates are interesting: visitors from ChatGPT converted at 15.9% vs. Google organic at 1.76%. Small in absolute volume, but not negligible.",[11,33361,33362],{},"Two search paradigms now coexist: the Google era of ranked blue links, and the LLM era of AI-synthesized answers with citations. You need a presence in both.",[40,33364,33366],{"id":33365},"geo-the-new-optimization-target","GEO - the new optimization target",[11,33368,33369,33372],{},[118,33370,33371],{},"Generative Engine Optimization (GEO)"," is the practice of optimizing to be cited in AI-generated responses across Google, ChatGPT, Perplexity, and Claude. It's distinct from traditional SEO in one critical way: instead of 10 competing results per query, LLMs cite 2-7 domains on average. The competition is fiercer and less visible.",[11,33374,561,33375,33380],{},[51,33376,33379],{"href":33377,"rel":33378},"https://arxiv.org/pdf/2311.09735",[55],"Princeton/Columbia/UMass research paper"," that established much of the GEO framework identified the signals that actually increase citation frequency:",[1822,33382,33383,33386,33389,33392,33395],{},[1825,33384,33385],{},"Direct answers in the first 40-60 words of each section (AI extracts from the opening, not from the middle)",[1825,33387,33388],{},"One statistic or verifiable fact every 150-200 words",[1825,33390,33391],{},"Question-based headings that mirror how users phrase queries",[1825,33393,33394],{},"Content updated within the last 30 days gets cited 3.2x more than stale content",[1825,33396,33397],{},"Sites that themselves cite authoritative sources get cited more frequently",[11,33399,33400],{},"That last point is counterintuitive but consistent: LLMs reward intellectual honesty. A page that links out to sources reads as more trustworthy than one that doesn't.",[40,33402,33404],{"id":33403},"ai-bots-are-already-crawling-your-site","AI bots are already crawling your site",[11,33406,33407,33408,33411],{},"There are now multiple major crawlers hitting your ",[15,33409,33410],{},"robots.txt"," beyond Googlebot. Each has its own user agent:",[59,33413,33414,33427],{},[62,33415,33416],{},[65,33417,33418,33421,33424],{},[68,33419,33420],{},"Crawler",[68,33422,33423],{},"User agent",[68,33425,33426],{},"Purpose",[78,33428,33429,33442,33455,33468],{},[65,33430,33431,33434,33439],{},[83,33432,33433],{},"OpenAI",[83,33435,33436],{},[15,33437,33438],{},"GPTBot",[83,33440,33441],{},"ChatGPT training + live search",[65,33443,33444,33447,33452],{},[83,33445,33446],{},"Anthropic",[83,33448,33449],{},[15,33450,33451],{},"ClaudeBot",[83,33453,33454],{},"Claude training data",[65,33456,33457,33460,33465],{},[83,33458,33459],{},"Perplexity",[83,33461,33462],{},[15,33463,33464],{},"PerplexityBot",[83,33466,33467],{},"Perplexity search index",[65,33469,33470,33473,33478],{},[83,33471,33472],{},"Google (Gemini)",[83,33474,33475],{},[15,33476,33477],{},"Google-Extended",[83,33479,33480],{},"Gemini training (separate from search)",[11,33482,33483,33484,33487,33488,33490],{},"You can allow or deny each independently. The distinction between ",[15,33485,33486],{},"Googlebot"," (search ranking) and ",[15,33489,33477],{}," (Gemini training) is especially relevant - you can opt out of feeding Gemini's training data while still ranking in Search.",[33492,33493],"seo-robots-txt-demo",{},[11,33495,33496],{},"A few things worth knowing about how these bots actually work:",[11,33498,33499,33502],{},[118,33500,33501],{},"Perplexity doesn't purely crawl."," It calls the Bing search API to retrieve SERPs, then programmatically scrapes the top 5-10 results at query time. If your page loads slowly or returns non-200 status codes, it gets skipped.",[11,33504,33505,33508],{},[118,33506,33507],{},"Most LLMs don't crawl in real time."," The \"what the model knows\" (training data) and \"what the model retrieves at query time\" (RAG) are separate processes. Optimizing for AI citation affects both, but differently.",[11,33510,33511,33514],{},[118,33512,33513],{},"SSR and SSG are a genuine advantage here."," Bots like ClaudeBot and PerplexityBot handle JavaScript-rendered content poorly. If you're serving a fully client-side SPA, a significant portion of your content may be invisible to AI crawlers. Nuxt's static generation solves this cleanly.",[2456,33516,8603,33518,33521],{"id":33517},"the-llmstxt-proposal",[15,33519,33520],{},"llms.txt"," proposal",[11,33523,33524,33525,33527,33528,33530,33531,33536],{},"There's a proposed standard - ",[15,33526,33520],{}," - modeled on ",[15,33529,33410],{}," but designed to give LLMs a structured, plain-text summary of your site's content and purpose. Anthropic has endorsed it in their documentation. Evidence of actual traffic impact is currently thin (",[51,33532,33535],{"href":33533,"rel":33534},"https://searchengineland.com/mastering-generative-engine-optimization-in-2026-full-guide-469142",[55],"Search Engine Land tested 9 sites, 8 saw no measurable change","), but the implementation cost is near-zero and the direction is clear.",[40,33538,33540],{"id":33539},"schema-markup-is-the-connective-tissue","Schema markup is the connective tissue",[11,33542,33543,33546,33547,33552,33553,33558],{},[118,33544,33545],{},"Structured data matters more now, not less."," Pages with rich results earn ",[51,33548,33551],{"href":33549,"rel":33550},"https://writesonic.com/blog/structured-data-in-ai-search",[55],"82% higher CTR"," than unformatted pages. A DataWorld benchmark found LLMs grounded in structured knowledge achieve 300% higher factual accuracy. ",[51,33554,33557],{"href":33555,"rel":33556},"https://www.brightedge.com/blog/structured-data-ai-search-era",[55],"SearchVIU testing confirmed"," that ChatGPT, Claude, Perplexity, and Gemini all actively process Schema Markup when accessing content directly.",[11,33560,33561],{},"The most impactful schema types in 2026:",[1822,33563,33564,33574,33580,33589],{},[1825,33565,33566,33569,33570,33573],{},[15,33567,33568],{},"Article"," / ",[15,33571,33572],{},"BlogPosting"," - establishes authorship, publication date, and topic. Required.",[1825,33575,33576,33579],{},[15,33577,33578],{},"FAQPage"," - feeds directly into People Also Ask and AI-extracted Q&A. High ROI.",[1825,33581,33582,33569,33585,33588],{},[15,33583,33584],{},"Person",[15,33586,33587],{},"Organization"," - entity recognition for E-E-A-T signals",[1825,33590,33591,33594],{},[15,33592,33593],{},"BreadcrumbList"," - navigation context for crawlers",[11,33596,33597,33598,33600],{},"Use JSON-LD. Google recommends it, and it's cleanest for AI parsing. Here's the minimum viable ",[15,33599,33568],{}," schema:",[255,33602,33604],{"className":10988,"code":33603,"language":2289,"meta":260,"style":260},"{\n  \"@context\": \"https://schema.org\",\n  \"@type\": \"BlogPosting\",\n  \"headline\": \"Your article title\",\n  \"author\": {\n    \"@type\": \"Person\",\n    \"name\": \"Your Name\",\n    \"url\": \"https://yoursite.com/about\"\n  },\n  \"datePublished\": \"2026-02-26\",\n  \"dateModified\": \"2026-02-26\",\n  \"publisher\": {\n    \"@type\": \"Organization\",\n    \"name\": \"Site Name\"\n  }\n}\n",[15,33605,33606,33610,33630,33649,33669,33681,33699,33718,33736,33740,33759,33778,33791,33809,33826,33830],{"__ignoreMap":260},[129,33607,33608],{"class":265,"line":266},[129,33609,6455],{"class":277},[129,33611,33612,33614,33617,33619,33621,33623,33626,33628],{"class":265,"line":297},[129,33613,11000],{"class":277},[129,33615,33616],{"class":269},"@context",[129,33618,2258],{"class":277},[129,33620,1380],{"class":277},[129,33622,11021],{"class":277},[129,33624,33625],{"class":427},"https://schema.org",[129,33627,2258],{"class":277},[129,33629,1386],{"class":277},[129,33631,33632,33634,33637,33639,33641,33643,33645,33647],{"class":265,"line":315},[129,33633,11000],{"class":277},[129,33635,33636],{"class":269},"@type",[129,33638,2258],{"class":277},[129,33640,1380],{"class":277},[129,33642,11021],{"class":277},[129,33644,33572],{"class":427},[129,33646,2258],{"class":277},[129,33648,1386],{"class":277},[129,33650,33651,33653,33656,33658,33660,33662,33665,33667],{"class":265,"line":332},[129,33652,11000],{"class":277},[129,33654,33655],{"class":269},"headline",[129,33657,2258],{"class":277},[129,33659,1380],{"class":277},[129,33661,11021],{"class":277},[129,33663,33664],{"class":427},"Your article title",[129,33666,2258],{"class":277},[129,33668,1386],{"class":277},[129,33670,33671,33673,33675,33677,33679],{"class":265,"line":339},[129,33672,11000],{"class":277},[129,33674,5832],{"class":269},[129,33676,2258],{"class":277},[129,33678,1380],{"class":277},[129,33680,1371],{"class":277},[129,33682,33683,33685,33687,33689,33691,33693,33695,33697],{"class":265,"line":356},[129,33684,32053],{"class":277},[129,33686,33636],{"class":2161},[129,33688,2258],{"class":277},[129,33690,1380],{"class":277},[129,33692,11021],{"class":277},[129,33694,33584],{"class":427},[129,33696,2258],{"class":277},[129,33698,1386],{"class":277},[129,33700,33701,33703,33705,33707,33709,33711,33714,33716],{"class":265,"line":651},[129,33702,32053],{"class":277},[129,33704,8164],{"class":2161},[129,33706,2258],{"class":277},[129,33708,1380],{"class":277},[129,33710,11021],{"class":277},[129,33712,33713],{"class":427},"Your Name",[129,33715,2258],{"class":277},[129,33717,1386],{"class":277},[129,33719,33720,33722,33725,33727,33729,33731,33734],{"class":265,"line":657},[129,33721,32053],{"class":277},[129,33723,33724],{"class":2161},"url",[129,33726,2258],{"class":277},[129,33728,1380],{"class":277},[129,33730,11021],{"class":277},[129,33732,33733],{"class":427},"https://yoursite.com/about",[129,33735,2292],{"class":277},[129,33737,33738],{"class":265,"line":669},[129,33739,1481],{"class":277},[129,33741,33742,33744,33747,33749,33751,33753,33755,33757],{"class":265,"line":693},[129,33743,11000],{"class":277},[129,33745,33746],{"class":269},"datePublished",[129,33748,2258],{"class":277},[129,33750,1380],{"class":277},[129,33752,11021],{"class":277},[129,33754,33101],{"class":427},[129,33756,2258],{"class":277},[129,33758,1386],{"class":277},[129,33760,33761,33763,33766,33768,33770,33772,33774,33776],{"class":265,"line":712},[129,33762,11000],{"class":277},[129,33764,33765],{"class":269},"dateModified",[129,33767,2258],{"class":277},[129,33769,1380],{"class":277},[129,33771,11021],{"class":277},[129,33773,33101],{"class":427},[129,33775,2258],{"class":277},[129,33777,1386],{"class":277},[129,33779,33780,33782,33785,33787,33789],{"class":265,"line":1521},[129,33781,11000],{"class":277},[129,33783,33784],{"class":269},"publisher",[129,33786,2258],{"class":277},[129,33788,1380],{"class":277},[129,33790,1371],{"class":277},[129,33792,33793,33795,33797,33799,33801,33803,33805,33807],{"class":265,"line":1527},[129,33794,32053],{"class":277},[129,33796,33636],{"class":2161},[129,33798,2258],{"class":277},[129,33800,1380],{"class":277},[129,33802,11021],{"class":277},[129,33804,33587],{"class":427},[129,33806,2258],{"class":277},[129,33808,1386],{"class":277},[129,33810,33811,33813,33815,33817,33819,33821,33824],{"class":265,"line":2295},[129,33812,32053],{"class":277},[129,33814,8164],{"class":2161},[129,33816,2258],{"class":277},[129,33818,1380],{"class":277},[129,33820,11021],{"class":277},[129,33822,33823],{"class":427},"Site Name",[129,33825,2292],{"class":277},[129,33827,33828],{"class":265,"line":2300},[129,33829,1524],{"class":277},[129,33831,33832],{"class":265,"line":2305},[129,33833,1530],{"class":277},[11,33835,33836,33837,33839],{},"If you have FAQ content, add ",[15,33838,33578],{}," schema to that section.",[40,33841,33843],{"id":33842},"e-e-a-t-has-become-load-bearing","E-E-A-T has become load-bearing",[11,33845,33846,33849],{},[118,33847,33848],{},"Google's E-E-A-T"," (Experience, Expertise, Authoritativeness, Trustworthiness) framework predates AI Overviews, but the AI era has made it the primary differentiator. The flood of low-quality AI-generated content has pushed Google to double down on signals of real human expertise.",[11,33851,33852],{},"The practical requirements:",[1822,33854,33855,33858,33861,33864],{},[1825,33856,33857],{},"Named author bylines with verifiable credentials (LinkedIn, GitHub, publication history)",[1825,33859,33860],{},"Transparent \"last updated\" dates on all content pages",[1825,33862,33863],{},"Your own content should cite authoritative external sources inline - not just claim things",[1825,33865,33866],{},"For technical content: demonstrate the work. Screenshots, real results, code that actually runs.",[11,33868,33869],{},"The connection to AI citation is direct: Google doesn't just evaluate your page in isolation. It cross-references author credibility across the web. If your author has no presence outside your own domain, that absence is itself a signal.",[40,33871,33873],{"id":33872},"what-to-stop-doing-what-to-start","What to stop doing, what to start",[11,33875,33876,33877,33880],{},"The clearest strategic shift is this: ",[118,33878,33879],{},"stop optimizing for informational queries you can't win anymore."," Thin definitional articles - \"what is X\", \"how does Y work\" - are being absorbed by AI Overviews entirely. If your content can be fully answered in one paragraph, AI will answer it and not link to you.",[11,33882,33883],{},"The content types that still drive clicks:",[1822,33885,33886,33892,33898,33904],{},[1825,33887,33888,33891],{},[118,33889,33890],{},"Original research with unique data"," - AI engines cite you because nobody else has this",[1825,33893,33894,33897],{},[118,33895,33896],{},"Comparison and \"versus\" content"," - transactional intent that AI Overviews don't fully resolve",[1825,33899,33900,33903],{},[118,33901,33902],{},"In-depth tutorials with real code and screenshots"," - can't be synthesized from scratch",[1825,33905,33906,33909],{},[118,33907,33908],{},"Interactive tools"," - pure functionality that AI cannot replicate",[11,33911,33912],{},"Update your best existing content. Content refreshed within 30 days gets cited 3.2x more frequently. This is not expensive to do and it compounds.",[11,33914,33915,33916,500,33919,500,33922,1653,33925,33928],{},"Add AI referral tracking to your analytics now. Filter for ",[15,33917,33918],{},"chatgpt.com",[15,33920,33921],{},"perplexity.ai",[15,33923,33924],{},"claude.ai",[15,33926,33927],{},"gemini.google.com"," as referral sources. This traffic is small today and growing at 527% year-over-year. Getting visibility into it early matters.",[40,33930,33932],{"id":33931},"the-new-kpis","The new KPIs",[11,33934,33935],{},"The metrics that matter have changed:",[59,33937,33938,33948],{},[62,33939,33940],{},[65,33941,33942,33945],{},[68,33943,33944],{},"Old",[68,33946,33947],{},"New",[78,33949,33950,33958,33966,33974],{},[65,33951,33952,33955],{},[83,33953,33954],{},"Position 1 ranking",[83,33956,33957],{},"Cited in AI Overview",[65,33959,33960,33963],{},[83,33961,33962],{},"Organic CTR",[83,33964,33965],{},"Answer Inclusion Rate",[65,33967,33968,33971],{},[83,33969,33970],{},"Impressions",[83,33972,33973],{},"Brand mentions in AI responses",[65,33975,33976,33979],{},[83,33977,33978],{},"Session duration",[83,33980,33981],{},"Conversion rate from AI-referred traffic",[11,33983,33984,33985,33990],{},"Tools like ",[51,33986,33989],{"href":33987,"rel":33988},"https://www.profound.com",[55],"Profound"," and Brandwatch now track how often your brand appears in AI responses. This is the equivalent of rank tracking for the LLM era.",[40,33992,33994],{"id":33993},"the-foundation-still-matters","The foundation still matters",[11,33996,33997],{},"Traditional technical SEO - crawlability, Core Web Vitals, clean HTML, sitemaps - is still the foundation. AI crawlers are less tolerant of broken infrastructure than Googlebot, not more. But if that's all you're doing, you're competing for a shrinking slice of a shrinking pie.",[11,33999,34000],{},"Getting cited in AI answers requires being genuinely useful at a level AI can't synthesize on its own - original data, real demos, actual expertise. Honestly, that's a higher bar than keyword density, which is probably a good thing.",[11,34002,34003],{},[118,34004,34005],{},"Sources:",[1822,34007,34008,34014,34020,34026,34032,34038,34044,34050,34057,34064],{},[1825,34009,34010],{},[51,34011,34013],{"href":33334,"rel":34012},[55],"Ahrefs: AI Overviews reduce clicks by 58%",[1825,34015,34016],{},[51,34017,34019],{"href":33340,"rel":34018},[55],"Seer Interactive: AIO impact on CTR, September 2025",[1825,34021,34022],{},[51,34023,34025],{"href":33356,"rel":34024},[55],"Superprompt: AI traffic up 527% study (400+ sites)",[1825,34027,34028],{},[51,34029,34031],{"href":33377,"rel":34030},[55],"GEO research paper - Princeton/Columbia/UMass (arXiv)",[1825,34033,34034],{},[51,34035,34037],{"href":33533,"rel":34036},[55],"Search Engine Land: Mastering GEO in 2026",[1825,34039,34040],{},[51,34041,34043],{"href":33549,"rel":34042},[55],"Writesonic: Structured data in AI search",[1825,34045,34046],{},[51,34047,34049],{"href":33555,"rel":34048},[55],"BrightEdge: Structured data in the AI search era",[1825,34051,34052],{},[51,34053,34056],{"href":34054,"rel":34055},"https://thedigitalbloom.com/learn/2025-organic-traffic-crisis-analysis-report/",[55],"The Digital Bloom: 2025 organic traffic crisis report",[1825,34058,34059],{},[51,34060,34063],{"href":34061,"rel":34062},"https://seranking.com/blog/ai-traffic-research-study/",[55],"SE Ranking: AI traffic research study 2025",[1825,34065,34066],{},[51,34067,34070],{"href":34068,"rel":34069},"https://www.dataslayer.ai/blog/zero-click-searches-the-new-seo-reality-thats-killing-your-traffic",[55],"Dataslayer: Zero-click searches and CTR",[2026,34072,34073],{},"html pre.shiki code .sMK4o, html code.shiki .sMK4o{--shiki-light:#39ADB5;--shiki-default:#89DDFF;--shiki-dark:#89DDFF}html pre.shiki code .spNyl, html code.shiki .spNyl{--shiki-light:#9C3EDA;--shiki-default:#C792EA;--shiki-dark:#C792EA}html pre.shiki code .sfazB, html code.shiki .sfazB{--shiki-light:#91B859;--shiki-default:#C3E88D;--shiki-dark:#C3E88D}html pre.shiki code .sBMFI, html code.shiki .sBMFI{--shiki-light:#E2931D;--shiki-default:#FFCB6B;--shiki-dark:#FFCB6B}html .light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html.light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}",{"title":260,"searchDepth":297,"depth":297,"links":34075},[34076,34077,34078,34082,34083,34084,34085,34086],{"id":33320,"depth":297,"text":33321},{"id":33365,"depth":297,"text":33366},{"id":33403,"depth":297,"text":33404,"children":34079},[34080],{"id":33517,"depth":315,"text":34081},"The llms.txt proposal",{"id":33539,"depth":297,"text":33540},{"id":33842,"depth":297,"text":33843},{"id":33872,"depth":297,"text":33873},{"id":33931,"depth":297,"text":33932},{"id":33993,"depth":297,"text":33994},"AI Overviews slashed organic CTR by 61%. Zero-click searches hit 69% in the US. And yet SEO might matter more than ever.",{},"/blog/seo-ai-era",{"title":33309,"description":34087},"blog/seo-ai-era",[34093,3839,17766,16484],"SEO","l17G34sf6pkdyUFFCb1X2HY4GGF_SNXJOMm2lPdek1Q",{"id":34096,"title":34097,"body":34098,"cover":2042,"date":33101,"description":35995,"extension":2045,"meta":35996,"navigation":335,"path":35997,"readingTime":693,"seo":35998,"stem":35999,"tags":36000,"__hash__":36002},"blog/blog/web-fonts-performance.md","Web fonts are costing 500ms",{"type":8,"value":34099,"toc":35985},[34100,34113,34130,34134,34137,34142,34153,34192,34197,34263,34269,34273,34279,34294,34304,34310,34390,34393,34486,34495,34498,34502,34518,34521,34623,34634,34694,34710,34719,34728,34751,34755,34760,34773,35016,35037,35045,35058,35128,35136,35140,35147,35169,35183,35192,35201,35207,35216,35224,35228,35233,35236,35373,35389,35398,35437,35441,35447,35485,35488,35672,35675,35711,35717,35905,35918,35925,35929,35944,35950,35971,35973,35979,35982],[11,34101,34102,34103,34106,34107,34109,34110,34112],{},"Most sites load fonts the same way: a ",[15,34104,34105],{},"\u003Clink>"," to Google Fonts in ",[15,34108,8809],{},", no ",[15,34111,12502],{},", no subsetting, no fallback metrics. The font loads whenever it loads, text flashes in, CLS spikes.",[11,34114,34115,34116,34119,34120,500,34125,160],{},"A single Google Fonts request on a cold mobile connection adds ",[118,34117,34118],{},"300-800ms to LCP",". Two font families with separate stylesheets can compound to 1.5 seconds. (",[51,34121,34124],{"href":34122,"rel":34123},"https://web.dev/articles/font-best-practices",[55],"web.dev: Best practices for fonts",[51,34126,34129],{"href":34127,"rel":34128},"https://csswizardry.com/2020/05/the-fastest-google-fonts/",[55],"Harry Roberts: Google Fonts performance analysis",[40,34131,34133],{"id":34132},"how-the-browser-actually-loads-fonts","How the browser actually loads fonts",[11,34135,34136],{},"The browser doesn't request fonts when it parses CSS. It requests them when it finds a text node that needs them - after building the full render tree. By the time a font request fires, the browser has already fetched HTML, parsed it, fetched and parsed CSS, built the DOM and CSSOM, combined them into a render tree, and found a text node using that font family.",[3733,34138,34139],{},[11,34140,34141],{},"On a slow 4G connection this chain alone is 1-2 seconds.",[11,34143,34144,34145,34148,34149,34152],{},"Then, if you're loading from Google Fonts, two more round trips: one to ",[15,34146,34147],{},"fonts.googleapis.com"," for the stylesheet, another to ",[15,34150,34151],{},"fonts.gstatic.com"," for the actual binary. Two separate domains, two separate TLS handshakes.",[255,34154,34156],{"className":10399,"code":34155,"language":10400,"meta":260,"style":260},"\u003C!-- What most sites do: two extra connections, zero preloading -->\n\u003Clink href=\"https://fonts.googleapis.com/css2?family=Inter:wght@400;700\" rel=\"stylesheet\">\n",[15,34157,34158,34163],{"__ignoreMap":260},[129,34159,34160],{"class":265,"line":266},[129,34161,34162],{"class":376},"\u003C!-- What most sites do: two extra connections, zero preloading -->\n",[129,34164,34165,34167,34169,34171,34173,34175,34178,34180,34182,34184,34186,34188,34190],{"class":265,"line":297},[129,34166,3945],{"class":277},[129,34168,10405],{"class":1376},[129,34170,10485],{"class":269},[129,34172,278],{"class":277},[129,34174,2258],{"class":277},[129,34176,34177],{"class":427},"https://fonts.googleapis.com/css2?family=Inter:wght@400;700",[129,34179,2258],{"class":277},[129,34181,10408],{"class":269},[129,34183,278],{"class":277},[129,34185,2258],{"class":277},[129,34187,10415],{"class":427},[129,34189,2258],{"class":277},[129,34191,4676],{"class":277},[11,34193,8603,34194,34196],{},[15,34195,12502],{}," hint at least eliminates the handshake latency for those two origins:",[255,34198,34200],{"className":10399,"code":34199,"language":10400,"meta":260,"style":260},"\u003Clink rel=\"preconnect\" href=\"https://fonts.googleapis.com\">\n\u003Clink rel=\"preconnect\" href=\"https://fonts.gstatic.com\" crossorigin>\n",[15,34201,34202,34231],{"__ignoreMap":260},[129,34203,34204,34206,34208,34210,34212,34214,34216,34218,34220,34222,34224,34227,34229],{"class":265,"line":266},[129,34205,3945],{"class":277},[129,34207,10405],{"class":1376},[129,34209,10408],{"class":269},[129,34211,278],{"class":277},[129,34213,2258],{"class":277},[129,34215,12502],{"class":427},[129,34217,2258],{"class":277},[129,34219,10485],{"class":269},[129,34221,278],{"class":277},[129,34223,2258],{"class":277},[129,34225,34226],{"class":427},"https://fonts.googleapis.com",[129,34228,2258],{"class":277},[129,34230,4676],{"class":277},[129,34232,34233,34235,34237,34239,34241,34243,34245,34247,34249,34251,34253,34256,34258,34261],{"class":265,"line":297},[129,34234,3945],{"class":277},[129,34236,10405],{"class":1376},[129,34238,10408],{"class":269},[129,34240,278],{"class":277},[129,34242,2258],{"class":277},[129,34244,12502],{"class":427},[129,34246,2258],{"class":277},[129,34248,10485],{"class":269},[129,34250,278],{"class":277},[129,34252,2258],{"class":277},[129,34254,34255],{"class":427},"https://fonts.gstatic.com",[129,34257,2258],{"class":277},[129,34259,34260],{"class":269}," crossorigin",[129,34262,4676],{"class":277},[11,34264,34265,34266,34268],{},"But ",[15,34267,12502],{}," is a bandage. The real fix is getting rid of the third-party requests entirely.",[40,34270,34272],{"id":34271},"font-display-the-most-misunderstood-property","font-display: the most misunderstood property",[11,34274,34275,34278],{},[15,34276,34277],{},"font-display"," controls what happens during those loading delays. There are three distinct behaviors:",[11,34280,34281,34284,34285,34287,34288,34293],{},[118,34282,34283],{},"FOIT (Flash of Invisible Text)"," - the default when ",[15,34286,34277],{}," is omitted. ",[51,34289,34292],{"href":34290,"rel":34291},"https://developer.mozilla.org/en-US/docs/Web/CSS/@font-face/font-display",[55],"Browsers hide text for up to 3 seconds"," while the font loads. Safari historically waited forever. From a user's perspective: blank paragraphs that suddenly appear.",[11,34295,34296,34299,34300,34303],{},[118,34297,34298],{},"FOUT (Flash of Unstyled Text)"," - text renders immediately in the fallback system font, then swaps when the web font arrives. ",[15,34301,34302],{},"font-display: swap",". The \"flash\" causes a visual jump - CLS - unless you've calibrated the fallback.",[11,34305,34306,34309],{},[118,34307,34308],{},"font-display: optional"," - the browser gives the font 100ms. If it hasn't arrived, the fallback stays for the entire page load. The font is still downloaded in the background and cached for future visits. Zero CLS. The most aggressive but also the cleanest.",[255,34311,34313],{"className":10730,"code":34312,"language":10731,"meta":260,"style":260},"@font-face {\n  font-family: 'Inter';\n  src: url('/fonts/inter-latin.woff2') format('woff2');\n  font-display: swap; /* or optional, or fallback */\n}\n",[15,34314,34315,34322,34338,34371,34386],{"__ignoreMap":260},[129,34316,34317,34320],{"class":265,"line":266},[129,34318,34319],{"class":2139},"@font-face",[129,34321,1371],{"class":277},[129,34323,34324,34327,34329,34331,34334,34336],{"class":265,"line":297},[129,34325,34326],{"class":10761},"  font-family",[129,34328,1380],{"class":277},[129,34330,4261],{"class":277},[129,34332,34333],{"class":427},"Inter",[129,34335,424],{"class":277},[129,34337,17120],{"class":277},[129,34339,34340,34343,34345,34347,34349,34351,34354,34356,34358,34360,34362,34364,34367,34369],{"class":265,"line":315},[129,34341,34342],{"class":10761},"  src",[129,34344,1380],{"class":277},[129,34346,21683],{"class":284},[129,34348,147],{"class":277},[129,34350,424],{"class":277},[129,34352,34353],{"class":427},"/fonts/inter-latin.woff2",[129,34355,424],{"class":277},[129,34357,160],{"class":277},[129,34359,2682],{"class":284},[129,34361,147],{"class":277},[129,34363,424],{"class":277},[129,34365,34366],{"class":427},"woff2",[129,34368,424],{"class":277},[129,34370,25930],{"class":277},[129,34372,34373,34376,34378,34381,34383],{"class":265,"line":332},[129,34374,34375],{"class":10761},"  font-display",[129,34377,1380],{"class":277},[129,34379,34380],{"class":273}," swap",[129,34382,7376],{"class":277},[129,34384,34385],{"class":376}," /* or optional, or fallback */\n",[129,34387,34388],{"class":265,"line":339},[129,34389,1530],{"class":277},[11,34391,34392],{},"Which value to use depends on the context:",[59,34394,34395,34412],{},[62,34396,34397],{},[65,34398,34399,34401,34404,34407,34410],{},[68,34400,2471],{},[68,34402,34403],{},"Invisible period",[68,34405,34406],{},"Swap window",[68,34408,34409],{},"CLS risk",[68,34411,12127],{},[78,34413,34414,34432,34450,34468],{},[65,34415,34416,34421,34424,34427,34429],{},[83,34417,34418],{},[15,34419,34420],{},"block",[83,34422,34423],{},"3000ms",[83,34425,34426],{},"infinite",[83,34428,12692],{},[83,34430,34431],{},"Icon fonts only",[65,34433,34434,34439,34442,34444,34447],{},[83,34435,34436],{},[15,34437,34438],{},"swap",[83,34440,34441],{},"0ms",[83,34443,34426],{},[83,34445,34446],{},"High",[83,34448,34449],{},"Body text with tuned fallback",[65,34451,34452,34457,34460,34462,34465],{},[83,34453,34454],{},[15,34455,34456],{},"fallback",[83,34458,34459],{},"100ms",[83,34461,34423],{},[83,34463,34464],{},"Medium",[83,34466,34467],{},"General purpose",[65,34469,34470,34475,34477,34480,34483],{},[83,34471,34472],{},[15,34473,34474],{},"optional",[83,34476,34459],{},[83,34478,34479],{},"none",[83,34481,34482],{},"Zero",[83,34484,34485],{},"Decorative, non-critical",[11,34487,34488,34489,34491,34492,34494],{},"If the font is your LCP element - a big hero heading - use ",[15,34490,34438],{}," with proper fallback metrics (covered below). For everything else, ",[15,34493,34474],{}," is underrated and Lighthouse will thank you.",[34496,34497],"font-display-demo",{},[40,34499,34501],{"id":34500},"variable-fonts","Variable fonts",[11,34503,34504,34505,500,34508,500,34511,500,34514,34517],{},"A variable font encodes a continuous design space in a single file. Instead of ",[15,34506,34507],{},"font-regular.woff2",[15,34509,34510],{},"font-medium.woff2",[15,34512,34513],{},"font-bold.woff2",[15,34515,34516],{},"font-bold-italic.woff2"," - one file, any value along the weight axis from 100 to 900.",[11,34519,34520],{},"The five standard axes:",[59,34522,34523,34536],{},[62,34524,34525],{},[65,34526,34527,34530,34533],{},[68,34528,34529],{},"Axis",[68,34531,34532],{},"CSS property",[68,34534,34535],{},"Example",[78,34537,34538,34555,34572,34589,34606],{},[65,34539,34540,34545,34550],{},[83,34541,34542],{},[15,34543,34544],{},"wght",[83,34546,34547],{},[15,34548,34549],{},"font-weight",[83,34551,34552],{},[15,34553,34554],{},"font-weight: 350",[65,34556,34557,34562,34567],{},[83,34558,34559],{},[15,34560,34561],{},"wdth",[83,34563,34564],{},[15,34565,34566],{},"font-stretch",[83,34568,34569],{},[15,34570,34571],{},"font-stretch: 87.5%",[65,34573,34574,34579,34584],{},[83,34575,34576],{},[15,34577,34578],{},"ital",[83,34580,34581],{},[15,34582,34583],{},"font-style",[83,34585,34586],{},[15,34587,34588],{},"font-style: italic",[65,34590,34591,34596,34601],{},[83,34592,34593],{},[15,34594,34595],{},"slnt",[83,34597,34598],{},[15,34599,34600],{},"font-style: oblique",[83,34602,34603],{},[15,34604,34605],{},"font-style: oblique -12deg",[65,34607,34608,34613,34618],{},[83,34609,34610],{},[15,34611,34612],{},"opsz",[83,34614,34615],{},[15,34616,34617],{},"font-optical-sizing",[83,34619,34620],{},[15,34621,34622],{},"font-optical-sizing: auto",[11,34624,34625,34626,34629,34630,34633],{},"Custom axes use uppercase 4-character tags. Recursive has ",[15,34627,34628],{},"CASL"," (casualness), ",[15,34631,34632],{},"CRSV"," (cursive). You can animate them:",[255,34635,34637],{"className":10730,"code":34636,"language":10731,"meta":260,"style":260},"@keyframes weight-pulse {\n  from { font-weight: 100; }\n  to   { font-weight: 900; }\n}\n/* Variable font axes are animatable in CSS */\n",[15,34638,34639,34649,34667,34685,34689],{"__ignoreMap":260},[129,34640,34641,34644,34647],{"class":265,"line":266},[129,34642,34643],{"class":2139},"@keyframes",[129,34645,34646],{"class":452}," weight-pulse",[129,34648,1371],{"class":277},[129,34650,34651,34654,34656,34659,34661,34663,34665],{"class":265,"line":297},[129,34652,34653],{"class":2161},"  from",[129,34655,1416],{"class":277},[129,34657,34658],{"class":10761}," font-weight",[129,34660,1380],{"class":277},[129,34662,18732],{"class":290},[129,34664,7376],{"class":277},[129,34666,1476],{"class":277},[129,34668,34669,34672,34674,34676,34678,34681,34683],{"class":265,"line":315},[129,34670,34671],{"class":2161},"  to",[129,34673,14053],{"class":277},[129,34675,34658],{"class":10761},[129,34677,1380],{"class":277},[129,34679,34680],{"class":290}," 900",[129,34682,7376],{"class":277},[129,34684,1476],{"class":277},[129,34686,34687],{"class":265,"line":332},[129,34688,1530],{"class":277},[129,34690,34691],{"class":265,"line":339},[129,34692,34693],{"class":376},"/* Variable font axes are animatable in CSS */\n",[11,34695,34696,34697,11299,34700,34705,34706,34709],{},"The file size argument is where it gets interesting. For Roboto - 12 static styles (Thin through Black including italics) - the total woff2 payload is ",[118,34698,34699],{},"~840KB",[51,34701,34704],{"href":34702,"rel":34703},"https://fonts.google.com/specimen/Roboto+Flex",[55],"Roboto Flex"," (the variable version) is ",[118,34707,34708],{},"~370KB",". Roughly half, with infinite intermediary weights included.",[11,34711,34712,34713,968,34715,34718],{},"The pattern holds across most major font families. If you're loading more than two static styles from the same family, a variable font is almost always the right call. Under two styles - ",[15,34714,2801],{},[15,34716,34717],{},"700"," for body and headings - the overhead of the variable font design space may actually make it larger than the two individual files.",[11,34720,34721,34722,34727],{},"Browser support for variable fonts is ",[51,34723,34726],{"href":34724,"rel":34725},"https://caniuse.com/variable-fonts",[55],"~97% globally",". This has not been a concern since 2020.",[11,34729,34730,34731,34734,34735,34738,34739,34742,34743,34745,34746,500,34748,34750],{},"One footgun: ",[15,34732,34733],{},"font-variation-settings"," doesn't inherit correctly when you set only some axes. If you write ",[15,34736,34737],{},"font-variation-settings: 'wght' 700"," somewhere and ",[15,34740,34741],{},"font-variation-settings: 'GRAD' 1"," in a child, the child loses ",[15,34744,34544],{},". Use the high-level CSS properties (",[15,34747,34549],{},[15,34749,34566],{},") where possible - they cascade correctly.",[40,34752,34754],{"id":34753},"subsetting-the-highest-leverage-optimization","Subsetting: the highest-leverage optimization",[3283,34756,34757],{},[11,34758,34759],{},"A full Unicode Inter contains roughly 3,900 glyphs covering Latin, Cyrillic, Greek, Vietnamese, and more. If your site is English, you need about 220 of them.",[11,34761,34762,34765,34766,34769,34770,34772],{},[118,34763,34764],{},"Subsetting"," strips the unused glyphs from the font file. The ",[15,34767,34768],{},"unicode-range"," descriptor in ",[15,34771,34319],{}," tells the browser to only download that file when the page actually contains characters from that range:",[255,34774,34776],{"className":10730,"code":34775,"language":10731,"meta":260,"style":260},"@font-face {\n  font-family: 'Inter';\n  src: url('/fonts/inter-latin.woff2') format('woff2');\n  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC,\n                 U+2000-206F, U+20AC, U+2122, U+2212, U+FEFF, U+FFFD;\n}\n\n@font-face {\n  font-family: 'Inter';\n  src: url('/fonts/inter-latin-ext.woff2') format('woff2');\n  unicode-range: U+0100-02AF, U+1E00-1E9F, U+1EF2-1EFF, U+A720-A7FF;\n}\n",[15,34777,34778,34784,34798,34828,34870,34907,34911,34915,34921,34935,34966,35012],{"__ignoreMap":260},[129,34779,34780,34782],{"class":265,"line":266},[129,34781,34319],{"class":2139},[129,34783,1371],{"class":277},[129,34785,34786,34788,34790,34792,34794,34796],{"class":265,"line":297},[129,34787,34326],{"class":10761},[129,34789,1380],{"class":277},[129,34791,4261],{"class":277},[129,34793,34333],{"class":427},[129,34795,424],{"class":277},[129,34797,17120],{"class":277},[129,34799,34800,34802,34804,34806,34808,34810,34812,34814,34816,34818,34820,34822,34824,34826],{"class":265,"line":315},[129,34801,34342],{"class":10761},[129,34803,1380],{"class":277},[129,34805,21683],{"class":284},[129,34807,147],{"class":277},[129,34809,424],{"class":277},[129,34811,34353],{"class":427},[129,34813,424],{"class":277},[129,34815,160],{"class":277},[129,34817,2682],{"class":284},[129,34819,147],{"class":277},[129,34821,424],{"class":277},[129,34823,34366],{"class":427},[129,34825,424],{"class":277},[129,34827,25930],{"class":277},[129,34829,34830,34833,34835,34838,34840,34843,34845,34848,34850,34853,34855,34858,34860,34863,34865,34868],{"class":265,"line":332},[129,34831,34832],{"class":10761},"  unicode-range",[129,34834,1380],{"class":277},[129,34836,34837],{"class":273}," U+0000",[129,34839,6768],{"class":277},[129,34841,34842],{"class":273},"00FF",[129,34844,1015],{"class":277},[129,34846,34847],{"class":273}," U+0131",[129,34849,1015],{"class":277},[129,34851,34852],{"class":273}," U+0152",[129,34854,6768],{"class":277},[129,34856,34857],{"class":273},"0153",[129,34859,1015],{"class":277},[129,34861,34862],{"class":273}," U+02BB",[129,34864,6768],{"class":277},[129,34866,34867],{"class":273},"02BC",[129,34869,1386],{"class":277},[129,34871,34872,34875,34877,34880,34882,34885,34887,34890,34892,34895,34897,34900,34902,34905],{"class":265,"line":339},[129,34873,34874],{"class":273},"                 U+2000",[129,34876,6768],{"class":277},[129,34878,34879],{"class":273},"206F",[129,34881,1015],{"class":277},[129,34883,34884],{"class":273}," U+20AC",[129,34886,1015],{"class":277},[129,34888,34889],{"class":273}," U+2122",[129,34891,1015],{"class":277},[129,34893,34894],{"class":273}," U+2212",[129,34896,1015],{"class":277},[129,34898,34899],{"class":273}," U+FEFF",[129,34901,1015],{"class":277},[129,34903,34904],{"class":273}," U+FFFD",[129,34906,17120],{"class":277},[129,34908,34909],{"class":265,"line":356},[129,34910,1530],{"class":277},[129,34912,34913],{"class":265,"line":651},[129,34914,336],{"emptyLinePlaceholder":335},[129,34916,34917,34919],{"class":265,"line":657},[129,34918,34319],{"class":2139},[129,34920,1371],{"class":277},[129,34922,34923,34925,34927,34929,34931,34933],{"class":265,"line":669},[129,34924,34326],{"class":10761},[129,34926,1380],{"class":277},[129,34928,4261],{"class":277},[129,34930,34333],{"class":427},[129,34932,424],{"class":277},[129,34934,17120],{"class":277},[129,34936,34937,34939,34941,34943,34945,34947,34950,34952,34954,34956,34958,34960,34962,34964],{"class":265,"line":693},[129,34938,34342],{"class":10761},[129,34940,1380],{"class":277},[129,34942,21683],{"class":284},[129,34944,147],{"class":277},[129,34946,424],{"class":277},[129,34948,34949],{"class":427},"/fonts/inter-latin-ext.woff2",[129,34951,424],{"class":277},[129,34953,160],{"class":277},[129,34955,2682],{"class":284},[129,34957,147],{"class":277},[129,34959,424],{"class":277},[129,34961,34366],{"class":427},[129,34963,424],{"class":277},[129,34965,25930],{"class":277},[129,34967,34968,34970,34972,34975,34977,34980,34982,34985,34987,34990,34992,34995,34997,35000,35002,35005,35007,35010],{"class":265,"line":712},[129,34969,34832],{"class":10761},[129,34971,1380],{"class":277},[129,34973,34974],{"class":273}," U+0100",[129,34976,6768],{"class":277},[129,34978,34979],{"class":273},"02AF",[129,34981,1015],{"class":277},[129,34983,34984],{"class":273}," U+1E00",[129,34986,6768],{"class":277},[129,34988,34989],{"class":273},"1E9F",[129,34991,1015],{"class":277},[129,34993,34994],{"class":273}," U+1EF2",[129,34996,6768],{"class":277},[129,34998,34999],{"class":273},"1EFF",[129,35001,1015],{"class":277},[129,35003,35004],{"class":273}," U+A720",[129,35006,6768],{"class":277},[129,35008,35009],{"class":273},"A7FF",[129,35011,17120],{"class":277},[129,35013,35014],{"class":265,"line":1521},[129,35015,1530],{"class":277},[11,35017,35018,35019,35022,35023,35026,35027,500,35030,500,35033,35036],{},"The browser downloads ",[15,35020,35021],{},"inter-latin.woff2"," for an English page. ",[15,35024,35025],{},"inter-latin-ext.woff2"," only downloads if it finds characters like ",[15,35028,35029],{},"ě",[15,35031,35032],{},"ą",[15,35034,35035],{},"ñ",". This is exactly what Google Fonts has been doing automatically for years - it's why their fonts feel fast even without any optimization on your end.",[11,35038,35039,35040,160],{},"The file size impact is significant. Inter Regular at full Unicode is ~310KB. Latin subset: ~54KB. That's an 83% reduction. For Noto Sans (designed to cover all scripts) the difference is even more extreme - 556KB down to 22KB, a 96% reduction. (Numbers measured from Google Fonts downloads via ",[51,35041,35044],{"href":35042,"rel":35043},"https://almanac.httparchive.org/en/2024/fonts",[55],"HTTP Archive 2024 Web Almanac - Fonts chapter",[11,35046,35047,35048,35053,35054,35057],{},"For subsetting your own font files, the standard tool is ",[51,35049,35052],{"href":35050,"rel":35051},"https://fonttools.readthedocs.io/en/latest/subset/",[55],"pyftsubset"," from the ",[15,35055,35056],{},"fonttools"," Python library:",[255,35059,35061],{"className":2520,"code":35060,"language":2522,"meta":260,"style":260},"pip install fonttools brotli\n\npyftsubset inter.ttf \\\n  --output-file=inter-latin.woff2 \\\n  --flavor=woff2 \\\n  --layout-features='*' \\\n  --unicodes=\"U+0000-00FF,U+0131,U+0152-0153,U+2000-206F,U+20AC,U+2122\"\n",[15,35062,35063,35075,35079,35089,35096,35103,35116],{"__ignoreMap":260},[129,35064,35065,35067,35069,35072],{"class":265,"line":266},[129,35066,2529],{"class":2161},[129,35068,2532],{"class":427},[129,35070,35071],{"class":427}," fonttools",[129,35073,35074],{"class":427}," brotli\n",[129,35076,35077],{"class":265,"line":297},[129,35078,336],{"emptyLinePlaceholder":335},[129,35080,35081,35083,35086],{"class":265,"line":315},[129,35082,35052],{"class":2161},[129,35084,35085],{"class":427}," inter.ttf",[129,35087,35088],{"class":273}," \\\n",[129,35090,35091,35094],{"class":265,"line":332},[129,35092,35093],{"class":427},"  --output-file=inter-latin.woff2",[129,35095,35088],{"class":273},[129,35097,35098,35101],{"class":265,"line":339},[129,35099,35100],{"class":427},"  --flavor=woff2",[129,35102,35088],{"class":273},[129,35104,35105,35108,35110,35112,35114],{"class":265,"line":356},[129,35106,35107],{"class":427},"  --layout-features=",[129,35109,424],{"class":277},[129,35111,5501],{"class":427},[129,35113,424],{"class":277},[129,35115,35088],{"class":273},[129,35117,35118,35121,35123,35126],{"class":265,"line":651},[129,35119,35120],{"class":427},"  --unicodes=",[129,35122,2258],{"class":277},[129,35124,35125],{"class":427},"U+0000-00FF,U+0131,U+0152-0153,U+2000-206F,U+20AC,U+2122",[129,35127,2292],{"class":277},[11,35129,35130,35135],{},[51,35131,35134],{"href":35132,"rel":35133},"https://github.com/zachleat/glyphhanger",[55],"glyphhanger"," (by Zach Leatherman) takes a different approach - crawl a URL, analyze the actual characters used, subset to exactly those. Most aggressive, but requires re-running when content changes.",[40,35137,35139],{"id":35138},"the-self-hosting-argument","The self-hosting argument",[11,35141,35142,35143,35146],{},"Google Fonts CDN was convenient when it had a genuine performance argument: shared cache. If ",[15,35144,35145],{},"fonts.gstatic.com/inter.woff2"," was already in the browser cache from another site, your page loaded it instantly.",[11,35148,35149,35150,35155,35156,35159,35160,11299,35163,35168],{},"That argument died in October 2020 when ",[51,35151,35154],{"href":35152,"rel":35153},"https://developer.chrome.com/blog/http-cache-partitioning",[55],"Chrome 86 implemented cache partitioning",". Each origin gets its own cache namespace. A font cached from ",[15,35157,35158],{},"site-a.com"," is not reused for ",[15,35161,35162],{},"site-b.com",[51,35164,35167],{"href":35165,"rel":35166},"https://bugzilla.mozilla.org/show_bug.cgi?id=1590107",[55],"Firefox"," and Safari followed. There is no more cache sharing. The CDN performance advantage - already small with HTTP/2 - is now the only remaining case, and serving fonts from your own CDN (Cloudflare, Fastly, Bunny) eliminates even that.",[3325,35170,35171],{},[11,35172,35173,35174,35176,35177,35182],{},"When a user's browser contacts ",[15,35175,34147],{},", it sends an IP address to Google's US servers. Under GDPR, IP = personal data. The ",[51,35178,35181],{"href":35179,"rel":35180},"https://gdprhub.eu/index.php?title=LG_M%C3%BCnchen_-_3_O_17493/20",[55],"Landgericht München ruled in January 2022"," that embedding Google Fonts without consent violates GDPR Art. 6(1). The Austrian DPA (DSB) issued similar findings. €100 fine in that case, but the legal exposure is real for EU-facing sites.",[11,35184,35185,35188,35189,35191],{},[118,35186,35187],{},"Self-hosting is strictly better in 2025",": no third-party connections, full caching control (",[15,35190,12258],{},"), preload possible (you know the exact filename), zero GDPR exposure.",[11,35193,35194,35195,35200],{},"If you still want a CDN API for convenience - without the Google tracking - ",[51,35196,35199],{"href":35197,"rel":35198},"https://fonts.bunny.net",[55],"Bunny Fonts"," is a drop-in compatible replacement. Same API surface, servers in EU, explicit no-IP-logging policy. Just change the domain:",[255,35202,35205],{"className":35203,"code":35204,"language":3237},[12199],"# Google\nhttps://fonts.googleapis.com/css2?family=Inter:wght@400;700\n\n# Bunny (same response format)\nhttps://fonts.bunny.net/css?family=inter:400,700\n",[15,35206,35204],{"__ignoreMap":260},[11,35208,35209,35210,35215],{},"For typography that doesn't look like every other site using Montserrat and Lato: ",[51,35211,35214],{"href":35212,"rel":35213},"https://www.fontshare.com",[55],"Fontshare"," by Indian Type Foundry. Free, no tracking, professional quality - Satoshi, Cabinet Grotesk, Clash Display, General Sans.",[3576,35217,35218],{},[11,35219,35220,35221,35223],{},"Most are offered as variable fonts. ",[15,35222,15871],{}," supports it natively.",[40,35225,35227],{"id":35226},"fallback-metrics-and-zero-cls-swaps","Fallback metrics and zero-CLS swaps",[11,35229,35230,35232],{},[15,35231,34302],{}," causes CLS because the fallback system font (Arial, Helvetica) has different glyph metrics than the web font. Different x-height, different character width, different line height behavior. A paragraph that's 200px tall in Arial may be 215px in Inter. Everything below shifts.",[11,35234,35235],{},"CSS font metric override descriptors let you adjust how the fallback renders to match the web font:",[255,35237,35239],{"className":10730,"code":35238,"language":10731,"meta":260,"style":260},"@font-face {\n  font-family: 'Inter-fallback';\n  src: local('Arial');\n  size-adjust: 107.06%;\n  ascent-override: 76.99%;\n  descent-override: 19.2%;\n  line-gap-override: 0%;\n}\n\nbody {\n  font-family: 'Inter', 'Inter-fallback', sans-serif;\n}\n",[15,35240,35241,35247,35262,35281,35293,35305,35317,35329,35333,35337,35343,35369],{"__ignoreMap":260},[129,35242,35243,35245],{"class":265,"line":266},[129,35244,34319],{"class":2139},[129,35246,1371],{"class":277},[129,35248,35249,35251,35253,35255,35258,35260],{"class":265,"line":297},[129,35250,34326],{"class":10761},[129,35252,1380],{"class":277},[129,35254,4261],{"class":277},[129,35256,35257],{"class":427},"Inter-fallback",[129,35259,424],{"class":277},[129,35261,17120],{"class":277},[129,35263,35264,35266,35268,35271,35273,35275,35277,35279],{"class":265,"line":315},[129,35265,34342],{"class":10761},[129,35267,1380],{"class":277},[129,35269,35270],{"class":284}," local",[129,35272,147],{"class":277},[129,35274,424],{"class":277},[129,35276,31353],{"class":427},[129,35278,424],{"class":277},[129,35280,25930],{"class":277},[129,35282,35283,35286,35288,35291],{"class":265,"line":332},[129,35284,35285],{"class":10761},"  size-adjust",[129,35287,1380],{"class":277},[129,35289,35290],{"class":290}," 107.06%",[129,35292,17120],{"class":277},[129,35294,35295,35298,35300,35303],{"class":265,"line":339},[129,35296,35297],{"class":273},"  ascent-override",[129,35299,1380],{"class":277},[129,35301,35302],{"class":290}," 76.99%",[129,35304,17120],{"class":277},[129,35306,35307,35310,35312,35315],{"class":265,"line":356},[129,35308,35309],{"class":273},"  descent-override",[129,35311,1380],{"class":277},[129,35313,35314],{"class":290}," 19.2%",[129,35316,17120],{"class":277},[129,35318,35319,35322,35324,35327],{"class":265,"line":651},[129,35320,35321],{"class":273},"  line-gap-override",[129,35323,1380],{"class":277},[129,35325,35326],{"class":290}," 0%",[129,35328,17120],{"class":277},[129,35330,35331],{"class":265,"line":657},[129,35332,1530],{"class":277},[129,35334,35335],{"class":265,"line":669},[129,35336,336],{"emptyLinePlaceholder":335},[129,35338,35339,35341],{"class":265,"line":693},[129,35340,14959],{"class":2161},[129,35342,1371],{"class":277},[129,35344,35345,35347,35349,35351,35353,35355,35357,35359,35361,35363,35365,35367],{"class":265,"line":712},[129,35346,34326],{"class":10761},[129,35348,1380],{"class":277},[129,35350,4261],{"class":277},[129,35352,34333],{"class":427},[129,35354,424],{"class":277},[129,35356,1015],{"class":277},[129,35358,4261],{"class":277},[129,35360,35257],{"class":427},[129,35362,424],{"class":277},[129,35364,1015],{"class":277},[129,35366,32606],{"class":273},[129,35368,17120],{"class":277},[129,35370,35371],{"class":265,"line":1521},[129,35372,1530],{"class":277},[11,35374,35375,35378,35379,500,35382,500,35385,35388],{},[15,35376,35377],{},"size-adjust"," scales the entire glyph box to compensate for different UPM (units per em) between the fonts - the single most impactful value. The override descriptors (",[15,35380,35381],{},"ascent-override",[15,35383,35384],{},"descent-override",[15,35386,35387],{},"line-gap-override",") fine-tune the line box geometry. Done correctly, CLS from the font swap drops to effectively zero.",[11,35390,35391,35392,35397],{},"Calculating these values requires reading the font's OpenType metrics tables. ",[51,35393,35396],{"href":35394,"rel":35395},"https://github.com/pixel-point/fontpie",[55],"fontpie"," automates this:",[255,35399,35401],{"className":2520,"code":35400,"language":2522,"meta":260,"style":260},"npx fontpie inter.woff2 --name Inter --style normal --weight 400\n# Outputs ready-to-paste @font-face with correct overrides\n",[15,35402,35403,35432],{"__ignoreMap":260},[129,35404,35405,35408,35411,35414,35417,35420,35423,35426,35429],{"class":265,"line":266},[129,35406,35407],{"class":2161},"npx",[129,35409,35410],{"class":427}," fontpie",[129,35412,35413],{"class":427}," inter.woff2",[129,35415,35416],{"class":427}," --name",[129,35418,35419],{"class":427}," Inter",[129,35421,35422],{"class":427}," --style",[129,35424,35425],{"class":427}," normal",[129,35427,35428],{"class":427}," --weight",[129,35430,35431],{"class":290}," 400\n",[129,35433,35434],{"class":265,"line":297},[129,35435,35436],{"class":376},"# Outputs ready-to-paste @font-face with correct overrides\n",[40,35438,35440],{"id":35439},"nuxtfonts-the-shortcut","@nuxt/fonts: the shortcut",[11,35442,35443,35444,35446],{},"If you're on Nuxt, ",[15,35445,15871],{}," handles the entire workflow automatically. Given what we've covered, here's what it does at build time:",[2086,35448,35449,35456,35459,35466,35476,35482],{},[1825,35450,35451,35452,35455],{},"Scans CSS and Vue components for ",[15,35453,35454],{},"font-family"," references",[1825,35457,35458],{},"Downloads font files from the configured provider (Google, Bunny, Fontshare, local)",[1825,35460,35461,35462,35465],{},"Stores them locally and rewrites CSS ",[15,35463,35464],{},"src"," URLs to your own origin",[1825,35467,35468,35469,35472,35473,35475],{},"Runs ",[15,35470,35471],{},"fontaine"," internally to calculate ",[15,35474,35377],{}," and metric overrides for each font",[1825,35477,35478,35479,35481],{},"Generates fallback ",[15,35480,34319],{}," declarations with the correct values",[1825,35483,35484],{},"Injects everything - you write nothing manually",[11,35486,35487],{},"The config is minimal:",[255,35489,35491],{"className":3922,"code":35490,"filename":17649,"language":3924,"meta":260,"style":260},"export default defineNuxtConfig({\n  modules: ['@nuxt/fonts'],\n  fonts: {\n    families: [\n      { name: 'Inter', provider: 'google' },\n      { name: 'Cabinet Grotesk', provider: 'fontshare' },\n    ],\n    defaults: {\n      weights: [400, 700],\n      subsets: ['latin'],\n    },\n  },\n})\n",[15,35492,35493,35505,35524,35533,35542,35572,35602,35609,35618,35638,35658,35662,35666],{"__ignoreMap":260},[129,35494,35495,35497,35499,35501,35503],{"class":265,"line":266},[129,35496,4050],{"class":2139},[129,35498,4053],{"class":2139},[129,35500,19019],{"class":284},[129,35502,147],{"class":273},[129,35504,6455],{"class":277},[129,35506,35507,35510,35512,35514,35516,35518,35520,35522],{"class":265,"line":297},[129,35508,35509],{"class":1376},"  modules",[129,35511,1380],{"class":277},[129,35513,1010],{"class":273},[129,35515,424],{"class":277},[129,35517,15871],{"class":427},[129,35519,424],{"class":277},[129,35521,14170],{"class":273},[129,35523,1386],{"class":277},[129,35525,35526,35529,35531],{"class":265,"line":315},[129,35527,35528],{"class":1376},"  fonts",[129,35530,1380],{"class":277},[129,35532,1371],{"class":277},[129,35534,35535,35538,35540],{"class":265,"line":332},[129,35536,35537],{"class":1376},"    families",[129,35539,1380],{"class":277},[129,35541,32222],{"class":273},[129,35543,35544,35547,35549,35551,35553,35555,35557,35559,35561,35563,35565,35568,35570],{"class":265,"line":339},[129,35545,35546],{"class":277},"      {",[129,35548,5407],{"class":1376},[129,35550,1380],{"class":277},[129,35552,4261],{"class":277},[129,35554,34333],{"class":427},[129,35556,424],{"class":277},[129,35558,1015],{"class":277},[129,35560,5079],{"class":1376},[129,35562,1380],{"class":277},[129,35564,4261],{"class":277},[129,35566,35567],{"class":427},"google",[129,35569,424],{"class":277},[129,35571,1444],{"class":277},[129,35573,35574,35576,35578,35580,35582,35585,35587,35589,35591,35593,35595,35598,35600],{"class":265,"line":356},[129,35575,35546],{"class":277},[129,35577,5407],{"class":1376},[129,35579,1380],{"class":277},[129,35581,4261],{"class":277},[129,35583,35584],{"class":427},"Cabinet Grotesk",[129,35586,424],{"class":277},[129,35588,1015],{"class":277},[129,35590,5079],{"class":1376},[129,35592,1380],{"class":277},[129,35594,4261],{"class":277},[129,35596,35597],{"class":427},"fontshare",[129,35599,424],{"class":277},[129,35601,1444],{"class":277},[129,35603,35604,35607],{"class":265,"line":651},[129,35605,35606],{"class":273},"    ]",[129,35608,1386],{"class":277},[129,35610,35611,35614,35616],{"class":265,"line":657},[129,35612,35613],{"class":1376},"    defaults",[129,35615,1380],{"class":277},[129,35617,1371],{"class":277},[129,35619,35620,35623,35625,35627,35629,35631,35634,35636],{"class":265,"line":669},[129,35621,35622],{"class":1376},"      weights",[129,35624,1380],{"class":277},[129,35626,1010],{"class":273},[129,35628,2801],{"class":290},[129,35630,1015],{"class":277},[129,35632,35633],{"class":290}," 700",[129,35635,14170],{"class":273},[129,35637,1386],{"class":277},[129,35639,35640,35643,35645,35647,35649,35652,35654,35656],{"class":265,"line":693},[129,35641,35642],{"class":1376},"      subsets",[129,35644,1380],{"class":277},[129,35646,1010],{"class":273},[129,35648,424],{"class":277},[129,35650,35651],{"class":427},"latin",[129,35653,424],{"class":277},[129,35655,14170],{"class":273},[129,35657,1386],{"class":277},[129,35659,35660],{"class":265,"line":712},[129,35661,19095],{"class":277},[129,35663,35664],{"class":265,"line":1521},[129,35665,1481],{"class":277},[129,35667,35668,35670],{"class":265,"line":1527},[129,35669,4028],{"class":277},[129,35671,294],{"class":273},[11,35673,35674],{},"Or just use the font in CSS and the module detects it:",[255,35676,35678],{"className":10730,"code":35677,"language":10731,"meta":260,"style":260},"body {\n  font-family: 'Inter', sans-serif; /* @nuxt/fonts auto-downloads and self-hosts */\n}\n",[15,35679,35680,35686,35707],{"__ignoreMap":260},[129,35681,35682,35684],{"class":265,"line":266},[129,35683,14959],{"class":2161},[129,35685,1371],{"class":277},[129,35687,35688,35690,35692,35694,35696,35698,35700,35702,35704],{"class":265,"line":297},[129,35689,34326],{"class":10761},[129,35691,1380],{"class":277},[129,35693,4261],{"class":277},[129,35695,34333],{"class":427},[129,35697,424],{"class":277},[129,35699,1015],{"class":277},[129,35701,32606],{"class":273},[129,35703,7376],{"class":277},[129,35705,35706],{"class":376}," /* @nuxt/fonts auto-downloads and self-hosts */\n",[129,35708,35709],{"class":265,"line":315},[129,35710,1530],{"class":277},[11,35712,35713,35714,35716],{},"What you get in the built CSS without writing any ",[15,35715,34319],{}," rules:",[255,35718,35720],{"className":10730,"code":35719,"language":10731,"meta":260,"style":260},"@font-face {\n  font-family: 'Inter';\n  src: url('/_fonts/inter-latin.woff2') format('woff2');\n  font-weight: 100 900;\n  font-display: swap;\n  unicode-range: U+0000-00FF, ...;\n}\n\n@font-face {\n  font-family: 'Inter fallback';\n  src: local('Arial');\n  size-adjust: 107.06%;\n  ascent-override: 76.99%;\n  descent-override: 19.2%;\n  line-gap-override: 0%;\n}\n",[15,35721,35722,35728,35742,35773,35786,35796,35814,35818,35822,35828,35843,35861,35871,35881,35891,35901],{"__ignoreMap":260},[129,35723,35724,35726],{"class":265,"line":266},[129,35725,34319],{"class":2139},[129,35727,1371],{"class":277},[129,35729,35730,35732,35734,35736,35738,35740],{"class":265,"line":297},[129,35731,34326],{"class":10761},[129,35733,1380],{"class":277},[129,35735,4261],{"class":277},[129,35737,34333],{"class":427},[129,35739,424],{"class":277},[129,35741,17120],{"class":277},[129,35743,35744,35746,35748,35750,35752,35754,35757,35759,35761,35763,35765,35767,35769,35771],{"class":265,"line":315},[129,35745,34342],{"class":10761},[129,35747,1380],{"class":277},[129,35749,21683],{"class":284},[129,35751,147],{"class":277},[129,35753,424],{"class":277},[129,35755,35756],{"class":427},"/_fonts/inter-latin.woff2",[129,35758,424],{"class":277},[129,35760,160],{"class":277},[129,35762,2682],{"class":284},[129,35764,147],{"class":277},[129,35766,424],{"class":277},[129,35768,34366],{"class":427},[129,35770,424],{"class":277},[129,35772,25930],{"class":277},[129,35774,35775,35778,35780,35782,35784],{"class":265,"line":332},[129,35776,35777],{"class":10761},"  font-weight",[129,35779,1380],{"class":277},[129,35781,18732],{"class":290},[129,35783,34680],{"class":290},[129,35785,17120],{"class":277},[129,35787,35788,35790,35792,35794],{"class":265,"line":339},[129,35789,34375],{"class":10761},[129,35791,1380],{"class":277},[129,35793,34380],{"class":273},[129,35795,17120],{"class":277},[129,35797,35798,35800,35802,35804,35806,35808,35810,35812],{"class":265,"line":356},[129,35799,34832],{"class":10761},[129,35801,1380],{"class":277},[129,35803,34837],{"class":273},[129,35805,6768],{"class":277},[129,35807,34842],{"class":273},[129,35809,1015],{"class":277},[129,35811,9121],{"class":273},[129,35813,17120],{"class":277},[129,35815,35816],{"class":265,"line":651},[129,35817,1530],{"class":277},[129,35819,35820],{"class":265,"line":657},[129,35821,336],{"emptyLinePlaceholder":335},[129,35823,35824,35826],{"class":265,"line":669},[129,35825,34319],{"class":2139},[129,35827,1371],{"class":277},[129,35829,35830,35832,35834,35836,35839,35841],{"class":265,"line":693},[129,35831,34326],{"class":10761},[129,35833,1380],{"class":277},[129,35835,4261],{"class":277},[129,35837,35838],{"class":427},"Inter fallback",[129,35840,424],{"class":277},[129,35842,17120],{"class":277},[129,35844,35845,35847,35849,35851,35853,35855,35857,35859],{"class":265,"line":712},[129,35846,34342],{"class":10761},[129,35848,1380],{"class":277},[129,35850,35270],{"class":284},[129,35852,147],{"class":277},[129,35854,424],{"class":277},[129,35856,31353],{"class":427},[129,35858,424],{"class":277},[129,35860,25930],{"class":277},[129,35862,35863,35865,35867,35869],{"class":265,"line":1521},[129,35864,35285],{"class":10761},[129,35866,1380],{"class":277},[129,35868,35290],{"class":290},[129,35870,17120],{"class":277},[129,35872,35873,35875,35877,35879],{"class":265,"line":1527},[129,35874,35297],{"class":273},[129,35876,1380],{"class":277},[129,35878,35302],{"class":290},[129,35880,17120],{"class":277},[129,35882,35883,35885,35887,35889],{"class":265,"line":2295},[129,35884,35309],{"class":273},[129,35886,1380],{"class":277},[129,35888,35314],{"class":290},[129,35890,17120],{"class":277},[129,35892,35893,35895,35897,35899],{"class":265,"line":2300},[129,35894,35321],{"class":273},[129,35896,1380],{"class":277},[129,35898,35326],{"class":290},[129,35900,17120],{"class":277},[129,35902,35903],{"class":265,"line":2305},[129,35904,1530],{"class":277},[11,35906,35907,35908,35910,35911,35913,35914,35917],{},"And it rewrites your ",[15,35909,35454],{}," stack to include the fallback automatically. No Google Fonts request at runtime, no GDPR exposure, near-zero CLS from font swap. The only thing it doesn't handle for you is the ",[15,35912,34277],{}," value - set that in your CSS or via ",[15,35915,35916],{},"defaults.display"," in config.",[11,35919,35920,35921,35924],{},"One caveat: the module downloads fonts at build time, which means a network request during ",[15,35922,35923],{},"nuxt build",". In CI environments without internet access, pre-download and commit the files to the repo.",[40,35926,35928],{"id":35927},"woff2-only","woff2 only",[11,35930,35931,35932,35937,35938,35943],{},"This warrants its own callout. woff2 uses Brotli compression and is ",[51,35933,35936],{"href":35934,"rel":35935},"https://www.w3.org/TR/WOFF2/",[55],"20-30% smaller"," than woff for the same font. ",[51,35939,35942],{"href":35940,"rel":35941},"https://caniuse.com/woff2",[55],"Browser support is 97% globally"," - Chrome 36+, Firefox 39+, Safari 10+, Edge 14+.",[11,35945,35946,35947,35949],{},"The only browsers that don't support woff2 are effectively dead (IE11, ancient Safari). Stop shipping woff, ttf, and eot fallbacks. Your ",[15,35948,35464],{}," line:",[255,35951,35953],{"className":10730,"code":35952,"language":10731,"meta":260,"style":260},"src: url('/fonts/inter-latin.woff2') format('woff2');\n/* No fallback formats. None needed. */\n",[15,35954,35955,35966],{"__ignoreMap":260},[129,35956,35957,35960,35963],{"class":265,"line":266},[129,35958,35959],{"class":273},"src: url('/fonts/",[129,35961,35962],{"class":2161},"inter-latin",[129,35964,35965],{"class":273},".woff2') format('woff2');\n",[129,35967,35968],{"class":265,"line":297},[129,35969,35970],{"class":376},"/* No fallback formats. None needed. */\n",[2001,35972],{},[11,35974,35975,35976,35978],{},"Eliminate the CDN chain, subset to Latin, switch to a variable font, add fallback metrics - on a cold mobile connection that's commonly 400-800ms off LCP and CLS that stops being a problem. ",[15,35977,15871],{}," handles most of this with a five-line config change.",[11,35980,35981],{},"After that, choosing a font is actually a design question again, which is where it should be.",[2026,35983,35984],{},"html pre.shiki code .sHwdD, html code.shiki .sHwdD{--shiki-light:#90A4AE;--shiki-light-font-style:italic;--shiki-default:#546E7A;--shiki-default-font-style:italic;--shiki-dark:#676E95;--shiki-dark-font-style:italic}html pre.shiki code .sMK4o, html code.shiki .sMK4o{--shiki-light:#39ADB5;--shiki-default:#89DDFF;--shiki-dark:#89DDFF}html pre.shiki code .swJcz, html code.shiki .swJcz{--shiki-light:#E53935;--shiki-default:#F07178;--shiki-dark:#F07178}html pre.shiki code .spNyl, html code.shiki .spNyl{--shiki-light:#9C3EDA;--shiki-default:#C792EA;--shiki-dark:#C792EA}html pre.shiki code .sfazB, html code.shiki .sfazB{--shiki-light:#91B859;--shiki-default:#C3E88D;--shiki-dark:#C3E88D}html .light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html.light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .s7zQu, html code.shiki .s7zQu{--shiki-light:#39ADB5;--shiki-light-font-style:italic;--shiki-default:#89DDFF;--shiki-default-font-style:italic;--shiki-dark:#89DDFF;--shiki-dark-font-style:italic}html pre.shiki code .sqsOY, html code.shiki .sqsOY{--shiki-light:#8796B0;--shiki-default:#B2CCD6;--shiki-dark:#B2CCD6}html pre.shiki code .s2Zo4, html code.shiki .s2Zo4{--shiki-light:#6182B8;--shiki-default:#82AAFF;--shiki-dark:#82AAFF}html pre.shiki code .sTEyZ, html code.shiki .sTEyZ{--shiki-light:#90A4AE;--shiki-default:#EEFFFF;--shiki-dark:#BABED8}html pre.shiki code .sHdIc, html code.shiki .sHdIc{--shiki-light:#90A4AE;--shiki-light-font-style:italic;--shiki-default:#EEFFFF;--shiki-default-font-style:italic;--shiki-dark:#BABED8;--shiki-dark-font-style:italic}html pre.shiki code .sBMFI, html code.shiki .sBMFI{--shiki-light:#E2931D;--shiki-default:#FFCB6B;--shiki-dark:#FFCB6B}html pre.shiki code .sbssI, html code.shiki .sbssI{--shiki-light:#F76D47;--shiki-default:#F78C6C;--shiki-dark:#F78C6C}",{"title":260,"searchDepth":297,"depth":297,"links":35986},[35987,35988,35989,35990,35991,35992,35993,35994],{"id":34132,"depth":297,"text":34133},{"id":34271,"depth":297,"text":34272},{"id":34500,"depth":297,"text":34501},{"id":34753,"depth":297,"text":34754},{"id":35138,"depth":297,"text":35139},{"id":35226,"depth":297,"text":35227},{"id":35439,"depth":297,"text":35440},{"id":35927,"depth":297,"text":35928},"Two extra HTTP connections, no subsetting, broken fallbacks, and a GDPR violation for good measure. A practical guide to doing fonts right in 2026.",{},"/blog/web-fonts-performance",{"title":34097,"description":35995},"blog/web-fonts-performance",[16484,36001,8631,17766],"CSS","ixDwVU24O5pb-OedKXmMR5-S06ieSLF6I2Hi36s7uFw",{"id":36004,"title":36005,"body":36006,"cover":2042,"date":33101,"description":36693,"extension":2045,"meta":36694,"navigation":335,"path":36695,"readingTime":669,"seo":36696,"stem":36697,"tags":36698,"__hash__":36699},"blog/blog/web-icons-2026.md","How web icons went from a font hack to 200,000 SVGs on demand",{"type":8,"value":36007,"toc":36684},[36008,36011,36033,36037,36044,36047,36059,36072,36078,36088,36094,36097,36101,36104,36114,36135,36155,36162,36166,36178,36197,36207,36210,36220,36224,36227,36246,36253,36257,36269,36279,36282,36292,36302,36320,36399,36403,36408,36430,36435,36444,36522,36529,36533,36538,36600,36647,36657,36673,36675,36681],[11,36009,36010],{},"For about a decade, the dominant way to add icons to a web page was to pretend they were text characters.",[11,36012,8603,36013,36016,36017,36020,36021,36024,36025,36028,36029,36032],{},[118,36014,36015],{},"icon font"," model: package every icon as a glyph in a ",[15,36018,36019],{},".woff"," file, assign it to a Private Use Area Unicode codepoint, reference it via CSS ",[15,36022,36023],{},"content: \"\\f015\""," in a ",[15,36026,36027],{},":before"," pseudo-element, add a class to your HTML element. That was ",[15,36030,36031],{},"\u003Ci class=\"fa fa-home\">\u003C/i>",". That was Font Awesome. That was how everyone did it from roughly 2012 to 2019 - and the problems were baked in from the start.",[40,36034,36036],{"id":36035},"why-icon-fonts-were-a-bad-idea-that-everyone-used-anyway","Why icon fonts were a bad idea that everyone used anyway",[11,36038,36039,36040,36043],{},"The appeal was genuine at the time. Fonts are vectors - scale without blurring. CSS ",[15,36041,36042],{},"color"," changes the icon color. One font file serves the entire set. Universal browser support. For several years this was considered best practice.",[11,36045,36046],{},"The problems:",[11,36048,36049,36052,36053,36058],{},[118,36050,36051],{},"Flash of unstyled icons."," Icon fonts are web fonts. They have the same loading behavior: if the font hasn't loaded when the browser renders text, you either get invisible boxes or raw Unicode characters from the Private Use Area - random glyphs or blank squares. ",[51,36054,36057],{"href":36055,"rel":36056},"https://github.com/FortAwesome/Font-Awesome",[55],"Font Awesome 5 Free's"," woff2 file ran 60-80KB, loaded in full before a single icon was visible.",[11,36060,36061,36064,36065,36067,36068,36071],{},[118,36062,36063],{},"Accessibility failures."," Screen readers read icon font characters as text. A home icon via ",[15,36066,36023],{}," gets announced as \"Private Use Area character\" or some other meaningless string - behavior varies by screen reader and OS. The workaround was ",[15,36069,36070],{},"aria-hidden=\"true\""," on every icon element plus separate visually-hidden text for screen readers. Boilerplate on every instance, and in practice it was usually skipped.",[11,36073,36074,36077],{},[118,36075,36076],{},"Rendering artifacts."," Icon fonts are anti-aliased as text, not as graphics. On certain browser/OS/DPI combinations - particularly Chrome on Windows at non-standard display scaling - icon glyphs rendered blurry or offset by half a pixel because text rendering hinting was applied. SVG never has this problem.",[11,36079,36080,36083,36084,36087],{},[118,36081,36082],{},"Single color only."," Everything through ",[15,36085,36086],{},"currentColor",". Multi-color icons, duotone variants, gradients - not possible with the font model. The hacks involving stacked glyphs with separate colors were genuinely unhinged to maintain.",[11,36089,36090,36093],{},[118,36091,36092],{},"Bundle: all or nothing."," You loaded the entire icon set even if your interface used 15 of the 1,500+ icons in Font Awesome. Subsetting tools existed but required a build pipeline most projects never configured.",[11,36095,36096],{},"The industry consensus shifted around 2018-2020 as SVG support became truly universal. But the transition wasn't immediately clean.",[40,36098,36100],{"id":36099},"the-svg-problem","The SVG problem",[11,36102,36103],{},"SVG icons are technically superior on every axis: crisp at any size, full color support, animatable, properly accessible, no font loading pipeline. The implementation paths were rough.",[11,36105,36106,36109,36110,36113],{},[118,36107,36108],{},"Copy-paste SVG"," - dropping raw ",[15,36111,36112],{},"\u003Csvg>"," markup directly into component templates - works perfectly and has zero dependencies. It also doesn't scale. A UI with 40 icons might have 3,000 lines of SVG path data scattered across components. Updating a single icon means hunting down every instance. Nobody maintains this well.",[11,36115,36116,36119,36120,968,36123,36126,36127,36130,36131,36134],{},[118,36117,36118],{},"SVG sprites"," using ",[15,36121,36122],{},"\u003Csymbol>",[15,36124,36125],{},"\u003Cuse>"," solve the duplication problem: define each icon once in a hidden sprite sheet, reference it anywhere with ",[15,36128,36129],{},"\u003Csvg>\u003Cuse href=\"#icon-home\"/>\u003C/svg>",". One definition, N references. But external sprite files run into CSS scoping issues - you cannot style icons from an external ",[15,36132,36133],{},".svg"," using the parent document's CSS, because the external file is in a separate document context. Everything either needs to be inlined in the HTML (bloating every page) or live with limited styling.",[11,36136,36137,36140,36141,500,36146,36151,36152,36154],{},[118,36138,36139],{},"Component-per-icon"," is the pattern the React ecosystem settled on. ",[51,36142,36145],{"href":36143,"rel":36144},"https://lucide.dev",[55],"Lucide React",[51,36147,36150],{"href":36148,"rel":36149},"https://heroicons.com",[55],"Heroicons",", and similar libraries ship each icon as an individual component that renders an ",[15,36153,36112],{}," inline. Bundlers tree-shake unused icons naturally. This is clean for libraries with a fixed, curated icon set. It scales awkwardly when you want access to dozens of different design vocabularies without installing and maintaining a separate package for each.",[11,36156,36157,36158,36161],{},"None of these answers the underlying question: what if you want ",[24,36159,36160],{},"any icon from any major open-source library"," in a consistent way?",[40,36163,36165],{"id":36164},"iconify-the-unification-layer","Iconify: the unification layer",[11,36167,36168,36173,36174,36177],{},[51,36169,36172],{"href":36170,"rel":36171},"https://iconify.design",[55],"Iconify"," takes a different approach. Rather than being another icon library, it's an abstraction layer over essentially all of them. A single standardized JSON format, a unified naming convention (",[15,36175,36176],{},"prefix:icon-name","), and a runtime that can serve any icon on demand.",[11,36179,36180,36181,36184,36185,500,36188,500,36191,500,36194,362],{},"The scale is significant: ",[118,36182,36183],{},"over 200,000 icons across 218+ collections"," - Material Design Icons, Phosphor, Lucide, Heroicons, Tabler, Bootstrap Icons, IBM Carbon, Fluent UI, Remix Icons, Simple Icons, and hundreds more. All accessed identically: ",[15,36186,36187],{},"\u003CIcon name=\"mdi:home\" />",[15,36189,36190],{},"\u003CIcon name=\"ph:house\" />",[15,36192,36193],{},"\u003CIcon name=\"lucide:home\" />",[15,36195,36196],{},"\u003CIcon name=\"tabler:home\" />",[11,36198,36199,36200,36202,36203,36206],{},"The underlying format is straightforward. Each collection is one JSON file: icon names mapped to SVG body strings (the inner content, not the outer ",[15,36201,36112],{}," wrapper). The runtime combines the body with the collection's shared ",[15,36204,36205],{},"viewBox"," at render time. Every collection has a stable prefix you use as a namespace.",[36208,36209],"iconify-demo",{},[11,36211,36212,36213,500,36216,36219],{},"What makes Iconify's approach durable is that it's a data format, not a rendering implementation. The same icon data works in ",[15,36214,36215],{},"@iconify/vue",[15,36217,36218],{},"@iconify/react",", a plain Web Component, Node.js build tools, and a VS Code extension that previews icons inline as you type names. One format, one naming convention, everything else builds on top.",[40,36221,36223],{"id":36222},"building-nuxt-icons-before-the-official-solution","Building nuxt-icons before the official solution",[11,36225,36226],{},"When I started using Nuxt 3 in early 2022 - still in RC at the time - there was no ergonomic official way to handle SVG icons. The module ecosystem hadn't caught up with Nuxt 3's architecture, and the options were: manually copy-paste SVGs, use a font library, or cobble something together from the available community modules.",[11,36228,36229,36230,36235,36236,968,36238,36241,36242,36245],{},"I built ",[51,36231,36234],{"href":36232,"rel":36233},"https://github.com/gitFoxCode/nuxt-icons",[55],"nuxt-icons"," to solve the specific problem I had: drop SVG files into a folder, use a single component, have it work. The module handled HMR, treated icons like CSS font elements (controllable via ",[15,36237,36042],{},[15,36239,36240],{},"font-size","), supported nested directories, and preserved original SVG colors for complex icons with the ",[15,36243,36244],{},"filled"," prop.",[11,36247,36248,36249,36252],{},"It picked up around 200 stars from people hitting the same gap. The module still works for the narrow use case of purely custom SVG icon collections with no dependency on Iconify's ecosystem. But if you're starting a new project today, ",[15,36250,36251],{},"@nuxt/icon"," is the right answer - and it handles custom collections too.",[40,36254,36256],{"id":36255},"nuxticon-how-it-actually-works","@nuxt/icon: how it actually works",[11,36258,36259,36265,36266,36268],{},[51,36260,36263],{"href":36261,"rel":36262},"https://nuxt.com/modules/icon",[55],[15,36264,36251],{}," is the official Nuxt icon module, now maintained by the Nuxt core team. It wraps ",[15,36267,36215],{}," and builds an SSR-aware serving architecture on top.",[11,36270,36271,36272,36274,36275,36278],{},"The full Iconify collection is available without installing separate icon packages. You use ",[15,36273,36187],{}," and the module handles fetching and caching the icon data. If you want a collection bundled into your server build for offline or performance-critical deployments, installing ",[15,36276,36277],{},"@iconify-json/[prefix]"," is optional but removes any runtime API dependency.",[11,36280,36281],{},"Icons are served through three tiers:",[11,36283,36284,36287,36288,36291],{},[118,36285,36286],{},"Client bundle"," - specific icon names statically analyzed at build time and inlined into the client JS. Zero network requests at runtime. Enable ",[15,36289,36290],{},"clientBundle.scan: true"," to auto-detect which icons your components use, or list them explicitly. Ideal for navigation icons, loading spinners, anything on every page.",[11,36293,36294,36297,36298,36301],{},[118,36295,36296],{},"Server endpoint"," - a cached Nitro handler at ",[15,36299,36300],{},"/_nuxt_icon/:collection"," with a 1-week SWR cache. On first request for a collection, the server returns icon data from locally installed packages or falls back to the public Iconify API. Subsequent requests hit the cache. Transparent to the client.",[11,36303,36304,36307,36308,36311,36312,36315,36316,36319],{},[118,36305,36306],{},"SSR prefetch"," - during each server render, the module calls ",[15,36309,36310],{},"onServerPrefetch"," for every ",[15,36313,36314],{},"\u003CIcon>"," in the tree, loads the icon data, and serializes it into ",[15,36317,36318],{},"nuxt.payload",". The client picks it up from the payload without making any additional network request. Icons are in the DOM on first paint, no separate API call.",[255,36321,36323],{"className":3922,"code":36322,"filename":17649,"language":3924,"meta":260,"style":260},"icon: {\n  clientBundle: {\n    scan: true,         // auto-detect icons from your components\n    sizeLimitKb: 256,   // fail build if bundle exceeds this\n  },\n  serverBundle: 'local', // bundle installed @iconify-json/* packages\n}\n",[15,36324,36325,36334,36343,36357,36372,36376,36395],{"__ignoreMap":260},[129,36326,36327,36330,36332],{"class":265,"line":266},[129,36328,36329],{"class":2161},"icon",[129,36331,1380],{"class":277},[129,36333,1371],{"class":277},[129,36335,36336,36339,36341],{"class":265,"line":297},[129,36337,36338],{"class":2161},"  clientBundle",[129,36340,1380],{"class":277},[129,36342,1371],{"class":277},[129,36344,36345,36348,36350,36352,36354],{"class":265,"line":315},[129,36346,36347],{"class":2161},"    scan",[129,36349,1380],{"class":277},[129,36351,4823],{"class":4822},[129,36353,1015],{"class":277},[129,36355,36356],{"class":376},"         // auto-detect icons from your components\n",[129,36358,36359,36362,36364,36367,36369],{"class":265,"line":332},[129,36360,36361],{"class":2161},"    sizeLimitKb",[129,36363,1380],{"class":277},[129,36365,36366],{"class":290}," 256",[129,36368,1015],{"class":277},[129,36370,36371],{"class":376},"   // fail build if bundle exceeds this\n",[129,36373,36374],{"class":265,"line":339},[129,36375,1481],{"class":277},[129,36377,36378,36381,36383,36385,36388,36390,36392],{"class":265,"line":356},[129,36379,36380],{"class":2161},"  serverBundle",[129,36382,1380],{"class":277},[129,36384,4261],{"class":277},[129,36386,36387],{"class":427},"local",[129,36389,424],{"class":277},[129,36391,1015],{"class":277},[129,36393,36394],{"class":376}," // bundle installed @iconify-json/* packages\n",[129,36396,36397],{"class":265,"line":651},[129,36398,1530],{"class":277},[40,36400,36402],{"id":36401},"css-mode-vs-svg-mode","CSS mode vs SVG mode",[11,36404,36405,36406,362],{},"This is the one architectural decision you actually make when using ",[15,36407,36251],{},[11,36409,36410,36413,36414,36417,36418,36421,36422,36425,36426,36429],{},[118,36411,36412],{},"CSS mode"," (the default) renders ",[15,36415,36416],{},"\u003Cspan class=\"iconify i--mdi--home\">"," - no SVG in the DOM. The icon is a ",[15,36419,36420],{},"mask-image"," with the SVG data URL-encoded into it, applied to a colored ",[15,36423,36424],{},"background-color",". The CSS for each unique icon name is injected once regardless of how many instances appear on the page - 100 instances of the same home icon generate exactly one CSS rule and 100 empty ",[15,36427,36428],{},"\u003Cspan>"," elements.",[11,36431,36432,36433,362],{},"This has a limit: the mask technique applies a single color to the entire icon shape. Multi-color icons and gradients do not render correctly in CSS mode - everything collapses to ",[15,36434,36086],{},[11,36436,36437,36440,36441,36443],{},[118,36438,36439],{},"SVG mode"," renders an actual ",[15,36442,36112],{}," element inline. Full color support, CSS filters apply, child elements are addressable. More DOM nodes per instance, but necessary for any icon with multiple colors.",[255,36445,36447],{"className":10399,"code":36446,"language":10400,"meta":260,"style":260},"\u003C!-- CSS mode (default) - best for single-color UI icons -->\n\u003CIcon name=\"mdi:home\" />\n\u003C!-- renders: \u003Cspan class=\"iconify i--mdi--home\">\u003C/span> -->\n\n\u003C!-- SVG mode - for multi-color or complex styling needs -->\n\u003CIcon name=\"mdi:home\" mode=\"svg\" />\n\u003C!-- renders: \u003Csvg viewBox=\"0 0 24 24\">\u003Cpath .../>\u003C/svg> -->\n",[15,36448,36449,36454,36474,36479,36483,36488,36517],{"__ignoreMap":260},[129,36450,36451],{"class":265,"line":266},[129,36452,36453],{"class":376},"\u003C!-- CSS mode (default) - best for single-color UI icons -->\n",[129,36455,36456,36458,36461,36463,36465,36467,36470,36472],{"class":265,"line":297},[129,36457,3945],{"class":277},[129,36459,36460],{"class":1376},"Icon",[129,36462,5407],{"class":269},[129,36464,278],{"class":277},[129,36466,2258],{"class":277},[129,36468,36469],{"class":427},"mdi:home",[129,36471,2258],{"class":277},[129,36473,20832],{"class":277},[129,36475,36476],{"class":265,"line":315},[129,36477,36478],{"class":376},"\u003C!-- renders: \u003Cspan class=\"iconify i--mdi--home\">\u003C/span> -->\n",[129,36480,36481],{"class":265,"line":332},[129,36482,336],{"emptyLinePlaceholder":335},[129,36484,36485],{"class":265,"line":339},[129,36486,36487],{"class":376},"\u003C!-- SVG mode - for multi-color or complex styling needs -->\n",[129,36489,36490,36492,36494,36496,36498,36500,36502,36504,36506,36508,36510,36513,36515],{"class":265,"line":356},[129,36491,3945],{"class":277},[129,36493,36460],{"class":1376},[129,36495,5407],{"class":269},[129,36497,278],{"class":277},[129,36499,2258],{"class":277},[129,36501,36469],{"class":427},[129,36503,2258],{"class":277},[129,36505,11246],{"class":269},[129,36507,278],{"class":277},[129,36509,2258],{"class":277},[129,36511,36512],{"class":427},"svg",[129,36514,2258],{"class":277},[129,36516,20832],{"class":277},[129,36518,36519],{"class":265,"line":651},[129,36520,36521],{"class":376},"\u003C!-- renders: \u003Csvg viewBox=\"0 0 24 24\">\u003Cpath .../>\u003C/svg> -->\n",[11,36523,36524,36525,36528],{},"Set the default at the module level and override per-instance with the ",[15,36526,36527],{},"mode"," prop. For a standard UI, CSS mode for everything is the right default.",[40,36530,36532],{"id":36531},"custom-collections-your-own-icons-same-api","Custom collections: your own icons, same API",[11,36534,36535,36537],{},[15,36536,36251],{}," treats local SVG directories as first-class collections. Drop your files in a folder, give the collection a prefix:",[255,36539,36541],{"className":3922,"code":36540,"filename":17649,"language":3924,"meta":260,"style":260},"icon: {\n  customCollections: [\n    { prefix: 'local', dir: './app/assets/icons' }\n  ]\n}\n",[15,36542,36543,36551,36560,36591,36596],{"__ignoreMap":260},[129,36544,36545,36547,36549],{"class":265,"line":266},[129,36546,36329],{"class":2161},[129,36548,1380],{"class":277},[129,36550,1371],{"class":277},[129,36552,36553,36556,36558],{"class":265,"line":297},[129,36554,36555],{"class":2161},"  customCollections",[129,36557,1380],{"class":277},[129,36559,32222],{"class":1376},[129,36561,36562,36564,36567,36569,36571,36573,36575,36577,36580,36582,36584,36587,36589],{"class":265,"line":315},[129,36563,13947],{"class":277},[129,36565,36566],{"class":1376}," prefix",[129,36568,1380],{"class":277},[129,36570,4261],{"class":277},[129,36572,36387],{"class":427},[129,36574,424],{"class":277},[129,36576,1015],{"class":277},[129,36578,36579],{"class":1376}," dir",[129,36581,1380],{"class":277},[129,36583,4261],{"class":277},[129,36585,36586],{"class":427},"./app/assets/icons",[129,36588,424],{"class":277},[129,36590,1476],{"class":277},[129,36592,36593],{"class":265,"line":332},[129,36594,36595],{"class":1376},"  ]\n",[129,36597,36598],{"class":265,"line":339},[129,36599,1530],{"class":277},[255,36601,36603],{"className":10399,"code":36602,"language":10400,"meta":260,"style":260},"\u003CIcon name=\"local:logo\" />\n\u003CIcon name=\"local:brand/wordmark\" />  \u003C!-- nested folder support -->\n",[15,36604,36605,36624],{"__ignoreMap":260},[129,36606,36607,36609,36611,36613,36615,36617,36620,36622],{"class":265,"line":266},[129,36608,3945],{"class":277},[129,36610,36460],{"class":1376},[129,36612,5407],{"class":269},[129,36614,278],{"class":277},[129,36616,2258],{"class":277},[129,36618,36619],{"class":427},"local:logo",[129,36621,2258],{"class":277},[129,36623,20832],{"class":277},[129,36625,36626,36628,36630,36632,36634,36636,36639,36641,36644],{"class":265,"line":297},[129,36627,3945],{"class":277},[129,36629,36460],{"class":1376},[129,36631,5407],{"class":269},[129,36633,278],{"class":277},[129,36635,2258],{"class":277},[129,36637,36638],{"class":427},"local:brand/wordmark",[129,36640,2258],{"class":277},[129,36642,36643],{"class":277}," />",[129,36645,36646],{"class":376},"  \u003C!-- nested folder support -->\n",[11,36648,36649,36650,36652,36653,36656],{},"No build step, no sprite generation, no separate component. Your custom product icons use the exact same ",[15,36651,36314],{}," component and ",[15,36654,36655],{},"prefix:name"," syntax as the 200,000+ icons in the Iconify ecosystem. This matters more than it seems: when you switch icon sets or rename icons, there is one place to change and one consistent API across the entire codebase.",[11,36658,36659,36660,500,36663,36665,36666,36669,36670,36672],{},"If you're on ",[15,36661,36662],{},"@nuxt/ui",[15,36664,36251],{}," is already included as a dependency. The ",[15,36667,36668],{},"icon: {}"," config block in ",[15,36671,17649],{}," still applies - custom collections, client bundle settings, rendering mode defaults - it all works the same.",[2001,36674],{},[11,36676,36677,36678,36680],{},"The icon problem is genuinely solved in 2026. Iconify's unified data layer plus ",[15,36679,36251],{},"'s SSR-aware architecture gives you any icon from any major open-source set, rendered without layout shift, without font flashes, with a one-line API. The only remaining question is which collection's visual style fits your design - which is a design question, not an infrastructure one.",[2026,36682,36683],{},"html pre.shiki code .sBMFI, html code.shiki .sBMFI{--shiki-light:#E2931D;--shiki-default:#FFCB6B;--shiki-dark:#FFCB6B}html pre.shiki code .sMK4o, html code.shiki .sMK4o{--shiki-light:#39ADB5;--shiki-default:#89DDFF;--shiki-dark:#89DDFF}html pre.shiki code .sfNiH, html code.shiki .sfNiH{--shiki-light:#FF5370;--shiki-default:#FF9CAC;--shiki-dark:#FF9CAC}html pre.shiki code .sHwdD, html code.shiki .sHwdD{--shiki-light:#90A4AE;--shiki-light-font-style:italic;--shiki-default:#546E7A;--shiki-default-font-style:italic;--shiki-dark:#676E95;--shiki-dark-font-style:italic}html pre.shiki code .sbssI, html code.shiki .sbssI{--shiki-light:#F76D47;--shiki-default:#F78C6C;--shiki-dark:#F78C6C}html pre.shiki code .sfazB, html code.shiki .sfazB{--shiki-light:#91B859;--shiki-default:#C3E88D;--shiki-dark:#C3E88D}html .light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html.light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .swJcz, html code.shiki .swJcz{--shiki-light:#E53935;--shiki-default:#F07178;--shiki-dark:#F07178}html pre.shiki code .spNyl, html code.shiki .spNyl{--shiki-light:#9C3EDA;--shiki-default:#C792EA;--shiki-dark:#C792EA}",{"title":260,"searchDepth":297,"depth":297,"links":36685},[36686,36687,36688,36689,36690,36691,36692],{"id":36035,"depth":297,"text":36036},{"id":36099,"depth":297,"text":36100},{"id":36164,"depth":297,"text":36165},{"id":36222,"depth":297,"text":36223},{"id":36255,"depth":297,"text":36256},{"id":36401,"depth":297,"text":36402},{"id":36531,"depth":297,"text":36532},"Icon fonts solved the wrong problem. SVG sprites were ergonomically painful. Copy-paste SVG didn't scale. Here's how the ecosystem finally arrived at a working answer.",{},"/blog/web-icons-2026",{"title":36005,"description":36693},"blog/web-icons-2026",[16484,8631,36001,17766],"gCt04coaJ9-ezOaoLyVVAjeXYX86ugzurUaYYsPyMzM",1776886070623]