| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315 |
- import { createResource, createSignal, For, onMount, Suspense } from "solid-js"
- import { render } from "solid-js/web"
- import { createVirtualizer, observeElementOffset, observeElementRect } from "@tanstack/solid-virtual"
- import { observeElementOffsetReconnectAware } from "../../../src/pages/session/timeline/observe-element-offset"
- const rowCount = 2_000
- const rowHeight = 40
- const parameters = new URLSearchParams(location.search)
- const resourceMode = parameters.get("resource") === "guard" ? "guard" : "baseline"
- const reconnectMode = parameters.get("reconnect") === "candidate" ? "candidate" : "baseline"
- type MutationEvent = {
- kind: "removed" | "added"
- callbackTime: number
- callbackFrame: number
- routeConnectedInCallback: boolean
- nativeOffsetInCallback: number
- }
- type Snapshot = {
- mode: {
- resource: "baseline" | "guard"
- reconnect: "baseline" | "candidate"
- }
- operation: {
- sequence: number
- phase: string
- time: number
- frame: number
- }
- resourceState: string
- routeConnected: boolean
- viewportConnected: boolean
- viewportOwnedByRoute: boolean
- sameRoute: boolean
- sameViewport: boolean
- sameSurface: boolean
- sameMountedRows: boolean
- nativeOffset: number
- coreOffset: number
- rangeStart: number
- rangeEnd: number
- indexes: number[]
- domIndexes: number[]
- logicalSurfaceHeight: number
- renderedSurfaceHeight: number
- viewportClientHeight: number
- viewportScrollHeight: number
- visibleRows: number
- minimumRowTop: number
- domScrollEvents: number
- lastScrollTrusted: boolean
- coreOffsetCallbackCalls: number
- offsetCallbackSources: "observer"[]
- rectObserverCallbacks: number
- ignoredDetachedZeroRects: number
- syntheticScrollDispatches: number
- mutationEvents: MutationEvent[]
- }
- declare global {
- interface Window {
- timelineSuspense: {
- prepare: () => Promise<Snapshot>
- trigger: () => void
- resolve: () => void
- frames: (count?: number) => Promise<void>
- snapshot: () => Snapshot
- }
- }
- }
- function App() {
- const [refresh, setRefresh] = createSignal(false)
- let resolveResource: (() => void) | undefined
- const [resource] = createResource(
- refresh,
- (version) =>
- new Promise<string>((resolve) => {
- resolveResource = () => resolve(`settled-${version}`)
- }),
- { initialValue: "settled" },
- )
- function Route() {
- let route: HTMLElement | undefined
- let viewport: HTMLDivElement | undefined
- let surface: HTMLDivElement | undefined
- let initialRoute: HTMLElement | undefined
- let initialViewport: HTMLDivElement | undefined
- let initialSurface: HTMLDivElement | undefined
- let initialRows: HTMLElement[] = []
- let phase = "mounting"
- let browserFrame = 0
- let snapshotSequence = 0
- let domScrollEvents = 0
- let lastScrollTrusted = false
- let coreOffsetCallbackCalls = 0
- let rectObserverCallbacks = 0
- let ignoredDetachedZeroRects = 0
- const offsetCallbackSources: "observer"[] = []
- const mutationEvents: MutationEvent[] = []
- const virtualizer = createVirtualizer<HTMLDivElement, HTMLDivElement>({
- count: rowCount,
- getScrollElement: () => viewport ?? null,
- estimateSize: () => rowHeight,
- initialRect: { width: 900, height: 600 },
- overscan: 2,
- observeElementRect: (instance, callback) =>
- observeElementRect(instance, (rect) => {
- rectObserverCallbacks++
- // A fixed 600px viewport has no usable geometry while detached. Keep the last connected rect.
- if (!instance.scrollElement?.isConnected && rect.height === 0) {
- ignoredDetachedZeroRects++
- return
- }
- callback(rect)
- }),
- observeElementOffset: (instance, callback) => {
- const deliver = (offset: number, isScrolling: boolean) => {
- coreOffsetCallbackCalls++
- offsetCallbackSources.push("observer")
- callback(offset, isScrolling)
- }
- if (reconnectMode === "candidate") return observeElementOffsetReconnectAware(instance, deliver)
- return observeElementOffset(instance, deliver)
- },
- })
- const frames = async (count = 2) => {
- for (let index = 0; index < count; index++) {
- await new Promise<void>((resolve) => requestAnimationFrame(() => resolve()))
- }
- }
- const mountedRows = () => [...(surface?.querySelectorAll<HTMLElement>("[data-row-index]") ?? [])]
- const snapshot = (): Snapshot => {
- const rows = mountedRows()
- const view = viewport?.getBoundingClientRect()
- const visibleRows =
- viewport?.isConnected && view
- ? rows.filter((row) => {
- const rect = row.getBoundingClientRect()
- return rect.bottom > view.top && rect.top < view.bottom
- }).length
- : 0
- return {
- mode: { resource: resourceMode, reconnect: reconnectMode },
- operation: {
- sequence: ++snapshotSequence,
- phase,
- time: performance.now(),
- frame: browserFrame,
- },
- resourceState: resource.state,
- routeConnected: route?.isConnected ?? false,
- viewportConnected: viewport?.isConnected ?? false,
- viewportOwnedByRoute: !!route && !!viewport && route.contains(viewport),
- sameRoute: route === initialRoute,
- sameViewport: viewport === initialViewport,
- sameSurface: surface === initialSurface,
- sameMountedRows:
- initialRows.length > 0 &&
- initialRows.length === rows.length &&
- initialRows.every((row, index) => row === rows[index]),
- nativeOffset: viewport?.scrollTop ?? -1,
- coreOffset: virtualizer.scrollOffset ?? -1,
- rangeStart: virtualizer.range?.startIndex ?? -1,
- rangeEnd: virtualizer.range?.endIndex ?? -1,
- indexes: virtualizer.getVirtualItems().map((item) => item.index),
- domIndexes: rows.map((row) => Number(row.dataset.rowIndex)),
- logicalSurfaceHeight: Number.parseFloat(surface?.style.height ?? "-1"),
- renderedSurfaceHeight: surface?.getBoundingClientRect().height ?? -1,
- viewportClientHeight: viewport?.clientHeight ?? -1,
- viewportScrollHeight: viewport?.scrollHeight ?? -1,
- visibleRows,
- minimumRowTop:
- rows.length && view ? Math.min(...rows.map((row) => row.getBoundingClientRect().top - view.top)) : -1,
- domScrollEvents,
- lastScrollTrusted,
- coreOffsetCallbackCalls,
- offsetCallbackSources: [...offsetCallbackSources],
- rectObserverCallbacks,
- ignoredDetachedZeroRects,
- syntheticScrollDispatches: 0,
- mutationEvents: mutationEvents.map((event) => ({ ...event })),
- }
- }
- onMount(() => {
- if (!route || !viewport || !surface) throw new Error("Timeline fixture did not mount")
- const routeRoot = route.parentElement
- if (!routeRoot) throw new Error("Timeline route root did not mount")
- initialRoute = route
- initialViewport = viewport
- initialSurface = surface
- viewport.addEventListener("scroll", (event) => {
- domScrollEvents++
- lastScrollTrusted = event.isTrusted
- })
- const countFrames = () => {
- browserFrame++
- requestAnimationFrame(countFrames)
- }
- requestAnimationFrame(countFrames)
- new MutationObserver((records) => {
- const callbackTime = performance.now()
- records.forEach((record) => {
- ;([...(record.removedNodes ?? [])] as Node[]).forEach((node) => {
- if (node !== route) return
- phase = "detached"
- mutationEvents.push({
- kind: "removed",
- callbackTime,
- callbackFrame: browserFrame,
- routeConnectedInCallback: route.isConnected,
- nativeOffsetInCallback: viewport.scrollTop,
- })
- })
- ;([...(record.addedNodes ?? [])] as Node[]).forEach((node) => {
- if (node !== route) return
- phase = "reinserted"
- mutationEvents.push({
- kind: "added",
- callbackTime,
- callbackFrame: browserFrame,
- routeConnectedInCallback: route.isConnected,
- nativeOffsetInCallback: viewport.scrollTop,
- })
- })
- })
- }).observe(routeRoot, { childList: true })
- window.timelineSuspense = {
- prepare: async () => {
- phase = "preparing"
- await frames(2)
- viewport.scrollTop = viewport.scrollHeight
- await frames(3)
- await new Promise((resolve) => setTimeout(resolve, 200))
- await frames(2)
- initialRows = mountedRows()
- phase = "prepared"
- return snapshot()
- },
- trigger: () => {
- phase = "triggering"
- setRefresh(true)
- },
- resolve: () => {
- if (!resolveResource) throw new Error("Resource is not pending")
- phase = "resolving"
- resolveResource()
- },
- frames,
- snapshot,
- }
- })
- return (
- <section ref={route} data-route style={{ width: "900px", margin: "0 auto" }}>
- <span aria-hidden="true" style={{ display: "none" }}>
- {resourceMode === "guard" && resource.state === "refreshing" ? resource.latest : resource()}
- </span>
- <div
- ref={viewport}
- data-viewport
- style={{
- height: "600px",
- overflow: "auto",
- "overflow-anchor": "none",
- position: "relative",
- background: "#202020",
- outline: "1px solid #3f3f46",
- }}
- >
- <div
- ref={surface}
- data-surface
- style={{ height: `${virtualizer.getTotalSize()}px`, position: "relative", "overflow-anchor": "none" }}
- >
- <For each={virtualizer.getVirtualItems()}>
- {(item) => (
- <div
- data-row-index={item.index}
- style={{
- position: "absolute",
- top: "0",
- left: "0",
- width: "100%",
- height: `${item.size}px`,
- transform: `translateY(${item.start}px)`,
- padding: "10px 14px",
- border: "0 solid #333",
- "border-bottom-width": "1px",
- }}
- >
- logical row {item.index}
- </div>
- )}
- </For>
- </div>
- </div>
- </section>
- )
- }
- return (
- <main>
- <Suspense>
- <Route />
- </Suspense>
- </main>
- )
- }
- render(() => <App />, document.getElementById("root")!)
|