@doync/query-virtualizer 0.1.0 → 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js.map +1 -1
- package/package.json +3 -3
- package/src/types.ts +2 -2
- package/src/use-query-virtualizer.ts +2 -2
- package/src/use-restore-scroll-state.ts +1 -1
- package/src/use-rows.ts +2 -2
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","names":["useQueryDefault","useQuery"],"sources":["../src/use-rows.ts","../src/use-query-virtualizer.ts","../src/use-pin-to-bottom.ts","../src/use-restore-scroll-state.ts"],"sourcesContent":["import type { BoundQuery } from '@doync/core'\n\nimport {\n useQuery as useQueryDefault,\n type FalsyQuery,\n type UseQueryOptions,\n type ViewStatus,\n} from '@doync/react'\nimport {\n assembleRows,\n buildAfterQuery,\n buildMainQuery,\n buildSingleQuery,\n permalinkMissing,\n type Anchor,\n type RowsSnapshot,\n} from '@rocicorp/zero-virtual/core'\nimport { useMemo } from 'react'\n\nimport type { GetPageQuery, GetSingleQuery, QueryResult } from './types'\n\n/**\n * Stage payload from core's `build*Query` helpers under doync's `{ query:\n * BoundQuery, options? }` factory contract (closeio/doync#199 / ADR-0027). Core\n * returns `null` for inactive stages; live stages carry a Bound query whose\n * args already ride inside the value.\n */\ntype Stage<\n TRow extends Record<string, unknown>,\n One extends boolean,\n> = QueryResult<TRow, One>\n\nconst UNKNOWN: ViewStatus = Object.freeze({ status: 'unknown' })\n\n/**\n * Aggregate live-stage ViewStatuses into the public virtualizer `status`\n * (closeio/doync#178 §F): first live error wins; assembled complete ⇒ complete;\n * else unknown. Skipped stages (core-null / falsy, or consumer `skip: true`)\n * never contribute.\n */\nexport function aggregateStatus(\n liveStatuses: readonly ViewStatus[],\n assembledComplete: boolean,\n): ViewStatus {\n for (const s of liveStatuses) {\n if (s.status === 'error') return s\n }\n if (assembledComplete) return { status: 'complete' }\n return UNKNOWN\n}\n\n/**\n * Injectable `useQuery` seam (closeio/doync#180 / #199): bound form only —\n * `useQuery(bound | falsy, options?)`. Adapter-first unit tests inject a fake\n * of this shape; the real hook assigns via a bound-form cast (its first public\n * overload rejects falsy).\n */\nexport type UseQueryLike = <\n Row extends Record<string, unknown> = Record<string, unknown>,\n>(\n query: BoundQuery<Row, boolean> | FalsyQuery,\n options?: UseQueryOptions,\n) => [readonly Row[] | Row | undefined, ViewStatus]\n\n/** Core-null or consumer-`skip: true` stages contribute nothing to ViewStatus. */\nfunction isLiveStage(\n stage: Stage<Record<string, unknown>, boolean> | null,\n): boolean {\n return stage !== null && stage.options?.skip !== true\n}\n\n/**\n * Internal three-slot adapter: stages Permalink / main / after via core\n * builders, always calls `useQuery` three times (hook-order stable), and passes\n * each stage's Bound query straight through — falsy when the builder returned\n * null (closeio/doync#199 dissolves the placeholder-under-skip workaround from\n * #180). Assembles a `RowsSnapshot` plus public `ViewStatus`.\n *\n * Not package-root-exported. Injectable `useQuery` for adapter-first unit\n * tests.\n */\nexport function useRows<TRow extends Record<string, unknown>, TStartRow>(\n {\n pageSize,\n anchor,\n settled,\n getPageQuery,\n getSingleQuery,\n toStartRow,\n }: {\n pageSize: number\n anchor: Anchor<TStartRow>\n settled: boolean\n getPageQuery: GetPageQuery<TRow, TStartRow>\n getSingleQuery: GetSingleQuery<TRow>\n toStartRow: (row: TRow) => TStartRow\n },\n // Bound-form cast: real useQuery's non-falsy first overload is not assignable\n // to the maybe-path UseQueryLike; both sides ARE BoundQuery | falsy (#199).\n useQuery: UseQueryLike = useQueryDefault as UseQueryLike,\n): { rows: RowsSnapshot<TRow>; status: ViewStatus } {\n const inputs = { pageSize, anchor, settled }\n\n // Stage 1: single-item lookup (permalink only). Null stage ⇒ falsy.\n const stage1 = buildSingleQuery(inputs, getSingleQuery as never) as Stage<\n TRow,\n true\n > | null\n const [singleRaw, singleStatus] = useQuery(\n stage1?.query ?? false,\n stage1?.options,\n )\n const typedSingleRow = (singleRaw as TRow | undefined) ?? undefined\n const singleComplete = singleStatus.status === 'complete'\n const notFound = permalinkMissing(inputs, typedSingleRow, singleComplete)\n const singleStart = typedSingleRow ? toStartRow(typedSingleRow) : null\n const live1 = isLiveStage(stage1)\n\n // Stage 2: page-before (permalink) or main page. Do not coerce\n // falsy-skipped multi-row `undefined` to `[]` (ADR-0027 a1; assembleRows\n // accepts it).\n const stage2 = buildMainQuery(\n inputs,\n getPageQuery as never,\n singleStart,\n notFound,\n ) as Stage<TRow, false> | null\n const [mainRaw, mainStatus] = useQuery(\n stage2?.query ?? false,\n stage2?.options,\n )\n const mainComplete = mainStatus.status === 'complete'\n const live2 = isLiveStage(stage2)\n\n // Stage 3: page-after (permalink only).\n const stage3 = buildAfterQuery(\n inputs,\n getPageQuery as never,\n singleStart,\n notFound,\n ) as Stage<TRow, false> | null\n const [afterRaw, afterStatus] = useQuery(\n stage3?.query ?? false,\n stage3?.options,\n )\n const afterComplete = afterStatus.status === 'complete'\n const live3 = isLiveStage(stage3)\n\n return useMemo(() => {\n const rows = assembleRows(\n { pageSize, anchor, settled },\n {\n singleRow: typedSingleRow,\n singleComplete,\n mainRows: mainRaw as readonly TRow[] | undefined as TRow[] | undefined,\n mainComplete,\n afterRows: afterRaw as readonly TRow[] | undefined as\n | TRow[]\n | undefined,\n afterComplete,\n },\n )\n const liveStatuses: ViewStatus[] = []\n if (live1) liveStatuses.push(singleStatus)\n if (live2) liveStatuses.push(mainStatus)\n if (live3) liveStatuses.push(afterStatus)\n return {\n rows,\n status: aggregateStatus(liveStatuses, rows.complete),\n }\n }, [\n pageSize,\n anchor,\n settled,\n typedSingleRow,\n singleComplete,\n singleStatus,\n live1,\n mainRaw,\n mainComplete,\n mainStatus,\n live2,\n afterRaw,\n afterComplete,\n afterStatus,\n live3,\n ])\n}\n","import type { ViewStatus } from '@doync/react'\n\nimport {\n observeElementOffset,\n observeElementRect,\n observeWindowOffset,\n observeWindowRect,\n resolveElementScrollElement,\n resolveWindowScrollElement,\n virtualizerResult,\n ZeroVirtualizer,\n type ResolvedScrollOptions,\n type ResolveScrollElement,\n} from '@rocicorp/zero-virtual/core'\nimport { useLayoutEffect, useMemo, useReducer, useState } from 'react'\n\nimport type {\n QueryVirtualizerResult,\n UseQueryVirtualizerOptions,\n} from './types'\n\nimport { useRows } from './use-rows'\n\n/**\n * Thin React binding over framework-agnostic `ZeroVirtualizer`\n * (closeio/doync#180): options and rows push silently every render; DOM work\n * runs from layout effects; re-renders come from the core's subscribe.\n *\n * Public result replaces core's `complete` boolean with aggregated `status:\n * ViewStatus` from live stages.\n */\nfunction useQueryVirtualizerImpl<\n TListContextParams,\n TRow extends Record<string, unknown>,\n TStartRow,\n>(\n options: UseQueryVirtualizerOptions<TListContextParams, TRow, TStartRow> &\n ResolvedScrollOptions,\n resolveScrollElement: ResolveScrollElement,\n): QueryVirtualizerResult<TRow> {\n const [, rerender] = useReducer(() => ({}), {})\n // One core instance per hook lifetime. Constructor is pure (no DOM /\n // listeners / timers), so Strict Mode double-construction is harmless —\n // and initializing paging from persisted scroll state here avoids Strict\n // Mode double-mounting the rows.\n const [core] = useState(\n () =>\n new ZeroVirtualizer<TListContextParams, TRow, TStartRow>(\n options,\n resolveScrollElement,\n ),\n )\n\n // Silent staging — never notifies during render.\n core.setOptions(options)\n const { pageSize, anchor, settled } = core.getQueryInputs()\n const { rows, status } = useRows<TRow, TStartRow>({\n pageSize,\n anchor,\n settled,\n getPageQuery: options.getPageQuery,\n getSingleQuery: options.getSingleQuery,\n toStartRow: options.toStartRow,\n })\n core.setRows(rows)\n\n // Mount/unmount: re-render subscription + listener teardown. Idempotent\n // across Strict Mode mount→unmount→mount (state survives detach; the\n // per-commit effect below re-attaches).\n useLayoutEffect(() => {\n const unsubscribe = core.subscribe(rerender)\n return () => {\n unsubscribe()\n core.detach()\n }\n }, [core])\n\n // Every commit, before paint: (re)wire the scroll element and run the\n // core's post-DOM-update pass (anchoring, restore/permalink, paging,\n // persistence).\n useLayoutEffect(() => {\n core.attach(options.getScrollElement())\n core.afterDOMUpdate()\n })\n\n const { getScrollElement, observeElementRect, observeElementOffset } = options\n const resultOptions = useMemo(\n () => ({ getScrollElement, observeElementRect, observeElementOffset }),\n [getScrollElement, observeElementRect, observeElementOffset],\n )\n const snapshot = core.getSnapshot()\n const statusRef = status\n return useMemo(() => {\n const base = virtualizerResult(\n snapshot,\n resultOptions,\n resolveScrollElement,\n )\n // Drop core's complete boolean; surface ViewStatus for consumers.\n const { complete: _complete, ...rest } = base\n void _complete\n return {\n ...rest,\n status: statusRef,\n }\n }, [snapshot, resultOptions, resolveScrollElement, statusRef])\n}\n\n/**\n * Virtualized infinite list inside an overflow element. `getScrollElement`\n * returns that element (also where rows render). Provide page/single query\n * factories via {@link UseQueryVirtualizerOptions}.\n *\n * ```ts\n * const v = useQueryVirtualizer({\n * getScrollElement: () => listRef.current,\n * estimateSize: () => 48,\n * getPageQuery: ({ limit, start, dir }) => ({\n * query: queries.items.page({ limit, start, dir }),\n * }),\n * getSingleQuery: ({ id }) => ({\n * query: queries.items.byId({ id }),\n * }),\n * // …\n * })\n * ```\n */\nexport function useQueryVirtualizer<\n TListContextParams,\n TRow extends Record<string, unknown>,\n TStartRow,\n>(\n options: UseQueryVirtualizerOptions<TListContextParams, TRow, TStartRow>,\n): QueryVirtualizerResult<TRow> {\n return useQueryVirtualizerImpl(\n {\n ...options,\n observeElementRect: options.observeElementRect ?? observeElementRect,\n observeElementOffset:\n options.observeElementOffset ?? observeElementOffset,\n },\n resolveElementScrollElement,\n )\n}\n\n/**\n * Like {@link useQueryVirtualizer}, but the window is the scroll container.\n * `getScrollElement` returns the element rows render into (normal page flow).\n */\nexport function useQueryWindowVirtualizer<\n TListContextParams,\n TRow extends Record<string, unknown>,\n TStartRow,\n>(\n options: UseQueryVirtualizerOptions<TListContextParams, TRow, TStartRow>,\n): QueryVirtualizerResult<TRow> {\n return useQueryVirtualizerImpl(\n {\n ...options,\n observeElementRect: options.observeElementRect ?? observeWindowRect,\n observeElementOffset: options.observeElementOffset ?? observeWindowOffset,\n },\n resolveWindowScrollElement,\n )\n}\n\n// Re-export for helper modules that type-check against the public result.\nexport type { QueryVirtualizerResult, ViewStatus }\n","import {\n createStickToBottomCache,\n DEFAULT_STICK_SLACK,\n type StickOptions,\n type StickToBottomCache,\n} from '@rocicorp/zero-virtual/core'\nimport { useLayoutEffect, useRef } from 'react'\n\nimport type { QueryVirtualizerResult } from './types'\n\n/** Options for {@link usePinToBottom}: `enabled` and bottom-edge `slack`. */\nexport type PinToBottomOptions = StickOptions\n\n/**\n * Keep a chat/log list pinned to the bottom when content grows — only while the\n * user is already at the bottom. Pass the result of `useQueryVirtualizer` /\n * `useQueryWindowVirtualizer`.\n */\nexport function usePinToBottom<TRow>(\n virtualizer: QueryVirtualizerResult<TRow>,\n { enabled = true, slack = DEFAULT_STICK_SLACK }: PinToBottomOptions = {},\n): void {\n const ref = useRef<StickToBottomCache | null>(null)\n\n // Runs per commit, pre-paint, with no deps on purpose: when the scroll\n // container renders conditionally (or before the first rows) elements can be\n // null and nothing else would re-run — ensure() retries each tick until they\n // exist, and is an identity-check no-op after that.\n useLayoutEffect(() => {\n if (!enabled) {\n ref.current?.detach()\n return\n }\n ;(ref.current ??= createStickToBottomCache()).ensure(virtualizer, slack)\n })\n\n useLayoutEffect(\n () => () => {\n ref.current?.detach()\n ref.current = null\n },\n [],\n )\n}\n","import {\n getHistoryStateServerSnapshot,\n getHistoryStateSnapshot,\n subscribeHistoryState,\n updateHistoryState,\n type ScrollHistoryState,\n} from '@rocicorp/zero-virtual/core'\nimport { useCallback, useMemo, useSyncExternalStore } from 'react'\n\nconst DEFAULT_KEY = 'scrollState'\n\n/**\n * Navigation API current-entry state as a React external store. Shell over pure\n * core history-state helpers — not a re-export of upstream `/react`\n * `useHistoryState` (closeio/doync#180).\n */\nfunction useNavigationHistoryState(): [\n state: unknown,\n setState: (state: unknown) => void,\n] {\n const state = useSyncExternalStore(\n subscribeHistoryState,\n getHistoryStateSnapshot,\n getHistoryStateServerSnapshot,\n )\n return [state, updateHistoryState]\n}\n\n/**\n * Persist virtualizer scroll under a key in `history.state` (Navigation API).\n * Pass the returned `[scrollState, setScrollState]` into `useQueryVirtualizer`.\n * Default key is `\"scrollState\"`; use distinct keys for multiple lists.\n * Requires the Navigation API (Firefox 147+).\n */\nexport function useRestoreScrollState<TStartRow>(\n key: string = DEFAULT_KEY,\n): [\n ScrollHistoryState<TStartRow> | null,\n (state: ScrollHistoryState<TStartRow> | null) => void,\n] {\n const [state, setState] = useNavigationHistoryState()\n\n // Memoize by serialized content so identity is stable when the nested key\n // is unchanged — core compares scrollState by reference on restore.\n const scrollState: ScrollHistoryState<TStartRow> | null = useMemo(() => {\n if (!state) return null\n return ((state as Record<string, unknown>)[key] ??\n null) as ScrollHistoryState<TStartRow> | null\n // eslint-disable-next-line react-hooks/exhaustive-deps -- content identity\n }, [state && JSON.stringify((state as Record<string, unknown>)[key]), key])\n\n const setScrollState = useCallback(\n (newState: ScrollHistoryState<TStartRow> | null) => {\n // Re-read the live history state instead of spreading the render-time\n // snapshot: the virtualizer calls this from a ~100ms persist debounce,\n // so another virtualizer (under a different key) or the app itself may\n // have written a sibling key since this closure was created — spreading\n // the stale snapshot would silently erase that write.\n const current = getHistoryStateSnapshot()\n setState({\n ...(current as Record<string, unknown>),\n [key]: newState,\n })\n },\n [setState, key],\n )\n\n return [scrollState, setScrollState]\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"index.js","names":["useQueryDefault","useQuery"],"sources":["../src/use-rows.ts","../src/use-query-virtualizer.ts","../src/use-pin-to-bottom.ts","../src/use-restore-scroll-state.ts"],"sourcesContent":["import type { BoundQuery } from '@doync/core'\n\nimport {\n type FalsyQuery,\n useQuery as useQueryDefault,\n type UseQueryOptions,\n type ViewStatus,\n} from '@doync/react'\nimport {\n type Anchor,\n assembleRows,\n buildAfterQuery,\n buildMainQuery,\n buildSingleQuery,\n permalinkMissing,\n type RowsSnapshot,\n} from '@rocicorp/zero-virtual/core'\nimport { useMemo } from 'react'\n\nimport type { GetPageQuery, GetSingleQuery, QueryResult } from './types'\n\n/**\n * Stage payload from core's `build*Query` helpers under doync's `{ query:\n * BoundQuery, options? }` factory contract (closeio/doync#199 / ADR-0027). Core\n * returns `null` for inactive stages; live stages carry a Bound query whose\n * args already ride inside the value.\n */\ntype Stage<\n TRow extends Record<string, unknown>,\n One extends boolean,\n> = QueryResult<TRow, One>\n\nconst UNKNOWN: ViewStatus = Object.freeze({ status: 'unknown' })\n\n/**\n * Aggregate live-stage ViewStatuses into the public virtualizer `status`\n * (closeio/doync#178 §F): first live error wins; assembled complete ⇒ complete;\n * else unknown. Skipped stages (core-null / falsy, or consumer `skip: true`)\n * never contribute.\n */\nexport function aggregateStatus(\n liveStatuses: readonly ViewStatus[],\n assembledComplete: boolean,\n): ViewStatus {\n for (const s of liveStatuses) {\n if (s.status === 'error') return s\n }\n if (assembledComplete) return { status: 'complete' }\n return UNKNOWN\n}\n\n/**\n * Injectable `useQuery` seam (closeio/doync#180 / #199): bound form only —\n * `useQuery(bound | falsy, options?)`. Adapter-first unit tests inject a fake\n * of this shape; the real hook assigns via a bound-form cast (its first public\n * overload rejects falsy).\n */\nexport type UseQueryLike = <\n Row extends Record<string, unknown> = Record<string, unknown>,\n>(\n query: BoundQuery<Row, boolean> | FalsyQuery,\n options?: UseQueryOptions,\n) => [readonly Row[] | Row | undefined, ViewStatus]\n\n/** Core-null or consumer-`skip: true` stages contribute nothing to ViewStatus. */\nfunction isLiveStage(\n stage: Stage<Record<string, unknown>, boolean> | null,\n): boolean {\n return stage !== null && stage.options?.skip !== true\n}\n\n/**\n * Internal three-slot adapter: stages Permalink / main / after via core\n * builders, always calls `useQuery` three times (hook-order stable), and passes\n * each stage's Bound query straight through — falsy when the builder returned\n * null (closeio/doync#199 dissolves the placeholder-under-skip workaround from\n * #180). Assembles a `RowsSnapshot` plus public `ViewStatus`.\n *\n * Not package-root-exported. Injectable `useQuery` for adapter-first unit\n * tests.\n */\nexport function useRows<TRow extends Record<string, unknown>, TStartRow>(\n {\n pageSize,\n anchor,\n settled,\n getPageQuery,\n getSingleQuery,\n toStartRow,\n }: {\n pageSize: number\n anchor: Anchor<TStartRow>\n settled: boolean\n getPageQuery: GetPageQuery<TRow, TStartRow>\n getSingleQuery: GetSingleQuery<TRow>\n toStartRow: (row: TRow) => TStartRow\n },\n // Bound-form cast: real useQuery's non-falsy first overload is not assignable\n // to the maybe-path UseQueryLike; both sides ARE BoundQuery | falsy (#199).\n useQuery: UseQueryLike = useQueryDefault as UseQueryLike,\n): { rows: RowsSnapshot<TRow>; status: ViewStatus } {\n const inputs = { pageSize, anchor, settled }\n\n // Stage 1: single-item lookup (permalink only). Null stage ⇒ falsy.\n const stage1 = buildSingleQuery(inputs, getSingleQuery as never) as Stage<\n TRow,\n true\n > | null\n const [singleRaw, singleStatus] = useQuery(\n stage1?.query ?? false,\n stage1?.options,\n )\n const typedSingleRow = (singleRaw as TRow | undefined) ?? undefined\n const singleComplete = singleStatus.status === 'complete'\n const notFound = permalinkMissing(inputs, typedSingleRow, singleComplete)\n const singleStart = typedSingleRow ? toStartRow(typedSingleRow) : null\n const live1 = isLiveStage(stage1)\n\n // Stage 2: page-before (permalink) or main page. Do not coerce\n // falsy-skipped multi-row `undefined` to `[]` (ADR-0027 a1; assembleRows\n // accepts it).\n const stage2 = buildMainQuery(\n inputs,\n getPageQuery as never,\n singleStart,\n notFound,\n ) as Stage<TRow, false> | null\n const [mainRaw, mainStatus] = useQuery(\n stage2?.query ?? false,\n stage2?.options,\n )\n const mainComplete = mainStatus.status === 'complete'\n const live2 = isLiveStage(stage2)\n\n // Stage 3: page-after (permalink only).\n const stage3 = buildAfterQuery(\n inputs,\n getPageQuery as never,\n singleStart,\n notFound,\n ) as Stage<TRow, false> | null\n const [afterRaw, afterStatus] = useQuery(\n stage3?.query ?? false,\n stage3?.options,\n )\n const afterComplete = afterStatus.status === 'complete'\n const live3 = isLiveStage(stage3)\n\n return useMemo(() => {\n const rows = assembleRows(\n { pageSize, anchor, settled },\n {\n singleRow: typedSingleRow,\n singleComplete,\n mainRows: mainRaw as readonly TRow[] | undefined as TRow[] | undefined,\n mainComplete,\n afterRows: afterRaw as readonly TRow[] | undefined as\n | TRow[]\n | undefined,\n afterComplete,\n },\n )\n const liveStatuses: ViewStatus[] = []\n if (live1) liveStatuses.push(singleStatus)\n if (live2) liveStatuses.push(mainStatus)\n if (live3) liveStatuses.push(afterStatus)\n return {\n rows,\n status: aggregateStatus(liveStatuses, rows.complete),\n }\n }, [\n pageSize,\n anchor,\n settled,\n typedSingleRow,\n singleComplete,\n singleStatus,\n live1,\n mainRaw,\n mainComplete,\n mainStatus,\n live2,\n afterRaw,\n afterComplete,\n afterStatus,\n live3,\n ])\n}\n","import type { ViewStatus } from '@doync/react'\n\nimport {\n observeElementOffset,\n observeElementRect,\n observeWindowOffset,\n observeWindowRect,\n type ResolvedScrollOptions,\n resolveElementScrollElement,\n type ResolveScrollElement,\n resolveWindowScrollElement,\n virtualizerResult,\n ZeroVirtualizer,\n} from '@rocicorp/zero-virtual/core'\nimport { useLayoutEffect, useMemo, useReducer, useState } from 'react'\n\nimport type {\n QueryVirtualizerResult,\n UseQueryVirtualizerOptions,\n} from './types'\n\nimport { useRows } from './use-rows'\n\n/**\n * Thin React binding over framework-agnostic `ZeroVirtualizer`\n * (closeio/doync#180): options and rows push silently every render; DOM work\n * runs from layout effects; re-renders come from the core's subscribe.\n *\n * Public result replaces core's `complete` boolean with aggregated `status:\n * ViewStatus` from live stages.\n */\nfunction useQueryVirtualizerImpl<\n TListContextParams,\n TRow extends Record<string, unknown>,\n TStartRow,\n>(\n options: UseQueryVirtualizerOptions<TListContextParams, TRow, TStartRow> &\n ResolvedScrollOptions,\n resolveScrollElement: ResolveScrollElement,\n): QueryVirtualizerResult<TRow> {\n const [, rerender] = useReducer(() => ({}), {})\n // One core instance per hook lifetime. Constructor is pure (no DOM /\n // listeners / timers), so Strict Mode double-construction is harmless —\n // and initializing paging from persisted scroll state here avoids Strict\n // Mode double-mounting the rows.\n const [core] = useState(\n () =>\n new ZeroVirtualizer<TListContextParams, TRow, TStartRow>(\n options,\n resolveScrollElement,\n ),\n )\n\n // Silent staging — never notifies during render.\n core.setOptions(options)\n const { pageSize, anchor, settled } = core.getQueryInputs()\n const { rows, status } = useRows<TRow, TStartRow>({\n pageSize,\n anchor,\n settled,\n getPageQuery: options.getPageQuery,\n getSingleQuery: options.getSingleQuery,\n toStartRow: options.toStartRow,\n })\n core.setRows(rows)\n\n // Mount/unmount: re-render subscription + listener teardown. Idempotent\n // across Strict Mode mount→unmount→mount (state survives detach; the\n // per-commit effect below re-attaches).\n useLayoutEffect(() => {\n const unsubscribe = core.subscribe(rerender)\n return () => {\n unsubscribe()\n core.detach()\n }\n }, [core])\n\n // Every commit, before paint: (re)wire the scroll element and run the\n // core's post-DOM-update pass (anchoring, restore/permalink, paging,\n // persistence).\n useLayoutEffect(() => {\n core.attach(options.getScrollElement())\n core.afterDOMUpdate()\n })\n\n const { getScrollElement, observeElementRect, observeElementOffset } = options\n const resultOptions = useMemo(\n () => ({ getScrollElement, observeElementRect, observeElementOffset }),\n [getScrollElement, observeElementRect, observeElementOffset],\n )\n const snapshot = core.getSnapshot()\n const statusRef = status\n return useMemo(() => {\n const base = virtualizerResult(\n snapshot,\n resultOptions,\n resolveScrollElement,\n )\n // Drop core's complete boolean; surface ViewStatus for consumers.\n const { complete: _complete, ...rest } = base\n void _complete\n return {\n ...rest,\n status: statusRef,\n }\n }, [snapshot, resultOptions, resolveScrollElement, statusRef])\n}\n\n/**\n * Virtualized infinite list inside an overflow element. `getScrollElement`\n * returns that element (also where rows render). Provide page/single query\n * factories via {@link UseQueryVirtualizerOptions}.\n *\n * ```ts\n * const v = useQueryVirtualizer({\n * getScrollElement: () => listRef.current,\n * estimateSize: () => 48,\n * getPageQuery: ({ limit, start, dir }) => ({\n * query: queries.items.page({ limit, start, dir }),\n * }),\n * getSingleQuery: ({ id }) => ({\n * query: queries.items.byId({ id }),\n * }),\n * // …\n * })\n * ```\n */\nexport function useQueryVirtualizer<\n TListContextParams,\n TRow extends Record<string, unknown>,\n TStartRow,\n>(\n options: UseQueryVirtualizerOptions<TListContextParams, TRow, TStartRow>,\n): QueryVirtualizerResult<TRow> {\n return useQueryVirtualizerImpl(\n {\n ...options,\n observeElementRect: options.observeElementRect ?? observeElementRect,\n observeElementOffset:\n options.observeElementOffset ?? observeElementOffset,\n },\n resolveElementScrollElement,\n )\n}\n\n/**\n * Like {@link useQueryVirtualizer}, but the window is the scroll container.\n * `getScrollElement` returns the element rows render into (normal page flow).\n */\nexport function useQueryWindowVirtualizer<\n TListContextParams,\n TRow extends Record<string, unknown>,\n TStartRow,\n>(\n options: UseQueryVirtualizerOptions<TListContextParams, TRow, TStartRow>,\n): QueryVirtualizerResult<TRow> {\n return useQueryVirtualizerImpl(\n {\n ...options,\n observeElementRect: options.observeElementRect ?? observeWindowRect,\n observeElementOffset: options.observeElementOffset ?? observeWindowOffset,\n },\n resolveWindowScrollElement,\n )\n}\n\n// Re-export for helper modules that type-check against the public result.\nexport type { QueryVirtualizerResult, ViewStatus }\n","import {\n createStickToBottomCache,\n DEFAULT_STICK_SLACK,\n type StickOptions,\n type StickToBottomCache,\n} from '@rocicorp/zero-virtual/core'\nimport { useLayoutEffect, useRef } from 'react'\n\nimport type { QueryVirtualizerResult } from './types'\n\n/** Options for {@link usePinToBottom}: `enabled` and bottom-edge `slack`. */\nexport type PinToBottomOptions = StickOptions\n\n/**\n * Keep a chat/log list pinned to the bottom when content grows — only while the\n * user is already at the bottom. Pass the result of `useQueryVirtualizer` /\n * `useQueryWindowVirtualizer`.\n */\nexport function usePinToBottom<TRow>(\n virtualizer: QueryVirtualizerResult<TRow>,\n { enabled = true, slack = DEFAULT_STICK_SLACK }: PinToBottomOptions = {},\n): void {\n const ref = useRef<StickToBottomCache | null>(null)\n\n // Runs per commit, pre-paint, with no deps on purpose: when the scroll\n // container renders conditionally (or before the first rows) elements can be\n // null and nothing else would re-run — ensure() retries each tick until they\n // exist, and is an identity-check no-op after that.\n useLayoutEffect(() => {\n if (!enabled) {\n ref.current?.detach()\n return\n }\n ;(ref.current ??= createStickToBottomCache()).ensure(virtualizer, slack)\n })\n\n useLayoutEffect(\n () => () => {\n ref.current?.detach()\n ref.current = null\n },\n [],\n )\n}\n","import {\n getHistoryStateServerSnapshot,\n getHistoryStateSnapshot,\n type ScrollHistoryState,\n subscribeHistoryState,\n updateHistoryState,\n} from '@rocicorp/zero-virtual/core'\nimport { useCallback, useMemo, useSyncExternalStore } from 'react'\n\nconst DEFAULT_KEY = 'scrollState'\n\n/**\n * Navigation API current-entry state as a React external store. Shell over pure\n * core history-state helpers — not a re-export of upstream `/react`\n * `useHistoryState` (closeio/doync#180).\n */\nfunction useNavigationHistoryState(): [\n state: unknown,\n setState: (state: unknown) => void,\n] {\n const state = useSyncExternalStore(\n subscribeHistoryState,\n getHistoryStateSnapshot,\n getHistoryStateServerSnapshot,\n )\n return [state, updateHistoryState]\n}\n\n/**\n * Persist virtualizer scroll under a key in `history.state` (Navigation API).\n * Pass the returned `[scrollState, setScrollState]` into `useQueryVirtualizer`.\n * Default key is `\"scrollState\"`; use distinct keys for multiple lists.\n * Requires the Navigation API (Firefox 147+).\n */\nexport function useRestoreScrollState<TStartRow>(\n key: string = DEFAULT_KEY,\n): [\n ScrollHistoryState<TStartRow> | null,\n (state: ScrollHistoryState<TStartRow> | null) => void,\n] {\n const [state, setState] = useNavigationHistoryState()\n\n // Memoize by serialized content so identity is stable when the nested key\n // is unchanged — core compares scrollState by reference on restore.\n const scrollState: ScrollHistoryState<TStartRow> | null = useMemo(() => {\n if (!state) return null\n return ((state as Record<string, unknown>)[key] ??\n null) as ScrollHistoryState<TStartRow> | null\n // eslint-disable-next-line react-hooks/exhaustive-deps -- content identity\n }, [state && JSON.stringify((state as Record<string, unknown>)[key]), key])\n\n const setScrollState = useCallback(\n (newState: ScrollHistoryState<TStartRow> | null) => {\n // Re-read the live history state instead of spreading the render-time\n // snapshot: the virtualizer calls this from a ~100ms persist debounce,\n // so another virtualizer (under a different key) or the app itself may\n // have written a sibling key since this closure was created — spreading\n // the stale snapshot would silently erase that write.\n const current = getHistoryStateSnapshot()\n setState({\n ...(current as Record<string, unknown>),\n [key]: newState,\n })\n },\n [setState, key],\n )\n\n return [scrollState, setScrollState]\n}\n"],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@doync/query-virtualizer",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.1",
|
|
4
4
|
"description": "doync React binding: virtualized infinite lists over Subscriptions via @rocicorp/zero-virtual/core",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"doync",
|
|
@@ -48,8 +48,8 @@
|
|
|
48
48
|
},
|
|
49
49
|
"dependencies": {
|
|
50
50
|
"@rocicorp/zero-virtual": "0.6.3",
|
|
51
|
-
"@doync/core": "0.1.
|
|
52
|
-
"@doync/react": "0.1.
|
|
51
|
+
"@doync/core": "0.1.1",
|
|
52
|
+
"@doync/react": "0.1.1"
|
|
53
53
|
},
|
|
54
54
|
"devDependencies": {
|
|
55
55
|
"@testing-library/react": "^16.1.0",
|
package/src/types.ts
CHANGED
|
@@ -2,13 +2,13 @@ import type { BoundQuery } from '@doync/core'
|
|
|
2
2
|
import type { UseQueryOptions, ViewStatus } from '@doync/react'
|
|
3
3
|
import type {
|
|
4
4
|
AnchoringMode,
|
|
5
|
+
VirtualizerBindingOptions as CoreVirtualizerBindingOptions,
|
|
6
|
+
VirtualizerResult as CoreVirtualizerResult,
|
|
5
7
|
GetPageQueryOptions,
|
|
6
8
|
GetSingleQueryOptions,
|
|
7
9
|
RowKey,
|
|
8
10
|
ScrollHistoryState,
|
|
9
11
|
VirtualRow,
|
|
10
|
-
VirtualizerBindingOptions as CoreVirtualizerBindingOptions,
|
|
11
|
-
VirtualizerResult as CoreVirtualizerResult,
|
|
12
12
|
} from '@rocicorp/zero-virtual/core'
|
|
13
13
|
|
|
14
14
|
/**
|
|
@@ -5,12 +5,12 @@ import {
|
|
|
5
5
|
observeElementRect,
|
|
6
6
|
observeWindowOffset,
|
|
7
7
|
observeWindowRect,
|
|
8
|
+
type ResolvedScrollOptions,
|
|
8
9
|
resolveElementScrollElement,
|
|
10
|
+
type ResolveScrollElement,
|
|
9
11
|
resolveWindowScrollElement,
|
|
10
12
|
virtualizerResult,
|
|
11
13
|
ZeroVirtualizer,
|
|
12
|
-
type ResolvedScrollOptions,
|
|
13
|
-
type ResolveScrollElement,
|
|
14
14
|
} from '@rocicorp/zero-virtual/core'
|
|
15
15
|
import { useLayoutEffect, useMemo, useReducer, useState } from 'react'
|
|
16
16
|
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import {
|
|
2
2
|
getHistoryStateServerSnapshot,
|
|
3
3
|
getHistoryStateSnapshot,
|
|
4
|
+
type ScrollHistoryState,
|
|
4
5
|
subscribeHistoryState,
|
|
5
6
|
updateHistoryState,
|
|
6
|
-
type ScrollHistoryState,
|
|
7
7
|
} from '@rocicorp/zero-virtual/core'
|
|
8
8
|
import { useCallback, useMemo, useSyncExternalStore } from 'react'
|
|
9
9
|
|
package/src/use-rows.ts
CHANGED
|
@@ -1,18 +1,18 @@
|
|
|
1
1
|
import type { BoundQuery } from '@doync/core'
|
|
2
2
|
|
|
3
3
|
import {
|
|
4
|
-
useQuery as useQueryDefault,
|
|
5
4
|
type FalsyQuery,
|
|
5
|
+
useQuery as useQueryDefault,
|
|
6
6
|
type UseQueryOptions,
|
|
7
7
|
type ViewStatus,
|
|
8
8
|
} from '@doync/react'
|
|
9
9
|
import {
|
|
10
|
+
type Anchor,
|
|
10
11
|
assembleRows,
|
|
11
12
|
buildAfterQuery,
|
|
12
13
|
buildMainQuery,
|
|
13
14
|
buildSingleQuery,
|
|
14
15
|
permalinkMissing,
|
|
15
|
-
type Anchor,
|
|
16
16
|
type RowsSnapshot,
|
|
17
17
|
} from '@rocicorp/zero-virtual/core'
|
|
18
18
|
import { useMemo } from 'react'
|