[{"data":1,"prerenderedAt":2457},["ShallowReactive",2],{"navigation_docs_en":3,"-en-examples-infinite-scroll":342,"-en-examples-infinite-scroll-surround":2452},[4,45,91,137,195,236,257,291,327],{"title":5,"icon":6,"path":7,"stem":8,"children":9,"page":44},"Getting Started","i-lucide-rocket","/en/getting-started","en/01.getting-started",[10,15,20,24,29,34,39],{"title":11,"path":12,"stem":13,"icon":14},"What is Qora?","/en/getting-started/what-is-qora","en/01.getting-started/2.what-is-qora","i-lucide-info",{"title":16,"path":17,"stem":18,"icon":19},"Installation","/en/getting-started/installation","en/01.getting-started/3.installation","i-lucide-download",{"title":21,"path":22,"stem":23,"icon":6},"Quick Start","/en/getting-started/quick-start","en/01.getting-started/4.quick-start",{"title":25,"path":26,"stem":27,"icon":28},"Comparison","/en/getting-started/comparison","en/01.getting-started/5.comparison","i-lucide-scale",{"title":30,"path":31,"stem":32,"icon":33},"Coming from TanStack Query","/en/getting-started/coming-from-tanstack-query","en/01.getting-started/6.coming-from-tanstack-query","i-lucide-arrow-left-right",{"title":35,"path":36,"stem":37,"icon":38},"Migrating from Other Libraries","/en/getting-started/migrating-from-other-libraries","en/01.getting-started/7.migrating-from-other-libraries","i-lucide-arrow-right-left",{"title":40,"path":41,"stem":42,"icon":43},"AI Assistants","/en/getting-started/ai-assistants","en/01.getting-started/8.ai-assistants","i-lucide-sparkles",false,{"title":46,"icon":47,"path":48,"stem":49,"children":50,"page":44},"Core Concepts","i-lucide-book-open","/en/core-concepts","en/02.core-concepts",[51,56,61,66,71,76,81,86],{"title":52,"path":53,"stem":54,"icon":55},"Queries","/en/core-concepts/queries","en/02.core-concepts/1.queries","i-lucide-database",{"title":57,"path":58,"stem":59,"icon":60},"Query Keys","/en/core-concepts/query-keys","en/02.core-concepts/2.query-keys","i-lucide-key-round",{"title":62,"path":63,"stem":64,"icon":65},"Query States","/en/core-concepts/query-states","en/02.core-concepts/3.query-states","i-lucide-activity",{"title":67,"path":68,"stem":69,"icon":70},"Caching","/en/core-concepts/caching","en/02.core-concepts/4.caching","i-lucide-layers-3",{"title":72,"path":73,"stem":74,"icon":75},"Stale-While-Revalidate","/en/core-concepts/stale-while-revalidate","en/02.core-concepts/5.stale-while-revalidate","i-lucide-timer-reset",{"title":77,"path":78,"stem":79,"icon":80},"Deduplication","/en/core-concepts/deduplication","en/02.core-concepts/6.deduplication","i-lucide-copy-minus",{"title":82,"path":83,"stem":84,"icon":85},"Mutations & Invalidation","/en/core-concepts/mutations","en/02.core-concepts/7.mutations","i-lucide-pencil-line",{"title":87,"path":88,"stem":89,"icon":90},"Offline & Network","/en/core-concepts/offline","en/02.core-concepts/8.offline","i-lucide-wifi-off",{"title":92,"icon":93,"path":94,"stem":95,"children":96,"page":44},"Flutter Integration","i-lucide-smartphone","/en/flutter-integration","en/03.flutter-integration",[97,102,107,112,117,122,127,132],{"title":98,"path":99,"stem":100,"icon":101},"Setup","/en/flutter-integration/setup","en/03.flutter-integration/1.setup","i-lucide-wrench",{"title":103,"path":104,"stem":105,"icon":106},"QoraScope","/en/flutter-integration/qora-scope","en/03.flutter-integration/2.qora-scope","i-lucide-share-2",{"title":108,"path":109,"stem":110,"icon":111},"QoraBuilder","/en/flutter-integration/qora-builder","en/03.flutter-integration/3.qora-builder","i-lucide-blocks",{"title":113,"path":114,"stem":115,"icon":116},"QoraMutationBuilder","/en/flutter-integration/qora-mutation-builder","en/03.flutter-integration/4.qora-mutation-builder","i-lucide-pencil",{"title":118,"path":119,"stem":120,"icon":121},"InfiniteQueryBuilder","/en/flutter-integration/infinite-query-builder","en/03.flutter-integration/5.infinite-query-builder","i-lucide-infinity",{"title":123,"path":124,"stem":125,"icon":126},"Hooks (flutter_hooks)","/en/flutter-integration/hooks","en/03.flutter-integration/6.hooks","i-lucide-anchor",{"title":128,"path":129,"stem":130,"icon":131},"Network Status Widgets","/en/flutter-integration/network-status","en/03.flutter-integration/7.network-status","i-lucide-wifi",{"title":133,"path":134,"stem":135,"icon":136},"Best Practices","/en/flutter-integration/best-practices","en/03.flutter-integration/8.best-practices","i-lucide-badge-check",{"title":138,"icon":139,"path":140,"stem":141,"children":142,"page":44},"Recipes","i-lucide-chef-hat","/en/recipes","en/04.recipes",[143,147,152,157,162,166,171,176,181,186,191],{"title":144,"path":145,"stem":146,"icon":85},"Mutations","/en/recipes/mutations","en/04.recipes/02.mutations",{"title":148,"path":149,"stem":150,"icon":151},"Optimistic Updates","/en/recipes/optimistic-updates","en/04.recipes/03.optimistic-updates","i-lucide-zap",{"title":153,"path":154,"stem":155,"icon":156},"Persistence","/en/recipes/persistence","en/04.recipes/04.persistence","i-lucide-hard-drive",{"title":158,"path":159,"stem":160,"icon":161},"Network Aware","/en/recipes/network-aware","en/04.recipes/05.network-aware","i-lucide-signal",{"title":163,"path":164,"stem":165,"icon":121},"Infinite Queries","/en/recipes/infinite-queries","en/04.recipes/06.infinite-queries",{"title":167,"path":168,"stem":169,"icon":170},"Dependent Queries","/en/recipes/dependent-queries","en/04.recipes/07.dependent-queries","i-lucide-link-2",{"title":172,"path":173,"stem":174,"icon":175},"Cancellation","/en/recipes/cancel-token","en/04.recipes/08.cancel-token","i-lucide-circle-slash",{"title":177,"path":178,"stem":179,"icon":180},"SSR Hydration","/en/recipes/ssr-hydration","en/04.recipes/09.ssr-hydration","i-lucide-server",{"title":182,"path":183,"stem":184,"icon":185},"Error Handling","/en/recipes/error-handling","en/04.recipes/10.error-handling","i-lucide-shield-alert",{"title":187,"path":188,"stem":189,"icon":190},"Testing","/en/recipes/testing","en/04.recipes/11.testing","i-lucide-flask-conical",{"title":192,"path":193,"stem":194},"Route-Level Prefetching","/en/recipes/prefetch","en/04.recipes/12.prefetch",{"title":196,"icon":197,"path":198,"stem":199,"children":200,"page":44},"API Reference","i-lucide-file-code","/en/api-reference","en/05.api-reference",[201,206,210,214,219,224,228,232],{"title":202,"path":203,"stem":204,"icon":205},"QoraClient","/en/api-reference/qora-client","en/05.api-reference/1.qora-client","i-lucide-cpu",{"title":207,"path":208,"stem":209,"icon":60},"QoraKey","/en/api-reference/qora-key","en/05.api-reference/2.qora-key",{"title":211,"path":212,"stem":213,"icon":65},"QoraState","/en/api-reference/qora-state","en/05.api-reference/3.qora-state",{"title":215,"path":216,"stem":217,"icon":218},"QoraOptions","/en/api-reference/qora-options","en/05.api-reference/4.qora-options","i-lucide-sliders-horizontal",{"title":220,"path":221,"stem":222,"icon":223},"QoraClientConfig","/en/api-reference/qora-client-config","en/05.api-reference/5.qora-client-config","i-lucide-settings-2",{"title":225,"path":226,"stem":227,"icon":85},"Mutation API","/en/api-reference/qora-mutation","en/05.api-reference/6.qora-mutation",{"title":229,"path":230,"stem":231,"icon":156},"PersistQoraClient","/en/api-reference/persist-qora-client","en/05.api-reference/7.persist-qora-client",{"title":233,"path":234,"stem":235,"icon":121},"Infinite Query","/en/api-reference/infinite-query","en/05.api-reference/8.infinite-query",{"title":237,"icon":238,"path":239,"stem":240,"children":241,"page":44},"DevTools","i-lucide-bug","/en/devtools","en/06.devtools",[242,247,252],{"title":243,"path":244,"stem":245,"icon":246},"Overview","/en/devtools/overview","en/06.devtools/1.overview","i-lucide-layout-dashboard",{"title":248,"path":249,"stem":250,"icon":251},"In-App Overlay","/en/devtools/overlay","en/06.devtools/2.overlay","i-lucide-layers",{"title":253,"path":254,"stem":255,"icon":256},"Data Serialization","/en/devtools/data-display","en/06.devtools/4.data-display","i-lucide-eye",{"title":258,"icon":259,"path":260,"stem":261,"children":262,"page":44},"Third Party","i-lucide-puzzle","/en/third-party","en/07.third-party",[263,268,272,277,282,286],{"title":264,"path":265,"stem":266,"icon":267},"With Provider","/en/third-party/with-provider","en/07.third-party/1.with-provider","i-lucide-plug",{"title":269,"path":270,"stem":271,"icon":111},"With BLoC","/en/third-party/with-bloc","en/07.third-party/2.with-bloc",{"title":273,"path":274,"stem":275,"icon":276},"With Riverpod","/en/third-party/with-riverpod","en/07.third-party/3.with-riverpod","i-lucide-droplets",{"title":278,"path":279,"stem":280,"icon":281},"With RxDart","/en/third-party/with-rxdart","en/07.third-party/4.with-rxdart","i-lucide-git-branch",{"title":283,"path":284,"stem":285},"With Dio","/en/third-party/with-dio","en/07.third-party/5.with-dio",{"title":287,"path":288,"stem":289,"icon":290},"With GraphQL","/en/third-party/with-graphql","en/07.third-party/6.with-graphql","i-lucide-binary",{"title":292,"icon":190,"path":293,"stem":294,"children":295,"page":44},"Examples","/en/examples","en/08.examples",[296,301,305,310,314,318,322],{"title":297,"path":298,"stem":299,"icon":300},"Case Study: User List & Detail","/en/examples/basic-query","en/08.examples/1.basic-query","i-lucide-play-circle",{"title":302,"path":303,"stem":304,"icon":151},"Case Study: Optimistic Mutations & Rollback","/en/examples/mutations-optimistic","en/08.examples/2.mutations-optimistic",{"title":306,"path":307,"stem":308,"icon":309},"Case Study: Infinite Scroll Feed","/en/examples/infinite-scroll","en/08.examples/3.infinite-scroll","i-lucide-scroll",{"title":311,"path":312,"stem":313,"icon":90},"Case Study: Offline-First Todo App","/en/examples/offline-first","en/08.examples/4.offline-first",{"title":315,"path":316,"stem":317,"icon":161},"Case Study: Network Awareness","/en/examples/network-awareness","en/08.examples/5.network-awareness",{"title":319,"path":320,"stem":321,"icon":126},"Case Study: Hooks Integration","/en/examples/hooks-integration","en/08.examples/6.hooks-integration",{"title":323,"path":324,"stem":325,"icon":326},"Case Study: Complete Todo App","/en/examples/complete-todo-app","en/08.examples/7.complete-todo-app","i-lucide-check-square",{"title":328,"icon":329,"path":330,"stem":331,"children":332,"page":44},"Help","i-lucide-life-buoy","/en/help","en/09.help",[333,338],{"title":334,"path":335,"stem":336,"icon":337},"FAQ","/en/help/faq","en/09.help/1.faq","i-lucide-circle-help",{"title":339,"path":340,"stem":341,"icon":101},"Troubleshooting","/en/help/troubleshooting","en/09.help/2.troubleshooting",{"id":343,"title":306,"body":344,"description":2443,"extension":2444,"links":2445,"meta":2446,"navigation":2447,"path":307,"seo":2448,"stem":308,"__hash__":2451},"docs_en/en/08.examples/3.infinite-scroll.md",{"type":345,"value":346,"toc":2426},"minimark",[347,352,365,372,375,379,465,467,471,489,499,513,826,835,837,841,846,878,891,1126,1132,1138,1716,1741,1747,1759,2017,2023,2036,2265,2267,2271,2277,2286,2288,2292,2374,2376,2380,2422],[348,349,351],"h2",{"id":350},"objective","Objective",[353,354,355,359,360,364],"p",{},[356,357,358],"strong",{},"Context",": An infinite scroll feed sounds simple: load more data as the user scrolls. The production constraints are strict: after 30 minutes of scrolling, your ",[361,362,363],"code",{},"ListView"," holds 2,000 widgets, memory spikes, and the OS terminates your app. Alternatively, if you use integer page numbers and a post is inserted mid-scroll, every subsequent page is offset by one and users see duplicates.",[353,366,367,368,371],{},"This example demonstrates two core controls. Cursor-based pagination eliminates offset drift. Windowed paging with ",[361,369,370],{},"maxPages"," enforces a bounded in-memory footprint while allowing backward page recovery on demand.",[373,374],"hr",{},[348,376,378],{"id":377},"key-features-demonstrated","Key Features Demonstrated",[380,381,382,398,404,412,424,435,445,453],"ul",{},[383,384,385,389,390,393,394,397],"li",{},[356,386,387],{},[361,388,118],{},": accumulates pages and exposes ",[361,391,392],{},"fetchNextPage"," / ",[361,395,396],{},"fetchPreviousPage"," through its controller",[383,399,400,403],{},[356,401,402],{},"Cursor-based pagination",": stable results regardless of server-side insertions",[383,405,406,411],{},[356,407,408,410],{},[361,409,370],{}," windowed memory management",": caps the in-memory page window; drops oldest/newest when exceeded",[383,413,414,423],{},[356,415,416,419,420],{},[361,417,418],{},"isFetchingNextPage"," + ",[361,421,422],{},"isFetchingPreviousPage",": drives the scroll-end loader without hiding existing content",[383,425,426,431,432],{},[356,427,428],{},[361,429,430],{},"InfiniteData.flatten()",": converts the page matrix into a flat item list for ",[361,433,434],{},"SliverList",[383,436,437,440,441,444],{},[356,438,439],{},"Optimistic post creation",": ",[361,442,443],{},"setInfiniteQueryData"," prepends a new post instantly; rollback on failure",[383,446,447,452],{},[356,448,449],{},[361,450,451],{},"refetch()",": pull-to-refresh that re-fetches all loaded pages while preserving the page count",[383,454,455,464],{},[356,456,457,460,461],{},[361,458,459],{},"InfiniteFailure"," with ",[361,462,463],{},"previousData",": load-more errors show a retry button without clearing the feed",[373,466],{},[348,468,470],{"id":469},"architecture-highlights","Architecture Highlights",[353,472,473,474,477,478,481,482,485,486,488],{},"The UI layer is built on a ",[361,475,476],{},"CustomScrollView"," with two slivers: a ",[361,479,480],{},"CupertinoSliverRefreshControl"," for pull-to-refresh and a ",[361,483,484],{},"SliverList.builder"," for the feed items. The ",[361,487,118],{}," wraps the scroll view and owns the observer lifecycle.",[490,491,497],"pre",{"className":492,"code":494,"language":495,"meta":496},[493],"language-text","FeedScreen\n└── CustomScrollView\n    ├── CupertinoSliverRefreshControl   ← controller.refetch() on pull\n    └── SliverList.builder\n        ├── items = data.flatten((p) => p.posts)   ← flat list from all pages\n        ├── [last item]\n        │   ├── hasNextPage → LoadMoreTrigger       ← fires controller.fetchNextPage()\n        │   └── isFetchingNextPage → PageLoader     ← spinner at list bottom\n        └── InfiniteFailure with previousData → ErrorRetryBanner\n","text","",[361,498,494],{"__ignoreMap":496},[353,500,501,502,504,505,508,509,512],{},"The ",[361,503,118],{}," is configured with a ",[361,506,507],{},"String"," cursor. An empty string means \"start from the beginning\": the server returns the most recent posts and a ",[361,510,511],{},"nextCursor"," pointing to older content.",[490,514,519],{"className":515,"code":516,"filename":517,"language":518,"meta":496,"style":496},"language-dart shiki shiki-themes material-theme-lighter material-theme material-theme-palenight","InfiniteQueryBuilder\u003CFeedPage, String>(\n  queryKey: const ['feed'],\n  fetcher: FeedApi.getFeed,\n  options: InfiniteQueryOptions\u003CFeedPage, String>(\n    initialPageParam: '', // '' = latest posts\n    getNextPageParam: (last, _) => last.nextCursor, // older posts\n    getPreviousPageParam: (first, _) => first.previousCursor, // non-null only after an eviction\n    maxPages: 3, // 3 to see eviction quickly; 8 to 10 in production\n    baseOptions: const QoraOptions(\n      staleTime: Duration(minutes: 2),\n      retryCount: 3,\n    ),\n  ),\n  builder: (context, state, controller) => FeedList(state: state, controller: controller),\n)\n","lib/features/feed/ui/feed_screen.dart","dart",[361,520,521,544,571,590,611,629,660,690,707,723,747,759,767,775,820],{"__ignoreMap":496},[522,523,526,529,533,536,539,541],"span",{"class":524,"line":525},"line",1,[522,527,118],{"class":528},"sBMFI",[522,530,532],{"class":531},"sTEyZ","\u003C",[522,534,535],{"class":528},"FeedPage",[522,537,538],{"class":531},", ",[522,540,507],{"class":528},[522,542,543],{"class":531},">(\n",[522,545,547,550,554,558,561,565,568],{"class":524,"line":546},2,[522,548,549],{"class":531},"  queryKey",[522,551,553],{"class":552},"sMK4o",":",[522,555,557],{"class":556},"spNyl"," const",[522,559,560],{"class":531}," [",[522,562,564],{"class":563},"sfazB","'feed'",[522,566,567],{"class":531},"]",[522,569,570],{"class":552},",\n",[522,572,574,577,579,582,585,588],{"class":524,"line":573},3,[522,575,576],{"class":531},"  fetcher",[522,578,553],{"class":552},[522,580,581],{"class":528}," FeedApi",[522,583,584],{"class":552},".",[522,586,587],{"class":531},"getFeed",[522,589,570],{"class":552},[522,591,593,596,598,601,603,605,607,609],{"class":524,"line":592},4,[522,594,595],{"class":531},"  options",[522,597,553],{"class":552},[522,599,600],{"class":528}," InfiniteQueryOptions",[522,602,532],{"class":531},[522,604,535],{"class":528},[522,606,538],{"class":531},[522,608,507],{"class":528},[522,610,543],{"class":531},[522,612,614,617,619,622,625],{"class":524,"line":613},5,[522,615,616],{"class":531},"    initialPageParam",[522,618,553],{"class":552},[522,620,621],{"class":563}," ''",[522,623,624],{"class":552},",",[522,626,628],{"class":627},"sHwdD"," // '' = latest posts\n",[522,630,632,635,637,640,642,645,648,651,653,655,657],{"class":524,"line":631},6,[522,633,634],{"class":531},"    getNextPageParam",[522,636,553],{"class":552},[522,638,639],{"class":531}," (last",[522,641,624],{"class":552},[522,643,644],{"class":531}," _) ",[522,646,647],{"class":552},"=>",[522,649,650],{"class":531}," last",[522,652,584],{"class":552},[522,654,511],{"class":531},[522,656,624],{"class":552},[522,658,659],{"class":627}," // older posts\n",[522,661,663,666,668,671,673,675,677,680,682,685,687],{"class":524,"line":662},7,[522,664,665],{"class":531},"    getPreviousPageParam",[522,667,553],{"class":552},[522,669,670],{"class":531}," (first",[522,672,624],{"class":552},[522,674,644],{"class":531},[522,676,647],{"class":552},[522,678,679],{"class":531}," first",[522,681,584],{"class":552},[522,683,684],{"class":531},"previousCursor",[522,686,624],{"class":552},[522,688,689],{"class":627}," // non-null only after an eviction\n",[522,691,693,696,698,702,704],{"class":524,"line":692},8,[522,694,695],{"class":531},"    maxPages",[522,697,553],{"class":552},[522,699,701],{"class":700},"sbssI"," 3",[522,703,624],{"class":552},[522,705,706],{"class":627}," // 3 to see eviction quickly; 8 to 10 in production\n",[522,708,710,713,715,717,720],{"class":524,"line":709},9,[522,711,712],{"class":531},"    baseOptions",[522,714,553],{"class":552},[522,716,557],{"class":556},[522,718,719],{"class":528}," QoraOptions",[522,721,722],{"class":531},"(\n",[522,724,726,729,731,734,737,739,742,745],{"class":524,"line":725},10,[522,727,728],{"class":531},"      staleTime",[522,730,553],{"class":552},[522,732,733],{"class":528}," Duration",[522,735,736],{"class":531},"(minutes",[522,738,553],{"class":552},[522,740,741],{"class":700}," 2",[522,743,744],{"class":531},")",[522,746,570],{"class":552},[522,748,750,753,755,757],{"class":524,"line":749},11,[522,751,752],{"class":531},"      retryCount",[522,754,553],{"class":552},[522,756,701],{"class":700},[522,758,570],{"class":552},[522,760,762,765],{"class":524,"line":761},12,[522,763,764],{"class":531},"    )",[522,766,570],{"class":552},[522,768,770,773],{"class":524,"line":769},13,[522,771,772],{"class":531},"  )",[522,774,570],{"class":552},[522,776,778,781,783,786,788,791,793,796,798,801,804,806,808,810,813,815,818],{"class":524,"line":777},14,[522,779,780],{"class":531},"  builder",[522,782,553],{"class":552},[522,784,785],{"class":531}," (context",[522,787,624],{"class":552},[522,789,790],{"class":531}," state",[522,792,624],{"class":552},[522,794,795],{"class":531}," controller) ",[522,797,647],{"class":552},[522,799,800],{"class":528}," FeedList",[522,802,803],{"class":531},"(state",[522,805,553],{"class":552},[522,807,790],{"class":531},[522,809,624],{"class":552},[522,811,812],{"class":531}," controller",[522,814,553],{"class":552},[522,816,817],{"class":531}," controller)",[522,819,570],{"class":552},[522,821,823],{"class":524,"line":822},15,[522,824,825],{"class":531},")\n",[353,827,828,830,831,834],{},[361,829,370],{}," bounds the ",[361,832,833],{},"InfiniteData"," held in memory. The example uses 3 so that eviction is visible after a few pages; at 20 posts per page, a production feed would typically use 8 to 10 (160 to 200 posts).",[373,836],{},[348,838,840],{"id":839},"implementation-details","Implementation Details",[842,843,845],"h3",{"id":844},"windowed-paging-scrolling-back-in-time","Windowed paging: scrolling back in time",[353,847,848,849,852,853,856,857,859,860,863,864,867,868,870,871,874,875,584],{},"When the user scrolls down far enough to load page 4, ",[361,850,851],{},"maxPages: 3"," causes page 1 (the newest posts) to be ",[356,854,855],{},"dropped"," from ",[361,858,833],{},". The state's ",[361,861,862],{},"hasPreviousPage"," becomes ",[361,865,866],{},"true",". If the user scrolls back up to the top, the ",[361,869,434],{}," detects this and calls ",[361,872,873],{},"controller.fetchPreviousPage()",", which re-fetches the dropped page using ",[361,876,877],{},"getPreviousPageParam",[353,879,880,881,460,884,886,887,890],{},"This entire mechanism: drop, detect and re-fetch is why ",[356,882,883],{},"cursor-based pagination is mandatory",[361,885,370],{},". Integer page numbers would break here: if 3 new posts were inserted while the user was scrolling, re-fetching \"page 1\" with ",[361,888,889],{},"?page=1&limit=20"," would return a different 20 posts than the first load. Cursors anchor to a specific server-side position.",[490,892,894],{"className":515,"code":893,"filename":517,"language":518,"meta":496,"style":496},"void _onScroll() {\n  final pos = _scrollController.position;\n\n  // Trigger next page when within 300px of the bottom\n  if (pos.extentAfter \u003C 300 &&\n      _latestState case InfiniteSuccess(:final hasNextPage, :final isFetchingNextPage)\n          when hasNextPage && !isFetchingNextPage) {\n    controller.fetchNextPage();\n  }\n\n  // Trigger previous page when within 300px of the top\n  // (only relevant after maxPages eviction has dropped page 1)\n  if (pos.extentBefore \u003C 300 &&\n      _latestState case InfiniteSuccess(:final hasPreviousPage, :final isFetchingPreviousPage)\n          when hasPreviousPage && !isFetchingPreviousPage) {\n    controller.fetchPreviousPage();\n  }\n}\n",[361,895,896,908,930,936,941,963,995,1012,1026,1031,1035,1040,1045,1062,1088,1102,1115,1120],{"__ignoreMap":496},[522,897,898,901,905],{"class":524,"line":525},[522,899,900],{"class":556},"void",[522,902,904],{"class":903},"s2Zo4"," _onScroll",[522,906,907],{"class":531},"() {\n",[522,909,910,913,916,919,922,924,927],{"class":524,"line":546},[522,911,912],{"class":556},"  final",[522,914,915],{"class":531}," pos ",[522,917,918],{"class":552},"=",[522,920,921],{"class":531}," _scrollController",[522,923,584],{"class":552},[522,925,926],{"class":531},"position",[522,928,929],{"class":552},";\n",[522,931,932],{"class":524,"line":573},[522,933,935],{"emptyLinePlaceholder":934},true,"\n",[522,937,938],{"class":524,"line":592},[522,939,940],{"class":627},"  // Trigger next page when within 300px of the bottom\n",[522,942,943,947,950,952,955,957,960],{"class":524,"line":613},[522,944,946],{"class":945},"s7zQu","  if",[522,948,949],{"class":531}," (pos",[522,951,584],{"class":552},[522,953,954],{"class":531},"extentAfter ",[522,956,532],{"class":552},[522,958,959],{"class":700}," 300",[522,961,962],{"class":552}," &&\n",[522,964,965,968,971,974,977,979,982,985,987,990,992],{"class":524,"line":631},[522,966,967],{"class":531},"      _latestState ",[522,969,970],{"class":945},"case",[522,972,973],{"class":528}," InfiniteSuccess",[522,975,976],{"class":531},"(",[522,978,553],{"class":552},[522,980,981],{"class":556},"final",[522,983,984],{"class":531}," hasNextPage",[522,986,624],{"class":552},[522,988,989],{"class":552}," :",[522,991,981],{"class":556},[522,993,994],{"class":531}," isFetchingNextPage)\n",[522,996,997,1000,1003,1006,1009],{"class":524,"line":662},[522,998,999],{"class":945},"          when",[522,1001,1002],{"class":531}," hasNextPage ",[522,1004,1005],{"class":552},"&&",[522,1007,1008],{"class":552}," !",[522,1010,1011],{"class":531},"isFetchingNextPage) {\n",[522,1013,1014,1017,1019,1021,1024],{"class":524,"line":692},[522,1015,1016],{"class":531},"    controller",[522,1018,584],{"class":552},[522,1020,392],{"class":903},[522,1022,1023],{"class":531},"()",[522,1025,929],{"class":552},[522,1027,1028],{"class":524,"line":709},[522,1029,1030],{"class":531},"  }\n",[522,1032,1033],{"class":524,"line":725},[522,1034,935],{"emptyLinePlaceholder":934},[522,1036,1037],{"class":524,"line":749},[522,1038,1039],{"class":627},"  // Trigger previous page when within 300px of the top\n",[522,1041,1042],{"class":524,"line":761},[522,1043,1044],{"class":627},"  // (only relevant after maxPages eviction has dropped page 1)\n",[522,1046,1047,1049,1051,1053,1056,1058,1060],{"class":524,"line":769},[522,1048,946],{"class":945},[522,1050,949],{"class":531},[522,1052,584],{"class":552},[522,1054,1055],{"class":531},"extentBefore ",[522,1057,532],{"class":552},[522,1059,959],{"class":700},[522,1061,962],{"class":552},[522,1063,1064,1066,1068,1070,1072,1074,1076,1079,1081,1083,1085],{"class":524,"line":777},[522,1065,967],{"class":531},[522,1067,970],{"class":945},[522,1069,973],{"class":528},[522,1071,976],{"class":531},[522,1073,553],{"class":552},[522,1075,981],{"class":556},[522,1077,1078],{"class":531}," hasPreviousPage",[522,1080,624],{"class":552},[522,1082,989],{"class":552},[522,1084,981],{"class":556},[522,1086,1087],{"class":531}," isFetchingPreviousPage)\n",[522,1089,1090,1092,1095,1097,1099],{"class":524,"line":822},[522,1091,999],{"class":945},[522,1093,1094],{"class":531}," hasPreviousPage ",[522,1096,1005],{"class":552},[522,1098,1008],{"class":552},[522,1100,1101],{"class":531},"isFetchingPreviousPage) {\n",[522,1103,1105,1107,1109,1111,1113],{"class":524,"line":1104},16,[522,1106,1016],{"class":531},[522,1108,584],{"class":552},[522,1110,396],{"class":903},[522,1112,1023],{"class":531},[522,1114,929],{"class":552},[522,1116,1118],{"class":524,"line":1117},17,[522,1119,1030],{"class":531},[522,1121,1123],{"class":524,"line":1122},18,[522,1124,1125],{"class":531},"}\n",[842,1127,1129,1130],{"id":1128},"optimistic-post-creation-with-setinfinitequerydata","Optimistic post creation with ",[361,1131,443],{},[353,1133,1134,1135,1137],{},"When the user creates a new post, it should appear at the top of the feed instantly: before the server responds. Qora exposes ",[361,1136,443],{}," to write directly into the infinite cache:",[490,1139,1142],{"className":515,"code":1140,"filename":1141,"language":518,"meta":496,"style":496},"Future\u003Cvoid> _publish(String content) async {\n  final client = context.qora;\n  const feedKey = ['feed'];\n\n  // 1. Snapshot for rollback\n  final snapshot = client.getInfiniteQueryData\u003CFeedPage, String?>(feedKey);\n\n  // 2. Optimistic prepend to first page\n  if (snapshot != null) {\n    client.setInfiniteQueryData\u003CFeedPage, String?>(\n      feedKey,\n      InfiniteData(\n        pages: [\n          FeedPage(\n            posts: [\n              Post.optimistic(\n                id: 'temp-${DateTime.now().millisecondsSinceEpoch}',\n                content: content,\n                author: currentUser,\n              ),\n              ...snapshot.pages.first.posts,\n            ],\n            nextCursor: snapshot.pages.first.nextCursor,\n            previousCursor: null, // optimistic post has no stable cursor yet\n          ),\n          ...snapshot.pages.skip(1),\n        ],\n        pageParams: snapshot.pageParams,\n      ),\n    );\n  }\n\n  try {\n    // 3. Fire the real mutation\n    await api.createPost(content);\n\n    // 4. Invalidate: replace the optimistic item with the server response\n    //    refetch() re-fetches all current pages preserving the page count\n    await controller.refetch();\n  } catch (error) {\n    // 5. Rollback: restore the exact pre-mutation snapshot\n    if (snapshot != null) {\n      client.setInfiniteQueryData(feedKey, snapshot);\n    }\n    _showError(error);\n  }\n}\n","lib/features/feed/ui/compose_sheet.dart",[361,1143,1144,1172,1191,1209,1213,1218,1248,1252,1257,1273,1293,1300,1307,1317,1324,1333,1345,1381,1393,1406,1414,1440,1448,1473,1487,1495,1521,1529,1546,1554,1561,1566,1571,1579,1585,1604,1609,1615,1621,1637,1649,1655,1669,1689,1695,1706,1711],{"__ignoreMap":496},[522,1145,1146,1149,1151,1153,1156,1159,1161,1163,1166,1169],{"class":524,"line":525},[522,1147,1148],{"class":528},"Future",[522,1150,532],{"class":531},[522,1152,900],{"class":556},[522,1154,1155],{"class":531},"> ",[522,1157,1158],{"class":903},"_publish",[522,1160,976],{"class":531},[522,1162,507],{"class":528},[522,1164,1165],{"class":531}," content) ",[522,1167,1168],{"class":945},"async",[522,1170,1171],{"class":531}," {\n",[522,1173,1174,1176,1179,1181,1184,1186,1189],{"class":524,"line":546},[522,1175,912],{"class":556},[522,1177,1178],{"class":531}," client ",[522,1180,918],{"class":552},[522,1182,1183],{"class":531}," context",[522,1185,584],{"class":552},[522,1187,1188],{"class":531},"qora",[522,1190,929],{"class":552},[522,1192,1193,1196,1199,1201,1203,1205,1207],{"class":524,"line":573},[522,1194,1195],{"class":556},"  const",[522,1197,1198],{"class":531}," feedKey ",[522,1200,918],{"class":552},[522,1202,560],{"class":531},[522,1204,564],{"class":563},[522,1206,567],{"class":531},[522,1208,929],{"class":552},[522,1210,1211],{"class":524,"line":592},[522,1212,935],{"emptyLinePlaceholder":934},[522,1214,1215],{"class":524,"line":613},[522,1216,1217],{"class":627},"  // 1. Snapshot for rollback\n",[522,1219,1220,1222,1225,1227,1230,1232,1235,1237,1239,1241,1243,1246],{"class":524,"line":631},[522,1221,912],{"class":556},[522,1223,1224],{"class":531}," snapshot ",[522,1226,918],{"class":552},[522,1228,1229],{"class":531}," client",[522,1231,584],{"class":552},[522,1233,1234],{"class":903},"getInfiniteQueryData",[522,1236,532],{"class":531},[522,1238,535],{"class":528},[522,1240,538],{"class":531},[522,1242,507],{"class":528},[522,1244,1245],{"class":531},"?>(feedKey)",[522,1247,929],{"class":552},[522,1249,1250],{"class":524,"line":662},[522,1251,935],{"emptyLinePlaceholder":934},[522,1253,1254],{"class":524,"line":692},[522,1255,1256],{"class":627},"  // 2. Optimistic prepend to first page\n",[522,1258,1259,1261,1264,1267,1270],{"class":524,"line":709},[522,1260,946],{"class":945},[522,1262,1263],{"class":531}," (snapshot ",[522,1265,1266],{"class":552},"!=",[522,1268,1269],{"class":552}," null",[522,1271,1272],{"class":531},") {\n",[522,1274,1275,1278,1280,1282,1284,1286,1288,1290],{"class":524,"line":725},[522,1276,1277],{"class":531},"    client",[522,1279,584],{"class":552},[522,1281,443],{"class":903},[522,1283,532],{"class":531},[522,1285,535],{"class":528},[522,1287,538],{"class":531},[522,1289,507],{"class":528},[522,1291,1292],{"class":531},"?>(\n",[522,1294,1295,1298],{"class":524,"line":749},[522,1296,1297],{"class":531},"      feedKey",[522,1299,570],{"class":552},[522,1301,1302,1305],{"class":524,"line":761},[522,1303,1304],{"class":528},"      InfiniteData",[522,1306,722],{"class":531},[522,1308,1309,1312,1314],{"class":524,"line":769},[522,1310,1311],{"class":531},"        pages",[522,1313,553],{"class":552},[522,1315,1316],{"class":531}," [\n",[522,1318,1319,1322],{"class":524,"line":777},[522,1320,1321],{"class":528},"          FeedPage",[522,1323,722],{"class":531},[522,1325,1326,1329,1331],{"class":524,"line":822},[522,1327,1328],{"class":531},"            posts",[522,1330,553],{"class":552},[522,1332,1316],{"class":531},[522,1334,1335,1338,1340,1343],{"class":524,"line":1104},[522,1336,1337],{"class":528},"              Post",[522,1339,584],{"class":552},[522,1341,1342],{"class":903},"optimistic",[522,1344,722],{"class":531},[522,1346,1347,1350,1352,1355,1358,1361,1363,1366,1369,1373,1376,1379],{"class":524,"line":1117},[522,1348,1349],{"class":531},"                id",[522,1351,553],{"class":552},[522,1353,1354],{"class":563}," 'temp-",[522,1356,1357],{"class":531},"${",[522,1359,1360],{"class":528},"DateTime",[522,1362,584],{"class":531},[522,1364,1365],{"class":903},"now",[522,1367,1368],{"class":531},"().",[522,1370,1372],{"class":1371},"sHdIc","millisecondsSinceEpoch",[522,1374,1375],{"class":531},"}",[522,1377,1378],{"class":563},"'",[522,1380,570],{"class":552},[522,1382,1383,1386,1388,1391],{"class":524,"line":1122},[522,1384,1385],{"class":531},"                content",[522,1387,553],{"class":552},[522,1389,1390],{"class":531}," content",[522,1392,570],{"class":552},[522,1394,1396,1399,1401,1404],{"class":524,"line":1395},19,[522,1397,1398],{"class":531},"                author",[522,1400,553],{"class":552},[522,1402,1403],{"class":531}," currentUser",[522,1405,570],{"class":552},[522,1407,1409,1412],{"class":524,"line":1408},20,[522,1410,1411],{"class":531},"              )",[522,1413,570],{"class":552},[522,1415,1417,1420,1423,1425,1428,1430,1433,1435,1438],{"class":524,"line":1416},21,[522,1418,1419],{"class":552},"              ...",[522,1421,1422],{"class":531},"snapshot",[522,1424,584],{"class":552},[522,1426,1427],{"class":531},"pages",[522,1429,584],{"class":552},[522,1431,1432],{"class":531},"first",[522,1434,584],{"class":552},[522,1436,1437],{"class":531},"posts",[522,1439,570],{"class":552},[522,1441,1443,1446],{"class":524,"line":1442},22,[522,1444,1445],{"class":531},"            ]",[522,1447,570],{"class":552},[522,1449,1451,1454,1456,1459,1461,1463,1465,1467,1469,1471],{"class":524,"line":1450},23,[522,1452,1453],{"class":531},"            nextCursor",[522,1455,553],{"class":552},[522,1457,1458],{"class":531}," snapshot",[522,1460,584],{"class":552},[522,1462,1427],{"class":531},[522,1464,584],{"class":552},[522,1466,1432],{"class":531},[522,1468,584],{"class":552},[522,1470,511],{"class":531},[522,1472,570],{"class":552},[522,1474,1476,1479,1481,1484],{"class":524,"line":1475},24,[522,1477,1478],{"class":531},"            previousCursor",[522,1480,553],{"class":552},[522,1482,1483],{"class":552}," null,",[522,1485,1486],{"class":627}," // optimistic post has no stable cursor yet\n",[522,1488,1490,1493],{"class":524,"line":1489},25,[522,1491,1492],{"class":531},"          )",[522,1494,570],{"class":552},[522,1496,1498,1501,1503,1505,1507,1509,1512,1514,1517,1519],{"class":524,"line":1497},26,[522,1499,1500],{"class":552},"          ...",[522,1502,1422],{"class":531},[522,1504,584],{"class":552},[522,1506,1427],{"class":531},[522,1508,584],{"class":552},[522,1510,1511],{"class":903},"skip",[522,1513,976],{"class":531},[522,1515,1516],{"class":700},"1",[522,1518,744],{"class":531},[522,1520,570],{"class":552},[522,1522,1524,1527],{"class":524,"line":1523},27,[522,1525,1526],{"class":531},"        ]",[522,1528,570],{"class":552},[522,1530,1532,1535,1537,1539,1541,1544],{"class":524,"line":1531},28,[522,1533,1534],{"class":531},"        pageParams",[522,1536,553],{"class":552},[522,1538,1458],{"class":531},[522,1540,584],{"class":552},[522,1542,1543],{"class":531},"pageParams",[522,1545,570],{"class":552},[522,1547,1549,1552],{"class":524,"line":1548},29,[522,1550,1551],{"class":531},"      )",[522,1553,570],{"class":552},[522,1555,1557,1559],{"class":524,"line":1556},30,[522,1558,764],{"class":531},[522,1560,929],{"class":552},[522,1562,1564],{"class":524,"line":1563},31,[522,1565,1030],{"class":531},[522,1567,1569],{"class":524,"line":1568},32,[522,1570,935],{"emptyLinePlaceholder":934},[522,1572,1574,1577],{"class":524,"line":1573},33,[522,1575,1576],{"class":945},"  try",[522,1578,1171],{"class":531},[522,1580,1582],{"class":524,"line":1581},34,[522,1583,1584],{"class":627},"    // 3. Fire the real mutation\n",[522,1586,1588,1591,1594,1596,1599,1602],{"class":524,"line":1587},35,[522,1589,1590],{"class":945},"    await",[522,1592,1593],{"class":531}," api",[522,1595,584],{"class":552},[522,1597,1598],{"class":903},"createPost",[522,1600,1601],{"class":531},"(content)",[522,1603,929],{"class":552},[522,1605,1607],{"class":524,"line":1606},36,[522,1608,935],{"emptyLinePlaceholder":934},[522,1610,1612],{"class":524,"line":1611},37,[522,1613,1614],{"class":627},"    // 4. Invalidate: replace the optimistic item with the server response\n",[522,1616,1618],{"class":524,"line":1617},38,[522,1619,1620],{"class":627},"    //    refetch() re-fetches all current pages preserving the page count\n",[522,1622,1624,1626,1628,1630,1633,1635],{"class":524,"line":1623},39,[522,1625,1590],{"class":945},[522,1627,812],{"class":531},[522,1629,584],{"class":552},[522,1631,1632],{"class":903},"refetch",[522,1634,1023],{"class":531},[522,1636,929],{"class":552},[522,1638,1640,1643,1646],{"class":524,"line":1639},40,[522,1641,1642],{"class":531},"  } ",[522,1644,1645],{"class":945},"catch",[522,1647,1648],{"class":531}," (error) {\n",[522,1650,1652],{"class":524,"line":1651},41,[522,1653,1654],{"class":627},"    // 5. Rollback: restore the exact pre-mutation snapshot\n",[522,1656,1658,1661,1663,1665,1667],{"class":524,"line":1657},42,[522,1659,1660],{"class":945},"    if",[522,1662,1263],{"class":531},[522,1664,1266],{"class":552},[522,1666,1269],{"class":552},[522,1668,1272],{"class":531},[522,1670,1672,1675,1677,1679,1682,1684,1687],{"class":524,"line":1671},43,[522,1673,1674],{"class":531},"      client",[522,1676,584],{"class":552},[522,1678,443],{"class":903},[522,1680,1681],{"class":531},"(feedKey",[522,1683,624],{"class":552},[522,1685,1686],{"class":531}," snapshot)",[522,1688,929],{"class":552},[522,1690,1692],{"class":524,"line":1691},44,[522,1693,1694],{"class":531},"    }\n",[522,1696,1698,1701,1704],{"class":524,"line":1697},45,[522,1699,1700],{"class":903},"    _showError",[522,1702,1703],{"class":531},"(error)",[522,1705,929],{"class":552},[522,1707,1709],{"class":524,"line":1708},46,[522,1710,1030],{"class":531},[522,1712,1714],{"class":524,"line":1713},47,[522,1715,1125],{"class":531},[353,1717,1718,1719,1722,1723,1726,1727,1730,1731,1733,1734,1736,1737,1740],{},"The critical detail here is ",[356,1720,1721],{},"step 4",": calling ",[361,1724,1725],{},"controller.refetch()"," rather than ",[361,1728,1729],{},"invalidateInfiniteQuery",". ",[361,1732,451],{}," re-fetches all currently loaded pages (8 pages × 20 items = 160 posts re-validated in sequence), which replaces the temp-ID optimistic post with the real server object. ",[361,1735,1729],{}," would reset the state to ",[361,1738,1739],{},"InfiniteInitial"," and restart from page 1, losing the user's scroll position.",[842,1742,1744,1746],{"id":1743},"infinitefailure-with-graceful-degradation",[361,1745,459],{}," with graceful degradation",[353,1748,1749,1750,1752,1753,1755,1756,1758],{},"Load-more errors must never erase the feed the user already loaded. ",[361,1751,459],{}," preserves ",[361,1754,463],{},": the last successful ",[361,1757,833],{},": for exactly this purpose:",[490,1760,1762],{"className":515,"code":1761,"filename":517,"language":518,"meta":496,"style":496},"case InfiniteFailure(:final error, :final previousData):\n  if (previousData != null) {\n    final posts = previousData.flatten((p) => p.posts);\n    return SliverList.builder(\n      itemCount: posts.length + 1, // +1 for the error footer\n      itemBuilder: (context, i) {\n        if (i \u003C posts.length) return PostTile(post: posts[i]);\n        // Error footer at the very end\n        return _LoadMoreErrorBanner(\n          error: error,\n          onRetry: controller.fetchNextPage,\n        );\n      },\n    );\n  }\n  // No data at all: full error screen\n  return SliverFillRemaining(child: ErrorScreen(error));\n",[361,1763,1764,1792,1805,1838,1853,1879,1893,1926,1931,1941,1952,1967,1974,1981,1987,1991,1996],{"__ignoreMap":496},[522,1765,1766,1768,1771,1773,1775,1777,1780,1782,1784,1786,1789],{"class":524,"line":525},[522,1767,970],{"class":945},[522,1769,1770],{"class":528}," InfiniteFailure",[522,1772,976],{"class":531},[522,1774,553],{"class":552},[522,1776,981],{"class":556},[522,1778,1779],{"class":531}," error",[522,1781,624],{"class":552},[522,1783,989],{"class":552},[522,1785,981],{"class":556},[522,1787,1788],{"class":531}," previousData)",[522,1790,1791],{"class":552},":\n",[522,1793,1794,1796,1799,1801,1803],{"class":524,"line":546},[522,1795,946],{"class":945},[522,1797,1798],{"class":531}," (previousData ",[522,1800,1266],{"class":552},[522,1802,1269],{"class":552},[522,1804,1272],{"class":531},[522,1806,1807,1810,1813,1815,1818,1820,1823,1826,1828,1831,1833,1836],{"class":524,"line":573},[522,1808,1809],{"class":556},"    final",[522,1811,1812],{"class":531}," posts ",[522,1814,918],{"class":552},[522,1816,1817],{"class":531}," previousData",[522,1819,584],{"class":552},[522,1821,1822],{"class":903},"flatten",[522,1824,1825],{"class":531},"((p) ",[522,1827,647],{"class":552},[522,1829,1830],{"class":531}," p",[522,1832,584],{"class":552},[522,1834,1835],{"class":531},"posts)",[522,1837,929],{"class":552},[522,1839,1840,1843,1846,1848,1851],{"class":524,"line":592},[522,1841,1842],{"class":945},"    return",[522,1844,1845],{"class":528}," SliverList",[522,1847,584],{"class":552},[522,1849,1850],{"class":903},"builder",[522,1852,722],{"class":531},[522,1854,1855,1858,1860,1863,1865,1868,1871,1874,1876],{"class":524,"line":613},[522,1856,1857],{"class":531},"      itemCount",[522,1859,553],{"class":552},[522,1861,1862],{"class":531}," posts",[522,1864,584],{"class":552},[522,1866,1867],{"class":531},"length ",[522,1869,1870],{"class":552},"+",[522,1872,1873],{"class":700}," 1",[522,1875,624],{"class":552},[522,1877,1878],{"class":627}," // +1 for the error footer\n",[522,1880,1881,1884,1886,1888,1890],{"class":524,"line":631},[522,1882,1883],{"class":531},"      itemBuilder",[522,1885,553],{"class":552},[522,1887,785],{"class":531},[522,1889,624],{"class":552},[522,1891,1892],{"class":531}," i) {\n",[522,1894,1895,1898,1901,1903,1905,1907,1910,1913,1916,1919,1921,1924],{"class":524,"line":662},[522,1896,1897],{"class":945},"        if",[522,1899,1900],{"class":531}," (i ",[522,1902,532],{"class":552},[522,1904,1862],{"class":531},[522,1906,584],{"class":552},[522,1908,1909],{"class":531},"length) ",[522,1911,1912],{"class":945},"return",[522,1914,1915],{"class":528}," PostTile",[522,1917,1918],{"class":531},"(post",[522,1920,553],{"class":552},[522,1922,1923],{"class":531}," posts[i])",[522,1925,929],{"class":552},[522,1927,1928],{"class":524,"line":692},[522,1929,1930],{"class":627},"        // Error footer at the very end\n",[522,1932,1933,1936,1939],{"class":524,"line":709},[522,1934,1935],{"class":945},"        return",[522,1937,1938],{"class":528}," _LoadMoreErrorBanner",[522,1940,722],{"class":531},[522,1942,1943,1946,1948,1950],{"class":524,"line":725},[522,1944,1945],{"class":531},"          error",[522,1947,553],{"class":552},[522,1949,1779],{"class":531},[522,1951,570],{"class":552},[522,1953,1954,1957,1959,1961,1963,1965],{"class":524,"line":749},[522,1955,1956],{"class":531},"          onRetry",[522,1958,553],{"class":552},[522,1960,812],{"class":531},[522,1962,584],{"class":552},[522,1964,392],{"class":531},[522,1966,570],{"class":552},[522,1968,1969,1972],{"class":524,"line":761},[522,1970,1971],{"class":531},"        )",[522,1973,929],{"class":552},[522,1975,1976,1979],{"class":524,"line":769},[522,1977,1978],{"class":531},"      }",[522,1980,570],{"class":552},[522,1982,1983,1985],{"class":524,"line":777},[522,1984,764],{"class":531},[522,1986,929],{"class":552},[522,1988,1989],{"class":524,"line":822},[522,1990,1030],{"class":531},[522,1992,1993],{"class":524,"line":1104},[522,1994,1995],{"class":627},"  // No data at all: full error screen\n",[522,1997,1998,2001,2004,2007,2009,2012,2015],{"class":524,"line":1117},[522,1999,2000],{"class":945},"  return",[522,2002,2003],{"class":528}," SliverFillRemaining",[522,2005,2006],{"class":531},"(child",[522,2008,553],{"class":552},[522,2010,2011],{"class":528}," ErrorScreen",[522,2013,2014],{"class":531},"(error))",[522,2016,929],{"class":552},[842,2018,501,2020,2022],{"id":2019},"the-flatten-call",[361,2021,1822],{}," call",[353,2024,2025,2028,2029,2032,2033,2035],{},[361,2026,2027],{},"InfiniteData\u003CFeedPage, String?>"," holds a ",[361,2030,2031],{},"List\u003CFeedPage>",": one item per loaded page. ",[361,2034,1822],{}," extracts the inner list from each page and concatenates them:",[490,2037,2039],{"className":515,"code":2038,"filename":517,"language":518,"meta":496,"style":496},"case InfiniteSuccess(:final data, :final hasNextPage, :final isFetchingNextPage):\n  final posts = data.flatten((page) => page.posts); // List\u003CPost>\n\n  return SliverList.builder(\n    itemCount: posts.length + 1,\n    itemBuilder: (context, index) {\n      if (index \u003C posts.length) return PostTile(post: posts[index]);\n\n      // Footer: spinner, \"load more\" trigger, or end-of-feed message\n      if (isFetchingNextPage) return const _PageLoader();\n      if (hasNextPage) return _LoadMoreButton(onTap: controller.fetchNextPage);\n      return const _EndOfFeedBanner();\n    },\n  );\n",[361,2040,2041,2075,2107,2111,2123,2142,2156,2185,2189,2194,2212,2238,2252,2259],{"__ignoreMap":496},[522,2042,2043,2045,2047,2049,2051,2053,2056,2058,2060,2062,2064,2066,2068,2070,2073],{"class":524,"line":525},[522,2044,970],{"class":945},[522,2046,973],{"class":528},[522,2048,976],{"class":531},[522,2050,553],{"class":552},[522,2052,981],{"class":556},[522,2054,2055],{"class":531}," data",[522,2057,624],{"class":552},[522,2059,989],{"class":552},[522,2061,981],{"class":556},[522,2063,984],{"class":531},[522,2065,624],{"class":552},[522,2067,989],{"class":552},[522,2069,981],{"class":556},[522,2071,2072],{"class":531}," isFetchingNextPage)",[522,2074,1791],{"class":552},[522,2076,2077,2079,2081,2083,2085,2087,2089,2092,2094,2097,2099,2101,2104],{"class":524,"line":546},[522,2078,912],{"class":556},[522,2080,1812],{"class":531},[522,2082,918],{"class":552},[522,2084,2055],{"class":531},[522,2086,584],{"class":552},[522,2088,1822],{"class":903},[522,2090,2091],{"class":531},"((page) ",[522,2093,647],{"class":552},[522,2095,2096],{"class":531}," page",[522,2098,584],{"class":552},[522,2100,1835],{"class":531},[522,2102,2103],{"class":552},";",[522,2105,2106],{"class":627}," // List\u003CPost>\n",[522,2108,2109],{"class":524,"line":573},[522,2110,935],{"emptyLinePlaceholder":934},[522,2112,2113,2115,2117,2119,2121],{"class":524,"line":592},[522,2114,2000],{"class":945},[522,2116,1845],{"class":528},[522,2118,584],{"class":552},[522,2120,1850],{"class":903},[522,2122,722],{"class":531},[522,2124,2125,2128,2130,2132,2134,2136,2138,2140],{"class":524,"line":613},[522,2126,2127],{"class":531},"    itemCount",[522,2129,553],{"class":552},[522,2131,1862],{"class":531},[522,2133,584],{"class":552},[522,2135,1867],{"class":531},[522,2137,1870],{"class":552},[522,2139,1873],{"class":700},[522,2141,570],{"class":552},[522,2143,2144,2147,2149,2151,2153],{"class":524,"line":631},[522,2145,2146],{"class":531},"    itemBuilder",[522,2148,553],{"class":552},[522,2150,785],{"class":531},[522,2152,624],{"class":552},[522,2154,2155],{"class":531}," index) {\n",[522,2157,2158,2161,2164,2166,2168,2170,2172,2174,2176,2178,2180,2183],{"class":524,"line":662},[522,2159,2160],{"class":945},"      if",[522,2162,2163],{"class":531}," (index ",[522,2165,532],{"class":552},[522,2167,1862],{"class":531},[522,2169,584],{"class":552},[522,2171,1909],{"class":531},[522,2173,1912],{"class":945},[522,2175,1915],{"class":528},[522,2177,1918],{"class":531},[522,2179,553],{"class":552},[522,2181,2182],{"class":531}," posts[index])",[522,2184,929],{"class":552},[522,2186,2187],{"class":524,"line":692},[522,2188,935],{"emptyLinePlaceholder":934},[522,2190,2191],{"class":524,"line":709},[522,2192,2193],{"class":627},"      // Footer: spinner, \"load more\" trigger, or end-of-feed message\n",[522,2195,2196,2198,2201,2203,2205,2208,2210],{"class":524,"line":725},[522,2197,2160],{"class":945},[522,2199,2200],{"class":531}," (isFetchingNextPage) ",[522,2202,1912],{"class":945},[522,2204,557],{"class":556},[522,2206,2207],{"class":528}," _PageLoader",[522,2209,1023],{"class":531},[522,2211,929],{"class":552},[522,2213,2214,2216,2219,2221,2224,2227,2229,2231,2233,2236],{"class":524,"line":749},[522,2215,2160],{"class":945},[522,2217,2218],{"class":531}," (hasNextPage) ",[522,2220,1912],{"class":945},[522,2222,2223],{"class":528}," _LoadMoreButton",[522,2225,2226],{"class":531},"(onTap",[522,2228,553],{"class":552},[522,2230,812],{"class":531},[522,2232,584],{"class":552},[522,2234,2235],{"class":531},"fetchNextPage)",[522,2237,929],{"class":552},[522,2239,2240,2243,2245,2248,2250],{"class":524,"line":761},[522,2241,2242],{"class":945},"      return",[522,2244,557],{"class":556},[522,2246,2247],{"class":528}," _EndOfFeedBanner",[522,2249,1023],{"class":531},[522,2251,929],{"class":552},[522,2253,2254,2257],{"class":524,"line":769},[522,2255,2256],{"class":531},"    }",[522,2258,570],{"class":552},[522,2260,2261,2263],{"class":524,"line":777},[522,2262,772],{"class":531},[522,2264,929],{"class":552},[373,2266],{},[348,2268,2270],{"id":2269},"source-code","Source Code",[490,2272,2275],{"className":2273,"code":2274,"language":495,"meta":496},[493],"examples/ex_03_infinite_scroll/\n├── lib/\n│   ├── main.dart                              ← QoraClient + QoraScope setup\n│   ├── features/\n│   │   └── feed/\n│   │       ├── data/\n│   │       │   └── feed_api.dart              ← cursor-based HTTP calls\n│   │       ├── models/\n│   │       │   ├── feed_page.dart             ← { posts, nextCursor, previousCursor }\n│   │       │   └── post.dart                  ← Post.optimistic() constructor\n│   │       └── ui/\n│   │           ├── feed_screen.dart           ← InfiniteQueryBuilder + scroll logic\n│   │           ├── post_tile.dart             ← single post widget\n│   │           ├── compose_sheet.dart         ← optimistic post creation\n│   │           └── widgets/\n│   │               ├── page_loader.dart       ← bottom-of-list spinner\n│   │               ├── load_more_error.dart   ← retry banner on page failure\n│   │               └── end_of_feed.dart       ← \"You're all caught up\" banner\n└── pubspec.yaml\n",[361,2276,2274],{"__ignoreMap":496},[353,2278,2279],{},[2280,2281,2285],"a",{"href":2282,"rel":2283},"https://github.com/meragix/qora/tree/main/examples/ex_03_infinite_scroll",[2284],"nofollow","View full source code",[373,2287],{},[348,2289,2291],{"id":2290},"performance-notes","Performance Notes",[2293,2294,2295,2308],"table",{},[2296,2297,2298],"thead",{},[2299,2300,2301,2305],"tr",{},[2302,2303,2304],"th",{},"Concern",[2302,2306,2307],{},"Solution",[2309,2310,2311,2322,2335,2343,2357],"tbody",{},[2299,2312,2313,2317],{},[2314,2315,2316],"td",{},"Memory bloat from long sessions",[2314,2318,2319,2321],{},[361,2320,370],{}," caps the page window; evicted pages are re-fetched on demand",[2299,2323,2324,2327],{},[2314,2325,2326],{},"Scroll jank from large lists",[2314,2328,2329,2331,2332,2334],{},[361,2330,484],{}," renders only visible items; ",[361,2333,833],{}," object itself is bounded",[2299,2336,2337,2340],{},[2314,2338,2339],{},"Cursor drift from server inserts",[2314,2341,2342],{},"Cursor-based pagination anchors each page to a stable server-side position",[2299,2344,2345,2350],{},[2314,2346,2347,2348],{},"Thundering-herd on ",[361,2349,451],{},[2314,2351,2352,2353,2356],{},"Pages are re-fetched ",[356,2354,2355],{},"sequentially",", not concurrently",[2299,2358,2359,2362],{},[2314,2360,2361],{},"Stale feed on return to tab",[2314,2363,2364,2367,2368,2370,2371],{},[361,2365,2366],{},"staleTime: 2 minutes"," triggers a background ",[361,2369,451],{}," on tab re-focus via ",[361,2372,2373],{},"FlutterLifecycleManager",[373,2375],{},[348,2377,2379],{"id":2378},"related-guides","Related Guides",[380,2381,2382,2394,2400,2415],{},[383,2383,2384,440,2388,538,2390,2393],{},[2280,2385,2387],{"href":2386},"../recipes/infinite-queries","Infinite Queries Guide",[361,2389,833],{},[361,2391,2392],{},"InfiniteQueryObserver"," for pure Dart, windowed paging, and bi-directional scroll",[383,2395,2396,2399],{},[2280,2397,118],{"href":2398},"../flutter-integration/infinite-query-builder",": Flutter widget binding",[383,2401,2402,440,2406,538,2409,538,2412],{},[2280,2403,2405],{"href":2404},"../recipes/optimistic-updates","Optimistic Updates Guide",[361,2407,2408],{},"setQueryData",[361,2410,2411],{},"getQueryData",[361,2413,2414],{},"restoreQueryData",[383,2416,2417,2421],{},[2280,2418,2420],{"href":2419},"../api-reference/infinite-query","Infinite Query API Reference",": full method reference",[2423,2424,2425],"style",{},"html pre.shiki code .sBMFI, html code.shiki .sBMFI{--shiki-light:#E2931D;--shiki-default:#FFCB6B;--shiki-dark:#FFCB6B}html pre.shiki code .sTEyZ, html code.shiki .sTEyZ{--shiki-light:#90A4AE;--shiki-default:#EEFFFF;--shiki-dark:#BABED8}html pre.shiki code .sMK4o, html code.shiki .sMK4o{--shiki-light:#39ADB5;--shiki-default:#89DDFF;--shiki-dark:#89DDFF}html pre.shiki code .spNyl, html code.shiki .spNyl{--shiki-light:#9C3EDA;--shiki-default:#C792EA;--shiki-dark:#C792EA}html pre.shiki code .sfazB, html code.shiki .sfazB{--shiki-light:#91B859;--shiki-default:#C3E88D;--shiki-dark:#C3E88D}html pre.shiki code .sHwdD, html code.shiki .sHwdD{--shiki-light:#90A4AE;--shiki-light-font-style:italic;--shiki-default:#546E7A;--shiki-default-font-style:italic;--shiki-dark:#676E95;--shiki-dark-font-style:italic}html pre.shiki code .sbssI, html code.shiki .sbssI{--shiki-light:#F76D47;--shiki-default:#F78C6C;--shiki-dark:#F78C6C}html .light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html.light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .s2Zo4, html code.shiki .s2Zo4{--shiki-light:#6182B8;--shiki-default:#82AAFF;--shiki-dark:#82AAFF}html pre.shiki code .s7zQu, html code.shiki .s7zQu{--shiki-light:#39ADB5;--shiki-light-font-style:italic;--shiki-default:#89DDFF;--shiki-default-font-style:italic;--shiki-dark:#89DDFF;--shiki-dark-font-style:italic}html pre.shiki code .sHdIc, html code.shiki .sHdIc{--shiki-light:#90A4AE;--shiki-light-font-style:italic;--shiki-default:#EEFFFF;--shiki-default-font-style:italic;--shiki-dark:#BABED8;--shiki-dark-font-style:italic}",{"title":496,"searchDepth":546,"depth":546,"links":2427},[2428,2429,2430,2431,2440,2441,2442],{"id":350,"depth":546,"text":351},{"id":377,"depth":546,"text":378},{"id":469,"depth":546,"text":470},{"id":839,"depth":546,"text":840,"children":2432},[2433,2434,2436,2438],{"id":844,"depth":573,"text":845},{"id":1128,"depth":573,"text":2435},"Optimistic post creation with setInfiniteQueryData",{"id":1743,"depth":573,"text":2437},"InfiniteFailure with graceful degradation",{"id":2019,"depth":573,"text":2439},"The flatten call",{"id":2269,"depth":546,"text":2270},{"id":2290,"depth":546,"text":2291},{"id":2378,"depth":546,"text":2379},"Build a high-performance social media-style feed with cursor-based pagination, windowed memory management, and instant optimistic post creation.","md",null,{},{"icon":309},{"title":2449,"description":2450},"Case Study: Infinite Scroll Feed - Qora","Production-grade infinite scroll in Flutter using InfiniteQueryBuilder, cursor pagination, maxPages windowing, and optimistic cache writes. No memory bloat, no jank.","LdpZqpYzuzrNeenAUJ7PhQ5lXYVej4oubeDszLmNaQI",[2453,2455],{"title":302,"path":303,"stem":304,"description":2454,"icon":151,"children":-1},"A complete optimistic update implementation: setQueryData for immediate UI feedback, restoreQueryData for automatic rollback on failure, and invalidateWhere for server reconciliation.",{"title":311,"path":312,"stem":313,"description":2456,"icon":90,"children":-1},"A todo app that keeps working without a network: saved data on start, queries that pause offline, and writes queued and replayed in order on reconnect.",1794040402921]