<html lang="en">
<head></head>
<body>

<form id="mainForm" method="post" action="https://stackblitz.com/run" target="_self">
<input type="hidden" name="project[files][CHANGELOG.md]" value="# @tanstack/query-example-lit-pagination

## 0.0.2

### Patch Changes

- Updated dependencies [[`4082894`](https://github.com/TanStack/query/commit/4082894509f31376ebeb8514cc3e167bbbfc7c46)]:
  - @tanstack/lit-query@0.2.0
">
<input type="hidden" name="project[files][README.md]" value="# Example

To run this example from the repo root:

- `pnpm --dir examples/lit/pagination run dev`
">
<input type="hidden" name="project[files][eslint.config.js]" value="import { tanstackConfig } from &#39;@tanstack/eslint-config&#39;
import pluginQuery from &#39;@tanstack/eslint-plugin-query&#39;

export default [...tanstackConfig, ...pluginQuery.configs[&#39;flat/recommended&#39;]]
">
<input type="hidden" name="project[files][index.html]" value="&lt;!doctype html&gt;
&lt;html lang=&quot;en&quot;&gt;
  &lt;head&gt;
    &lt;meta charset=&quot;UTF-8&quot; /&gt;
    &lt;meta name=&quot;viewport&quot; content=&quot;width=device-width, initial-scale=1.0&quot; /&gt;
    &lt;title&gt;TanStack Lit Query Pagination Demo&lt;/title&gt;
  &lt;/head&gt;
  &lt;body&gt;
    &lt;pagination-demo-root&gt;&lt;/pagination-demo-root&gt;
    &lt;script type=&quot;module&quot; src=&quot;/src/main.ts&quot;&gt;&lt;/script&gt;
  &lt;/body&gt;
&lt;/html&gt;
">
<input type="hidden" name="project[files][package.json]" value="{
  &quot;name&quot;: &quot;@tanstack/query-example-lit-pagination&quot;,
  &quot;private&quot;: true,
  &quot;type&quot;: &quot;module&quot;,
  &quot;scripts&quot;: {
    &quot;dev&quot;: &quot;node ./scripts/dev.mjs&quot;,
    &quot;build&quot;: &quot;tsc --noEmit &amp;&amp; vite build&quot;,
    &quot;preview&quot;: &quot;vite preview&quot;,
    &quot;test:eslint&quot;: &quot;eslint ./src&quot;
  },
  &quot;dependencies&quot;: {
    &quot;@tanstack/lit-query&quot;: &quot;https://pkg.pr.new/TanStack/query/@tanstack/lit-query@a3d92cf&quot;,
    &quot;@tanstack/query-core&quot;: &quot;https://pkg.pr.new/TanStack/query/@tanstack/query-core@a3d92cf&quot;,
    &quot;lit&quot;: &quot;^3.3.3&quot;
  },
  &quot;devDependencies&quot;: {
    &quot;@tanstack/eslint-plugin-query&quot;: &quot;https://pkg.pr.new/TanStack/query/@tanstack/eslint-plugin-query@a3d92cf&quot;,
    &quot;typescript&quot;: &quot;5.8.3&quot;,
    &quot;vite&quot;: &quot;^8.3.1&quot;
  }
}">
<input type="hidden" name="project[files][tsconfig.json]" value="{
  &quot;compilerOptions&quot;: {
    &quot;target&quot;: &quot;ES2022&quot;,
    &quot;module&quot;: &quot;ESNext&quot;,
    &quot;moduleResolution&quot;: &quot;Bundler&quot;,
    &quot;lib&quot;: [&quot;ES2022&quot;, &quot;DOM&quot;],
    &quot;strict&quot;: true,
    &quot;noEmit&quot;: true,
    &quot;isolatedModules&quot;: true,
    &quot;skipLibCheck&quot;: true,
    &quot;allowJs&quot;: true,
    &quot;useDefineForClassFields&quot;: false
  },
  &quot;include&quot;: [&quot;src/**/*.ts&quot;, &quot;vite.config.ts&quot;, &quot;eslint.config.js&quot;]
}
">
<input type="hidden" name="project[files][vite.config.ts]" value="import { defineConfig } from &#39;vite&#39;
import { DEMO_PORT } from &#39;./config/ports.js&#39;

export default defineConfig({
  server: {
    host: &#39;127.0.0.1&#39;,
    port: DEMO_PORT,
    strictPort: true,
  },
  preview: {
    host: &#39;127.0.0.1&#39;,
    port: DEMO_PORT,
    strictPort: true,
  },
})
">
<input type="hidden" name="project[files][config/ports.d.ts]" value="export const DEMO_PORT: number
export const API_PORT: number
">
<input type="hidden" name="project[files][config/ports.js]" value="const DEFAULT_DEMO_PORT = 4183
const DEFAULT_API_PORT = 4184

function readPortFromEnv(name, fallback) {
  const rawValue = process.env[name]
  if (!rawValue) {
    return fallback
  }

  const parsed = Number.parseInt(rawValue, 10)
  const valid = Number.isInteger(parsed) &amp;&amp; parsed &gt;= 1 &amp;&amp; parsed &lt;= 65535

  if (!valid) {
    throw new Error(
      `Invalid ${name}=&quot;${rawValue}&quot;. Expected integer in [1, 65535].`,
    )
  }

  return parsed
}

export const DEMO_PORT = readPortFromEnv(
  &#39;PAGINATION_DEMO_PORT&#39;,
  DEFAULT_DEMO_PORT,
)
export const API_PORT = readPortFromEnv(&#39;PAGINATION_API_PORT&#39;, DEFAULT_API_PORT)
">
<input type="hidden" name="project[files][scripts/dev.mjs]" value="import { spawn } from &#39;node:child_process&#39;
import { once } from &#39;node:events&#39;
import { API_PORT } from &#39;../config/ports.js&#39;

const viteCommand = process.platform === &#39;win32&#39; ? &#39;vite.cmd&#39; : &#39;vite&#39;
const cwd = new URL(&#39;..&#39;, import.meta.url)

function forwardOutput(prefix, stream, output) {
  stream.on(&#39;data&#39;, (chunk) =&gt; {
    output.write(`${prefix}${chunk}`)
  })
}

function start(name, command, args, extraEnv = {}) {
  const child = spawn(command, args, {
    cwd,
    stdio: [&#39;ignore&#39;, &#39;pipe&#39;, &#39;pipe&#39;],
    env: {
      ...process.env,
      ...extraEnv,
    },
  })

  forwardOutput(`[${name}] `, child.stdout, process.stdout)
  forwardOutput(`[${name}] `, child.stderr, process.stderr)

  return child
}

async function stop(child) {
  if (!child || child.exitCode !== null) {
    return
  }

  child.kill(&#39;SIGTERM&#39;)
  await Promise.race([
    once(child, &#39;exit&#39;),
    new Promise((resolve) =&gt; setTimeout(resolve, 2000)),
  ])

  if (child.exitCode === null) {
    child.kill(&#39;SIGKILL&#39;)
    await Promise.race([
      once(child, &#39;exit&#39;),
      new Promise((resolve) =&gt; setTimeout(resolve, 2000)),
    ])
  }
}

async function run() {
  const api = start(&#39;api&#39;, process.execPath, [&#39;./server/index.mjs&#39;])
  const web = start(&#39;web&#39;, viteCommand, [], {
    VITE_PAGINATION_API_PORT: String(API_PORT),
  })

  const shutdown = async () =&gt; {
    await Promise.all([stop(web), stop(api)])
  }

  process.on(&#39;SIGINT&#39;, shutdown)
  process.on(&#39;SIGTERM&#39;, shutdown)

  const [winner] = await Promise.race([
    once(api, &#39;exit&#39;).then(([code]) =&gt; ({ name: &#39;api&#39;, code })),
    once(web, &#39;exit&#39;).then(([code]) =&gt; ({ name: &#39;web&#39;, code })),
  ])

  await shutdown()

  if (winner.code !== 0 &amp;&amp; winner.code !== null) {
    process.exitCode = winner.code
  } else {
    process.exitCode = 1
  }
}

run().catch((error) =&gt; {
  console.error(error)
  process.exitCode = 1
})
">
<input type="hidden" name="project[files][server/index.mjs]" value="import { createServer } from &#39;node:http&#39;
import { API_PORT } from &#39;../config/ports.js&#39;

const PAGE_SIZE = 10
const TOTAL_PROJECTS = 50
const JSON_CONTENT_TYPE = &#39;application/json&#39;

function createSeedProjects() {
  return Array.from({ length: TOTAL_PROJECTS }, (_, index) =&gt; ({
    id: index + 1,
    name: `Project ${index + 1}`,
    owner: `Team ${(index % 5) + 1}`,
    isFavorite: false,
  }))
}

let projects = createSeedProjects()
let nextProjectId = projects.length + 1
let totalRequestCount = 0
let totalMutationCount = 0
let failNextMutation = false
const perPageRequestCount = new Map()

function writeJson(res, status, body) {
  const payload = JSON.stringify(body)
  res.writeHead(status, {
    &#39;content-type&#39;: &#39;application/json; charset=utf-8&#39;,
    &#39;cache-control&#39;: &#39;no-store&#39;,
    &#39;access-control-allow-origin&#39;: &#39;*&#39;,
    &#39;access-control-allow-methods&#39;: &#39;GET,POST,PATCH,OPTIONS&#39;,
    &#39;access-control-allow-headers&#39;: &#39;content-type&#39;,
    &#39;content-length&#39;: Buffer.byteLength(payload),
  })
  res.end(payload)
}

function parsePositiveInt(rawValue, fallback) {
  if (rawValue == null || rawValue === &#39;&#39;) {
    return fallback
  }

  const parsed = Number.parseInt(rawValue, 10)
  if (!Number.isInteger(parsed) || parsed &lt; 1) {
    return undefined
  }

  return parsed
}

function parseNonNegativeInt(rawValue, fallback) {
  if (rawValue == null || rawValue === &#39;&#39;) {
    return fallback
  }

  const parsed = Number.parseInt(rawValue, 10)
  if (!Number.isInteger(parsed) || parsed &lt; 0) {
    return undefined
  }

  return parsed
}

function resetState() {
  projects = createSeedProjects()
  nextProjectId = projects.length + 1
  totalRequestCount = 0
  totalMutationCount = 0
  failNextMutation = false
  perPageRequestCount.clear()
}

async function sleep(ms) {
  if (!ms || ms &lt;= 0) {
    return
  }

  await new Promise((resolve) =&gt; {
    setTimeout(resolve, ms)
  })
}

async function readJsonBody(req) {
  const contentType = req.headers[&#39;content-type&#39;]
  if (!contentType || !contentType.startsWith(JSON_CONTENT_TYPE)) {
    return {
      ok: false,
      status: 415,
      error: &#39;Expected application/json request body&#39;,
    }
  }

  const chunks = []
  for await (const chunk of req) {
    chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk))
  }

  const raw = Buffer.concat(chunks).toString(&#39;utf8&#39;)
  if (!raw) {
    return {
      ok: false,
      status: 400,
      error: &#39;Request body is required&#39;,
    }
  }

  try {
    return {
      ok: true,
      value: JSON.parse(raw),
    }
  } catch {
    return {
      ok: false,
      status: 400,
      error: &#39;Request body must be valid JSON&#39;,
    }
  }
}

function buildProjectsPage(page) {
  const totalPages = Math.max(1, Math.ceil(projects.length / PAGE_SIZE))
  const boundedPage = Math.min(page, totalPages)
  const startIndex = (boundedPage - 1) * PAGE_SIZE
  const endIndex = startIndex + PAGE_SIZE
  const items = projects.slice(startIndex, endIndex)

  totalRequestCount += 1
  const pageRequests = (perPageRequestCount.get(boundedPage) ?? 0) + 1
  perPageRequestCount.set(boundedPage, pageRequests)

  return {
    page: boundedPage,
    pageSize: PAGE_SIZE,
    totalPages,
    totalProjects: projects.length,
    hasMore: boundedPage &lt; totalPages,
    projects: items,
    requestMeta: {
      totalRequestCount,
      pageRequestCount: pageRequests,
      totalMutationCount,
    },
  }
}

function maybeFailMutation(res) {
  if (!failNextMutation) {
    return false
  }

  failNextMutation = false
  writeJson(res, 500, { error: &#39;Forced mutation failure (test)&#39; })
  return true
}

function validateProjectName(name) {
  if (typeof name !== &#39;string&#39;) {
    return &#39;Project name must be a string&#39;
  }

  const trimmed = name.trim()
  if (trimmed.length &lt; 3) {
    return &#39;Project name must be at least 3 characters&#39;
  }

  if (trimmed.length &gt; 60) {
    return &#39;Project name must be 60 characters or fewer&#39;
  }

  return null
}

function validateOwner(owner) {
  if (typeof owner !== &#39;string&#39;) {
    return &#39;Owner must be a string&#39;
  }

  const trimmed = owner.trim()
  if (!trimmed) {
    return &#39;Owner is required&#39;
  }

  if (trimmed.length &gt; 40) {
    return &#39;Owner must be 40 characters or fewer&#39;
  }

  return null
}

const server = createServer(async (req, res) =&gt; {
  if (req.method === &#39;OPTIONS&#39;) {
    res.writeHead(204, {
      &#39;access-control-allow-origin&#39;: &#39;*&#39;,
      &#39;access-control-allow-methods&#39;: &#39;GET,POST,PATCH,OPTIONS&#39;,
      &#39;access-control-allow-headers&#39;: &#39;content-type&#39;,
      &#39;cache-control&#39;: &#39;no-store&#39;,
    })
    res.end()
    return
  }

  if (!req.url) {
    writeJson(res, 400, { error: &#39;Missing URL&#39; })
    return
  }

  const requestUrl = new URL(req.url, `http://127.0.0.1:${API_PORT}`)

  if (requestUrl.pathname === &#39;/api/projects&#39; &amp;&amp; req.method === &#39;GET&#39;) {
    const page = parsePositiveInt(requestUrl.searchParams.get(&#39;page&#39;), 1)
    if (!page) {
      writeJson(res, 400, { error: &#39;Invalid page parameter&#39; })
      return
    }

    const delayMs = parseNonNegativeInt(requestUrl.searchParams.get(&#39;delay&#39;), 0)
    if (delayMs === undefined) {
      writeJson(res, 400, { error: &#39;Invalid delay parameter&#39; })
      return
    }

    await sleep(delayMs)

    if (requestUrl.searchParams.get(&#39;error&#39;) === &#39;true&#39;) {
      writeJson(res, 500, {
        error: &#39;Forced server error (test)&#39;,
        page,
      })
      return
    }

    writeJson(res, 200, buildProjectsPage(page))
    return
  }

  if (requestUrl.pathname === &#39;/api/projects&#39; &amp;&amp; req.method === &#39;POST&#39;) {
    if (maybeFailMutation(res)) {
      return
    }

    const payload = await readJsonBody(req)
    if (!payload.ok) {
      writeJson(res, payload.status, { error: payload.error })
      return
    }

    const nameError = validateProjectName(payload.value?.name)
    if (nameError) {
      writeJson(res, 422, { error: nameError })
      return
    }

    const ownerError = validateOwner(payload.value?.owner)
    if (ownerError) {
      writeJson(res, 422, { error: ownerError })
      return
    }

    const project = {
      id: nextProjectId,
      name: payload.value.name.trim(),
      owner: payload.value.owner.trim(),
      isFavorite: false,
    }

    nextProjectId += 1
    totalMutationCount += 1
    projects = [project, ...projects]

    writeJson(res, 201, {
      project,
      mutationCount: totalMutationCount,
    })
    return
  }

  const patchMatch = requestUrl.pathname.match(/^\/api\/projects\/(\d+)$/)
  if (patchMatch &amp;&amp; req.method === &#39;PATCH&#39;) {
    if (maybeFailMutation(res)) {
      return
    }

    const projectId = Number.parseInt(patchMatch[1], 10)
    const projectIndex = projects.findIndex(
      (project) =&gt; project.id === projectId,
    )
    if (projectIndex === -1) {
      writeJson(res, 404, { error: `Project ${projectId} was not found` })
      return
    }

    const payload = await readJsonBody(req)
    if (!payload.ok) {
      writeJson(res, payload.status, { error: payload.error })
      return
    }

    if (typeof payload.value?.isFavorite !== &#39;boolean&#39;) {
      writeJson(res, 422, { error: &#39;isFavorite must be a boolean&#39; })
      return
    }

    const nextProject = {
      ...projects[projectIndex],
      isFavorite: payload.value.isFavorite,
    }

    totalMutationCount += 1
    projects = projects.map((project, index) =&gt;
      index === projectIndex ? nextProject : project,
    )

    writeJson(res, 200, {
      project: nextProject,
      mutationCount: totalMutationCount,
    })
    return
  }

  if (
    requestUrl.pathname === &#39;/api/testing/fail-next-mutation&#39; &amp;&amp;
    req.method === &#39;POST&#39;
  ) {
    failNextMutation = true
    writeJson(res, 200, { ok: true })
    return
  }

  if (requestUrl.pathname === &#39;/api/reset&#39; &amp;&amp; req.method === &#39;POST&#39;) {
    resetState()
    writeJson(res, 200, { ok: true })
    return
  }

  writeJson(res, 404, { error: &#39;Not found&#39; })
})

server.listen(API_PORT, &#39;127.0.0.1&#39;, () =&gt; {
  console.log(`[api] listening on http://127.0.0.1:${API_PORT}`)
})
">
<input type="hidden" name="project[files][src/api.ts]" value="export type Project = {
  id: number
  name: string
  owner: string
  isFavorite: boolean
}

export type ProjectsPageResponse = {
  page: number
  pageSize: number
  totalPages: number
  totalProjects: number
  hasMore: boolean
  projects: Array&lt;Project&gt;
  requestMeta: {
    totalRequestCount: number
    pageRequestCount: number
    totalMutationCount: number
  }
}

export type CreateProjectInput = {
  name: string
  owner: string
}

export type ToggleProjectFavoriteInput = {
  id: number
  isFavorite: boolean
}

export type ProjectMutationResponse = {
  project: Project
  mutationCount: number
}

export type ProjectsQueryKey = readonly [&#39;projects&#39;, number, number, boolean]

const DEFAULT_API_PORT = 4184
const configuredApiPort = Number.parseInt(
  import.meta.env.VITE_PAGINATION_API_PORT ?? String(DEFAULT_API_PORT),
  10,
)
const API_PORT = Number.isInteger(configuredApiPort)
  ? configuredApiPort
  : DEFAULT_API_PORT
const API_BASE_URL = `http://127.0.0.1:${API_PORT}`

function buildProjectsUrl(
  page: number,
  delayMs: number,
  forceError: boolean,
): URL {
  const url = new URL(&#39;/api/projects&#39;, API_BASE_URL)
  url.searchParams.set(&#39;page&#39;, String(page))

  if (delayMs &gt; 0) {
    url.searchParams.set(&#39;delay&#39;, String(delayMs))
  }

  if (forceError) {
    url.searchParams.set(&#39;error&#39;, &#39;true&#39;)
  }

  return url
}

async function readJsonOrThrow&lt;T&gt;(
  response: Response,
  fallbackMessage: string,
): Promise&lt;T&gt; {
  if (response.ok) {
    return (await response.json()) as T
  }

  const payload = (await response.json().catch(() =&gt; null)) as {
    error?: string
  } | null

  throw new Error(
    payload &amp;&amp; typeof payload === &#39;object&#39; &amp;&amp; &#39;error&#39; in payload
      ? String(payload.error ?? fallbackMessage)
      : fallbackMessage,
  )
}

async function requestJson&lt;T&gt;(
  url: URL,
  init: RequestInit,
  fallbackMessage: string,
): Promise&lt;T&gt; {
  const response = await fetch(url, init)
  return readJsonOrThrow&lt;T&gt;(response, fallbackMessage)
}

export function projectsQueryKey(
  page: number,
  delayMs: number,
  forceError: boolean,
): ProjectsQueryKey {
  return [&#39;projects&#39;, page, delayMs, forceError] as const
}

export async function fetchProjectsPage(
  page: number,
  delayMs: number,
  forceError: boolean,
): Promise&lt;ProjectsPageResponse&gt; {
  const response = await fetch(buildProjectsUrl(page, delayMs, forceError))
  return readJsonOrThrow&lt;ProjectsPageResponse&gt;(
    response,
    `Failed to fetch projects page ${page}`,
  )
}

export async function createProjectOnServer(
  input: CreateProjectInput,
): Promise&lt;ProjectMutationResponse&gt; {
  return requestJson&lt;ProjectMutationResponse&gt;(
    new URL(&#39;/api/projects&#39;, API_BASE_URL),
    {
      method: &#39;POST&#39;,
      headers: {
        &#39;content-type&#39;: &#39;application/json&#39;,
      },
      body: JSON.stringify(input),
    },
    &#39;Failed to create project&#39;,
  )
}

export async function toggleProjectFavoriteOnServer(
  input: ToggleProjectFavoriteInput,
): Promise&lt;ProjectMutationResponse&gt; {
  return requestJson&lt;ProjectMutationResponse&gt;(
    new URL(`/api/projects/${input.id}`, API_BASE_URL),
    {
      method: &#39;PATCH&#39;,
      headers: {
        &#39;content-type&#39;: &#39;application/json&#39;,
      },
      body: JSON.stringify({ isFavorite: input.isFavorite }),
    },
    `Failed to update project ${input.id}`,
  )
}

export async function armNextProjectMutationFailureOnServer(): Promise&lt;void&gt; {
  await requestJson&lt;{ ok: true }&gt;(
    new URL(&#39;/api/testing/fail-next-mutation&#39;, API_BASE_URL),
    {
      method: &#39;POST&#39;,
    },
    &#39;Failed to arm next mutation failure&#39;,
  )
}

export async function resetProjectsApiState(): Promise&lt;void&gt; {
  await requestJson&lt;{ ok: true }&gt;(
    new URL(&#39;/api/reset&#39;, API_BASE_URL),
    {
      method: &#39;POST&#39;,
    },
    &#39;Failed to reset API state&#39;,
  )
}
">
<input type="hidden" name="project[files][src/main.ts]" value="import { LitElement, html } from &#39;lit&#39;
import {
  QueryClient,
  QueryClientProvider,
  createMutationController,
  createQueryController,
  keepPreviousData,
  noop,
} from &#39;@tanstack/lit-query&#39;
import {
  armNextProjectMutationFailureOnServer,
  createProjectOnServer,
  fetchProjectsPage,
  projectsQueryKey,
  resetProjectsApiState,
  toggleProjectFavoriteOnServer,
} from &#39;./api&#39;
import type {
  CreateQueryOptions,
  MutationResultAccessor,
  QueryKey,
  QueryResultAccessor,
} from &#39;@tanstack/lit-query&#39;
import type {
  CreateProjectInput,
  Project,
  ProjectsPageResponse,
  ToggleProjectFavoriteInput,
} from &#39;./api&#39;

type ProjectsCacheSnapshot = Array&lt;[QueryKey, ProjectsPageResponse | undefined]&gt;
type FavoriteMutationContext = {
  snapshots: ProjectsCacheSnapshot
}

const queryClient = new QueryClient({
  defaultOptions: {
    queries: {
      retry: false,
      staleTime: 5000,
    },
    mutations: {
      retry: false,
    },
  },
})

class PaginationQueryProvider extends QueryClientProvider {
  constructor() {
    super()
    this.client = queryClient
  }

  protected override createRenderRoot(): HTMLElement | DocumentFragment {
    return this
  }
}

customElements.define(&#39;pagination-query-provider&#39;, PaginationQueryProvider)

class PaginationDemo extends LitElement {
  static properties = {
    page: { state: true },
    delayMs: { state: true },
    forceErrorMode: { state: true },
    prefetchStatus: { state: true },
    resetError: { state: true },
    draftName: { state: true },
    draftOwner: { state: true },
    mutationControlStatus: { state: true },
    mutationControlError: { state: true },
  }

  private page = 1
  private delayMs = 250
  private forceErrorMode = false
  private prefetchStatus = &#39;idle&#39;
  private resetError: string | undefined
  private draftName = &#39;Platform Rollout&#39;
  private draftOwner = &#39;Team 6&#39;
  private mutationControlStatus = &#39;idle&#39;
  private mutationControlError: string | undefined
  private lastAutoPrefetchPage = 0
  private readonly projectsQueryOptions: CreateQueryOptions&lt;
    ProjectsPageResponse,
    Error
  &gt;
  private readonly projectsQuery: QueryResultAccessor&lt;
    ProjectsPageResponse,
    Error
  &gt;
  private readonly createProjectMutation: MutationResultAccessor&lt;
    Project,
    Error,
    CreateProjectInput,
    unknown
  &gt;
  private readonly favoriteMutation: MutationResultAccessor&lt;
    Project,
    Error,
    ToggleProjectFavoriteInput,
    FavoriteMutationContext
  &gt;

  constructor() {
    super()

    this.projectsQueryOptions = {
      queryKey: projectsQueryKey(this.page, this.delayMs, this.forceErrorMode),
      queryFn: () =&gt;
        fetchProjectsPage(this.page, this.delayMs, this.forceErrorMode),
      placeholderData: keepPreviousData,
    }

    this.projectsQuery = createQueryController&lt;ProjectsPageResponse, Error&gt;(
      this,
      this.projectsQueryOptions,
    )

    this.createProjectMutation = createMutationController&lt;
      Project,
      Error,
      CreateProjectInput
    &gt;(
      this,
      {
        mutationKey: [&#39;create-project&#39;],
        mutationFn: async (input) =&gt; {
          const response = await createProjectOnServer(input)
          return response.project
        },
        onMutate: () =&gt; {
          this.mutationControlStatus = &#39;idle&#39;
          this.mutationControlError = undefined
        },
        onSuccess: async () =&gt; {
          this.page = 1
          this.lastAutoPrefetchPage = 0
          this.prefetchStatus = &#39;idle&#39;
          this.draftName = &#39;&#39;
          this.draftOwner = &#39;Team 6&#39;
          this.syncProjectsQueryOptions()
          await queryClient.invalidateQueries({
            queryKey: [&#39;projects&#39;],
            refetchType: &#39;none&#39;,
          })
          await this.projectsQuery.refetch()
        },
      },
      queryClient,
    )

    this.favoriteMutation = createMutationController&lt;
      Project,
      Error,
      ToggleProjectFavoriteInput,
      FavoriteMutationContext
    &gt;(
      this,
      {
        mutationKey: [&#39;toggle-project-favorite&#39;],
        mutationFn: async (input) =&gt; {
          const response = await toggleProjectFavoriteOnServer(input)
          return response.project
        },
        onMutate: async (variables) =&gt; {
          this.mutationControlStatus = &#39;idle&#39;
          this.mutationControlError = undefined
          await queryClient.cancelQueries({ queryKey: [&#39;projects&#39;] })

          const snapshots = queryClient.getQueriesData&lt;ProjectsPageResponse&gt;({
            queryKey: [&#39;projects&#39;],
          })

          for (const [key, existing] of snapshots) {
            if (!existing) {
              continue
            }

            queryClient.setQueryData&lt;ProjectsPageResponse&gt;(key, {
              ...existing,
              projects: existing.projects.map((project) =&gt;
                project.id === variables.id
                  ? { ...project, isFavorite: variables.isFavorite }
                  : project,
              ),
            })
          }

          return { snapshots }
        },
        onError: (_error, _variables, context) =&gt; {
          for (const [key, snapshot] of context?.snapshots ?? []) {
            queryClient.setQueryData(key, snapshot)
          }
        },
        onSettled: async () =&gt; {
          await queryClient.invalidateQueries({ queryKey: [&#39;projects&#39;] })
        },
      },
      queryClient,
    )
  }

  protected override createRenderRoot(): HTMLElement | DocumentFragment {
    return this
  }

  override updated(): void {
    this.maybePrefetchNextPage()
  }

  private syncProjectsQueryOptions(): void {
    this.projectsQueryOptions.queryKey = projectsQueryKey(
      this.page,
      this.delayMs,
      this.forceErrorMode,
    )
    this.projectsQueryOptions.queryFn = () =&gt;
      fetchProjectsPage(this.page, this.delayMs, this.forceErrorMode)
  }

  private refetchForCurrentState(): void {
    this.syncProjectsQueryOptions()
    void this.projectsQuery.refetch()
  }

  private async maybePrefetchNextPage(): Promise&lt;void&gt; {
    const query = this.projectsQuery()
    const currentData = query.data

    if (!currentData || query.isPlaceholderData || !currentData.hasMore) {
      return
    }

    if (this.lastAutoPrefetchPage === currentData.page) {
      return
    }

    this.lastAutoPrefetchPage = currentData.page
    await this.prefetchPage(currentData.page + 1)
  }

  private onDelayInput(event: Event): void {
    const target = event.target as HTMLInputElement
    const nextValue = Number.parseInt(target.value, 10)

    if (!Number.isInteger(nextValue) || nextValue &lt; 0) {
      return
    }

    this.delayMs = nextValue
    this.refetchForCurrentState()
  }

  private onErrorModeToggle(event: Event): void {
    const target = event.target as HTMLInputElement
    this.forceErrorMode = target.checked
    this.refetchForCurrentState()
  }

  private onDraftNameInput(event: Event): void {
    const target = event.target as HTMLInputElement
    this.draftName = target.value
  }

  private onDraftOwnerInput(event: Event): void {
    const target = event.target as HTMLInputElement
    this.draftOwner = target.value
  }

  private async prefetchPage(targetPage: number): Promise&lt;void&gt; {
    this.prefetchStatus = `pending:${targetPage}`

    try {
      await queryClient
        .query({
          queryKey: projectsQueryKey(
            targetPage,
            this.delayMs,
            this.forceErrorMode,
          ),
          queryFn: () =&gt;
            fetchProjectsPage(targetPage, this.delayMs, this.forceErrorMode),
        })
        .catch(noop)
      this.prefetchStatus = `ready:${targetPage}`
    } catch (error) {
      this.prefetchStatus = `error:${String(error)}`
    }
  }

  private async prefetchNext(): Promise&lt;void&gt; {
    const query = this.projectsQuery()
    const currentData = query.data

    if (!currentData?.hasMore) {
      this.prefetchStatus = &#39;skipped:no-next-page&#39;
      return
    }

    await this.prefetchPage(currentData.page + 1)
  }

  private goToPreviousPage(): void {
    if (this.page &gt; 1) {
      this.page -= 1
      this.refetchForCurrentState()
    }
  }

  private goToNextPage(): void {
    const currentData = this.projectsQuery().data
    if (!currentData?.hasMore) {
      return
    }

    this.page += 1
    this.refetchForCurrentState()
  }

  private async resetDemoState(): Promise&lt;void&gt; {
    this.resetError = undefined

    try {
      await resetProjectsApiState()
      this.page = 1
      this.delayMs = 250
      this.forceErrorMode = false
      this.prefetchStatus = &#39;idle&#39;
      this.resetError = undefined
      this.draftName = &#39;Platform Rollout&#39;
      this.draftOwner = &#39;Team 6&#39;
      this.mutationControlStatus = &#39;idle&#39;
      this.mutationControlError = undefined
      this.lastAutoPrefetchPage = 0
      this.syncProjectsQueryOptions()
      this.createProjectMutation.reset()
      this.favoriteMutation.reset()
      await queryClient.resetQueries({ queryKey: [&#39;projects&#39;] })
      await this.projectsQuery.refetch()
    } catch (error) {
      this.resetError = String(error)
    }
  }

  private submitCreateProject(): void {
    const name = this.draftName.trim()
    const owner = this.draftOwner.trim()

    if (!name || !owner) {
      return
    }

    this.createProjectMutation.mutate({ name, owner })
  }

  private toggleFavorite(project: Project): void {
    this.favoriteMutation.mutate({
      id: project.id,
      isFavorite: !project.isFavorite,
    })
  }

  private async armNextMutationFailure(): Promise&lt;void&gt; {
    this.mutationControlError = undefined

    try {
      await armNextProjectMutationFailureOnServer()
      this.mutationControlStatus = &#39;armed&#39;
    } catch (error) {
      this.mutationControlStatus = &#39;error&#39;
      this.mutationControlError = String(error)
    }
  }

  render() {
    const query = this.projectsQuery()
    const projects = query.data?.projects ?? []
    const hasMore = query.data?.hasMore ?? false
    const createProject = this.createProjectMutation()
    const favoriteProject = this.favoriteMutation()

    return html`
      &lt;main&gt;
        &lt;h1&gt;TanStack Lit Query Pagination Demo&lt;/h1&gt;
        &lt;p&gt;
          Pagination + mutation demo with optimistic favorite toggles,
          invalidation, and deterministic server failures.
        &lt;/p&gt;

        &lt;section&gt;
          &lt;div data-testid=&quot;query-status&quot;&gt;query: ${query.status}&lt;/div&gt;
          &lt;div data-testid=&quot;is-fetching&quot;&gt;
            isFetching: ${query.isFetching ? &#39;yes&#39; : &#39;no&#39;}
          &lt;/div&gt;
          &lt;div data-testid=&quot;is-placeholder&quot;&gt;
            isPlaceholderData: ${query.isPlaceholderData ? &#39;yes&#39; : &#39;no&#39;}
          &lt;/div&gt;
          &lt;div data-testid=&quot;current-page&quot;&gt;page: ${this.page}&lt;/div&gt;
          &lt;div data-testid=&quot;response-page&quot;&gt;
            response-page: ${query.data?.page ?? &#39;-&#39;}
          &lt;/div&gt;
          &lt;div data-testid=&quot;has-more&quot;&gt;has-more: ${hasMore ? &#39;yes&#39; : &#39;no&#39;}&lt;/div&gt;
          &lt;div data-testid=&quot;total-projects&quot;&gt;
            total-projects: ${query.data?.totalProjects ?? 0}
          &lt;/div&gt;
          &lt;div data-testid=&quot;total-request-count&quot;&gt;
            total-requests: ${query.data?.requestMeta.totalRequestCount ?? 0}
          &lt;/div&gt;
          &lt;div data-testid=&quot;page-request-count&quot;&gt;
            page-requests: ${query.data?.requestMeta.pageRequestCount ?? 0}
          &lt;/div&gt;
          &lt;div data-testid=&quot;total-mutation-count&quot;&gt;
            total-mutations: ${query.data?.requestMeta.totalMutationCount ?? 0}
          &lt;/div&gt;
          &lt;div data-testid=&quot;prefetch-status&quot;&gt;
            prefetch: ${this.prefetchStatus}
          &lt;/div&gt;
        &lt;/section&gt;

        ${
          query.isError
            ? html`&lt;p data-testid=&quot;query-error&quot;&gt;${String(query.error)}&lt;/p&gt;`
            : null
        }
        ${
          this.resetError
            ? html`&lt;p data-testid=&quot;reset-error&quot;&gt;${this.resetError}&lt;/p&gt;`
            : null
        }

        &lt;section&gt;
          &lt;label for=&quot;delayInput&quot;&gt;Delay (ms)&lt;/label&gt;
          &lt;input
            id=&quot;delayInput&quot;
            data-testid=&quot;delay-input&quot;
            type=&quot;number&quot;
            min=&quot;0&quot;
            .value=${String(this.delayMs)}
            @input=${this.onDelayInput}
          /&gt;

          &lt;label for=&quot;errorModeToggle&quot;&gt;Force query error&lt;/label&gt;
          &lt;input
            id=&quot;errorModeToggle&quot;
            data-testid=&quot;force-error-toggle&quot;
            type=&quot;checkbox&quot;
            .checked=${this.forceErrorMode}
            @change=${this.onErrorModeToggle}
          /&gt;

          &lt;button
            data-testid=&quot;reset-demo-state&quot;
            @click=${() =&gt; this.resetDemoState()}
          &gt;
            Reset Demo State
          &lt;/button&gt;
          &lt;button
            data-testid=&quot;refetch&quot;
            @click=${() =&gt; this.projectsQuery.refetch()}
          &gt;
            Refetch
          &lt;/button&gt;
          &lt;button
            data-testid=&quot;prefetch-next&quot;
            @click=${() =&gt; this.prefetchNext()}
          &gt;
            Prefetch Next
          &lt;/button&gt;
          &lt;button
            data-testid=&quot;arm-mutation-error&quot;
            @click=${() =&gt; this.armNextMutationFailure()}
          &gt;
            Fail Next Mutation
          &lt;/button&gt;
          &lt;div data-testid=&quot;mutation-control-status&quot;&gt;
            mutation-control: ${this.mutationControlStatus}
          &lt;/div&gt;
          ${
            this.mutationControlError
              ? html`&lt;div data-testid=&quot;mutation-control-error&quot;&gt;
                  ${this.mutationControlError}
                &lt;/div&gt;`
              : null
          }
        &lt;/section&gt;

        &lt;section&gt;
          &lt;div data-testid=&quot;create-mutation-status&quot;&gt;
            create-mutation: ${createProject.status}
          &lt;/div&gt;
          ${
            createProject.isError
              ? html`&lt;div data-testid=&quot;create-mutation-error&quot;&gt;
                  ${String(createProject.error)}
                &lt;/div&gt;`
              : null
          }

          &lt;label for=&quot;projectNameInput&quot;&gt;Project name&lt;/label&gt;
          &lt;input
            id=&quot;projectNameInput&quot;
            data-testid=&quot;project-name-input&quot;
            .value=${this.draftName}
            @input=${this.onDraftNameInput}
          /&gt;

          &lt;label for=&quot;projectOwnerInput&quot;&gt;Owner&lt;/label&gt;
          &lt;input
            id=&quot;projectOwnerInput&quot;
            data-testid=&quot;project-owner-input&quot;
            .value=${this.draftOwner}
            @input=${this.onDraftOwnerInput}
          /&gt;

          &lt;button
            data-testid=&quot;create-project&quot;
            ?disabled=${createProject.isPending}
            @click=${() =&gt; this.submitCreateProject()}
          &gt;
            Create Project
          &lt;/button&gt;
        &lt;/section&gt;

        &lt;section&gt;
          &lt;div data-testid=&quot;favorite-mutation-status&quot;&gt;
            favorite-mutation: ${favoriteProject.status}
          &lt;/div&gt;
          ${
            favoriteProject.isError
              ? html`&lt;div data-testid=&quot;favorite-mutation-error&quot;&gt;
                  ${String(favoriteProject.error)}
                &lt;/div&gt;`
              : null
          }
        &lt;/section&gt;

        &lt;section&gt;
          &lt;button
            data-testid=&quot;previous-page&quot;
            ?disabled=${this.page === 1 || query.isPlaceholderData}
            @click=${() =&gt; this.goToPreviousPage()}
          &gt;
            Previous
          &lt;/button&gt;
          &lt;button
            data-testid=&quot;next-page&quot;
            ?disabled=${!hasMore || query.isPlaceholderData}
            @click=${() =&gt; this.goToNextPage()}
          &gt;
            Next
          &lt;/button&gt;
        &lt;/section&gt;

        &lt;ul data-testid=&quot;project-list&quot;&gt;
          ${projects.map(
            (project) =&gt; html`
              &lt;li data-testid=&quot;project-item&quot;&gt;
                &lt;span data-testid=&quot;project-name&quot;
                  &gt;${project.id}: ${project.name} (${project.owner})&lt;/span
                &gt;
                &lt;span data-testid=&quot;project-favorite-state&quot;
                  &gt;${project.isFavorite ? &#39;favorite&#39; : &#39;standard&#39;}&lt;/span
                &gt;
                &lt;button
                  data-testid=&quot;toggle-favorite&quot;
                  ?disabled=${favoriteProject.isPending}
                  @click=${() =&gt; this.toggleFavorite(project)}
                &gt;
                  ${project.isFavorite ? &#39;Unfavorite&#39; : &#39;Favorite&#39;}
                &lt;/button&gt;
              &lt;/li&gt;
            `,
          )}
        &lt;/ul&gt;
      &lt;/main&gt;
    `
  }
}

customElements.define(&#39;pagination-demo&#39;, PaginationDemo)

class PaginationDemoRoot extends LitElement {
  protected override createRenderRoot(): HTMLElement | DocumentFragment {
    return this
  }

  render() {
    return html`
      &lt;pagination-query-provider&gt;
        &lt;pagination-demo&gt;&lt;/pagination-demo&gt;
      &lt;/pagination-query-provider&gt;
    `
  }
}

customElements.define(&#39;pagination-demo-root&#39;, PaginationDemoRoot)
">
<input type="hidden" name="project[files][src/vite-env.d.ts]" value="/// &lt;reference types=&quot;vite/client&quot; /&gt;

interface ImportMetaEnv {
  readonly VITE_PAGINATION_API_PORT?: string
}

interface ImportMeta {
  readonly env: ImportMetaEnv
}
">
<input type="hidden" name="project[description]" value="generated by https://pkg.pr.new">
<input type="hidden" name="project[template]" value="node">
<input type="hidden" name="project[title]" value="@tanstack/query-example-lit-pagination">
</form>
<script>document.getElementById("mainForm").submit();</script>

</body></html>