[{"data":1,"prerenderedAt":4497},["ShallowReactive",2],{"page-\u002Fvue\u002F12-fetching-data-and-async":3},{"id":4,"title":5,"body":6,"description":4490,"extension":4491,"meta":4492,"navigation":69,"path":4493,"seo":4494,"stem":4495,"__hash__":4496},"content\u002Fvue\u002F12-fetching-data-and-async.md","Vue 3 Engineering Reference — Fetching Data & Async",{"type":7,"value":8,"toc":4479},"minimark",[9,14,19,835,839,1409,1413,1976,1980,2623,2627,3105,3109,4475],[10,11,13],"h1",{"id":12},"_12-fetching-data-async","12 — Fetching Data & Async",[15,16,18],"h2",{"id":17},"usefetch-production-race-condition-handler","useFetch — Production Race Condition Handler",[20,21,24],"code-wrapper",{"filename":22,"language":23},"useFetch.ts","typescript",[25,26,30],"pre",{"className":27,"code":28,"language":23,"meta":29,"style":29},"language-typescript shiki shiki-themes github-light github-dark","import { ref, watchEffect, toValue, type MaybeRefOrGetter, type Ref } from 'vue'\n\ninterface FetchOptions {\n  immediate?: boolean\n  refetch?: boolean  \u002F\u002F re-fetch when source changes\n  transform?: (data: any) => any\n}\n\n\u002F\u002F ── Race condition prevention via AbortController ──\n\u002F\u002F Every time the URL changes, the previous fetch is aborted.\n\u002F\u002F Only the latest request's response is committed to state.\nexport function useFetch\u003CT>(\n  url: MaybeRefOrGetter\u003Cstring>,\n  options: FetchOptions = {}\n) {\n  const data: Ref\u003CT | null> = ref(null)\n  const error: Ref\u003CError | null> = ref(null)\n  const loading = ref(false)\n  const statusCode = ref\u003Cnumber | null>(null)\n\n  let abortController: AbortController | null = null\n\n  async function execute() {\n    const targetUrl = toValue(url)\n    if (!targetUrl) return\n\n    \u002F\u002F ── Abort any in-flight request before starting a new one ──\n    abortController?.abort()\n    abortController = new AbortController()\n\n    loading.value = true\n    error.value = null\n\n    try {\n      const res = await fetch(targetUrl, { signal: abortController.signal })\n      statusCode.value = res.status\n      if (!res.ok) throw new Error(`HTTP ${res.status}: ${res.statusText}`)\n      let json = await res.json()\n      if (options.transform) json = options.transform(json)\n      data.value = json as T\n    } catch (e) {\n      \u002F\u002F ── AbortError is expected — don't set it as an error ──\n      if (e instanceof DOMException && e.name === 'AbortError') return\n      error.value = e as Error\n    } finally {\n      loading.value = false\n    }\n  }\n\n  if (options.refetch !== false) {\n    \u002F\u002F ── watchEffect auto-tracks toValue(url) — re-fetches on URL change ──\n    watchEffect(execute)\n  } else if (options.immediate) {\n    execute()  \u002F\u002F one-time fetch, no re-fetch on change\n  }\n\n  return { data, error, loading, statusCode, refresh: execute }\n}\n","",[31,32,33,64,71,84,98,113,142,148,153,159,165,171,192,211,227,233,275,308,327,355,360,383,388,402,419,436,441,447,459,474,479,490,500,505,513,533,544,594,615,634,650,662,668,698,714,724,735,741,747,752,769,775,784,799,811,816,821,830],"code",{"__ignoreMap":29},[34,35,38,42,46,49,52,54,57,60],"span",{"class":36,"line":37},"line",1,[34,39,41],{"class":40},"svdQ7","import",[34,43,45],{"class":44},"ssxIu"," { ref, watchEffect, toValue, ",[34,47,48],{"class":40},"type",[34,50,51],{"class":44}," MaybeRefOrGetter, ",[34,53,48],{"class":40},[34,55,56],{"class":44}," Ref } ",[34,58,59],{"class":40},"from",[34,61,63],{"class":62},"sJ6F3"," 'vue'\n",[34,65,67],{"class":36,"line":66},2,[34,68,70],{"emptyLinePlaceholder":69},true,"\n",[34,72,74,77,81],{"class":36,"line":73},3,[34,75,76],{"class":40},"interface",[34,78,80],{"class":79},"sIsaT"," FetchOptions",[34,82,83],{"class":44}," {\n",[34,85,87,91,94],{"class":36,"line":86},4,[34,88,90],{"class":89},"sCrzJ","  immediate",[34,92,93],{"class":40},"?:",[34,95,97],{"class":96},"snvgF"," boolean\n",[34,99,101,104,106,109],{"class":36,"line":100},5,[34,102,103],{"class":89},"  refetch",[34,105,93],{"class":40},[34,107,108],{"class":96}," boolean",[34,110,112],{"class":111},"sdCPZ","  \u002F\u002F re-fetch when source changes\n",[34,114,116,119,121,124,127,130,133,136,139],{"class":36,"line":115},6,[34,117,118],{"class":79},"  transform",[34,120,93],{"class":40},[34,122,123],{"class":44}," (",[34,125,126],{"class":89},"data",[34,128,129],{"class":40},":",[34,131,132],{"class":96}," any",[34,134,135],{"class":44},") ",[34,137,138],{"class":40},"=>",[34,140,141],{"class":96}," any\n",[34,143,145],{"class":36,"line":144},7,[34,146,147],{"class":44},"}\n",[34,149,151],{"class":36,"line":150},8,[34,152,70],{"emptyLinePlaceholder":69},[34,154,156],{"class":36,"line":155},9,[34,157,158],{"class":111},"\u002F\u002F ── Race condition prevention via AbortController ──\n",[34,160,162],{"class":36,"line":161},10,[34,163,164],{"class":111},"\u002F\u002F Every time the URL changes, the previous fetch is aborted.\n",[34,166,168],{"class":36,"line":167},11,[34,169,170],{"class":111},"\u002F\u002F Only the latest request's response is committed to state.\n",[34,172,174,177,180,183,186,189],{"class":36,"line":173},12,[34,175,176],{"class":40},"export",[34,178,179],{"class":40}," function",[34,181,182],{"class":79}," useFetch",[34,184,185],{"class":44},"\u003C",[34,187,188],{"class":79},"T",[34,190,191],{"class":44},">(\n",[34,193,195,198,200,203,205,208],{"class":36,"line":194},13,[34,196,197],{"class":89},"  url",[34,199,129],{"class":40},[34,201,202],{"class":79}," MaybeRefOrGetter",[34,204,185],{"class":44},[34,206,207],{"class":96},"string",[34,209,210],{"class":44},">,\n",[34,212,214,217,219,221,224],{"class":36,"line":213},14,[34,215,216],{"class":89},"  options",[34,218,129],{"class":40},[34,220,80],{"class":79},[34,222,223],{"class":40}," =",[34,225,226],{"class":44}," {}\n",[34,228,230],{"class":36,"line":229},15,[34,231,232],{"class":44},") {\n",[34,234,236,239,242,244,247,249,251,254,257,260,263,266,269,272],{"class":36,"line":235},16,[34,237,238],{"class":40},"  const",[34,240,241],{"class":96}," data",[34,243,129],{"class":40},[34,245,246],{"class":79}," Ref",[34,248,185],{"class":44},[34,250,188],{"class":79},[34,252,253],{"class":40}," |",[34,255,256],{"class":96}," null",[34,258,259],{"class":44},"> ",[34,261,262],{"class":40},"=",[34,264,265],{"class":79}," ref",[34,267,268],{"class":44},"(",[34,270,271],{"class":96},"null",[34,273,274],{"class":44},")\n",[34,276,278,280,283,285,287,289,292,294,296,298,300,302,304,306],{"class":36,"line":277},17,[34,279,238],{"class":40},[34,281,282],{"class":96}," error",[34,284,129],{"class":40},[34,286,246],{"class":79},[34,288,185],{"class":44},[34,290,291],{"class":79},"Error",[34,293,253],{"class":40},[34,295,256],{"class":96},[34,297,259],{"class":44},[34,299,262],{"class":40},[34,301,265],{"class":79},[34,303,268],{"class":44},[34,305,271],{"class":96},[34,307,274],{"class":44},[34,309,311,313,316,318,320,322,325],{"class":36,"line":310},18,[34,312,238],{"class":40},[34,314,315],{"class":96}," loading",[34,317,223],{"class":40},[34,319,265],{"class":79},[34,321,268],{"class":44},[34,323,324],{"class":96},"false",[34,326,274],{"class":44},[34,328,330,332,335,337,339,341,344,346,348,351,353],{"class":36,"line":329},19,[34,331,238],{"class":40},[34,333,334],{"class":96}," statusCode",[34,336,223],{"class":40},[34,338,265],{"class":79},[34,340,185],{"class":44},[34,342,343],{"class":96},"number",[34,345,253],{"class":40},[34,347,256],{"class":96},[34,349,350],{"class":44},">(",[34,352,271],{"class":96},[34,354,274],{"class":44},[34,356,358],{"class":36,"line":357},20,[34,359,70],{"emptyLinePlaceholder":69},[34,361,363,366,369,371,374,376,378,380],{"class":36,"line":362},21,[34,364,365],{"class":40},"  let",[34,367,368],{"class":44}," abortController",[34,370,129],{"class":40},[34,372,373],{"class":79}," AbortController",[34,375,253],{"class":40},[34,377,256],{"class":96},[34,379,223],{"class":40},[34,381,382],{"class":96}," null\n",[34,384,386],{"class":36,"line":385},22,[34,387,70],{"emptyLinePlaceholder":69},[34,389,391,394,396,399],{"class":36,"line":390},23,[34,392,393],{"class":40},"  async",[34,395,179],{"class":40},[34,397,398],{"class":79}," execute",[34,400,401],{"class":44},"() {\n",[34,403,405,408,411,413,416],{"class":36,"line":404},24,[34,406,407],{"class":40},"    const",[34,409,410],{"class":96}," targetUrl",[34,412,223],{"class":40},[34,414,415],{"class":79}," toValue",[34,417,418],{"class":44},"(url)\n",[34,420,422,425,427,430,433],{"class":36,"line":421},25,[34,423,424],{"class":40},"    if",[34,426,123],{"class":44},[34,428,429],{"class":40},"!",[34,431,432],{"class":44},"targetUrl) ",[34,434,435],{"class":40},"return\n",[34,437,439],{"class":36,"line":438},26,[34,440,70],{"emptyLinePlaceholder":69},[34,442,444],{"class":36,"line":443},27,[34,445,446],{"class":111},"    \u002F\u002F ── Abort any in-flight request before starting a new one ──\n",[34,448,450,453,456],{"class":36,"line":449},28,[34,451,452],{"class":44},"    abortController?.",[34,454,455],{"class":79},"abort",[34,457,458],{"class":44},"()\n",[34,460,462,465,467,470,472],{"class":36,"line":461},29,[34,463,464],{"class":44},"    abortController ",[34,466,262],{"class":40},[34,468,469],{"class":40}," new",[34,471,373],{"class":79},[34,473,458],{"class":44},[34,475,477],{"class":36,"line":476},30,[34,478,70],{"emptyLinePlaceholder":69},[34,480,482,485,487],{"class":36,"line":481},31,[34,483,484],{"class":44},"    loading.value ",[34,486,262],{"class":40},[34,488,489],{"class":96}," true\n",[34,491,493,496,498],{"class":36,"line":492},32,[34,494,495],{"class":44},"    error.value ",[34,497,262],{"class":40},[34,499,382],{"class":96},[34,501,503],{"class":36,"line":502},33,[34,504,70],{"emptyLinePlaceholder":69},[34,506,508,511],{"class":36,"line":507},34,[34,509,510],{"class":40},"    try",[34,512,83],{"class":44},[34,514,516,519,522,524,527,530],{"class":36,"line":515},35,[34,517,518],{"class":40},"      const",[34,520,521],{"class":96}," res",[34,523,223],{"class":40},[34,525,526],{"class":40}," await",[34,528,529],{"class":79}," fetch",[34,531,532],{"class":44},"(targetUrl, { signal: abortController.signal })\n",[34,534,536,539,541],{"class":36,"line":535},36,[34,537,538],{"class":44},"      statusCode.value ",[34,540,262],{"class":40},[34,542,543],{"class":44}," res.status\n",[34,545,547,550,552,554,557,560,562,565,567,570,573,576,579,582,584,586,589,592],{"class":36,"line":546},37,[34,548,549],{"class":40},"      if",[34,551,123],{"class":44},[34,553,429],{"class":40},[34,555,556],{"class":44},"res.ok) ",[34,558,559],{"class":40},"throw",[34,561,469],{"class":40},[34,563,564],{"class":79}," Error",[34,566,268],{"class":44},[34,568,569],{"class":62},"`HTTP ${",[34,571,572],{"class":44},"res",[34,574,575],{"class":62},".",[34,577,578],{"class":44},"status",[34,580,581],{"class":62},"}: ${",[34,583,572],{"class":44},[34,585,575],{"class":62},[34,587,588],{"class":44},"statusText",[34,590,591],{"class":62},"}`",[34,593,274],{"class":44},[34,595,597,600,603,605,607,610,613],{"class":36,"line":596},38,[34,598,599],{"class":40},"      let",[34,601,602],{"class":44}," json ",[34,604,262],{"class":40},[34,606,526],{"class":40},[34,608,609],{"class":44}," res.",[34,611,612],{"class":79},"json",[34,614,458],{"class":44},[34,616,618,620,623,625,628,631],{"class":36,"line":617},39,[34,619,549],{"class":40},[34,621,622],{"class":44}," (options.transform) json ",[34,624,262],{"class":40},[34,626,627],{"class":44}," options.",[34,629,630],{"class":79},"transform",[34,632,633],{"class":44},"(json)\n",[34,635,637,640,642,644,647],{"class":36,"line":636},40,[34,638,639],{"class":44},"      data.value ",[34,641,262],{"class":40},[34,643,602],{"class":44},[34,645,646],{"class":40},"as",[34,648,649],{"class":79}," T\n",[34,651,653,656,659],{"class":36,"line":652},41,[34,654,655],{"class":44},"    } ",[34,657,658],{"class":40},"catch",[34,660,661],{"class":44}," (e) {\n",[34,663,665],{"class":36,"line":664},42,[34,666,667],{"class":111},"      \u002F\u002F ── AbortError is expected — don't set it as an error ──\n",[34,669,671,673,676,679,682,685,688,691,694,696],{"class":36,"line":670},43,[34,672,549],{"class":40},[34,674,675],{"class":44}," (e ",[34,677,678],{"class":40},"instanceof",[34,680,681],{"class":79}," DOMException",[34,683,684],{"class":40}," &&",[34,686,687],{"class":44}," e.name ",[34,689,690],{"class":40},"===",[34,692,693],{"class":62}," 'AbortError'",[34,695,135],{"class":44},[34,697,435],{"class":40},[34,699,701,704,706,709,711],{"class":36,"line":700},44,[34,702,703],{"class":44},"      error.value ",[34,705,262],{"class":40},[34,707,708],{"class":44}," e ",[34,710,646],{"class":40},[34,712,713],{"class":79}," Error\n",[34,715,717,719,722],{"class":36,"line":716},45,[34,718,655],{"class":44},[34,720,721],{"class":40},"finally",[34,723,83],{"class":44},[34,725,727,730,732],{"class":36,"line":726},46,[34,728,729],{"class":44},"      loading.value ",[34,731,262],{"class":40},[34,733,734],{"class":96}," false\n",[34,736,738],{"class":36,"line":737},47,[34,739,740],{"class":44},"    }\n",[34,742,744],{"class":36,"line":743},48,[34,745,746],{"class":44},"  }\n",[34,748,750],{"class":36,"line":749},49,[34,751,70],{"emptyLinePlaceholder":69},[34,753,755,758,761,764,767],{"class":36,"line":754},50,[34,756,757],{"class":40},"  if",[34,759,760],{"class":44}," (options.refetch ",[34,762,763],{"class":40},"!==",[34,765,766],{"class":96}," false",[34,768,232],{"class":44},[34,770,772],{"class":36,"line":771},51,[34,773,774],{"class":111},"    \u002F\u002F ── watchEffect auto-tracks toValue(url) — re-fetches on URL change ──\n",[34,776,778,781],{"class":36,"line":777},52,[34,779,780],{"class":79},"    watchEffect",[34,782,783],{"class":44},"(execute)\n",[34,785,787,790,793,796],{"class":36,"line":786},53,[34,788,789],{"class":44},"  } ",[34,791,792],{"class":40},"else",[34,794,795],{"class":40}," if",[34,797,798],{"class":44}," (options.immediate) {\n",[34,800,802,805,808],{"class":36,"line":801},54,[34,803,804],{"class":79},"    execute",[34,806,807],{"class":44},"()  ",[34,809,810],{"class":111},"\u002F\u002F one-time fetch, no re-fetch on change\n",[34,812,814],{"class":36,"line":813},55,[34,815,746],{"class":44},[34,817,819],{"class":36,"line":818},56,[34,820,70],{"emptyLinePlaceholder":69},[34,822,824,827],{"class":36,"line":823},57,[34,825,826],{"class":40},"  return",[34,828,829],{"class":44}," { data, error, loading, statusCode, refresh: execute }\n",[34,831,833],{"class":36,"line":832},58,[34,834,147],{"class":44},[15,836,838],{"id":837},"request-deduplication-coalescing-concurrent-fetches","Request Deduplication — Coalescing Concurrent Fetches",[20,840,842],{"filename":841,"language":23},"useDedupFetch.ts",[25,843,845],{"className":27,"code":844,"language":23,"meta":29,"style":29},"import { ref, type Ref } from 'vue'\n\n\u002F\u002F ── Deduplication: if a request to the same URL is in flight,\n\u002F\u002F    reuse its promise instead of firing a second request. ──\n\u002F\u002F Critical for components that mount simultaneously and fetch the same data.\n\nconst cache = new Map\u003Cstring, Promise\u003Cany>>()\n\nexport function useDedupFetch\u003CT>(url: string): {\n  data: Ref\u003CT | null>\n  loading: Ref\u003Cboolean>\n  error: Ref\u003CError | null>\n} {\n  const data = ref\u003CT | null>(null) as Ref\u003CT | null>\n  const loading = ref(true)\n  const error = ref\u003CError | null>(null)\n\n  async function load() {\n    \u002F\u002F ── Check cache: is a request for this URL already in flight? ──\n    if (cache.has(url)) {\n      try {\n        data.value = await cache.get(url)!\n        return\n      } catch (e) {\n        error.value = e as Error\n        return\n      } finally {\n        loading.value = false\n      }\n    }\n\n    \u002F\u002F ── Start a new request and cache the PROMISE (not the result) ──\n    const promise = fetch(url).then(res => {\n      if (!res.ok) throw new Error(`HTTP ${res.status}`)\n      return res.json()\n    })\n\n    cache.set(url, promise)  \u002F\u002F other concurrent callers will await this same promise\n\n    try {\n      data.value = await promise\n    } catch (e) {\n      error.value = e as Error\n    } finally {\n      loading.value = false\n      cache.delete(url)  \u002F\u002F clear cache entry after resolution\n    }\n  }\n\n  load()\n  return { data, loading, error }\n}\n",[31,846,847,862,866,871,876,881,885,918,922,952,972,988,1007,1012,1050,1067,1091,1095,1106,1111,1124,1131,1152,1157,1166,1179,1183,1191,1200,1205,1209,1213,1218,1244,1274,1285,1290,1294,1308,1312,1318,1329,1337,1349,1357,1365,1379,1383,1387,1391,1398,1405],{"__ignoreMap":29},[34,848,849,851,854,856,858,860],{"class":36,"line":37},[34,850,41],{"class":40},[34,852,853],{"class":44}," { ref, ",[34,855,48],{"class":40},[34,857,56],{"class":44},[34,859,59],{"class":40},[34,861,63],{"class":62},[34,863,864],{"class":36,"line":66},[34,865,70],{"emptyLinePlaceholder":69},[34,867,868],{"class":36,"line":73},[34,869,870],{"class":111},"\u002F\u002F ── Deduplication: if a request to the same URL is in flight,\n",[34,872,873],{"class":36,"line":86},[34,874,875],{"class":111},"\u002F\u002F    reuse its promise instead of firing a second request. ──\n",[34,877,878],{"class":36,"line":100},[34,879,880],{"class":111},"\u002F\u002F Critical for components that mount simultaneously and fetch the same data.\n",[34,882,883],{"class":36,"line":115},[34,884,70],{"emptyLinePlaceholder":69},[34,886,887,890,893,895,897,900,902,904,907,910,912,915],{"class":36,"line":144},[34,888,889],{"class":40},"const",[34,891,892],{"class":96}," cache",[34,894,223],{"class":40},[34,896,469],{"class":40},[34,898,899],{"class":79}," Map",[34,901,185],{"class":44},[34,903,207],{"class":96},[34,905,906],{"class":44},", ",[34,908,909],{"class":79},"Promise",[34,911,185],{"class":44},[34,913,914],{"class":96},"any",[34,916,917],{"class":44},">>()\n",[34,919,920],{"class":36,"line":150},[34,921,70],{"emptyLinePlaceholder":69},[34,923,924,926,928,931,933,935,937,940,942,945,948,950],{"class":36,"line":155},[34,925,176],{"class":40},[34,927,179],{"class":40},[34,929,930],{"class":79}," useDedupFetch",[34,932,185],{"class":44},[34,934,188],{"class":79},[34,936,350],{"class":44},[34,938,939],{"class":89},"url",[34,941,129],{"class":40},[34,943,944],{"class":96}," string",[34,946,947],{"class":44},")",[34,949,129],{"class":40},[34,951,83],{"class":44},[34,953,954,957,959,961,963,965,967,969],{"class":36,"line":161},[34,955,956],{"class":89},"  data",[34,958,129],{"class":40},[34,960,246],{"class":79},[34,962,185],{"class":44},[34,964,188],{"class":79},[34,966,253],{"class":40},[34,968,256],{"class":96},[34,970,971],{"class":44},">\n",[34,973,974,977,979,981,983,986],{"class":36,"line":167},[34,975,976],{"class":89},"  loading",[34,978,129],{"class":40},[34,980,246],{"class":79},[34,982,185],{"class":44},[34,984,985],{"class":96},"boolean",[34,987,971],{"class":44},[34,989,990,993,995,997,999,1001,1003,1005],{"class":36,"line":173},[34,991,992],{"class":89},"  error",[34,994,129],{"class":40},[34,996,246],{"class":79},[34,998,185],{"class":44},[34,1000,291],{"class":79},[34,1002,253],{"class":40},[34,1004,256],{"class":96},[34,1006,971],{"class":44},[34,1008,1009],{"class":36,"line":194},[34,1010,1011],{"class":44},"} {\n",[34,1013,1014,1016,1018,1020,1022,1024,1026,1028,1030,1032,1034,1036,1038,1040,1042,1044,1046,1048],{"class":36,"line":213},[34,1015,238],{"class":40},[34,1017,241],{"class":96},[34,1019,223],{"class":40},[34,1021,265],{"class":79},[34,1023,185],{"class":44},[34,1025,188],{"class":79},[34,1027,253],{"class":40},[34,1029,256],{"class":96},[34,1031,350],{"class":44},[34,1033,271],{"class":96},[34,1035,135],{"class":44},[34,1037,646],{"class":40},[34,1039,246],{"class":79},[34,1041,185],{"class":44},[34,1043,188],{"class":79},[34,1045,253],{"class":40},[34,1047,256],{"class":96},[34,1049,971],{"class":44},[34,1051,1052,1054,1056,1058,1060,1062,1065],{"class":36,"line":229},[34,1053,238],{"class":40},[34,1055,315],{"class":96},[34,1057,223],{"class":40},[34,1059,265],{"class":79},[34,1061,268],{"class":44},[34,1063,1064],{"class":96},"true",[34,1066,274],{"class":44},[34,1068,1069,1071,1073,1075,1077,1079,1081,1083,1085,1087,1089],{"class":36,"line":235},[34,1070,238],{"class":40},[34,1072,282],{"class":96},[34,1074,223],{"class":40},[34,1076,265],{"class":79},[34,1078,185],{"class":44},[34,1080,291],{"class":79},[34,1082,253],{"class":40},[34,1084,256],{"class":96},[34,1086,350],{"class":44},[34,1088,271],{"class":96},[34,1090,274],{"class":44},[34,1092,1093],{"class":36,"line":277},[34,1094,70],{"emptyLinePlaceholder":69},[34,1096,1097,1099,1101,1104],{"class":36,"line":310},[34,1098,393],{"class":40},[34,1100,179],{"class":40},[34,1102,1103],{"class":79}," load",[34,1105,401],{"class":44},[34,1107,1108],{"class":36,"line":329},[34,1109,1110],{"class":111},"    \u002F\u002F ── Check cache: is a request for this URL already in flight? ──\n",[34,1112,1113,1115,1118,1121],{"class":36,"line":357},[34,1114,424],{"class":40},[34,1116,1117],{"class":44}," (cache.",[34,1119,1120],{"class":79},"has",[34,1122,1123],{"class":44},"(url)) {\n",[34,1125,1126,1129],{"class":36,"line":362},[34,1127,1128],{"class":40},"      try",[34,1130,83],{"class":44},[34,1132,1133,1136,1138,1140,1143,1146,1149],{"class":36,"line":385},[34,1134,1135],{"class":44},"        data.value ",[34,1137,262],{"class":40},[34,1139,526],{"class":40},[34,1141,1142],{"class":44}," cache.",[34,1144,1145],{"class":79},"get",[34,1147,1148],{"class":44},"(url)",[34,1150,1151],{"class":40},"!\n",[34,1153,1154],{"class":36,"line":390},[34,1155,1156],{"class":40},"        return\n",[34,1158,1159,1162,1164],{"class":36,"line":404},[34,1160,1161],{"class":44},"      } ",[34,1163,658],{"class":40},[34,1165,661],{"class":44},[34,1167,1168,1171,1173,1175,1177],{"class":36,"line":421},[34,1169,1170],{"class":44},"        error.value ",[34,1172,262],{"class":40},[34,1174,708],{"class":44},[34,1176,646],{"class":40},[34,1178,713],{"class":79},[34,1180,1181],{"class":36,"line":438},[34,1182,1156],{"class":40},[34,1184,1185,1187,1189],{"class":36,"line":443},[34,1186,1161],{"class":44},[34,1188,721],{"class":40},[34,1190,83],{"class":44},[34,1192,1193,1196,1198],{"class":36,"line":449},[34,1194,1195],{"class":44},"        loading.value ",[34,1197,262],{"class":40},[34,1199,734],{"class":96},[34,1201,1202],{"class":36,"line":461},[34,1203,1204],{"class":44},"      }\n",[34,1206,1207],{"class":36,"line":476},[34,1208,740],{"class":44},[34,1210,1211],{"class":36,"line":481},[34,1212,70],{"emptyLinePlaceholder":69},[34,1214,1215],{"class":36,"line":492},[34,1216,1217],{"class":111},"    \u002F\u002F ── Start a new request and cache the PROMISE (not the result) ──\n",[34,1219,1220,1222,1225,1227,1229,1232,1235,1237,1239,1242],{"class":36,"line":502},[34,1221,407],{"class":40},[34,1223,1224],{"class":96}," promise",[34,1226,223],{"class":40},[34,1228,529],{"class":79},[34,1230,1231],{"class":44},"(url).",[34,1233,1234],{"class":79},"then",[34,1236,268],{"class":44},[34,1238,572],{"class":89},[34,1240,1241],{"class":40}," =>",[34,1243,83],{"class":44},[34,1245,1246,1248,1250,1252,1254,1256,1258,1260,1262,1264,1266,1268,1270,1272],{"class":36,"line":507},[34,1247,549],{"class":40},[34,1249,123],{"class":44},[34,1251,429],{"class":40},[34,1253,556],{"class":44},[34,1255,559],{"class":40},[34,1257,469],{"class":40},[34,1259,564],{"class":79},[34,1261,268],{"class":44},[34,1263,569],{"class":62},[34,1265,572],{"class":44},[34,1267,575],{"class":62},[34,1269,578],{"class":44},[34,1271,591],{"class":62},[34,1273,274],{"class":44},[34,1275,1276,1279,1281,1283],{"class":36,"line":515},[34,1277,1278],{"class":40},"      return",[34,1280,609],{"class":44},[34,1282,612],{"class":79},[34,1284,458],{"class":44},[34,1286,1287],{"class":36,"line":535},[34,1288,1289],{"class":44},"    })\n",[34,1291,1292],{"class":36,"line":546},[34,1293,70],{"emptyLinePlaceholder":69},[34,1295,1296,1299,1302,1305],{"class":36,"line":596},[34,1297,1298],{"class":44},"    cache.",[34,1300,1301],{"class":79},"set",[34,1303,1304],{"class":44},"(url, promise)  ",[34,1306,1307],{"class":111},"\u002F\u002F other concurrent callers will await this same promise\n",[34,1309,1310],{"class":36,"line":617},[34,1311,70],{"emptyLinePlaceholder":69},[34,1313,1314,1316],{"class":36,"line":636},[34,1315,510],{"class":40},[34,1317,83],{"class":44},[34,1319,1320,1322,1324,1326],{"class":36,"line":652},[34,1321,639],{"class":44},[34,1323,262],{"class":40},[34,1325,526],{"class":40},[34,1327,1328],{"class":44}," promise\n",[34,1330,1331,1333,1335],{"class":36,"line":664},[34,1332,655],{"class":44},[34,1334,658],{"class":40},[34,1336,661],{"class":44},[34,1338,1339,1341,1343,1345,1347],{"class":36,"line":670},[34,1340,703],{"class":44},[34,1342,262],{"class":40},[34,1344,708],{"class":44},[34,1346,646],{"class":40},[34,1348,713],{"class":79},[34,1350,1351,1353,1355],{"class":36,"line":700},[34,1352,655],{"class":44},[34,1354,721],{"class":40},[34,1356,83],{"class":44},[34,1358,1359,1361,1363],{"class":36,"line":716},[34,1360,729],{"class":44},[34,1362,262],{"class":40},[34,1364,734],{"class":96},[34,1366,1367,1370,1373,1376],{"class":36,"line":726},[34,1368,1369],{"class":44},"      cache.",[34,1371,1372],{"class":79},"delete",[34,1374,1375],{"class":44},"(url)  ",[34,1377,1378],{"class":111},"\u002F\u002F clear cache entry after resolution\n",[34,1380,1381],{"class":36,"line":737},[34,1382,740],{"class":44},[34,1384,1385],{"class":36,"line":743},[34,1386,746],{"class":44},[34,1388,1389],{"class":36,"line":749},[34,1390,70],{"emptyLinePlaceholder":69},[34,1392,1393,1396],{"class":36,"line":754},[34,1394,1395],{"class":79},"  load",[34,1397,458],{"class":44},[34,1399,1400,1402],{"class":36,"line":771},[34,1401,826],{"class":40},[34,1403,1404],{"class":44}," { data, loading, error }\n",[34,1406,1407],{"class":36,"line":777},[34,1408,147],{"class":44},[15,1410,1412],{"id":1411},"swr-stale-while-revalidate-pattern","SWR (Stale-While-Revalidate) Pattern",[20,1414,1416],{"filename":1415,"language":23},"useSWR.ts",[25,1417,1419],{"className":27,"code":1418,"language":23,"meta":29,"style":29},"import { ref, watchEffect, toValue, type MaybeRefOrGetter, type Ref } from 'vue'\n\n\u002F\u002F ── SWR: show cached (stale) data immediately, revalidate in background ──\n\u002F\u002F Provides instant UI render from cache while fetching fresh data.\n\nconst cache = new Map\u003Cstring, { data: any; timestamp: number }>()\n\nexport function useSWR\u003CT>(\n  key: MaybeRefOrGetter\u003Cstring>,\n  fetcher: (url: string) => Promise\u003CT>,\n  ttl = 60_000  \u002F\u002F cache validity: 60 seconds\n): { data: Ref\u003CT | null>; loading: Ref\u003Cboolean>; error: Ref\u003CError | null> } {\n  const data: Ref\u003CT | null> = ref(null) as Ref\u003CT | null>\n  const loading = ref(true)\n  const error = ref\u003CError | null>(null)\n\n  watchEffect(async () => {\n    const url = toValue(key)\n    if (!url) return\n\n    const cached = cache.get(url)\n\n    \u002F\u002F ── Phase 1: serve stale data immediately (instant render) ──\n    if (cached) {\n      data.value = cached.data\n      loading.value = false\n\n      \u002F\u002F If cache is fresh, no revalidation needed\n      if (Date.now() - cached.timestamp \u003C ttl) return\n    }\n\n    \u002F\u002F ── Phase 2: revalidate in background (fresh fetch) ──\n    try {\n      const fresh = await fetcher(url)\n      data.value = fresh\n      cache.set(url, { data: fresh, timestamp: Date.now() })\n    } catch (e) {\n      \u002F\u002F If we have stale data, keep it — don't overwrite with error\n      if (!cached) error.value = e as Error\n    } finally {\n      loading.value = false\n    }\n  })\n\n  return { data, loading, error }\n}\n",[31,1420,1421,1439,1443,1448,1453,1457,1496,1500,1515,1530,1558,1571,1628,1672,1688,1712,1716,1733,1747,1760,1764,1779,1783,1788,1795,1804,1812,1816,1821,1847,1851,1855,1860,1866,1882,1891,1905,1913,1918,1937,1945,1953,1957,1962,1966,1972],{"__ignoreMap":29},[34,1422,1423,1425,1427,1429,1431,1433,1435,1437],{"class":36,"line":37},[34,1424,41],{"class":40},[34,1426,45],{"class":44},[34,1428,48],{"class":40},[34,1430,51],{"class":44},[34,1432,48],{"class":40},[34,1434,56],{"class":44},[34,1436,59],{"class":40},[34,1438,63],{"class":62},[34,1440,1441],{"class":36,"line":66},[34,1442,70],{"emptyLinePlaceholder":69},[34,1444,1445],{"class":36,"line":73},[34,1446,1447],{"class":111},"\u002F\u002F ── SWR: show cached (stale) data immediately, revalidate in background ──\n",[34,1449,1450],{"class":36,"line":86},[34,1451,1452],{"class":111},"\u002F\u002F Provides instant UI render from cache while fetching fresh data.\n",[34,1454,1455],{"class":36,"line":100},[34,1456,70],{"emptyLinePlaceholder":69},[34,1458,1459,1461,1463,1465,1467,1469,1471,1473,1476,1478,1480,1482,1485,1488,1490,1493],{"class":36,"line":115},[34,1460,889],{"class":40},[34,1462,892],{"class":96},[34,1464,223],{"class":40},[34,1466,469],{"class":40},[34,1468,899],{"class":79},[34,1470,185],{"class":44},[34,1472,207],{"class":96},[34,1474,1475],{"class":44},", { ",[34,1477,126],{"class":89},[34,1479,129],{"class":40},[34,1481,132],{"class":96},[34,1483,1484],{"class":44},"; ",[34,1486,1487],{"class":89},"timestamp",[34,1489,129],{"class":40},[34,1491,1492],{"class":96}," number",[34,1494,1495],{"class":44}," }>()\n",[34,1497,1498],{"class":36,"line":144},[34,1499,70],{"emptyLinePlaceholder":69},[34,1501,1502,1504,1506,1509,1511,1513],{"class":36,"line":150},[34,1503,176],{"class":40},[34,1505,179],{"class":40},[34,1507,1508],{"class":79}," useSWR",[34,1510,185],{"class":44},[34,1512,188],{"class":79},[34,1514,191],{"class":44},[34,1516,1517,1520,1522,1524,1526,1528],{"class":36,"line":155},[34,1518,1519],{"class":89},"  key",[34,1521,129],{"class":40},[34,1523,202],{"class":79},[34,1525,185],{"class":44},[34,1527,207],{"class":96},[34,1529,210],{"class":44},[34,1531,1532,1535,1537,1539,1541,1543,1545,1547,1549,1552,1554,1556],{"class":36,"line":161},[34,1533,1534],{"class":79},"  fetcher",[34,1536,129],{"class":40},[34,1538,123],{"class":44},[34,1540,939],{"class":89},[34,1542,129],{"class":40},[34,1544,944],{"class":96},[34,1546,135],{"class":44},[34,1548,138],{"class":40},[34,1550,1551],{"class":79}," Promise",[34,1553,185],{"class":44},[34,1555,188],{"class":79},[34,1557,210],{"class":44},[34,1559,1560,1563,1565,1568],{"class":36,"line":167},[34,1561,1562],{"class":89},"  ttl",[34,1564,223],{"class":40},[34,1566,1567],{"class":96}," 60_000",[34,1569,1570],{"class":111},"  \u002F\u002F cache validity: 60 seconds\n",[34,1572,1573,1575,1577,1580,1582,1584,1586,1588,1590,1592,1594,1597,1600,1602,1604,1606,1608,1610,1613,1615,1617,1619,1621,1623,1625],{"class":36,"line":173},[34,1574,947],{"class":44},[34,1576,129],{"class":40},[34,1578,1579],{"class":44}," { ",[34,1581,126],{"class":89},[34,1583,129],{"class":40},[34,1585,246],{"class":79},[34,1587,185],{"class":44},[34,1589,188],{"class":79},[34,1591,253],{"class":40},[34,1593,256],{"class":96},[34,1595,1596],{"class":44},">; ",[34,1598,1599],{"class":89},"loading",[34,1601,129],{"class":40},[34,1603,246],{"class":79},[34,1605,185],{"class":44},[34,1607,985],{"class":96},[34,1609,1596],{"class":44},[34,1611,1612],{"class":89},"error",[34,1614,129],{"class":40},[34,1616,246],{"class":79},[34,1618,185],{"class":44},[34,1620,291],{"class":79},[34,1622,253],{"class":40},[34,1624,256],{"class":96},[34,1626,1627],{"class":44},"> } {\n",[34,1629,1630,1632,1634,1636,1638,1640,1642,1644,1646,1648,1650,1652,1654,1656,1658,1660,1662,1664,1666,1668,1670],{"class":36,"line":194},[34,1631,238],{"class":40},[34,1633,241],{"class":96},[34,1635,129],{"class":40},[34,1637,246],{"class":79},[34,1639,185],{"class":44},[34,1641,188],{"class":79},[34,1643,253],{"class":40},[34,1645,256],{"class":96},[34,1647,259],{"class":44},[34,1649,262],{"class":40},[34,1651,265],{"class":79},[34,1653,268],{"class":44},[34,1655,271],{"class":96},[34,1657,135],{"class":44},[34,1659,646],{"class":40},[34,1661,246],{"class":79},[34,1663,185],{"class":44},[34,1665,188],{"class":79},[34,1667,253],{"class":40},[34,1669,256],{"class":96},[34,1671,971],{"class":44},[34,1673,1674,1676,1678,1680,1682,1684,1686],{"class":36,"line":213},[34,1675,238],{"class":40},[34,1677,315],{"class":96},[34,1679,223],{"class":40},[34,1681,265],{"class":79},[34,1683,268],{"class":44},[34,1685,1064],{"class":96},[34,1687,274],{"class":44},[34,1689,1690,1692,1694,1696,1698,1700,1702,1704,1706,1708,1710],{"class":36,"line":229},[34,1691,238],{"class":40},[34,1693,282],{"class":96},[34,1695,223],{"class":40},[34,1697,265],{"class":79},[34,1699,185],{"class":44},[34,1701,291],{"class":79},[34,1703,253],{"class":40},[34,1705,256],{"class":96},[34,1707,350],{"class":44},[34,1709,271],{"class":96},[34,1711,274],{"class":44},[34,1713,1714],{"class":36,"line":235},[34,1715,70],{"emptyLinePlaceholder":69},[34,1717,1718,1721,1723,1726,1729,1731],{"class":36,"line":277},[34,1719,1720],{"class":79},"  watchEffect",[34,1722,268],{"class":44},[34,1724,1725],{"class":40},"async",[34,1727,1728],{"class":44}," () ",[34,1730,138],{"class":40},[34,1732,83],{"class":44},[34,1734,1735,1737,1740,1742,1744],{"class":36,"line":310},[34,1736,407],{"class":40},[34,1738,1739],{"class":96}," url",[34,1741,223],{"class":40},[34,1743,415],{"class":79},[34,1745,1746],{"class":44},"(key)\n",[34,1748,1749,1751,1753,1755,1758],{"class":36,"line":329},[34,1750,424],{"class":40},[34,1752,123],{"class":44},[34,1754,429],{"class":40},[34,1756,1757],{"class":44},"url) ",[34,1759,435],{"class":40},[34,1761,1762],{"class":36,"line":357},[34,1763,70],{"emptyLinePlaceholder":69},[34,1765,1766,1768,1771,1773,1775,1777],{"class":36,"line":362},[34,1767,407],{"class":40},[34,1769,1770],{"class":96}," cached",[34,1772,223],{"class":40},[34,1774,1142],{"class":44},[34,1776,1145],{"class":79},[34,1778,418],{"class":44},[34,1780,1781],{"class":36,"line":385},[34,1782,70],{"emptyLinePlaceholder":69},[34,1784,1785],{"class":36,"line":390},[34,1786,1787],{"class":111},"    \u002F\u002F ── Phase 1: serve stale data immediately (instant render) ──\n",[34,1789,1790,1792],{"class":36,"line":404},[34,1791,424],{"class":40},[34,1793,1794],{"class":44}," (cached) {\n",[34,1796,1797,1799,1801],{"class":36,"line":421},[34,1798,639],{"class":44},[34,1800,262],{"class":40},[34,1802,1803],{"class":44}," cached.data\n",[34,1805,1806,1808,1810],{"class":36,"line":438},[34,1807,729],{"class":44},[34,1809,262],{"class":40},[34,1811,734],{"class":96},[34,1813,1814],{"class":36,"line":443},[34,1815,70],{"emptyLinePlaceholder":69},[34,1817,1818],{"class":36,"line":449},[34,1819,1820],{"class":111},"      \u002F\u002F If cache is fresh, no revalidation needed\n",[34,1822,1823,1825,1828,1831,1834,1837,1840,1842,1845],{"class":36,"line":461},[34,1824,549],{"class":40},[34,1826,1827],{"class":44}," (Date.",[34,1829,1830],{"class":79},"now",[34,1832,1833],{"class":44},"() ",[34,1835,1836],{"class":40},"-",[34,1838,1839],{"class":44}," cached.timestamp ",[34,1841,185],{"class":40},[34,1843,1844],{"class":44}," ttl) ",[34,1846,435],{"class":40},[34,1848,1849],{"class":36,"line":476},[34,1850,740],{"class":44},[34,1852,1853],{"class":36,"line":481},[34,1854,70],{"emptyLinePlaceholder":69},[34,1856,1857],{"class":36,"line":492},[34,1858,1859],{"class":111},"    \u002F\u002F ── Phase 2: revalidate in background (fresh fetch) ──\n",[34,1861,1862,1864],{"class":36,"line":502},[34,1863,510],{"class":40},[34,1865,83],{"class":44},[34,1867,1868,1870,1873,1875,1877,1880],{"class":36,"line":507},[34,1869,518],{"class":40},[34,1871,1872],{"class":96}," fresh",[34,1874,223],{"class":40},[34,1876,526],{"class":40},[34,1878,1879],{"class":79}," fetcher",[34,1881,418],{"class":44},[34,1883,1884,1886,1888],{"class":36,"line":515},[34,1885,639],{"class":44},[34,1887,262],{"class":40},[34,1889,1890],{"class":44}," fresh\n",[34,1892,1893,1895,1897,1900,1902],{"class":36,"line":535},[34,1894,1369],{"class":44},[34,1896,1301],{"class":79},[34,1898,1899],{"class":44},"(url, { data: fresh, timestamp: Date.",[34,1901,1830],{"class":79},[34,1903,1904],{"class":44},"() })\n",[34,1906,1907,1909,1911],{"class":36,"line":546},[34,1908,655],{"class":44},[34,1910,658],{"class":40},[34,1912,661],{"class":44},[34,1914,1915],{"class":36,"line":596},[34,1916,1917],{"class":111},"      \u002F\u002F If we have stale data, keep it — don't overwrite with error\n",[34,1919,1920,1922,1924,1926,1929,1931,1933,1935],{"class":36,"line":617},[34,1921,549],{"class":40},[34,1923,123],{"class":44},[34,1925,429],{"class":40},[34,1927,1928],{"class":44},"cached) error.value ",[34,1930,262],{"class":40},[34,1932,708],{"class":44},[34,1934,646],{"class":40},[34,1936,713],{"class":79},[34,1938,1939,1941,1943],{"class":36,"line":636},[34,1940,655],{"class":44},[34,1942,721],{"class":40},[34,1944,83],{"class":44},[34,1946,1947,1949,1951],{"class":36,"line":652},[34,1948,729],{"class":44},[34,1950,262],{"class":40},[34,1952,734],{"class":96},[34,1954,1955],{"class":36,"line":664},[34,1956,740],{"class":44},[34,1958,1959],{"class":36,"line":670},[34,1960,1961],{"class":44},"  })\n",[34,1963,1964],{"class":36,"line":700},[34,1965,70],{"emptyLinePlaceholder":69},[34,1967,1968,1970],{"class":36,"line":716},[34,1969,826],{"class":40},[34,1971,1404],{"class":44},[34,1973,1974],{"class":36,"line":726},[34,1975,147],{"class":44},[15,1977,1979],{"id":1978},"pagination-cursor-based-with-accumulated-results","Pagination — Cursor-Based with Accumulated Results",[20,1981,1983],{"filename":1982,"language":23},"usePaginatedFetch.ts",[25,1984,1986],{"className":27,"code":1985,"language":23,"meta":29,"style":29},"import { ref, computed, type Ref } from 'vue'\n\ninterface PageResult\u003CT> {\n  items: T[]\n  nextCursor: string | null\n  hasMore: boolean\n}\n\n\u002F\u002F ── Cursor pagination: appends new pages to existing results ──\n\u002F\u002F Better than offset pagination: stable under inserts\u002Fdeletes.\nexport function usePaginatedFetch\u003CT>(\n  baseUrl: string,\n  pageSize = 20\n) {\n  const items: Ref\u003CT[]> = ref([]) as Ref\u003CT[]>\n  const cursor = ref\u003Cstring | null>(null)\n  const loading = ref(false)\n  const hasMore = ref(true)\n  const error = ref\u003CError | null>(null)\n\n  \u002F\u002F ── Computed: total count is derived from items array length ──\n  const total = computed(() => items.value.length)\n\n  async function loadNext() {\n    if (loading.value || !hasMore.value) return\n\n    loading.value = true\n    error.value = null\n\n    try {\n      const params = new URLSearchParams({ limit: String(pageSize) })\n      if (cursor.value) params.set('cursor', cursor.value)\n\n      const res = await fetch(`${baseUrl}?${params}`)\n      if (!res.ok) throw new Error(`HTTP ${res.status}`)\n\n      const page: PageResult\u003CT> = await res.json()\n\n      \u002F\u002F ── Append, don't replace — preserves previously loaded items ──\n      items.value.push(...page.items)\n      cursor.value = page.nextCursor\n      hasMore.value = page.hasMore\n    } catch (e) {\n      error.value = e as Error\n    } finally {\n      loading.value = false\n    }\n  }\n\n  function reset() {\n    items.value = []\n    cursor.value = null\n    hasMore.value = true\n    error.value = null\n  }\n\n  return { items, loading, hasMore, total, error, loadNext, reset }\n}\n",[31,1987,1988,2003,2007,2021,2034,2047,2056,2060,2064,2069,2074,2089,2101,2111,2115,2151,2176,2192,2209,2233,2237,2242,2267,2271,2282,2300,2304,2312,2320,2324,2330,2353,2370,2374,2404,2434,2438,2465,2469,2474,2490,2500,2510,2518,2530,2538,2546,2550,2554,2558,2568,2578,2587,2596,2604,2608,2612,2619],{"__ignoreMap":29},[34,1989,1990,1992,1995,1997,1999,2001],{"class":36,"line":37},[34,1991,41],{"class":40},[34,1993,1994],{"class":44}," { ref, computed, ",[34,1996,48],{"class":40},[34,1998,56],{"class":44},[34,2000,59],{"class":40},[34,2002,63],{"class":62},[34,2004,2005],{"class":36,"line":66},[34,2006,70],{"emptyLinePlaceholder":69},[34,2008,2009,2011,2014,2016,2018],{"class":36,"line":73},[34,2010,76],{"class":40},[34,2012,2013],{"class":79}," PageResult",[34,2015,185],{"class":44},[34,2017,188],{"class":79},[34,2019,2020],{"class":44},"> {\n",[34,2022,2023,2026,2028,2031],{"class":36,"line":86},[34,2024,2025],{"class":89},"  items",[34,2027,129],{"class":40},[34,2029,2030],{"class":79}," T",[34,2032,2033],{"class":44},"[]\n",[34,2035,2036,2039,2041,2043,2045],{"class":36,"line":100},[34,2037,2038],{"class":89},"  nextCursor",[34,2040,129],{"class":40},[34,2042,944],{"class":96},[34,2044,253],{"class":40},[34,2046,382],{"class":96},[34,2048,2049,2052,2054],{"class":36,"line":115},[34,2050,2051],{"class":89},"  hasMore",[34,2053,129],{"class":40},[34,2055,97],{"class":96},[34,2057,2058],{"class":36,"line":144},[34,2059,147],{"class":44},[34,2061,2062],{"class":36,"line":150},[34,2063,70],{"emptyLinePlaceholder":69},[34,2065,2066],{"class":36,"line":155},[34,2067,2068],{"class":111},"\u002F\u002F ── Cursor pagination: appends new pages to existing results ──\n",[34,2070,2071],{"class":36,"line":161},[34,2072,2073],{"class":111},"\u002F\u002F Better than offset pagination: stable under inserts\u002Fdeletes.\n",[34,2075,2076,2078,2080,2083,2085,2087],{"class":36,"line":167},[34,2077,176],{"class":40},[34,2079,179],{"class":40},[34,2081,2082],{"class":79}," usePaginatedFetch",[34,2084,185],{"class":44},[34,2086,188],{"class":79},[34,2088,191],{"class":44},[34,2090,2091,2094,2096,2098],{"class":36,"line":173},[34,2092,2093],{"class":89},"  baseUrl",[34,2095,129],{"class":40},[34,2097,944],{"class":96},[34,2099,2100],{"class":44},",\n",[34,2102,2103,2106,2108],{"class":36,"line":194},[34,2104,2105],{"class":89},"  pageSize",[34,2107,223],{"class":40},[34,2109,2110],{"class":96}," 20\n",[34,2112,2113],{"class":36,"line":213},[34,2114,232],{"class":44},[34,2116,2117,2119,2122,2124,2126,2128,2130,2133,2135,2137,2140,2142,2144,2146,2148],{"class":36,"line":229},[34,2118,238],{"class":40},[34,2120,2121],{"class":96}," items",[34,2123,129],{"class":40},[34,2125,246],{"class":79},[34,2127,185],{"class":44},[34,2129,188],{"class":79},[34,2131,2132],{"class":44},"[]> ",[34,2134,262],{"class":40},[34,2136,265],{"class":79},[34,2138,2139],{"class":44},"([]) ",[34,2141,646],{"class":40},[34,2143,246],{"class":79},[34,2145,185],{"class":44},[34,2147,188],{"class":79},[34,2149,2150],{"class":44},"[]>\n",[34,2152,2153,2155,2158,2160,2162,2164,2166,2168,2170,2172,2174],{"class":36,"line":235},[34,2154,238],{"class":40},[34,2156,2157],{"class":96}," cursor",[34,2159,223],{"class":40},[34,2161,265],{"class":79},[34,2163,185],{"class":44},[34,2165,207],{"class":96},[34,2167,253],{"class":40},[34,2169,256],{"class":96},[34,2171,350],{"class":44},[34,2173,271],{"class":96},[34,2175,274],{"class":44},[34,2177,2178,2180,2182,2184,2186,2188,2190],{"class":36,"line":277},[34,2179,238],{"class":40},[34,2181,315],{"class":96},[34,2183,223],{"class":40},[34,2185,265],{"class":79},[34,2187,268],{"class":44},[34,2189,324],{"class":96},[34,2191,274],{"class":44},[34,2193,2194,2196,2199,2201,2203,2205,2207],{"class":36,"line":310},[34,2195,238],{"class":40},[34,2197,2198],{"class":96}," hasMore",[34,2200,223],{"class":40},[34,2202,265],{"class":79},[34,2204,268],{"class":44},[34,2206,1064],{"class":96},[34,2208,274],{"class":44},[34,2210,2211,2213,2215,2217,2219,2221,2223,2225,2227,2229,2231],{"class":36,"line":329},[34,2212,238],{"class":40},[34,2214,282],{"class":96},[34,2216,223],{"class":40},[34,2218,265],{"class":79},[34,2220,185],{"class":44},[34,2222,291],{"class":79},[34,2224,253],{"class":40},[34,2226,256],{"class":96},[34,2228,350],{"class":44},[34,2230,271],{"class":96},[34,2232,274],{"class":44},[34,2234,2235],{"class":36,"line":357},[34,2236,70],{"emptyLinePlaceholder":69},[34,2238,2239],{"class":36,"line":362},[34,2240,2241],{"class":111},"  \u002F\u002F ── Computed: total count is derived from items array length ──\n",[34,2243,2244,2246,2249,2251,2254,2257,2259,2262,2265],{"class":36,"line":385},[34,2245,238],{"class":40},[34,2247,2248],{"class":96}," total",[34,2250,223],{"class":40},[34,2252,2253],{"class":79}," computed",[34,2255,2256],{"class":44},"(() ",[34,2258,138],{"class":40},[34,2260,2261],{"class":44}," items.value.",[34,2263,2264],{"class":96},"length",[34,2266,274],{"class":44},[34,2268,2269],{"class":36,"line":390},[34,2270,70],{"emptyLinePlaceholder":69},[34,2272,2273,2275,2277,2280],{"class":36,"line":404},[34,2274,393],{"class":40},[34,2276,179],{"class":40},[34,2278,2279],{"class":79}," loadNext",[34,2281,401],{"class":44},[34,2283,2284,2286,2289,2292,2295,2298],{"class":36,"line":421},[34,2285,424],{"class":40},[34,2287,2288],{"class":44}," (loading.value ",[34,2290,2291],{"class":40},"||",[34,2293,2294],{"class":40}," !",[34,2296,2297],{"class":44},"hasMore.value) ",[34,2299,435],{"class":40},[34,2301,2302],{"class":36,"line":438},[34,2303,70],{"emptyLinePlaceholder":69},[34,2305,2306,2308,2310],{"class":36,"line":443},[34,2307,484],{"class":44},[34,2309,262],{"class":40},[34,2311,489],{"class":96},[34,2313,2314,2316,2318],{"class":36,"line":449},[34,2315,495],{"class":44},[34,2317,262],{"class":40},[34,2319,382],{"class":96},[34,2321,2322],{"class":36,"line":461},[34,2323,70],{"emptyLinePlaceholder":69},[34,2325,2326,2328],{"class":36,"line":476},[34,2327,510],{"class":40},[34,2329,83],{"class":44},[34,2331,2332,2334,2337,2339,2341,2344,2347,2350],{"class":36,"line":481},[34,2333,518],{"class":40},[34,2335,2336],{"class":96}," params",[34,2338,223],{"class":40},[34,2340,469],{"class":40},[34,2342,2343],{"class":79}," URLSearchParams",[34,2345,2346],{"class":44},"({ limit: ",[34,2348,2349],{"class":79},"String",[34,2351,2352],{"class":44},"(pageSize) })\n",[34,2354,2355,2357,2360,2362,2364,2367],{"class":36,"line":492},[34,2356,549],{"class":40},[34,2358,2359],{"class":44}," (cursor.value) params.",[34,2361,1301],{"class":79},[34,2363,268],{"class":44},[34,2365,2366],{"class":62},"'cursor'",[34,2368,2369],{"class":44},", cursor.value)\n",[34,2371,2372],{"class":36,"line":502},[34,2373,70],{"emptyLinePlaceholder":69},[34,2375,2376,2378,2380,2382,2384,2386,2388,2391,2394,2397,2400,2402],{"class":36,"line":507},[34,2377,518],{"class":40},[34,2379,521],{"class":96},[34,2381,223],{"class":40},[34,2383,526],{"class":40},[34,2385,529],{"class":79},[34,2387,268],{"class":44},[34,2389,2390],{"class":62},"`${",[34,2392,2393],{"class":44},"baseUrl",[34,2395,2396],{"class":62},"}?${",[34,2398,2399],{"class":44},"params",[34,2401,591],{"class":62},[34,2403,274],{"class":44},[34,2405,2406,2408,2410,2412,2414,2416,2418,2420,2422,2424,2426,2428,2430,2432],{"class":36,"line":515},[34,2407,549],{"class":40},[34,2409,123],{"class":44},[34,2411,429],{"class":40},[34,2413,556],{"class":44},[34,2415,559],{"class":40},[34,2417,469],{"class":40},[34,2419,564],{"class":79},[34,2421,268],{"class":44},[34,2423,569],{"class":62},[34,2425,572],{"class":44},[34,2427,575],{"class":62},[34,2429,578],{"class":44},[34,2431,591],{"class":62},[34,2433,274],{"class":44},[34,2435,2436],{"class":36,"line":535},[34,2437,70],{"emptyLinePlaceholder":69},[34,2439,2440,2442,2445,2447,2449,2451,2453,2455,2457,2459,2461,2463],{"class":36,"line":546},[34,2441,518],{"class":40},[34,2443,2444],{"class":96}," page",[34,2446,129],{"class":40},[34,2448,2013],{"class":79},[34,2450,185],{"class":44},[34,2452,188],{"class":79},[34,2454,259],{"class":44},[34,2456,262],{"class":40},[34,2458,526],{"class":40},[34,2460,609],{"class":44},[34,2462,612],{"class":79},[34,2464,458],{"class":44},[34,2466,2467],{"class":36,"line":596},[34,2468,70],{"emptyLinePlaceholder":69},[34,2470,2471],{"class":36,"line":617},[34,2472,2473],{"class":111},"      \u002F\u002F ── Append, don't replace — preserves previously loaded items ──\n",[34,2475,2476,2479,2482,2484,2487],{"class":36,"line":636},[34,2477,2478],{"class":44},"      items.value.",[34,2480,2481],{"class":79},"push",[34,2483,268],{"class":44},[34,2485,2486],{"class":40},"...",[34,2488,2489],{"class":44},"page.items)\n",[34,2491,2492,2495,2497],{"class":36,"line":652},[34,2493,2494],{"class":44},"      cursor.value ",[34,2496,262],{"class":40},[34,2498,2499],{"class":44}," page.nextCursor\n",[34,2501,2502,2505,2507],{"class":36,"line":664},[34,2503,2504],{"class":44},"      hasMore.value ",[34,2506,262],{"class":40},[34,2508,2509],{"class":44}," page.hasMore\n",[34,2511,2512,2514,2516],{"class":36,"line":670},[34,2513,655],{"class":44},[34,2515,658],{"class":40},[34,2517,661],{"class":44},[34,2519,2520,2522,2524,2526,2528],{"class":36,"line":700},[34,2521,703],{"class":44},[34,2523,262],{"class":40},[34,2525,708],{"class":44},[34,2527,646],{"class":40},[34,2529,713],{"class":79},[34,2531,2532,2534,2536],{"class":36,"line":716},[34,2533,655],{"class":44},[34,2535,721],{"class":40},[34,2537,83],{"class":44},[34,2539,2540,2542,2544],{"class":36,"line":726},[34,2541,729],{"class":44},[34,2543,262],{"class":40},[34,2545,734],{"class":96},[34,2547,2548],{"class":36,"line":737},[34,2549,740],{"class":44},[34,2551,2552],{"class":36,"line":743},[34,2553,746],{"class":44},[34,2555,2556],{"class":36,"line":749},[34,2557,70],{"emptyLinePlaceholder":69},[34,2559,2560,2563,2566],{"class":36,"line":754},[34,2561,2562],{"class":40},"  function",[34,2564,2565],{"class":79}," reset",[34,2567,401],{"class":44},[34,2569,2570,2573,2575],{"class":36,"line":771},[34,2571,2572],{"class":44},"    items.value ",[34,2574,262],{"class":40},[34,2576,2577],{"class":44}," []\n",[34,2579,2580,2583,2585],{"class":36,"line":777},[34,2581,2582],{"class":44},"    cursor.value ",[34,2584,262],{"class":40},[34,2586,382],{"class":96},[34,2588,2589,2592,2594],{"class":36,"line":786},[34,2590,2591],{"class":44},"    hasMore.value ",[34,2593,262],{"class":40},[34,2595,489],{"class":96},[34,2597,2598,2600,2602],{"class":36,"line":801},[34,2599,495],{"class":44},[34,2601,262],{"class":40},[34,2603,382],{"class":96},[34,2605,2606],{"class":36,"line":813},[34,2607,746],{"class":44},[34,2609,2610],{"class":36,"line":818},[34,2611,70],{"emptyLinePlaceholder":69},[34,2613,2614,2616],{"class":36,"line":823},[34,2615,826],{"class":40},[34,2617,2618],{"class":44}," { items, loading, hasMore, total, error, loadNext, reset }\n",[34,2620,2621],{"class":36,"line":832},[34,2622,147],{"class":44},[15,2624,2626],{"id":2625},"optimistic-updates-rollback-on-failure","Optimistic Updates — Rollback on Failure",[20,2628,2630],{"filename":2629,"language":23},"optimistic-update.ts",[25,2631,2633],{"className":27,"code":2632,"language":23,"meta":29,"style":29},"import { ref, type Ref } from 'vue'\n\n\u002F\u002F ── Optimistic update: update UI before server confirms ──\n\u002F\u002F Rollback on failure — user sees instant response, no waiting.\nexport function useOptimisticUpdate\u003CT extends { id: string }>(\n  items: Ref\u003CT[]>,\n  updateFn: (item: T) => Promise\u003CT>\n) {\n  const error: Ref\u003Cstring | null> = ref(null)\n\n  async function update(item: T, patch: Partial\u003CT>) {\n    error.value = null\n\n    \u002F\u002F ── Save original for potential rollback ──\n    const original = items.value.find(i => i.id === item.id)\n    if (!original) return\n\n    \u002F\u002F ── Phase 1: apply patch optimistically (instant UI) ──\n    const optimistic = { ...original, ...patch }\n    items.value = items.value.map(i => i.id === item.id ? optimistic : i)\n\n    try {\n      \u002F\u002F ── Phase 2: send to server, wait for confirmation ──\n      const confirmed = await updateFn(optimistic)\n      \u002F\u002F Replace with server-confirmed version (may differ from optimistic)\n      items.value = items.value.map(i => i.id === confirmed.id ? confirmed : i)\n    } catch (e) {\n      \u002F\u002F ── Phase 3: rollback on failure ──\n      items.value = items.value.map(i => i.id === original.id ? original : i)\n      error.value = (e as Error).message\n    }\n  }\n\n  return { update, error }\n}\n\n\u002F\u002F ── Usage: ──────────────────────────────────────────────\n\u002F\u002F const { update, error } = useOptimisticUpdate(todos, api.updateTodo)\n\u002F\u002F await update(todo, { completed: true })  \u002F\u002F UI flips instantly, rolls back on error\n",[31,2634,2635,2649,2653,2658,2663,2691,2706,2734,2738,2768,2772,2806,2814,2818,2823,2852,2865,2869,2874,2895,2930,2934,2940,2945,2962,2967,3000,3008,3013,3045,3060,3064,3068,3072,3079,3083,3087,3092,3097],{"__ignoreMap":29},[34,2636,2637,2639,2641,2643,2645,2647],{"class":36,"line":37},[34,2638,41],{"class":40},[34,2640,853],{"class":44},[34,2642,48],{"class":40},[34,2644,56],{"class":44},[34,2646,59],{"class":40},[34,2648,63],{"class":62},[34,2650,2651],{"class":36,"line":66},[34,2652,70],{"emptyLinePlaceholder":69},[34,2654,2655],{"class":36,"line":73},[34,2656,2657],{"class":111},"\u002F\u002F ── Optimistic update: update UI before server confirms ──\n",[34,2659,2660],{"class":36,"line":86},[34,2661,2662],{"class":111},"\u002F\u002F Rollback on failure — user sees instant response, no waiting.\n",[34,2664,2665,2667,2669,2672,2674,2676,2679,2681,2684,2686,2688],{"class":36,"line":100},[34,2666,176],{"class":40},[34,2668,179],{"class":40},[34,2670,2671],{"class":79}," useOptimisticUpdate",[34,2673,185],{"class":44},[34,2675,188],{"class":79},[34,2677,2678],{"class":40}," extends",[34,2680,1579],{"class":44},[34,2682,2683],{"class":89},"id",[34,2685,129],{"class":40},[34,2687,944],{"class":96},[34,2689,2690],{"class":44}," }>(\n",[34,2692,2693,2695,2697,2699,2701,2703],{"class":36,"line":115},[34,2694,2025],{"class":89},[34,2696,129],{"class":40},[34,2698,246],{"class":79},[34,2700,185],{"class":44},[34,2702,188],{"class":79},[34,2704,2705],{"class":44},"[]>,\n",[34,2707,2708,2711,2713,2715,2718,2720,2722,2724,2726,2728,2730,2732],{"class":36,"line":144},[34,2709,2710],{"class":79},"  updateFn",[34,2712,129],{"class":40},[34,2714,123],{"class":44},[34,2716,2717],{"class":89},"item",[34,2719,129],{"class":40},[34,2721,2030],{"class":79},[34,2723,135],{"class":44},[34,2725,138],{"class":40},[34,2727,1551],{"class":79},[34,2729,185],{"class":44},[34,2731,188],{"class":79},[34,2733,971],{"class":44},[34,2735,2736],{"class":36,"line":150},[34,2737,232],{"class":44},[34,2739,2740,2742,2744,2746,2748,2750,2752,2754,2756,2758,2760,2762,2764,2766],{"class":36,"line":155},[34,2741,238],{"class":40},[34,2743,282],{"class":96},[34,2745,129],{"class":40},[34,2747,246],{"class":79},[34,2749,185],{"class":44},[34,2751,207],{"class":96},[34,2753,253],{"class":40},[34,2755,256],{"class":96},[34,2757,259],{"class":44},[34,2759,262],{"class":40},[34,2761,265],{"class":79},[34,2763,268],{"class":44},[34,2765,271],{"class":96},[34,2767,274],{"class":44},[34,2769,2770],{"class":36,"line":161},[34,2771,70],{"emptyLinePlaceholder":69},[34,2773,2774,2776,2778,2781,2783,2785,2787,2789,2791,2794,2796,2799,2801,2803],{"class":36,"line":167},[34,2775,393],{"class":40},[34,2777,179],{"class":40},[34,2779,2780],{"class":79}," update",[34,2782,268],{"class":44},[34,2784,2717],{"class":89},[34,2786,129],{"class":40},[34,2788,2030],{"class":79},[34,2790,906],{"class":44},[34,2792,2793],{"class":89},"patch",[34,2795,129],{"class":40},[34,2797,2798],{"class":79}," Partial",[34,2800,185],{"class":44},[34,2802,188],{"class":79},[34,2804,2805],{"class":44},">) {\n",[34,2807,2808,2810,2812],{"class":36,"line":173},[34,2809,495],{"class":44},[34,2811,262],{"class":40},[34,2813,382],{"class":96},[34,2815,2816],{"class":36,"line":194},[34,2817,70],{"emptyLinePlaceholder":69},[34,2819,2820],{"class":36,"line":213},[34,2821,2822],{"class":111},"    \u002F\u002F ── Save original for potential rollback ──\n",[34,2824,2825,2827,2830,2832,2834,2837,2839,2842,2844,2847,2849],{"class":36,"line":229},[34,2826,407],{"class":40},[34,2828,2829],{"class":96}," original",[34,2831,223],{"class":40},[34,2833,2261],{"class":44},[34,2835,2836],{"class":79},"find",[34,2838,268],{"class":44},[34,2840,2841],{"class":89},"i",[34,2843,1241],{"class":40},[34,2845,2846],{"class":44}," i.id ",[34,2848,690],{"class":40},[34,2850,2851],{"class":44}," item.id)\n",[34,2853,2854,2856,2858,2860,2863],{"class":36,"line":235},[34,2855,424],{"class":40},[34,2857,123],{"class":44},[34,2859,429],{"class":40},[34,2861,2862],{"class":44},"original) ",[34,2864,435],{"class":40},[34,2866,2867],{"class":36,"line":277},[34,2868,70],{"emptyLinePlaceholder":69},[34,2870,2871],{"class":36,"line":310},[34,2872,2873],{"class":111},"    \u002F\u002F ── Phase 1: apply patch optimistically (instant UI) ──\n",[34,2875,2876,2878,2881,2883,2885,2887,2890,2892],{"class":36,"line":329},[34,2877,407],{"class":40},[34,2879,2880],{"class":96}," optimistic",[34,2882,223],{"class":40},[34,2884,1579],{"class":44},[34,2886,2486],{"class":40},[34,2888,2889],{"class":44},"original, ",[34,2891,2486],{"class":40},[34,2893,2894],{"class":44},"patch }\n",[34,2896,2897,2899,2901,2903,2906,2908,2910,2912,2914,2916,2919,2922,2925,2927],{"class":36,"line":357},[34,2898,2572],{"class":44},[34,2900,262],{"class":40},[34,2902,2261],{"class":44},[34,2904,2905],{"class":79},"map",[34,2907,268],{"class":44},[34,2909,2841],{"class":89},[34,2911,1241],{"class":40},[34,2913,2846],{"class":44},[34,2915,690],{"class":40},[34,2917,2918],{"class":44}," item.id ",[34,2920,2921],{"class":40},"?",[34,2923,2924],{"class":44}," optimistic ",[34,2926,129],{"class":40},[34,2928,2929],{"class":44}," i)\n",[34,2931,2932],{"class":36,"line":362},[34,2933,70],{"emptyLinePlaceholder":69},[34,2935,2936,2938],{"class":36,"line":385},[34,2937,510],{"class":40},[34,2939,83],{"class":44},[34,2941,2942],{"class":36,"line":390},[34,2943,2944],{"class":111},"      \u002F\u002F ── Phase 2: send to server, wait for confirmation ──\n",[34,2946,2947,2949,2952,2954,2956,2959],{"class":36,"line":404},[34,2948,518],{"class":40},[34,2950,2951],{"class":96}," confirmed",[34,2953,223],{"class":40},[34,2955,526],{"class":40},[34,2957,2958],{"class":79}," updateFn",[34,2960,2961],{"class":44},"(optimistic)\n",[34,2963,2964],{"class":36,"line":421},[34,2965,2966],{"class":111},"      \u002F\u002F Replace with server-confirmed version (may differ from optimistic)\n",[34,2968,2969,2972,2974,2976,2978,2980,2982,2984,2986,2988,2991,2993,2996,2998],{"class":36,"line":438},[34,2970,2971],{"class":44},"      items.value ",[34,2973,262],{"class":40},[34,2975,2261],{"class":44},[34,2977,2905],{"class":79},[34,2979,268],{"class":44},[34,2981,2841],{"class":89},[34,2983,1241],{"class":40},[34,2985,2846],{"class":44},[34,2987,690],{"class":40},[34,2989,2990],{"class":44}," confirmed.id ",[34,2992,2921],{"class":40},[34,2994,2995],{"class":44}," confirmed ",[34,2997,129],{"class":40},[34,2999,2929],{"class":44},[34,3001,3002,3004,3006],{"class":36,"line":443},[34,3003,655],{"class":44},[34,3005,658],{"class":40},[34,3007,661],{"class":44},[34,3009,3010],{"class":36,"line":449},[34,3011,3012],{"class":111},"      \u002F\u002F ── Phase 3: rollback on failure ──\n",[34,3014,3015,3017,3019,3021,3023,3025,3027,3029,3031,3033,3036,3038,3041,3043],{"class":36,"line":461},[34,3016,2971],{"class":44},[34,3018,262],{"class":40},[34,3020,2261],{"class":44},[34,3022,2905],{"class":79},[34,3024,268],{"class":44},[34,3026,2841],{"class":89},[34,3028,1241],{"class":40},[34,3030,2846],{"class":44},[34,3032,690],{"class":40},[34,3034,3035],{"class":44}," original.id ",[34,3037,2921],{"class":40},[34,3039,3040],{"class":44}," original ",[34,3042,129],{"class":40},[34,3044,2929],{"class":44},[34,3046,3047,3049,3051,3053,3055,3057],{"class":36,"line":476},[34,3048,703],{"class":44},[34,3050,262],{"class":40},[34,3052,675],{"class":44},[34,3054,646],{"class":40},[34,3056,564],{"class":79},[34,3058,3059],{"class":44},").message\n",[34,3061,3062],{"class":36,"line":481},[34,3063,740],{"class":44},[34,3065,3066],{"class":36,"line":492},[34,3067,746],{"class":44},[34,3069,3070],{"class":36,"line":502},[34,3071,70],{"emptyLinePlaceholder":69},[34,3073,3074,3076],{"class":36,"line":507},[34,3075,826],{"class":40},[34,3077,3078],{"class":44}," { update, error }\n",[34,3080,3081],{"class":36,"line":515},[34,3082,147],{"class":44},[34,3084,3085],{"class":36,"line":535},[34,3086,70],{"emptyLinePlaceholder":69},[34,3088,3089],{"class":36,"line":546},[34,3090,3091],{"class":111},"\u002F\u002F ── Usage: ──────────────────────────────────────────────\n",[34,3093,3094],{"class":36,"line":596},[34,3095,3096],{"class":111},"\u002F\u002F const { update, error } = useOptimisticUpdate(todos, api.updateTodo)\n",[34,3098,3099,3102],{"class":36,"line":617},[34,3100,3101],{"class":111},"\u002F\u002F await update(todo, { completed: true })",[34,3103,3104],{"class":111},"  \u002F\u002F UI flips instantly, rolls back on error\n",[15,3106,3108],{"id":3107},"suspense-async-component-orchestration","Suspense — Async Component Orchestration",[20,3110,3113,3452,3595,3599,3977,3981,4109,4113,4117,4242],{"filename":3111,"language":3112},"SuspensePattern.vue","vue",[25,3114,3117],{"className":3115,"code":3116,"language":3112,"meta":29,"style":29},"language-vue shiki shiki-themes github-light github-dark","\u003Cscript setup>\nimport { defineAsyncComponent, Suspense, ref, onErrorCaptured } from 'vue'\n\n\u002F\u002F ── Suspense: declarative async component loading ──\n\u002F\u002F Parent shows fallback while child's async setup() resolves.\nconst AsyncDashboard = defineAsyncComponent(() => import('.\u002FDashboard.vue'))\n\nconst error = ref(null)\n\n\u002F\u002F ── onErrorCaptured: catches errors from async setup in children ──\nonErrorCaptured((err) => {\n  error.value = err\n  return false  \u002F\u002F stop propagation\n})\n\u003C\u002Fscript>\n\n\u003Ctemplate>\n  \u003C!-- ── Suspense boundary: fallback slot while async children load ── -->\n  \u003CSuspense>\n    \u003Ctemplate #default>\n      \u003CAsyncDashboard \u002F>\n    \u003C\u002Ftemplate>\n    \u003Ctemplate #fallback>\n      \u003Cdiv class=\"loading-skeleton\">Loading dashboard…\u003C\u002Fdiv>\n    \u003C\u002Ftemplate>\n  \u003C\u002FSuspense>\n\n  \u003C!-- ── Error state (Suspense doesn't handle errors, parent does) ── -->\n  \u003Cdiv v-if=\"error\" class=\"error\">\n    Failed to load: {{ error.message }}\n    \u003Cbutton @click=\"error = null\">Retry\u003C\u002Fbutton>\n  \u003C\u002Fdiv>\n\u003C\u002Ftemplate>\n",[31,3118,3119,3132,3143,3147,3152,3157,3184,3188,3204,3208,3213,3230,3240,3249,3254,3263,3267,3276,3281,3291,3306,3317,3326,3339,3361,3369,3378,3382,3387,3409,3414,3436,3444],{"__ignoreMap":29},[34,3120,3121,3123,3127,3130],{"class":36,"line":37},[34,3122,185],{"class":44},[34,3124,3126],{"class":3125},"sk71V","script",[34,3128,3129],{"class":79}," setup",[34,3131,971],{"class":44},[34,3133,3134,3136,3139,3141],{"class":36,"line":66},[34,3135,41],{"class":40},[34,3137,3138],{"class":44}," { defineAsyncComponent, Suspense, ref, onErrorCaptured } ",[34,3140,59],{"class":40},[34,3142,63],{"class":62},[34,3144,3145],{"class":36,"line":73},[34,3146,70],{"emptyLinePlaceholder":69},[34,3148,3149],{"class":36,"line":86},[34,3150,3151],{"class":111},"\u002F\u002F ── Suspense: declarative async component loading ──\n",[34,3153,3154],{"class":36,"line":100},[34,3155,3156],{"class":111},"\u002F\u002F Parent shows fallback while child's async setup() resolves.\n",[34,3158,3159,3161,3164,3166,3169,3171,3173,3176,3178,3181],{"class":36,"line":115},[34,3160,889],{"class":40},[34,3162,3163],{"class":96}," AsyncDashboard",[34,3165,223],{"class":40},[34,3167,3168],{"class":79}," defineAsyncComponent",[34,3170,2256],{"class":44},[34,3172,138],{"class":40},[34,3174,3175],{"class":40}," import",[34,3177,268],{"class":44},[34,3179,3180],{"class":62},"'.\u002FDashboard.vue'",[34,3182,3183],{"class":44},"))\n",[34,3185,3186],{"class":36,"line":144},[34,3187,70],{"emptyLinePlaceholder":69},[34,3189,3190,3192,3194,3196,3198,3200,3202],{"class":36,"line":150},[34,3191,889],{"class":40},[34,3193,282],{"class":96},[34,3195,223],{"class":40},[34,3197,265],{"class":79},[34,3199,268],{"class":44},[34,3201,271],{"class":96},[34,3203,274],{"class":44},[34,3205,3206],{"class":36,"line":155},[34,3207,70],{"emptyLinePlaceholder":69},[34,3209,3210],{"class":36,"line":161},[34,3211,3212],{"class":111},"\u002F\u002F ── onErrorCaptured: catches errors from async setup in children ──\n",[34,3214,3215,3218,3221,3224,3226,3228],{"class":36,"line":167},[34,3216,3217],{"class":79},"onErrorCaptured",[34,3219,3220],{"class":44},"((",[34,3222,3223],{"class":89},"err",[34,3225,135],{"class":44},[34,3227,138],{"class":40},[34,3229,83],{"class":44},[34,3231,3232,3235,3237],{"class":36,"line":173},[34,3233,3234],{"class":44},"  error.value ",[34,3236,262],{"class":40},[34,3238,3239],{"class":44}," err\n",[34,3241,3242,3244,3246],{"class":36,"line":194},[34,3243,826],{"class":40},[34,3245,766],{"class":96},[34,3247,3248],{"class":111},"  \u002F\u002F stop propagation\n",[34,3250,3251],{"class":36,"line":213},[34,3252,3253],{"class":44},"})\n",[34,3255,3256,3259,3261],{"class":36,"line":229},[34,3257,3258],{"class":44},"\u003C\u002F",[34,3260,3126],{"class":3125},[34,3262,971],{"class":44},[34,3264,3265],{"class":36,"line":235},[34,3266,70],{"emptyLinePlaceholder":69},[34,3268,3269,3271,3274],{"class":36,"line":277},[34,3270,185],{"class":44},[34,3272,3273],{"class":3125},"template",[34,3275,971],{"class":44},[34,3277,3278],{"class":36,"line":310},[34,3279,3280],{"class":111},"  \u003C!-- ── Suspense boundary: fallback slot while async children load ── -->\n",[34,3282,3283,3286,3289],{"class":36,"line":329},[34,3284,3285],{"class":44},"  \u003C",[34,3287,3288],{"class":3125},"Suspense",[34,3290,971],{"class":44},[34,3292,3293,3296,3298,3301,3304],{"class":36,"line":357},[34,3294,3295],{"class":44},"    \u003C",[34,3297,3273],{"class":3125},[34,3299,3300],{"class":44}," #",[34,3302,3303],{"class":79},"default",[34,3305,971],{"class":44},[34,3307,3308,3311,3314],{"class":36,"line":362},[34,3309,3310],{"class":44},"      \u003C",[34,3312,3313],{"class":3125},"AsyncDashboard",[34,3315,3316],{"class":44}," \u002F>\n",[34,3318,3319,3322,3324],{"class":36,"line":385},[34,3320,3321],{"class":44},"    \u003C\u002F",[34,3323,3273],{"class":3125},[34,3325,971],{"class":44},[34,3327,3328,3330,3332,3334,3337],{"class":36,"line":390},[34,3329,3295],{"class":44},[34,3331,3273],{"class":3125},[34,3333,3300],{"class":44},[34,3335,3336],{"class":79},"fallback",[34,3338,971],{"class":44},[34,3340,3341,3343,3346,3349,3351,3354,3357,3359],{"class":36,"line":404},[34,3342,3310],{"class":44},[34,3344,3345],{"class":3125},"div",[34,3347,3348],{"class":79}," class",[34,3350,262],{"class":44},[34,3352,3353],{"class":62},"\"loading-skeleton\"",[34,3355,3356],{"class":44},">Loading dashboard…\u003C\u002F",[34,3358,3345],{"class":3125},[34,3360,971],{"class":44},[34,3362,3363,3365,3367],{"class":36,"line":421},[34,3364,3321],{"class":44},[34,3366,3273],{"class":3125},[34,3368,971],{"class":44},[34,3370,3371,3374,3376],{"class":36,"line":438},[34,3372,3373],{"class":44},"  \u003C\u002F",[34,3375,3288],{"class":3125},[34,3377,971],{"class":44},[34,3379,3380],{"class":36,"line":443},[34,3381,70],{"emptyLinePlaceholder":69},[34,3383,3384],{"class":36,"line":449},[34,3385,3386],{"class":111},"  \u003C!-- ── Error state (Suspense doesn't handle errors, parent does) ── -->\n",[34,3388,3389,3391,3393,3396,3398,3401,3403,3405,3407],{"class":36,"line":461},[34,3390,3285],{"class":44},[34,3392,3345],{"class":3125},[34,3394,3395],{"class":79}," v-if",[34,3397,262],{"class":44},[34,3399,3400],{"class":62},"\"error\"",[34,3402,3348],{"class":79},[34,3404,262],{"class":44},[34,3406,3400],{"class":62},[34,3408,971],{"class":44},[34,3410,3411],{"class":36,"line":476},[34,3412,3413],{"class":44},"    Failed to load: {{ error.message }}\n",[34,3415,3416,3418,3421,3424,3426,3429,3432,3434],{"class":36,"line":481},[34,3417,3295],{"class":44},[34,3419,3420],{"class":3125},"button",[34,3422,3423],{"class":79}," @click",[34,3425,262],{"class":44},[34,3427,3428],{"class":62},"\"error = null\"",[34,3430,3431],{"class":44},">Retry\u003C\u002F",[34,3433,3420],{"class":3125},[34,3435,971],{"class":44},[34,3437,3438,3440,3442],{"class":36,"line":492},[34,3439,3373],{"class":44},[34,3441,3345],{"class":3125},[34,3443,971],{"class":44},[34,3445,3446,3448,3450],{"class":36,"line":502},[34,3447,3258],{"class":44},[34,3449,3273],{"class":3125},[34,3451,971],{"class":44},[20,3453,3455],{"filename":3454,"language":3112},"DashboardChild.vue",[25,3456,3458],{"className":3115,"code":3457,"language":3112,"meta":29,"style":29},"\u003Cscript setup>\nimport { ref } from 'vue'\n\n\u002F\u002F ── Top-level await makes this component async ──\n\u002F\u002F Must be inside a \u003CSuspense> boundary or it throws.\nconst data = ref(null)\n\n\u002F\u002F Suspense waits for this promise to resolve before rendering the component.\nconst res = await fetch('\u002Fapi\u002Fdashboard-data')\ndata.value = await res.json()\n\u003C\u002Fscript>\n\n\u003Ctemplate>\n  \u003Cdiv>{{ data }}\u003C\u002Fdiv>\n\u003C\u002Ftemplate>\n",[31,3459,3460,3470,3481,3485,3490,3495,3511,3515,3520,3539,3554,3562,3566,3574,3587],{"__ignoreMap":29},[34,3461,3462,3464,3466,3468],{"class":36,"line":37},[34,3463,185],{"class":44},[34,3465,3126],{"class":3125},[34,3467,3129],{"class":79},[34,3469,971],{"class":44},[34,3471,3472,3474,3477,3479],{"class":36,"line":66},[34,3473,41],{"class":40},[34,3475,3476],{"class":44}," { ref } ",[34,3478,59],{"class":40},[34,3480,63],{"class":62},[34,3482,3483],{"class":36,"line":73},[34,3484,70],{"emptyLinePlaceholder":69},[34,3486,3487],{"class":36,"line":86},[34,3488,3489],{"class":111},"\u002F\u002F ── Top-level await makes this component async ──\n",[34,3491,3492],{"class":36,"line":100},[34,3493,3494],{"class":111},"\u002F\u002F Must be inside a \u003CSuspense> boundary or it throws.\n",[34,3496,3497,3499,3501,3503,3505,3507,3509],{"class":36,"line":115},[34,3498,889],{"class":40},[34,3500,241],{"class":96},[34,3502,223],{"class":40},[34,3504,265],{"class":79},[34,3506,268],{"class":44},[34,3508,271],{"class":96},[34,3510,274],{"class":44},[34,3512,3513],{"class":36,"line":144},[34,3514,70],{"emptyLinePlaceholder":69},[34,3516,3517],{"class":36,"line":150},[34,3518,3519],{"class":111},"\u002F\u002F Suspense waits for this promise to resolve before rendering the component.\n",[34,3521,3522,3524,3526,3528,3530,3532,3534,3537],{"class":36,"line":155},[34,3523,889],{"class":40},[34,3525,521],{"class":96},[34,3527,223],{"class":40},[34,3529,526],{"class":40},[34,3531,529],{"class":79},[34,3533,268],{"class":44},[34,3535,3536],{"class":62},"'\u002Fapi\u002Fdashboard-data'",[34,3538,274],{"class":44},[34,3540,3541,3544,3546,3548,3550,3552],{"class":36,"line":161},[34,3542,3543],{"class":44},"data.value ",[34,3545,262],{"class":40},[34,3547,526],{"class":40},[34,3549,609],{"class":44},[34,3551,612],{"class":79},[34,3553,458],{"class":44},[34,3555,3556,3558,3560],{"class":36,"line":167},[34,3557,3258],{"class":44},[34,3559,3126],{"class":3125},[34,3561,971],{"class":44},[34,3563,3564],{"class":36,"line":173},[34,3565,70],{"emptyLinePlaceholder":69},[34,3567,3568,3570,3572],{"class":36,"line":194},[34,3569,185],{"class":44},[34,3571,3273],{"class":3125},[34,3573,971],{"class":44},[34,3575,3576,3578,3580,3583,3585],{"class":36,"line":213},[34,3577,3285],{"class":44},[34,3579,3345],{"class":3125},[34,3581,3582],{"class":44},">{{ data }}\u003C\u002F",[34,3584,3345],{"class":3125},[34,3586,971],{"class":44},[34,3588,3589,3591,3593],{"class":36,"line":229},[34,3590,3258],{"class":44},[34,3592,3273],{"class":3125},[34,3594,971],{"class":44},[15,3596,3598],{"id":3597},"tips-tricks","💡 Tips & Tricks",[20,3600,3602],{"filename":3601,"language":23},"tips.ts",[25,3603,3605],{"className":27,"code":3604,"language":23,"meta":29,"style":29},"\u002F\u002F ── 1. Polling: setInterval inside watch + cleanup ──\nwatchEffect((onCleanup) => {\n  const id = setInterval(() => fetchData(), 5000)\n  onCleanup(() => clearInterval(id))\n})\n\n\u002F\u002F ── 2. Request cancellation on unmount: AbortController + onScopeDispose ──\nconst controller = new AbortController()\nfetch(url, { signal: controller.signal })\nonScopeDispose(() => controller.abort())  \u002F\u002F cancels pending fetch on unmount\n\n\u002F\u002F ── 3. Exponential backoff retry ──\nasync function fetchWithRetry(url: string, maxRetries = 3) {\n  for (let i = 0; i \u003C maxRetries; i++) {\n    try { return await fetch(url) }\n    catch (e) {\n      if (i === maxRetries - 1) throw e\n      await new Promise(r => setTimeout(r, 2 ** i * 1000))  \u002F\u002F 1s, 2s, 4s\n    }\n  }\n}\n\n\u002F\u002F ── 4. Prefetch on hover ──\nfunction prefetch() {\n  const link = document.createElement('link')\n  link.rel = 'prefetch'\n  link.href = '\u002Fapi\u002Fnext-page-data'\n  document.head.appendChild(link)\n}\n\u002F\u002F \u003CRouterLink @mouseover=\"prefetch\" to=\"\u002Fnext\">Next\u003C\u002FRouterLink>\n",[31,3606,3607,3612,3628,3655,3670,3674,3678,3683,3698,3706,3726,3730,3735,3764,3795,3811,3818,3842,3884,3888,3892,3896,3900,3905,3915,3937,3947,3957,3968,3972],{"__ignoreMap":29},[34,3608,3609],{"class":36,"line":37},[34,3610,3611],{"class":111},"\u002F\u002F ── 1. Polling: setInterval inside watch + cleanup ──\n",[34,3613,3614,3617,3619,3622,3624,3626],{"class":36,"line":66},[34,3615,3616],{"class":79},"watchEffect",[34,3618,3220],{"class":44},[34,3620,3621],{"class":89},"onCleanup",[34,3623,135],{"class":44},[34,3625,138],{"class":40},[34,3627,83],{"class":44},[34,3629,3630,3632,3635,3637,3640,3642,3644,3647,3650,3653],{"class":36,"line":73},[34,3631,238],{"class":40},[34,3633,3634],{"class":96}," id",[34,3636,223],{"class":40},[34,3638,3639],{"class":79}," setInterval",[34,3641,2256],{"class":44},[34,3643,138],{"class":40},[34,3645,3646],{"class":79}," fetchData",[34,3648,3649],{"class":44},"(), ",[34,3651,3652],{"class":96},"5000",[34,3654,274],{"class":44},[34,3656,3657,3660,3662,3664,3667],{"class":36,"line":86},[34,3658,3659],{"class":79},"  onCleanup",[34,3661,2256],{"class":44},[34,3663,138],{"class":40},[34,3665,3666],{"class":79}," clearInterval",[34,3668,3669],{"class":44},"(id))\n",[34,3671,3672],{"class":36,"line":100},[34,3673,3253],{"class":44},[34,3675,3676],{"class":36,"line":115},[34,3677,70],{"emptyLinePlaceholder":69},[34,3679,3680],{"class":36,"line":144},[34,3681,3682],{"class":111},"\u002F\u002F ── 2. Request cancellation on unmount: AbortController + onScopeDispose ──\n",[34,3684,3685,3687,3690,3692,3694,3696],{"class":36,"line":150},[34,3686,889],{"class":40},[34,3688,3689],{"class":96}," controller",[34,3691,223],{"class":40},[34,3693,469],{"class":40},[34,3695,373],{"class":79},[34,3697,458],{"class":44},[34,3699,3700,3703],{"class":36,"line":155},[34,3701,3702],{"class":79},"fetch",[34,3704,3705],{"class":44},"(url, { signal: controller.signal })\n",[34,3707,3708,3711,3713,3715,3718,3720,3723],{"class":36,"line":161},[34,3709,3710],{"class":79},"onScopeDispose",[34,3712,2256],{"class":44},[34,3714,138],{"class":40},[34,3716,3717],{"class":44}," controller.",[34,3719,455],{"class":79},[34,3721,3722],{"class":44},"())  ",[34,3724,3725],{"class":111},"\u002F\u002F cancels pending fetch on unmount\n",[34,3727,3728],{"class":36,"line":167},[34,3729,70],{"emptyLinePlaceholder":69},[34,3731,3732],{"class":36,"line":173},[34,3733,3734],{"class":111},"\u002F\u002F ── 3. Exponential backoff retry ──\n",[34,3736,3737,3739,3741,3744,3746,3748,3750,3752,3754,3757,3759,3762],{"class":36,"line":194},[34,3738,1725],{"class":40},[34,3740,179],{"class":40},[34,3742,3743],{"class":79}," fetchWithRetry",[34,3745,268],{"class":44},[34,3747,939],{"class":89},[34,3749,129],{"class":40},[34,3751,944],{"class":96},[34,3753,906],{"class":44},[34,3755,3756],{"class":89},"maxRetries",[34,3758,223],{"class":40},[34,3760,3761],{"class":96}," 3",[34,3763,232],{"class":44},[34,3765,3766,3769,3771,3774,3777,3779,3782,3785,3787,3790,3793],{"class":36,"line":213},[34,3767,3768],{"class":40},"  for",[34,3770,123],{"class":44},[34,3772,3773],{"class":40},"let",[34,3775,3776],{"class":44}," i ",[34,3778,262],{"class":40},[34,3780,3781],{"class":96}," 0",[34,3783,3784],{"class":44},"; i ",[34,3786,185],{"class":40},[34,3788,3789],{"class":44}," maxRetries; i",[34,3791,3792],{"class":40},"++",[34,3794,232],{"class":44},[34,3796,3797,3799,3801,3804,3806,3808],{"class":36,"line":229},[34,3798,510],{"class":40},[34,3800,1579],{"class":44},[34,3802,3803],{"class":40},"return",[34,3805,526],{"class":40},[34,3807,529],{"class":79},[34,3809,3810],{"class":44},"(url) }\n",[34,3812,3813,3816],{"class":36,"line":235},[34,3814,3815],{"class":40},"    catch",[34,3817,661],{"class":44},[34,3819,3820,3822,3825,3827,3830,3832,3835,3837,3839],{"class":36,"line":277},[34,3821,549],{"class":40},[34,3823,3824],{"class":44}," (i ",[34,3826,690],{"class":40},[34,3828,3829],{"class":44}," maxRetries ",[34,3831,1836],{"class":40},[34,3833,3834],{"class":96}," 1",[34,3836,135],{"class":44},[34,3838,559],{"class":40},[34,3840,3841],{"class":44}," e\n",[34,3843,3844,3847,3849,3851,3853,3856,3858,3861,3864,3867,3870,3872,3875,3878,3881],{"class":36,"line":310},[34,3845,3846],{"class":40},"      await",[34,3848,469],{"class":40},[34,3850,1551],{"class":96},[34,3852,268],{"class":44},[34,3854,3855],{"class":89},"r",[34,3857,1241],{"class":40},[34,3859,3860],{"class":79}," setTimeout",[34,3862,3863],{"class":44},"(r, ",[34,3865,3866],{"class":96},"2",[34,3868,3869],{"class":40}," **",[34,3871,3776],{"class":44},[34,3873,3874],{"class":40},"*",[34,3876,3877],{"class":96}," 1000",[34,3879,3880],{"class":44},"))  ",[34,3882,3883],{"class":111},"\u002F\u002F 1s, 2s, 4s\n",[34,3885,3886],{"class":36,"line":329},[34,3887,740],{"class":44},[34,3889,3890],{"class":36,"line":357},[34,3891,746],{"class":44},[34,3893,3894],{"class":36,"line":362},[34,3895,147],{"class":44},[34,3897,3898],{"class":36,"line":385},[34,3899,70],{"emptyLinePlaceholder":69},[34,3901,3902],{"class":36,"line":390},[34,3903,3904],{"class":111},"\u002F\u002F ── 4. Prefetch on hover ──\n",[34,3906,3907,3910,3913],{"class":36,"line":404},[34,3908,3909],{"class":40},"function",[34,3911,3912],{"class":79}," prefetch",[34,3914,401],{"class":44},[34,3916,3917,3919,3922,3924,3927,3930,3932,3935],{"class":36,"line":421},[34,3918,238],{"class":40},[34,3920,3921],{"class":96}," link",[34,3923,223],{"class":40},[34,3925,3926],{"class":44}," document.",[34,3928,3929],{"class":79},"createElement",[34,3931,268],{"class":44},[34,3933,3934],{"class":62},"'link'",[34,3936,274],{"class":44},[34,3938,3939,3942,3944],{"class":36,"line":438},[34,3940,3941],{"class":44},"  link.rel ",[34,3943,262],{"class":40},[34,3945,3946],{"class":62}," 'prefetch'\n",[34,3948,3949,3952,3954],{"class":36,"line":443},[34,3950,3951],{"class":44},"  link.href ",[34,3953,262],{"class":40},[34,3955,3956],{"class":62}," '\u002Fapi\u002Fnext-page-data'\n",[34,3958,3959,3962,3965],{"class":36,"line":449},[34,3960,3961],{"class":44},"  document.head.",[34,3963,3964],{"class":79},"appendChild",[34,3966,3967],{"class":44},"(link)\n",[34,3969,3970],{"class":36,"line":461},[34,3971,147],{"class":44},[34,3973,3974],{"class":36,"line":476},[34,3975,3976],{"class":111},"\u002F\u002F \u003CRouterLink @mouseover=\"prefetch\" to=\"\u002Fnext\">Next\u003C\u002FRouterLink>\n",[15,3978,3980],{"id":3979},"️-edge-cases-gotchas","⚠️ Edge Cases & Gotchas",[20,3982,3984],{"filename":3983,"language":23},"edge-cases.ts",[25,3985,3987],{"className":27,"code":3986,"language":23,"meta":29,"style":29},"\u002F\u002F ── 1. Stale closure in async callbacks ──\n\u002F\u002F watchEffect(() => { fetchData().then(data => items.value = data) })\n\u002F\u002F If items is refactored, the closure captures the old reference.\n\u002F\u002F Fix: use refs, not local variables, for values that survive async.\n\n\u002F\u002F ── 2. Race condition without AbortController ──\n\u002F\u002F Fast typing → fetch(\"a\") starts → fetch(\"ab\") starts\n\u002F\u002F fetch(\"ab\") resolves first, fetch(\"a\") resolves last → stale data overwrites.\n\u002F\u002F Always abort previous request on new trigger.\n\n\u002F\u002F ── 3. Suspense doesn't retry on error ──\n\u002F\u002F If async setup throws, Suspense shows nothing (error must be caught by parent).\n\u002F\u002F Use onErrorCaptured + v-if error state to show retry button.\n\n\u002F\u002F ── 4. Pagination reset on URL change ──\n\u002F\u002F If the base URL changes (filter applied), cursor must reset to null.\n\u002F\u002F Otherwise, the cursor from the old query is sent to the new endpoint.\n\n\u002F\u002F ── 5. Optimistic update without rollback = permanent inconsistency ──\n\u002F\u002F If the server fails and you don't rollback, UI shows state the server rejected.\n\u002F\u002F Always save original state before optimistic mutation.\n\n\u002F\u002F ── 6. JSON parse error isn't a network error ──\n\u002F\u002F fetch() resolves even on 404\u002F500 — check res.ok before parsing.\n\u002F\u002F res.json() throws on non-JSON body (HTML error page) — wrap in try\u002Fcatch.\n",[31,3988,3989,3994,3999,4004,4009,4013,4018,4023,4028,4033,4037,4042,4047,4052,4056,4061,4066,4071,4075,4080,4085,4090,4094,4099,4104],{"__ignoreMap":29},[34,3990,3991],{"class":36,"line":37},[34,3992,3993],{"class":111},"\u002F\u002F ── 1. Stale closure in async callbacks ──\n",[34,3995,3996],{"class":36,"line":66},[34,3997,3998],{"class":111},"\u002F\u002F watchEffect(() => { fetchData().then(data => items.value = data) })\n",[34,4000,4001],{"class":36,"line":73},[34,4002,4003],{"class":111},"\u002F\u002F If items is refactored, the closure captures the old reference.\n",[34,4005,4006],{"class":36,"line":86},[34,4007,4008],{"class":111},"\u002F\u002F Fix: use refs, not local variables, for values that survive async.\n",[34,4010,4011],{"class":36,"line":100},[34,4012,70],{"emptyLinePlaceholder":69},[34,4014,4015],{"class":36,"line":115},[34,4016,4017],{"class":111},"\u002F\u002F ── 2. Race condition without AbortController ──\n",[34,4019,4020],{"class":36,"line":144},[34,4021,4022],{"class":111},"\u002F\u002F Fast typing → fetch(\"a\") starts → fetch(\"ab\") starts\n",[34,4024,4025],{"class":36,"line":150},[34,4026,4027],{"class":111},"\u002F\u002F fetch(\"ab\") resolves first, fetch(\"a\") resolves last → stale data overwrites.\n",[34,4029,4030],{"class":36,"line":155},[34,4031,4032],{"class":111},"\u002F\u002F Always abort previous request on new trigger.\n",[34,4034,4035],{"class":36,"line":161},[34,4036,70],{"emptyLinePlaceholder":69},[34,4038,4039],{"class":36,"line":167},[34,4040,4041],{"class":111},"\u002F\u002F ── 3. Suspense doesn't retry on error ──\n",[34,4043,4044],{"class":36,"line":173},[34,4045,4046],{"class":111},"\u002F\u002F If async setup throws, Suspense shows nothing (error must be caught by parent).\n",[34,4048,4049],{"class":36,"line":194},[34,4050,4051],{"class":111},"\u002F\u002F Use onErrorCaptured + v-if error state to show retry button.\n",[34,4053,4054],{"class":36,"line":213},[34,4055,70],{"emptyLinePlaceholder":69},[34,4057,4058],{"class":36,"line":229},[34,4059,4060],{"class":111},"\u002F\u002F ── 4. Pagination reset on URL change ──\n",[34,4062,4063],{"class":36,"line":235},[34,4064,4065],{"class":111},"\u002F\u002F If the base URL changes (filter applied), cursor must reset to null.\n",[34,4067,4068],{"class":36,"line":277},[34,4069,4070],{"class":111},"\u002F\u002F Otherwise, the cursor from the old query is sent to the new endpoint.\n",[34,4072,4073],{"class":36,"line":310},[34,4074,70],{"emptyLinePlaceholder":69},[34,4076,4077],{"class":36,"line":329},[34,4078,4079],{"class":111},"\u002F\u002F ── 5. Optimistic update without rollback = permanent inconsistency ──\n",[34,4081,4082],{"class":36,"line":357},[34,4083,4084],{"class":111},"\u002F\u002F If the server fails and you don't rollback, UI shows state the server rejected.\n",[34,4086,4087],{"class":36,"line":362},[34,4088,4089],{"class":111},"\u002F\u002F Always save original state before optimistic mutation.\n",[34,4091,4092],{"class":36,"line":385},[34,4093,70],{"emptyLinePlaceholder":69},[34,4095,4096],{"class":36,"line":390},[34,4097,4098],{"class":111},"\u002F\u002F ── 6. JSON parse error isn't a network error ──\n",[34,4100,4101],{"class":36,"line":404},[34,4102,4103],{"class":111},"\u002F\u002F fetch() resolves even on 404\u002F500 — check res.ok before parsing.\n",[34,4105,4106],{"class":36,"line":421},[34,4107,4108],{"class":111},"\u002F\u002F res.json() throws on non-JSON body (HTML error page) — wrap in try\u002Fcatch.\n",[15,4110,4112],{"id":4111},"spot-the-bug","🧠 Spot the Bug",[4114,4115,4116],"p",{},"A search component shows results from a previous query after the user types a new one.",[20,4118,4120],{"filename":4119,"language":23},"AsyncBug.ts",[25,4121,4123],{"className":27,"code":4122,"language":23,"meta":29,"style":29},"import { ref, watch } from 'vue'\n\nconst query = ref('')\nconst results = ref([])\n\nwatch(query, async (q) => {\n  const res = await fetch(`\u002Fapi\u002Fsearch?q=${q}`)\n  results.value = await res.json()  \u002F\u002F ← stale response can overwrite fresh data\n})\n",[31,4124,4125,4136,4140,4158,4172,4176,4197,4220,4238],{"__ignoreMap":29},[34,4126,4127,4129,4132,4134],{"class":36,"line":37},[34,4128,41],{"class":40},[34,4130,4131],{"class":44}," { ref, watch } ",[34,4133,59],{"class":40},[34,4135,63],{"class":62},[34,4137,4138],{"class":36,"line":66},[34,4139,70],{"emptyLinePlaceholder":69},[34,4141,4142,4144,4147,4149,4151,4153,4156],{"class":36,"line":73},[34,4143,889],{"class":40},[34,4145,4146],{"class":96}," query",[34,4148,223],{"class":40},[34,4150,265],{"class":79},[34,4152,268],{"class":44},[34,4154,4155],{"class":62},"''",[34,4157,274],{"class":44},[34,4159,4160,4162,4165,4167,4169],{"class":36,"line":86},[34,4161,889],{"class":40},[34,4163,4164],{"class":96}," results",[34,4166,223],{"class":40},[34,4168,265],{"class":79},[34,4170,4171],{"class":44},"([])\n",[34,4173,4174],{"class":36,"line":100},[34,4175,70],{"emptyLinePlaceholder":69},[34,4177,4178,4181,4184,4186,4188,4191,4193,4195],{"class":36,"line":115},[34,4179,4180],{"class":79},"watch",[34,4182,4183],{"class":44},"(query, ",[34,4185,1725],{"class":40},[34,4187,123],{"class":44},[34,4189,4190],{"class":89},"q",[34,4192,135],{"class":44},[34,4194,138],{"class":40},[34,4196,83],{"class":44},[34,4198,4199,4201,4203,4205,4207,4209,4211,4214,4216,4218],{"class":36,"line":144},[34,4200,238],{"class":40},[34,4202,521],{"class":96},[34,4204,223],{"class":40},[34,4206,526],{"class":40},[34,4208,529],{"class":79},[34,4210,268],{"class":44},[34,4212,4213],{"class":62},"`\u002Fapi\u002Fsearch?q=${",[34,4215,4190],{"class":44},[34,4217,591],{"class":62},[34,4219,274],{"class":44},[34,4221,4222,4225,4227,4229,4231,4233,4235],{"class":36,"line":150},[34,4223,4224],{"class":44},"  results.value ",[34,4226,262],{"class":40},[34,4228,526],{"class":40},[34,4230,609],{"class":44},[34,4232,612],{"class":79},[34,4234,807],{"class":44},[34,4236,4237],{"class":111},"\u002F\u002F ← stale response can overwrite fresh data\n",[34,4239,4240],{"class":36,"line":155},[34,4241,3253],{"class":44},[4243,4244,4245,4249,4260,4267,4469],"details",{},[4246,4247,4248],"summary",{},"Answer",[4114,4250,4251,4252,4255,4256,4259],{},"No abort\u002Fcancellation — if ",[31,4253,4254],{},"fetch(\"a\")"," resolves after ",[31,4257,4258],{},"fetch(\"ab\")",", the stale \"a\" results overwrite the correct \"ab\" results.",[4114,4261,4262,4266],{},[4263,4264,4265],"strong",{},"Fix"," — abort the previous request on each new query:",[20,4268,4270],{"filename":4269,"language":23},"AsyncFixed.ts",[25,4271,4273],{"className":27,"code":4272,"language":23,"meta":29,"style":29},"import { ref, watch } from 'vue'\n\nconst query = ref('')\nconst results = ref([])\n\nwatch(query, async (q, _old, onCleanup) => {\n  const controller = new AbortController()\n  onCleanup(() => controller.abort())  \u002F\u002F cancel previous fetch\n\n  try {\n    const res = await fetch(`\u002Fapi\u002Fsearch?q=${q}`, { signal: controller.signal })\n    results.value = await res.json()\n  } catch (e) {\n    if (e.name === 'AbortError') return  \u002F\u002F expected — stale request canceled\n    throw e\n  }\n})\n",[31,4274,4275,4285,4289,4305,4317,4321,4348,4362,4379,4383,4390,4413,4428,4436,4454,4461,4465],{"__ignoreMap":29},[34,4276,4277,4279,4281,4283],{"class":36,"line":37},[34,4278,41],{"class":40},[34,4280,4131],{"class":44},[34,4282,59],{"class":40},[34,4284,63],{"class":62},[34,4286,4287],{"class":36,"line":66},[34,4288,70],{"emptyLinePlaceholder":69},[34,4290,4291,4293,4295,4297,4299,4301,4303],{"class":36,"line":73},[34,4292,889],{"class":40},[34,4294,4146],{"class":96},[34,4296,223],{"class":40},[34,4298,265],{"class":79},[34,4300,268],{"class":44},[34,4302,4155],{"class":62},[34,4304,274],{"class":44},[34,4306,4307,4309,4311,4313,4315],{"class":36,"line":86},[34,4308,889],{"class":40},[34,4310,4164],{"class":96},[34,4312,223],{"class":40},[34,4314,265],{"class":79},[34,4316,4171],{"class":44},[34,4318,4319],{"class":36,"line":100},[34,4320,70],{"emptyLinePlaceholder":69},[34,4322,4323,4325,4327,4329,4331,4333,4335,4338,4340,4342,4344,4346],{"class":36,"line":115},[34,4324,4180],{"class":79},[34,4326,4183],{"class":44},[34,4328,1725],{"class":40},[34,4330,123],{"class":44},[34,4332,4190],{"class":89},[34,4334,906],{"class":44},[34,4336,4337],{"class":89},"_old",[34,4339,906],{"class":44},[34,4341,3621],{"class":89},[34,4343,135],{"class":44},[34,4345,138],{"class":40},[34,4347,83],{"class":44},[34,4349,4350,4352,4354,4356,4358,4360],{"class":36,"line":144},[34,4351,238],{"class":40},[34,4353,3689],{"class":96},[34,4355,223],{"class":40},[34,4357,469],{"class":40},[34,4359,373],{"class":79},[34,4361,458],{"class":44},[34,4363,4364,4366,4368,4370,4372,4374,4376],{"class":36,"line":150},[34,4365,3659],{"class":79},[34,4367,2256],{"class":44},[34,4369,138],{"class":40},[34,4371,3717],{"class":44},[34,4373,455],{"class":79},[34,4375,3722],{"class":44},[34,4377,4378],{"class":111},"\u002F\u002F cancel previous fetch\n",[34,4380,4381],{"class":36,"line":155},[34,4382,70],{"emptyLinePlaceholder":69},[34,4384,4385,4388],{"class":36,"line":161},[34,4386,4387],{"class":40},"  try",[34,4389,83],{"class":44},[34,4391,4392,4394,4396,4398,4400,4402,4404,4406,4408,4410],{"class":36,"line":167},[34,4393,407],{"class":40},[34,4395,521],{"class":96},[34,4397,223],{"class":40},[34,4399,526],{"class":40},[34,4401,529],{"class":79},[34,4403,268],{"class":44},[34,4405,4213],{"class":62},[34,4407,4190],{"class":44},[34,4409,591],{"class":62},[34,4411,4412],{"class":44},", { signal: controller.signal })\n",[34,4414,4415,4418,4420,4422,4424,4426],{"class":36,"line":173},[34,4416,4417],{"class":44},"    results.value ",[34,4419,262],{"class":40},[34,4421,526],{"class":40},[34,4423,609],{"class":44},[34,4425,612],{"class":79},[34,4427,458],{"class":44},[34,4429,4430,4432,4434],{"class":36,"line":194},[34,4431,789],{"class":44},[34,4433,658],{"class":40},[34,4435,661],{"class":44},[34,4437,4438,4440,4443,4445,4447,4449,4451],{"class":36,"line":213},[34,4439,424],{"class":40},[34,4441,4442],{"class":44}," (e.name ",[34,4444,690],{"class":40},[34,4446,693],{"class":62},[34,4448,135],{"class":44},[34,4450,3803],{"class":40},[34,4452,4453],{"class":111},"  \u002F\u002F expected — stale request canceled\n",[34,4455,4456,4459],{"class":36,"line":229},[34,4457,4458],{"class":40},"    throw",[34,4460,3841],{"class":44},[34,4462,4463],{"class":36,"line":235},[34,4464,746],{"class":44},[34,4466,4467],{"class":36,"line":277},[34,4468,3253],{"class":44},[4114,4470,4471,4474],{},[4263,4472,4473],{},"The lesson",": async watchers must register cleanup to cancel stale work. Without it, out-of-order network responses cause data races — the last-resolved (not the last-requested) response wins.",[4476,4477,4478],"style",{},"html pre.shiki code .svdQ7, html code.shiki .svdQ7{--shiki-default:#D73A49;--shiki-github-dark:#F97583}html pre.shiki code .ssxIu, html code.shiki .ssxIu{--shiki-default:#24292E;--shiki-github-dark:#E1E4E8}html pre.shiki code .sJ6F3, html code.shiki .sJ6F3{--shiki-default:#032F62;--shiki-github-dark:#9ECBFF}html pre.shiki code .sIsaT, html code.shiki .sIsaT{--shiki-default:#6F42C1;--shiki-github-dark:#B392F0}html pre.shiki code .sCrzJ, html code.shiki .sCrzJ{--shiki-default:#E36209;--shiki-github-dark:#FFAB70}html pre.shiki code .snvgF, html code.shiki .snvgF{--shiki-default:#005CC5;--shiki-github-dark:#79B8FF}html pre.shiki code .sdCPZ, html code.shiki .sdCPZ{--shiki-default:#6A737D;--shiki-github-dark:#6A737D}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 .github-dark .shiki span {color: var(--shiki-github-dark);background: var(--shiki-github-dark-bg);font-style: var(--shiki-github-dark-font-style);font-weight: var(--shiki-github-dark-font-weight);text-decoration: var(--shiki-github-dark-text-decoration);}html.github-dark .shiki span {color: var(--shiki-github-dark);background: var(--shiki-github-dark-bg);font-style: var(--shiki-github-dark-font-style);font-weight: var(--shiki-github-dark-font-weight);text-decoration: var(--shiki-github-dark-text-decoration);}html pre.shiki code .sk71V, html code.shiki .sk71V{--shiki-default:#22863A;--shiki-github-dark:#85E89D}",{"title":29,"searchDepth":66,"depth":66,"links":4480},[4481,4482,4483,4484,4485,4486,4487,4488,4489],{"id":17,"depth":66,"text":18},{"id":837,"depth":66,"text":838},{"id":1411,"depth":66,"text":1412},{"id":1978,"depth":66,"text":1979},{"id":2625,"depth":66,"text":2626},{"id":3107,"depth":66,"text":3108},{"id":3597,"depth":66,"text":3598},{"id":3979,"depth":66,"text":3980},{"id":4111,"depth":66,"text":4112},"Race condition prevention with AbortController, request deduplication, SWR pattern, pagination cursor management, optimistic updates, and Suspense-based async orchestration.","md",{},"\u002Fvue\u002F12-fetching-data-and-async",{"title":5,"description":4490},"vue\u002F12-fetching-data-and-async","AhXdYp1GTxQrzU5DNYc8H1Icj1DbrXw7NsQD1TaBLIk",1789924655633]