@ttt-productions/query-core 0.14.4 → 0.14.6
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/cache-helpers.d.ts +17 -0
- package/dist/cache-helpers.d.ts.map +1 -1
- package/dist/cache-helpers.js +48 -0
- package/dist/cache-helpers.js.map +1 -1
- package/dist/firestore/types.d.ts +31 -4
- package/dist/firestore/types.d.ts.map +1 -1
- package/dist/firestore/types.js.map +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/react/firestore/doc-subscription-registry.d.ts +7 -1
- package/dist/react/firestore/doc-subscription-registry.d.ts.map +1 -1
- package/dist/react/firestore/doc-subscription-registry.js +24 -6
- package/dist/react/firestore/doc-subscription-registry.js.map +1 -1
- package/dist/react/firestore/subscription-status.d.ts +21 -0
- package/dist/react/firestore/subscription-status.d.ts.map +1 -0
- package/dist/react/firestore/subscription-status.js +29 -0
- package/dist/react/firestore/subscription-status.js.map +1 -0
- package/dist/react/firestore/use-firestore-collection.d.ts.map +1 -1
- package/dist/react/firestore/use-firestore-collection.js +10 -13
- package/dist/react/firestore/use-firestore-collection.js.map +1 -1
- package/dist/react/firestore/use-firestore-doc.d.ts.map +1 -1
- package/dist/react/firestore/use-firestore-doc.js +10 -13
- package/dist/react/firestore/use-firestore-doc.js.map +1 -1
- package/dist/react/firestore/use-firestore-live-infinite.d.ts +31 -14
- package/dist/react/firestore/use-firestore-live-infinite.d.ts.map +1 -1
- package/dist/react/firestore/use-firestore-live-infinite.js +192 -45
- package/dist/react/firestore/use-firestore-live-infinite.js.map +1 -1
- package/dist/react/firestore/use-firestore-paginated.d.ts +27 -24
- package/dist/react/firestore/use-firestore-paginated.d.ts.map +1 -1
- package/dist/react/firestore/use-firestore-paginated.js +102 -92
- package/dist/react/firestore/use-firestore-paginated.js.map +1 -1
- package/dist/react/firestore/useBatchFirestoreDocs.d.ts +12 -0
- package/dist/react/firestore/useBatchFirestoreDocs.d.ts.map +1 -1
- package/dist/react/firestore/useBatchFirestoreDocs.js +80 -26
- package/dist/react/firestore/useBatchFirestoreDocs.js.map +1 -1
- package/dist/react/index.d.ts +1 -1
- package/dist/react/index.d.ts.map +1 -1
- package/dist/react/index.js.map +1 -1
- package/package.json +3 -4
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"use-firestore-live-infinite.js","sourceRoot":"","sources":["../../../src/react/firestore/use-firestore-live-infinite.ts"],"names":[],"mappings":"AAAA,YAAY,CAAC;AAEb,OAAO,KAAK,KAAK,MAAM,OAAO,CAAC;AAC/B,OAAO,EAAE,gBAAgB,EAAE,MAAM,uBAAuB,CAAC;AACzD,OAAO,EACL,UAAU,EACV,KAAK,EACL,OAAO,EACP,KAAK,EACL,UAAU,EACV,OAAO,EACP,UAAU,GAGX,MAAM,oBAAoB,CAAC;AAC5B,OAAO,EAAE,cAAc,EAAE,MAAM,cAAc,CAAC;AAG9C,MAAM,aAAa,GAAG,GAAG,CAAC;AAiB1B;;;;;;;;;;GAUG;AACH,MAAM,UAAU,wBAAwB,CAAoC,EAC1E,cAAc,EACd,QAAQ,EACR,YAAY,EACZ,WAAW,GAAG,EAAE,EAChB,QAAQ,EAAE,iBAAiB,GAAG,EAAE,EAChC,OAAO,GAAG,IAAI,EACd,MAAM,EACN,YAAY,EACZ,IAAI,GAAG,KAAK,GACoB;IAChC,MAAM,EAAE,GAAG,cAAc,EAAE,CAAC;IAC5B,MAAM,QAAQ,GAAG,IAAI,CAAC,GAAG,CAAC,iBAAiB,EAAE,aAAa,CAAC,CAAC;IAE5D,4EAA4E;IAC5E,gCAAgC;IAChC,MAAM,SAAS,GAAG,KAAK,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC;IACvC,SAAS,CAAC,OAAO,GAAG,MAAM,CAAC;IAC3B,MAAM,eAAe,GAAG,KAAK,CAAC,MAAM,CAAC,YAAY,CAAC,CAAC;IACnD,eAAe,CAAC,OAAO,GAAG,YAAY,CAAC;IAEvC,MAAM,OAAO,GAAG,KAAK,CAAC,WAAW,CAC/B,CAAC,IAAyC,EAAY,EAAE;QACtD,MAAM,UAAU,GAAkC,EAAE,EAAE,EAAE,IAAI,CAAC,EAAE,EAAE,GAAG,IAAI,CAAC,IAAI,EAAE,EAAE,CAAC;QAClF,MAAM,IAAI,GAAG,SAAS,CAAC,OAAO,CAAC,CAAC,CAAC,SAAS,CAAC,OAAO,CAAC,UAAU,CAAC,CAAC,CAAC,CAAE,UAA2B,CAAC;QAC9F,MAAM,SAAS,GAAG,eAAe,CAAC,OAAO;YACvC,CAAC,CAAC,eAAe,CAAC,OAAO,CAAC,UAAU,CAAC;YACrC,CAAC,CAAE,UAAU,CAAC,YAAY,CAAY,CAAC;QACzC,OAAO,EAAE,KAAK,EAAE,IAAI,CAAC,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC;IAC7C,CAAC,EACD,CAAC,YAAY,CAAC,CACf,CAAC;IAEF,8EAA8E;IAC9E,4DAA4D;IAC5D,MAAM,YAAY,GAAG,IAAI,CAAC,SAAS,CAAC,QAAQ,CAAC,CAAC;IAC9C,MAAM,eAAe,GAAG,IAAI,CAAC,SAAS,CAAC,WAAW,CAAC,CAAC;IAEpD,MAAM,CAAC,WAAW,EAAE,cAAc,CAAC,GAAG,KAAK,CAAC,QAAQ,CAIjD,EAAE,KAAK,EAAE,KAAK,EAAE,OAAO,EAAE,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;IAEnD,KAAK,CAAC,SAAS,CAAC,GAAG,EAAE;QACnB,IAAI,CAAC,OAAO,EAAE,CAAC;YACb,cAAc,CAAC,EAAE,KAAK,EAAE,KAAK,EAAE,OAAO,EAAE,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;YAC/D,OAAO;QACT,CAAC;QACD,MAAM,MAAM,GAAG,UAAU,CAAC,EAAE,EAAE,cAAc,CAAC,CAAC;QAC9C,MAAM,CAAC,GAAG,KAAK,CAAC,MAAM,EAAE,GAAG,WAAW,EAAE,OAAO,CAAC,YAAY,EAAE,MAAM,CAAC,EAAE,KAAK,CAAC,QAAQ,CAAC,CAAC,CAAC;QACxF,MAAM,KAAK,GAAG,UAAU,CAAC,CAAC,EAAE,CAAC,IAAI,EAAE,EAAE;YACnC,MAAM,IAAI,GAAG,IAAI,CAAC,IAA6C,CAAC;YAChE,cAAc,CAAC;gBACb,KAAK,EAAE,IAAI;gBACX,OAAO,EAAE,IAAI,CAAC,GAAG,CAAC,OAAO,CAAC;gBAC1B,SAAS,EAAE,IAAI,CAAC,IAAI,CAAC,MAAM,GAAG,CAAC,CAAC,IAAI,IAAI;aACzC,CAAC,CAAC;QACL,CAAC,CAAC,CAAC;QACH,OAAO,GAAG,EAAE,CAAC,KAAK,EAAE,CAAC;QACrB,0FAA0F;QAC1F,uDAAuD;IACzD,CAAC,EAAE,CAAC,EAAE,EAAE,cAAc,EAAE,YAAY,EAAE,QAAQ,EAAE,OAAO,EAAE,YAAY,EAAE,eAAe,EAAE,OAAO,CAAC,CAAC,CAAC;IAElG,MAAM,KAAK,GAAG,gBAAgB,CAAC;QAC7B,QAAQ,EAAE,CAAC,GAAG,QAAQ,EAAE,OAAO,CAAC;QAChC,OAAO,EAAE,OAAO,IAAI,WAAW,CAAC,KAAK;QACrC,gBAAgB,EAAE,SAA4D;QAC9E,OAAO,EAAE,KAAK,EAAE,EAAE,SAAS,EAAE,EAAE,EAAE;YAC/B,MAAM,MAAM,GAAG,SAAS,IAAI,WAAW,CAAC,SAAS,IAAI,SAAS,CAAC;YAC/D,IAAI,CAAC,MAAM,EAAE,CAAC;gBACZ,OAAO;oBACL,OAAO,EAAE,EAAgB;oBACzB,UAAU,EAAE,SAA4D;iBACzE,CAAC;YACJ,CAAC;YACD,MAAM,MAAM,GAAG,UAAU,CAAC,EAAE,EAAE,cAAc,CAAC,CAAC;YAC9C,MAAM,CAAC,GAAG,KAAK,CACb,MAAM,EACN,GAAG,WAAW,EACd,OAAO,CAAC,YAAY,EAAE,MAAM,CAAC,EAC7B,UAAU,CAAC,MAAM,CAAC,EAClB,KAAK,CAAC,QAAQ,CAAC,CAChB,CAAC;YACF,MAAM,IAAI,GAAG,MAAM,OAAO,CAAC,CAAC,CAAC,CAAC;YAC9B,MAAM,IAAI,GAAG,IAAI,CAAC,IAA6C,CAAC;YAChE,OAAO;gBACL,OAAO,EAAE,IAAI,CAAC,GAAG,CAAC,OAAO,CAAC;gBAC1B,UAAU,EAAE,IAAI,CAAC,MAAM,KAAK,QAAQ,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,SAAS;aACzE,CAAC;QACJ,CAAC;QACD,gBAAgB,EAAE,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,UAAU;KAC5C,CAAC,CAAC;IAEH,MAAM,EAAE,aAAa,EAAE,GAAG,KAAK,CAAC;IAChC,MAAM,UAAU,GAAG,KAAK,CAAC,WAAW,CAAC,KAAK,IAAI,EAAE;QAC9C,MAAM,aAAa,EAAE,CAAC;IACxB,CAAC,EAAE,CAAC,aAAa,CAAC,CAAC,CAAC;IAEpB,MAAM,KAAK,GAAG,KAAK,CAAC,OAAO,CAAC,GAAG,EAAE;QAC/B,IAAI,CAAC,OAAO;YAAE,OAAO,EAAE,CAAC;QACxB,MAAM,YAAY,GAAG,CAAC,KAAK,CAAC,IAAI,EAAE,KAAK,IAAI,EAAE,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC;QACzE,sEAAsE;QACtE,MAAM,MAAM,GAAG,IAAI,GAAG,EAAoB,CAAC;QAC3C,KAAK,MAAM,KAAK,IAAI,CAAC,GAAG,WAAW,CAAC,OAAO,EAAE,GAAG,YAAY,CAAC,EAAE,CAAC;YAC9D,IAAI,CAAC,MAAM,CAAC,GAAG,CAAC,KAAK,CAAC,KAAK,CAAC;gBAAE,MAAM,CAAC,GAAG,CAAC,KAAK,CAAC,KAAK,EAAE,KAAK,CAAC,CAAC;QAC/D,CAAC;QACD,MAAM,GAAG,GAAG,KAAK,CAAC,IAAI,CAAC,MAAM,CAAC,MAAM,EAAE,CAAC,CAAC;QACxC,GAAG,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,IAAI,KAAK,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,SAAS,GAAG,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC,CAAC,SAAS,GAAG,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC;QAC7F,OAAO,GAAG,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC;IAChC,CAAC,EAAE,CAAC,OAAO,EAAE,KAAK,CAAC,IAAI,EAAE,WAAW,CAAC,OAAO,EAAE,IAAI,CAAC,CAAC,CAAC;IAErD,OAAO;QACL,KAAK;QACL,gBAAgB,EAAE,OAAO,CAAC,CAAC,CAAC,CAAC,WAAW,CAAC,KAAK,CAAC,CAAC,CAAC,KAAK;QACtD,UAAU;QACV,QAAQ,EAAE,OAAO,CAAC,KAAK,CAAC,WAAW,CAAC;QACpC,eAAe,EAAE,KAAK,CAAC,kBAAkB;KAC1C,CAAC;AACJ,CAAC","sourcesContent":["'use client';\n\nimport * as React from 'react';\nimport { useInfiniteQuery } from '@tanstack/react-query';\nimport {\n collection,\n query,\n orderBy,\n limit,\n startAfter,\n getDocs,\n onSnapshot,\n type DocumentData,\n type QueryDocumentSnapshot,\n} from 'firebase/firestore';\nimport { useFirestoreDb } from './context.js';\nimport type { FirestoreLiveInfiniteOptions } from '../../firestore/types.js';\n\nconst MAX_PAGE_SIZE = 100;\n\nexport interface FirestoreLiveInfiniteResult<T> {\n /** Merged + deduped live-window and older items, sorted per `sort` (default asc). */\n items: T[];\n /** True until the live window's first snapshot arrives. */\n isInitialLoading: boolean;\n /** Load the next older page. */\n fetchOlder: () => Promise<void>;\n /** Whether more older pages remain. */\n hasOlder: boolean;\n /** Whether an older page is currently being fetched. */\n isFetchingOlder: boolean;\n}\n\ntype Entry<T> = { docId: string; sortValue: number; item: T };\n\n/**\n * A live newest-window plus cursor-bridged older pagination, merged into one\n * ordered list. The newest page is a realtime `onSnapshot` window (orderBy\n * descending, limited to `pageSize`); older pages load on demand via `getDocs`,\n * each seeded with `startAfter(previousOldestDoc)` — the first older page starts\n * after the live window's oldest document, so there is no gap and no re-read of\n * pages already in memory.\n *\n * Generic enough for any live feed (chat threads, live comments, activity\n * feeds). `db` comes from `<FirestoreProvider>`.\n */\nexport function useFirestoreLiveInfinite<T = DocumentData & { id: string }>({\n collectionPath,\n queryKey,\n orderByField,\n constraints = [],\n pageSize: requestedPageSize = 20,\n enabled = true,\n select,\n getSortValue,\n sort = 'asc',\n}: FirestoreLiveInfiniteOptions<T>): FirestoreLiveInfiniteResult<T> {\n const db = useFirestoreDb();\n const pageSize = Math.min(requestedPageSize, MAX_PAGE_SIZE);\n\n // Latest-callback refs: an inline select/getSortValue must not re-subscribe\n // the listener on every render.\n const selectRef = React.useRef(select);\n selectRef.current = select;\n const getSortValueRef = React.useRef(getSortValue);\n getSortValueRef.current = getSortValue;\n\n const toEntry = React.useCallback(\n (snap: QueryDocumentSnapshot<DocumentData>): Entry<T> => {\n const dataWithId: DocumentData & { id: string } = { id: snap.id, ...snap.data() };\n const item = selectRef.current ? selectRef.current(dataWithId) : (dataWithId as unknown as T);\n const sortValue = getSortValueRef.current\n ? getSortValueRef.current(dataWithId)\n : (dataWithId[orderByField] as number);\n return { docId: snap.id, sortValue, item };\n },\n [orderByField],\n );\n\n // Stable string deps for the subscription effect (values are captured via the\n // queryKey, which the caller varies per collection/thread).\n const queryKeyMemo = JSON.stringify(queryKey);\n const constraintsMemo = JSON.stringify(constraints);\n\n const [windowState, setWindowState] = React.useState<{\n ready: boolean;\n entries: Entry<T>[];\n oldestDoc: QueryDocumentSnapshot<DocumentData> | null;\n }>({ ready: false, entries: [], oldestDoc: null });\n\n React.useEffect(() => {\n if (!enabled) {\n setWindowState({ ready: false, entries: [], oldestDoc: null });\n return;\n }\n const colRef = collection(db, collectionPath);\n const q = query(colRef, ...constraints, orderBy(orderByField, 'desc'), limit(pageSize));\n const unsub = onSnapshot(q, (snap) => {\n const docs = snap.docs as QueryDocumentSnapshot<DocumentData>[];\n setWindowState({\n ready: true,\n entries: docs.map(toEntry),\n oldestDoc: docs[docs.length - 1] ?? null,\n });\n });\n return () => unsub();\n // constraints captured via constraintsMemo; queryKey via queryKeyMemo; toEntry is stable.\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [db, collectionPath, orderByField, pageSize, enabled, queryKeyMemo, constraintsMemo, toEntry]);\n\n const older = useInfiniteQuery({\n queryKey: [...queryKey, 'older'],\n enabled: enabled && windowState.ready,\n initialPageParam: undefined as QueryDocumentSnapshot<DocumentData> | undefined,\n queryFn: async ({ pageParam }) => {\n const cursor = pageParam ?? windowState.oldestDoc ?? undefined;\n if (!cursor) {\n return {\n entries: [] as Entry<T>[],\n nextCursor: undefined as QueryDocumentSnapshot<DocumentData> | undefined,\n };\n }\n const colRef = collection(db, collectionPath);\n const q = query(\n colRef,\n ...constraints,\n orderBy(orderByField, 'desc'),\n startAfter(cursor),\n limit(pageSize),\n );\n const snap = await getDocs(q);\n const docs = snap.docs as QueryDocumentSnapshot<DocumentData>[];\n return {\n entries: docs.map(toEntry),\n nextCursor: docs.length === pageSize ? docs[docs.length - 1] : undefined,\n };\n },\n getNextPageParam: (last) => last.nextCursor,\n });\n\n const { fetchNextPage } = older;\n const fetchOlder = React.useCallback(async () => {\n await fetchNextPage();\n }, [fetchNextPage]);\n\n const items = React.useMemo(() => {\n if (!enabled) return [];\n const olderEntries = (older.data?.pages ?? []).flatMap((p) => p.entries);\n // Window entries win on overlap (they are the freshest, live copies).\n const merged = new Map<string, Entry<T>>();\n for (const entry of [...windowState.entries, ...olderEntries]) {\n if (!merged.has(entry.docId)) merged.set(entry.docId, entry);\n }\n const arr = Array.from(merged.values());\n arr.sort((a, b) => (sort === 'asc' ? a.sortValue - b.sortValue : b.sortValue - a.sortValue));\n return arr.map((e) => e.item);\n }, [enabled, older.data, windowState.entries, sort]);\n\n return {\n items,\n isInitialLoading: enabled ? !windowState.ready : false,\n fetchOlder,\n hasOlder: Boolean(older.hasNextPage),\n isFetchingOlder: older.isFetchingNextPage,\n };\n}\n"]}
|
|
1
|
+
{"version":3,"file":"use-firestore-live-infinite.js","sourceRoot":"","sources":["../../../src/react/firestore/use-firestore-live-infinite.ts"],"names":[],"mappings":"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","sourcesContent":["'use client';\n\nimport * as React from 'react';\nimport { useInfiniteQuery } from '@tanstack/react-query';\nimport {\n collection,\n query,\n orderBy,\n limit,\n startAfter,\n endAt,\n getDocs,\n onSnapshot,\n type DocumentData,\n type QueryDocumentSnapshot,\n} from 'firebase/firestore';\nimport { useFirestoreDb } from './context.js';\nimport { RESUBSCRIBE_DELAYS_MS, isPermissionDeniedError } from './resubscribe.js';\nimport type { FirestoreLiveInfiniteOptions, FirestoreSourceState } from '../../firestore/types.js';\n\nconst MAX_PAGE_SIZE = 100;\n\nexport interface FirestoreLiveInfiniteResult<T> {\n /** The current subscription identity's rows — live set plus loaded older pages, sorted per `sort`. */\n items: T[];\n /** True until the current identity's first snapshot arrives, unless its listener has failed. */\n isInitialLoading: boolean;\n /** Liveness of the current identity's listener; `connecting` from that identity's first render. */\n sourceState: FirestoreSourceState;\n /** The listener's failure for the current identity; `null` while connecting, live, or offline. */\n error: Error | null;\n /** Load the next older page. Never rejects — a failed read lands in `olderError`. */\n fetchOlder: () => Promise<void>;\n /** Whether older rows exist that are not loaded yet (a one-doc look-ahead, never a full-page guess). */\n hasOlder: boolean;\n /** Whether an older page is being read (including a `retry` of one). */\n isFetchingOlder: boolean;\n /** The last older-page read failed; `null` once an older read succeeds. */\n olderError: Error | null;\n /** Re-run what failed: resubscribe a failed listener, re-read a failed older page. */\n retry: () => void;\n}\n\ntype Entry<T> = { docId: string; sortValue: number; item: T };\ntype Snap = QueryDocumentSnapshot<DocumentData>;\n\ninterface WindowState<T> {\n identity: string;\n ready: boolean;\n entries: Entry<T>[];\n anchor: Snap | null;\n olderExists: boolean;\n sourceState: FirestoreSourceState;\n error: Error | null;\n}\n\nfunction freshWindow<T>(identity: string): WindowState<T> {\n return {\n identity,\n ready: false,\n entries: [],\n anchor: null,\n olderExists: false,\n sourceState: 'connecting',\n error: null,\n };\n}\n\n/**\n * A live list (newest rows realtime) with older rows paged in on demand, merged into\n * one ordered list.\n *\n * Until the first server-confirmed snapshot that has rows, the listener reads the newest\n * `pageSize` rows plus one look-ahead row. That snapshot fixes an anchor — the oldest row\n * of the window — and from then on the listener covers anchor → newest with no limit, so\n * it grows only with rows that arrive while the list is open, and every older page starts\n * after the anchor. Nothing can fall between the live set and the older pages, however\n * many rows arrive. Older pages carry the same one-row look-ahead, so `hasOlder` is false\n * exactly when nothing older exists.\n *\n * Everything read is tagged to its subscription identity (path, key, ordering,\n * constraints, page size, enabled): a new identity renders empty and connecting in its\n * first render, and a retired identity's late callback or older page never lands.\n *\n * A `permission-denied` listener error resubscribes on the `RESUBSCRIBE_DELAYS_MS` ladder\n * before surfacing; any other listener error surfaces at once. Rows already shown are\n * kept beside the error. `db` comes from `<FirestoreProvider>`.\n */\nexport function useFirestoreLiveInfinite<T = DocumentData & { id: string }>({\n collectionPath,\n queryKey,\n orderByField,\n constraints = [],\n pageSize: requestedPageSize = 20,\n enabled = true,\n select,\n getSortValue,\n sort = 'asc',\n}: FirestoreLiveInfiniteOptions<T>): FirestoreLiveInfiniteResult<T> {\n const db = useFirestoreDb();\n const pageSize = Math.min(requestedPageSize, MAX_PAGE_SIZE);\n\n // Latest-callback refs: an inline select/getSortValue must not re-subscribe\n // the listener on every render.\n const selectRef = React.useRef(select);\n selectRef.current = select;\n const getSortValueRef = React.useRef(getSortValue);\n getSortValueRef.current = getSortValue;\n\n const toEntry = React.useCallback(\n (snap: Snap): Entry<T> => {\n const dataWithId: DocumentData & { id: string } = { id: snap.id, ...snap.data() };\n const item = selectRef.current ? selectRef.current(dataWithId) : (dataWithId as unknown as T);\n const sortValue = getSortValueRef.current\n ? getSortValueRef.current(dataWithId)\n : (dataWithId[orderByField] as number);\n return { docId: snap.id, sortValue, item };\n },\n [orderByField],\n );\n\n // Constraint objects are fresh every render; their serialized form is the identity.\n const queryKeyMemo = JSON.stringify(queryKey);\n const constraintsMemo = JSON.stringify(constraints);\n const identity = JSON.stringify([\n collectionPath,\n queryKeyMemo,\n orderByField,\n constraintsMemo,\n pageSize,\n enabled,\n ]);\n\n const [windowState, setWindowState] = React.useState<WindowState<T>>(() => freshWindow(identity));\n const windowRef = React.useRef(windowState);\n windowRef.current = windowState;\n const [retryNonce, setRetryNonce] = React.useState(0);\n\n const current: WindowState<T> =\n windowState.identity === identity ? windowState : freshWindow<T>(identity);\n\n React.useEffect(() => {\n if (!enabled) {\n // Retire the enabled identity's rows, so re-enabling starts afresh rather than resuming.\n setWindowState(freshWindow<T>(identity));\n return;\n }\n\n // A retry of the same identity resumes from its anchor and keeps its rows.\n const prior = windowRef.current;\n const resumed = prior.identity === identity;\n setWindowState((prev) =>\n prev.identity === identity\n ? { ...prev, error: null, sourceState: 'connecting' }\n : freshWindow<T>(identity),\n );\n\n let disposed = false;\n let unsubscribe: (() => void) | null = null;\n let retryTimer: ReturnType<typeof setTimeout> | null = null;\n let attempt = 0;\n let currentAnchor: Snap | null = resumed ? prior.anchor : null;\n\n const update = (fn: (prev: WindowState<T>) => WindowState<T>) => {\n if (disposed) return;\n setWindowState((prev) => (prev.identity === identity ? fn(prev) : prev));\n };\n\n const listen = (anchor: Snap | null) => {\n if (disposed) return;\n currentAnchor = anchor;\n const colRef = collection(db, collectionPath);\n const q = anchor\n ? query(colRef, ...constraints, orderBy(orderByField, 'desc'), endAt(anchor))\n : query(colRef, ...constraints, orderBy(orderByField, 'desc'), limit(pageSize + 1));\n let serverConfirmed = false;\n\n unsubscribe = onSnapshot(\n q,\n { includeMetadataChanges: true },\n (snap) => {\n if (disposed) return;\n attempt = 0;\n const docs = snap.docs as Snap[];\n const fromCache = snap.metadata?.fromCache ?? false;\n const nextSource = (prev: FirestoreSourceState): FirestoreSourceState => {\n if (!fromCache) return 'live';\n if (!serverConfirmed) return prev === 'error' ? 'connecting' : prev;\n return 'offline';\n };\n if (!fromCache) serverConfirmed = true;\n\n if (anchor) {\n update((prev) => ({\n ...prev,\n ready: true,\n entries: docs.map(toEntry),\n sourceState: nextSource(prev.sourceState),\n error: null,\n }));\n return;\n }\n\n const windowDocs = docs.slice(0, pageSize);\n if (fromCache || windowDocs.length === 0) {\n update((prev) => ({\n ...prev,\n ready: true,\n entries: windowDocs.map(toEntry),\n sourceState: nextSource(prev.sourceState),\n error: null,\n }));\n return;\n }\n\n const newAnchor = windowDocs[windowDocs.length - 1];\n update((prev) => ({\n ...prev,\n ready: true,\n entries: windowDocs.map(toEntry),\n anchor: newAnchor,\n olderExists: docs.length > pageSize,\n sourceState: 'live',\n error: null,\n }));\n unsubscribe?.();\n unsubscribe = null;\n listen(newAnchor);\n },\n (error) => {\n if (disposed) return;\n unsubscribe?.();\n unsubscribe = null;\n if (isPermissionDeniedError(error) && attempt < RESUBSCRIBE_DELAYS_MS.length) {\n const delay = RESUBSCRIBE_DELAYS_MS[attempt];\n attempt += 1;\n update((prev) => ({ ...prev, sourceState: 'connecting' }));\n retryTimer = setTimeout(() => listen(currentAnchor), delay);\n return;\n }\n console.error('[useFirestoreLiveInfinite] Subscription error:', error);\n update((prev) => ({ ...prev, sourceState: 'error', error }));\n },\n );\n };\n\n listen(currentAnchor);\n\n return () => {\n disposed = true;\n if (retryTimer) clearTimeout(retryTimer);\n unsubscribe?.();\n };\n // `identity` carries collectionPath, queryKey, orderByField, constraints, pageSize, and\n // enabled; constraints are read from the render that produced it.\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [db, identity, retryNonce, toEntry]);\n\n const anchor = enabled ? current.anchor : null;\n const olderEnabled = anchor !== null && current.olderExists;\n\n const older = useInfiniteQuery({\n queryKey: [...queryKey, 'older', identity, anchor?.id ?? null],\n enabled: olderEnabled,\n initialPageParam: undefined as Snap | undefined,\n queryFn: async ({ pageParam }) => {\n const cursor = pageParam ?? anchor;\n if (!cursor) {\n return { entries: [] as Entry<T>[], nextCursor: undefined as Snap | undefined };\n }\n const colRef = collection(db, collectionPath);\n const q = query(\n colRef,\n ...constraints,\n orderBy(orderByField, 'desc'),\n startAfter(cursor),\n limit(pageSize + 1),\n );\n const snap = await getDocs(q);\n const docs = snap.docs as Snap[];\n const page = docs.slice(0, pageSize);\n return {\n entries: page.map(toEntry),\n nextCursor: docs.length > pageSize ? page[page.length - 1] : undefined,\n };\n },\n getNextPageParam: (last) => last.nextCursor,\n });\n\n const {\n data: olderData,\n isFetching: olderFetching,\n isError: olderIsError,\n error: olderErr,\n hasNextPage: olderHasNext,\n fetchNextPage,\n refetch: refetchOlder,\n } = older;\n\n const olderPages = olderEnabled ? olderData?.pages : undefined;\n const hasOlder = olderEnabled && (olderPages ? Boolean(olderHasNext) : true);\n const olderError = olderEnabled && olderIsError ? olderErr : null;\n\n const fetchOlder = React.useCallback(async () => {\n if (!olderEnabled || olderFetching) return;\n if (!olderPages) {\n await refetchOlder();\n return;\n }\n if (olderHasNext) await fetchNextPage();\n }, [olderEnabled, olderFetching, olderPages, olderHasNext, refetchOlder, fetchNextPage]);\n\n const listenerFailed = enabled && current.error !== null;\n const retry = React.useCallback(() => {\n if (listenerFailed) {\n setWindowState((prev) =>\n prev.identity === identity ? { ...prev, error: null, sourceState: 'connecting' } : prev,\n );\n setRetryNonce((n) => n + 1);\n }\n if (olderError && !olderFetching) {\n if (olderPages) void fetchNextPage();\n else void refetchOlder();\n }\n }, [listenerFailed, identity, olderError, olderFetching, olderPages, fetchNextPage, refetchOlder]);\n\n const items = React.useMemo(() => {\n if (!enabled) return [];\n const olderEntries = (olderPages ?? []).flatMap((p) => p.entries);\n // Live entries win on overlap (they are the freshest copies).\n const merged = new Map<string, Entry<T>>();\n for (const entry of [...current.entries, ...olderEntries]) {\n if (!merged.has(entry.docId)) merged.set(entry.docId, entry);\n }\n const arr = Array.from(merged.values());\n arr.sort((a, b) => (sort === 'asc' ? a.sortValue - b.sortValue : b.sortValue - a.sortValue));\n return arr.map((e) => e.item);\n }, [enabled, olderPages, current.entries, sort]);\n\n return {\n items,\n isInitialLoading: enabled ? !current.ready && current.error === null : false,\n sourceState: current.sourceState,\n error: enabled ? current.error : null,\n fetchOlder,\n hasOlder,\n isFetchingOlder: olderEnabled && olderFetching,\n olderError,\n retry,\n };\n}\n"]}
|
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
import { type UseQueryResult } from '@tanstack/react-query';
|
|
2
2
|
import { type DocumentData } from 'firebase/firestore';
|
|
3
|
-
import type { FirestorePaginatedOptions, WithId } from '../../firestore/types.js';
|
|
3
|
+
import type { FirestorePaginatedOptions, PaginatedPage, WithId } from '../../firestore/types.js';
|
|
4
4
|
/**
|
|
5
|
-
* Return type for useFirestorePaginated hook
|
|
5
|
+
* Return type for useFirestorePaginated hook. `data` is the displayed page's rows; the
|
|
6
|
+
* page's cache entry (rows, cursor, has-more) lives at `paginatedPageKey(queryKey, page)`.
|
|
6
7
|
*/
|
|
7
|
-
export type UseFirestorePaginatedResult<T> = UseQueryResult<WithId<T
|
|
8
|
+
export type UseFirestorePaginatedResult<T> = Omit<UseQueryResult<PaginatedPage<WithId<T>>, Error>, 'data'> & {
|
|
9
|
+
data: WithId<T>[] | undefined;
|
|
8
10
|
page: number;
|
|
9
11
|
pageSize: number;
|
|
10
12
|
hasNextPage: boolean;
|
|
@@ -12,31 +14,32 @@ export type UseFirestorePaginatedResult<T> = UseQueryResult<WithId<T>[], Error>
|
|
|
12
14
|
setPage: (page: number) => void;
|
|
13
15
|
nextPage: () => void;
|
|
14
16
|
prevPage: () => void;
|
|
15
|
-
resetToFirstPage: () => void;
|
|
16
17
|
};
|
|
17
18
|
/**
|
|
18
|
-
* Page-based pagination for Firestore collections.
|
|
19
|
-
*
|
|
20
|
-
*
|
|
19
|
+
* Page-based pagination for Firestore collections (Previous / Next, one page at a time).
|
|
20
|
+
*
|
|
21
|
+
* Each page is ONE cache entry at `[...queryKey, 'page', n]` holding its rows, its last
|
|
22
|
+
* row as the next page's cursor, and whether a further page exists — read with a one-row
|
|
23
|
+
* look-ahead, so `hasNextPage` is false exactly when nothing follows the displayed page.
|
|
24
|
+
* Page n reads page n-1's cursor from the cache (loading the chain first if it is
|
|
25
|
+
* missing) and records it as `after`. A displayed page whose `after` no longer matches
|
|
26
|
+
* page n-1's cached cursor (page n-1 was re-read and its last row moved) is re-read before
|
|
27
|
+
* it is shown, so every instance, remount, and refetch of a key agrees on where a page
|
|
28
|
+
* starts. The page number belongs to the query identity (path, key, constraints, page
|
|
29
|
+
* size): a new identity renders `initialPage` in its first render.
|
|
30
|
+
*
|
|
31
|
+
* `queryKey` must encode everything that changes the read — path, filters, ordering, and
|
|
32
|
+
* page size — because the cache entries are shared by key.
|
|
33
|
+
*
|
|
34
|
+
* @example
|
|
21
35
|
* ```tsx
|
|
22
|
-
* const {
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
* hasPrevPage,
|
|
28
|
-
* isLoading,
|
|
29
|
-
* } = useFirestorePaginated<Task>({
|
|
30
|
-
* collectionPath: 'tasks',
|
|
31
|
-
* queryKey: ['tasks', 'list'],
|
|
32
|
-
* constraints: [where('status', '==', 'active'), orderBy('createdAt', 'desc')],
|
|
33
|
-
* pageSize: 10,
|
|
36
|
+
* const { data, page, nextPage, prevPage, hasNextPage, hasPrevPage } = useFirestorePaginated<Task>({
|
|
37
|
+
* collectionPath: 'tasks',
|
|
38
|
+
* queryKey: ['tasks', 'active', { pageSize: 10 }],
|
|
39
|
+
* constraints: [where('status', '==', 'active'), orderBy('createdAt', 'desc')],
|
|
40
|
+
* pageSize: 10,
|
|
34
41
|
* });
|
|
35
|
-
* * // Render pagination controls
|
|
36
|
-
* <button onClick={() => setPage(page - 1)} disabled={!hasPrevPage}>Previous</button>
|
|
37
|
-
* <span>Page {page}</span>
|
|
38
|
-
* <button onClick={() => setPage(page + 1)} disabled={!hasNextPage}>Next</button>
|
|
39
42
|
* ```
|
|
40
43
|
*/
|
|
41
|
-
export declare function useFirestorePaginated<T extends DocumentData = DocumentData>({ collectionPath, queryKey, constraints, pageSize, initialPage, enabled, staleTime, gcTime, select, }: FirestorePaginatedOptions<T>): UseFirestorePaginatedResult<T>;
|
|
44
|
+
export declare function useFirestorePaginated<T extends DocumentData = DocumentData>({ collectionPath, queryKey, constraints, pageSize, initialPage, enabled, staleTime, gcTime, refetchInterval, select, }: FirestorePaginatedOptions<T>): UseFirestorePaginatedResult<T>;
|
|
42
45
|
//# sourceMappingURL=use-firestore-paginated.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"use-firestore-paginated.d.ts","sourceRoot":"","sources":["../../../src/react/firestore/use-firestore-paginated.ts"],"names":[],"mappings":"AAGA,OAAO,EAA4B,KAAK,cAAc,EAAE,MAAM,uBAAuB,CAAC;AACtF,OAAO,EAML,KAAK,YAAY,EAElB,MAAM,oBAAoB,CAAC;
|
|
1
|
+
{"version":3,"file":"use-firestore-paginated.d.ts","sourceRoot":"","sources":["../../../src/react/firestore/use-firestore-paginated.ts"],"names":[],"mappings":"AAGA,OAAO,EAA4B,KAAK,cAAc,EAAE,MAAM,uBAAuB,CAAC;AACtF,OAAO,EAML,KAAK,YAAY,EAElB,MAAM,oBAAoB,CAAC;AAG5B,OAAO,KAAK,EAAE,yBAAyB,EAAE,aAAa,EAAE,MAAM,EAAE,MAAM,0BAA0B,CAAC;AAIjG;;;GAGG;AACH,MAAM,MAAM,2BAA2B,CAAC,CAAC,IAAI,IAAI,CAAC,cAAc,CAAC,aAAa,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,EAAE,KAAK,CAAC,EAAE,MAAM,CAAC,GAAG;IAC3G,IAAI,EAAE,MAAM,CAAC,CAAC,CAAC,EAAE,GAAG,SAAS,CAAC;IAC9B,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE,MAAM,CAAC;IACjB,WAAW,EAAE,OAAO,CAAC;IACrB,WAAW,EAAE,OAAO,CAAC;IACrB,OAAO,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;IAChC,QAAQ,EAAE,MAAM,IAAI,CAAC;IACrB,QAAQ,EAAE,MAAM,IAAI,CAAC;CACtB,CAAC;AAEF;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AACH,wBAAgB,qBAAqB,CAAC,CAAC,SAAS,YAAY,GAAG,YAAY,EAAE,EAC3E,cAAc,EACd,QAAQ,EACR,WAAgB,EAChB,QAA4B,EAC5B,WAAe,EACf,OAAc,EACd,SAAS,EACT,MAAM,EACN,eAAe,EACf,MAAM,GACP,EAAE,yBAAyB,CAAC,CAAC,CAAC,GAAG,2BAA2B,CAAC,CAAC,CAAC,CAmH/D"}
|
|
@@ -1,122 +1,132 @@
|
|
|
1
1
|
'use client';
|
|
2
|
-
import { useState, useCallback,
|
|
2
|
+
import { useState, useCallback, useEffect, useRef } from 'react';
|
|
3
3
|
import { useQuery, useQueryClient } from '@tanstack/react-query';
|
|
4
4
|
import { collection, query, getDocs, limit, startAfter, } from 'firebase/firestore';
|
|
5
5
|
import { useFirestoreDb } from './context.js';
|
|
6
|
+
import { paginatedPageKey } from '../../cache-helpers.js';
|
|
6
7
|
const DEFAULT_PAGE_SIZE = 10;
|
|
7
|
-
const MAX_CACHED_CURSORS = 20;
|
|
8
8
|
/**
|
|
9
|
-
* Page-based pagination for Firestore collections.
|
|
10
|
-
*
|
|
11
|
-
*
|
|
9
|
+
* Page-based pagination for Firestore collections (Previous / Next, one page at a time).
|
|
10
|
+
*
|
|
11
|
+
* Each page is ONE cache entry at `[...queryKey, 'page', n]` holding its rows, its last
|
|
12
|
+
* row as the next page's cursor, and whether a further page exists — read with a one-row
|
|
13
|
+
* look-ahead, so `hasNextPage` is false exactly when nothing follows the displayed page.
|
|
14
|
+
* Page n reads page n-1's cursor from the cache (loading the chain first if it is
|
|
15
|
+
* missing) and records it as `after`. A displayed page whose `after` no longer matches
|
|
16
|
+
* page n-1's cached cursor (page n-1 was re-read and its last row moved) is re-read before
|
|
17
|
+
* it is shown, so every instance, remount, and refetch of a key agrees on where a page
|
|
18
|
+
* starts. The page number belongs to the query identity (path, key, constraints, page
|
|
19
|
+
* size): a new identity renders `initialPage` in its first render.
|
|
20
|
+
*
|
|
21
|
+
* `queryKey` must encode everything that changes the read — path, filters, ordering, and
|
|
22
|
+
* page size — because the cache entries are shared by key.
|
|
23
|
+
*
|
|
24
|
+
* @example
|
|
12
25
|
* ```tsx
|
|
13
|
-
* const {
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
* hasPrevPage,
|
|
19
|
-
* isLoading,
|
|
20
|
-
* } = useFirestorePaginated<Task>({
|
|
21
|
-
* collectionPath: 'tasks',
|
|
22
|
-
* queryKey: ['tasks', 'list'],
|
|
23
|
-
* constraints: [where('status', '==', 'active'), orderBy('createdAt', 'desc')],
|
|
24
|
-
* pageSize: 10,
|
|
26
|
+
* const { data, page, nextPage, prevPage, hasNextPage, hasPrevPage } = useFirestorePaginated<Task>({
|
|
27
|
+
* collectionPath: 'tasks',
|
|
28
|
+
* queryKey: ['tasks', 'active', { pageSize: 10 }],
|
|
29
|
+
* constraints: [where('status', '==', 'active'), orderBy('createdAt', 'desc')],
|
|
30
|
+
* pageSize: 10,
|
|
25
31
|
* });
|
|
26
|
-
* * // Render pagination controls
|
|
27
|
-
* <button onClick={() => setPage(page - 1)} disabled={!hasPrevPage}>Previous</button>
|
|
28
|
-
* <span>Page {page}</span>
|
|
29
|
-
* <button onClick={() => setPage(page + 1)} disabled={!hasNextPage}>Next</button>
|
|
30
32
|
* ```
|
|
31
33
|
*/
|
|
32
|
-
export function useFirestorePaginated({ collectionPath, queryKey, constraints = [], pageSize = DEFAULT_PAGE_SIZE, initialPage = 1, enabled = true, staleTime, gcTime, select, }) {
|
|
34
|
+
export function useFirestorePaginated({ collectionPath, queryKey, constraints = [], pageSize = DEFAULT_PAGE_SIZE, initialPage = 1, enabled = true, staleTime, gcTime, refetchInterval, select, }) {
|
|
33
35
|
const db = useFirestoreDb();
|
|
34
36
|
const queryClient = useQueryClient();
|
|
35
|
-
const
|
|
36
|
-
const
|
|
37
|
-
const [
|
|
38
|
-
|
|
39
|
-
const
|
|
40
|
-
const
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
...(cursor ? [startAfter(cursor)] : []),
|
|
50
|
-
limit(pageSize),
|
|
51
|
-
];
|
|
52
|
-
const q = query(collectionRef, ...queryConstraints);
|
|
53
|
-
const snapshot = await getDocs(q);
|
|
54
|
-
// Store the last doc as cursor for next page
|
|
55
|
-
if (snapshot.docs.length > 0) {
|
|
56
|
-
const lastDoc = snapshot.docs[snapshot.docs.length - 1];
|
|
57
|
-
setCursors((prev) => {
|
|
58
|
-
const next = new Map(prev);
|
|
59
|
-
next.set(page, lastDoc);
|
|
60
|
-
// Prune old cursors if too many (prevent memory leaks)
|
|
61
|
-
if (next.size > MAX_CACHED_CURSORS) {
|
|
62
|
-
const sorted = Array.from(next.keys()).sort((a, b) => a - b);
|
|
63
|
-
const toDelete = sorted.slice(0, sorted.length - MAX_CACHED_CURSORS);
|
|
64
|
-
toDelete.forEach((k) => next.delete(k));
|
|
65
|
-
}
|
|
66
|
-
return next;
|
|
67
|
-
});
|
|
68
|
-
}
|
|
69
|
-
// Determine if there are more pages
|
|
70
|
-
setHasMore(snapshot.docs.length === pageSize);
|
|
71
|
-
return snapshot.docs.map((docSnap) => {
|
|
72
|
-
const rawData = docSnap.data();
|
|
73
|
-
// Include id in data passed to select, so it can be renamed/transformed
|
|
74
|
-
const dataWithId = { id: docSnap.id, ...rawData };
|
|
75
|
-
const data = select ? select(dataWithId) : dataWithId;
|
|
76
|
-
return data;
|
|
37
|
+
const queryKeyMemo = JSON.stringify(queryKey);
|
|
38
|
+
const constraintsMemo = JSON.stringify(constraints);
|
|
39
|
+
const identity = JSON.stringify([collectionPath, queryKeyMemo, constraintsMemo, pageSize]);
|
|
40
|
+
const firstPage = Math.max(1, Math.floor(initialPage));
|
|
41
|
+
const [pageState, setPageState] = useState(() => ({ identity, page: firstPage }));
|
|
42
|
+
const page = pageState.identity === identity ? pageState.page : firstPage;
|
|
43
|
+
const selectRef = useRef(select);
|
|
44
|
+
selectRef.current = select;
|
|
45
|
+
const fetchPage = useCallback(async (n) => {
|
|
46
|
+
let cursor = null;
|
|
47
|
+
if (n > 1) {
|
|
48
|
+
const previous = await queryClient.ensureQueryData({
|
|
49
|
+
queryKey: paginatedPageKey(queryKey, n - 1),
|
|
50
|
+
queryFn: () => fetchPage(n - 1),
|
|
77
51
|
});
|
|
78
|
-
|
|
52
|
+
if (!previous.hasMore || !previous.cursor) {
|
|
53
|
+
return { items: [], cursor: null, hasMore: false, after: previous.cursor?.id ?? null };
|
|
54
|
+
}
|
|
55
|
+
cursor = previous.cursor;
|
|
56
|
+
}
|
|
57
|
+
const q = query(collection(db, collectionPath), ...constraints, ...(cursor ? [startAfter(cursor)] : []), limit(pageSize + 1));
|
|
58
|
+
const snapshot = await getDocs(q);
|
|
59
|
+
const pageDocs = snapshot.docs.slice(0, pageSize);
|
|
60
|
+
return {
|
|
61
|
+
items: pageDocs.map((docSnap) => {
|
|
62
|
+
const dataWithId = { id: docSnap.id, ...docSnap.data() };
|
|
63
|
+
const mapped = selectRef.current ? selectRef.current(dataWithId) : dataWithId;
|
|
64
|
+
return mapped;
|
|
65
|
+
}),
|
|
66
|
+
cursor: pageDocs[pageDocs.length - 1] ?? null,
|
|
67
|
+
hasMore: snapshot.docs.length > pageSize,
|
|
68
|
+
after: cursor?.id ?? null,
|
|
69
|
+
};
|
|
70
|
+
},
|
|
71
|
+
// queryKey and constraints are captured through their serialized identity.
|
|
72
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
73
|
+
[db, queryClient, identity, selectRef]);
|
|
74
|
+
const queryResult = useQuery({
|
|
75
|
+
queryKey: paginatedPageKey(queryKey, page),
|
|
76
|
+
queryFn: () => fetchPage(page),
|
|
79
77
|
enabled,
|
|
80
78
|
staleTime,
|
|
81
79
|
gcTime,
|
|
80
|
+
refetchInterval,
|
|
82
81
|
});
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
82
|
+
// The page before the displayed one, observed from the cache only (never fetched here).
|
|
83
|
+
// When it is re-read and its last row moves, the displayed entry no longer starts where the
|
|
84
|
+
// chain says it must: it is re-read before it is shown as current.
|
|
85
|
+
const previousPage = useQuery({
|
|
86
|
+
queryKey: paginatedPageKey(queryKey, Math.max(1, page - 1)),
|
|
87
|
+
queryFn: () => fetchPage(Math.max(1, page - 1)),
|
|
88
|
+
enabled: false,
|
|
89
|
+
});
|
|
90
|
+
const cachedEntry = queryResult.data;
|
|
91
|
+
const outOfChain = page > 1 &&
|
|
92
|
+
cachedEntry !== undefined &&
|
|
93
|
+
(previousPage.data === undefined || cachedEntry.after !== (previousPage.data.cursor?.id ?? null));
|
|
94
|
+
const { refetch, isFetching } = queryResult;
|
|
95
|
+
useEffect(() => {
|
|
96
|
+
if (enabled && outOfChain && !isFetching)
|
|
97
|
+
void refetch();
|
|
98
|
+
}, [enabled, outOfChain, isFetching, refetch]);
|
|
99
|
+
const entry = outOfChain ? undefined : cachedEntry;
|
|
100
|
+
const hasNextPage = Boolean(entry?.hasMore && entry.cursor);
|
|
101
|
+
const setPage = useCallback((target) => {
|
|
102
|
+
const next = Math.floor(target);
|
|
103
|
+
if (next < 1 || next === page)
|
|
89
104
|
return;
|
|
105
|
+
if (next > page + 1) {
|
|
106
|
+
const before = queryClient.getQueryData(paginatedPageKey(queryKey, next - 1));
|
|
107
|
+
if (!before?.hasMore)
|
|
108
|
+
return;
|
|
90
109
|
}
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
}, [page, setPage]);
|
|
101
|
-
const resetToFirstPage = useCallback(() => {
|
|
102
|
-
setCursors(new Map());
|
|
103
|
-
setPageInternal(1);
|
|
104
|
-
setHasMore(true);
|
|
105
|
-
// Invalidate all cached pages
|
|
106
|
-
queryClient.invalidateQueries({ queryKey, exact: false });
|
|
107
|
-
}, [queryClient, queryKey]);
|
|
110
|
+
if (next === page + 1 && !hasNextPage)
|
|
111
|
+
return;
|
|
112
|
+
setPageState({ identity, page: next });
|
|
113
|
+
},
|
|
114
|
+
// queryKey is captured through its serialized identity.
|
|
115
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
116
|
+
[page, hasNextPage, identity, queryClient]);
|
|
117
|
+
const nextPage = useCallback(() => setPage(page + 1), [page, setPage]);
|
|
118
|
+
const prevPage = useCallback(() => setPage(page - 1), [page, setPage]);
|
|
108
119
|
return {
|
|
109
120
|
...queryResult,
|
|
110
|
-
|
|
121
|
+
...(outOfChain ? { isLoading: true, isPending: true, isSuccess: false, status: 'pending' } : {}),
|
|
122
|
+
data: entry?.items,
|
|
111
123
|
page,
|
|
112
124
|
pageSize,
|
|
113
|
-
hasNextPage
|
|
125
|
+
hasNextPage,
|
|
114
126
|
hasPrevPage: page > 1,
|
|
115
|
-
// Navigation functions
|
|
116
127
|
setPage,
|
|
117
128
|
nextPage,
|
|
118
129
|
prevPage,
|
|
119
|
-
resetToFirstPage,
|
|
120
130
|
};
|
|
121
131
|
}
|
|
122
132
|
//# sourceMappingURL=use-firestore-paginated.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"use-firestore-paginated.js","sourceRoot":"","sources":["../../../src/react/firestore/use-firestore-paginated.ts"],"names":[],"mappings":"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","sourcesContent":["'use client';\n\nimport { useState, useCallback, useMemo } from 'react';\nimport { useQuery, useQueryClient, type UseQueryResult } from '@tanstack/react-query';\nimport {\n collection,\n query,\n getDocs,\n limit,\n startAfter,\n type DocumentData,\n type DocumentSnapshot,\n} from 'firebase/firestore';\nimport { useFirestoreDb } from './context.js';\nimport type { FirestorePaginatedOptions, WithId } from '../../firestore/types.js';\n\nconst DEFAULT_PAGE_SIZE = 10;\nconst MAX_CACHED_CURSORS = 20;\n\n/**\n * Return type for useFirestorePaginated hook\n */\nexport type UseFirestorePaginatedResult<T> = UseQueryResult<WithId<T>[], Error> & {\n page: number;\n pageSize: number;\n hasNextPage: boolean;\n hasPrevPage: boolean;\n setPage: (page: number) => void;\n nextPage: () => void;\n prevPage: () => void;\n resetToFirstPage: () => void;\n};\n\n/**\n * Page-based pagination for Firestore collections.\n * Supports navigating to specific pages (1, 2, 3...) rather than infinite scroll.\n * * @example\n * ```tsx\n * const {\n * data,\n * page,\n * setPage,\n * hasNextPage,\n * hasPrevPage,\n * isLoading,\n * } = useFirestorePaginated<Task>({\n * collectionPath: 'tasks',\n * queryKey: ['tasks', 'list'],\n * constraints: [where('status', '==', 'active'), orderBy('createdAt', 'desc')],\n * pageSize: 10,\n * });\n * * // Render pagination controls\n * <button onClick={() => setPage(page - 1)} disabled={!hasPrevPage}>Previous</button>\n * <span>Page {page}</span>\n * <button onClick={() => setPage(page + 1)} disabled={!hasNextPage}>Next</button>\n * ```\n */\nexport function useFirestorePaginated<T extends DocumentData = DocumentData>({\n collectionPath,\n queryKey,\n constraints = [],\n pageSize = DEFAULT_PAGE_SIZE,\n initialPage = 1,\n enabled = true,\n staleTime,\n gcTime,\n select,\n}: FirestorePaginatedOptions<T>): UseFirestorePaginatedResult<T> {\n const db = useFirestoreDb();\n const queryClient = useQueryClient();\n\n const [page, setPageInternal] = useState(initialPage);\n const [cursors, setCursors] = useState<Map<number, DocumentSnapshot>>(new Map());\n const [hasMore, setHasMore] = useState(true);\n\n // Create a stable query key that includes the page\n const pageQueryKey = useMemo(\n () => [...queryKey, 'page', page],\n [queryKey, page]\n );\n\n const queryResult = useQuery({\n queryKey: pageQueryKey,\n queryFn: async (): Promise<WithId<T>[]> => {\n const collectionRef = collection(db, collectionPath);\n\n // Get cursor for current page (if not page 1)\n const cursor = page > 1 ? cursors.get(page - 1) : undefined;\n\n // Build query\n const queryConstraints = [\n ...constraints,\n ...(cursor ? [startAfter(cursor)] : []),\n limit(pageSize),\n ];\n\n const q = query(collectionRef, ...queryConstraints);\n const snapshot = await getDocs(q);\n\n // Store the last doc as cursor for next page\n if (snapshot.docs.length > 0) {\n const lastDoc = snapshot.docs[snapshot.docs.length - 1];\n setCursors((prev) => {\n const next = new Map(prev);\n next.set(page, lastDoc);\n\n // Prune old cursors if too many (prevent memory leaks)\n if (next.size > MAX_CACHED_CURSORS) {\n const sorted = Array.from(next.keys()).sort((a, b) => a - b);\n const toDelete = sorted.slice(0, sorted.length - MAX_CACHED_CURSORS);\n toDelete.forEach((k) => next.delete(k));\n }\n\n return next;\n });\n }\n\n // Determine if there are more pages\n setHasMore(snapshot.docs.length === pageSize);\n\n return snapshot.docs.map((docSnap) => {\n const rawData = docSnap.data();\n // Include id in data passed to select, so it can be renamed/transformed\n const dataWithId = { id: docSnap.id, ...rawData };\n const data = select ? select(dataWithId) : dataWithId;\n return data as WithId<T>;\n });\n },\n enabled,\n staleTime,\n gcTime,\n });\n\n const setPage = useCallback((newPage: number) => {\n if (newPage < 1) return;\n\n // Can only go forward one page at a time if cursor doesn't exist\n if (newPage > page + 1 && !cursors.has(newPage - 1)) {\n console.warn('[useFirestorePaginated] Cannot skip pages. Navigate sequentially.');\n return;\n }\n\n setPageInternal(newPage);\n }, [page, cursors]);\n\n const nextPage = useCallback(() => {\n if (hasMore) setPage(page + 1);\n }, [page, hasMore, setPage]);\n\n const prevPage = useCallback(() => {\n if (page > 1) setPage(page - 1);\n }, [page, setPage]);\n\n const resetToFirstPage = useCallback(() => {\n setCursors(new Map());\n setPageInternal(1);\n setHasMore(true);\n // Invalidate all cached pages\n queryClient.invalidateQueries({ queryKey, exact: false });\n }, [queryClient, queryKey]);\n\n return {\n ...queryResult,\n // Pagination state\n page,\n pageSize,\n hasNextPage: hasMore,\n hasPrevPage: page > 1,\n // Navigation functions\n setPage,\n nextPage,\n prevPage,\n resetToFirstPage,\n };\n}\n"]}
|
|
1
|
+
{"version":3,"file":"use-firestore-paginated.js","sourceRoot":"","sources":["../../../src/react/firestore/use-firestore-paginated.ts"],"names":[],"mappings":"AAAA,YAAY,CAAC;AAEb,OAAO,EAAE,QAAQ,EAAE,WAAW,EAAE,SAAS,EAAE,MAAM,EAAE,MAAM,OAAO,CAAC;AACjE,OAAO,EAAE,QAAQ,EAAE,cAAc,EAAuB,MAAM,uBAAuB,CAAC;AACtF,OAAO,EACL,UAAU,EACV,KAAK,EACL,OAAO,EACP,KAAK,EACL,UAAU,GAGX,MAAM,oBAAoB,CAAC;AAC5B,OAAO,EAAE,cAAc,EAAE,MAAM,cAAc,CAAC;AAC9C,OAAO,EAAE,gBAAgB,EAAE,MAAM,wBAAwB,CAAC;AAG1D,MAAM,iBAAiB,GAAG,EAAE,CAAC;AAiB7B;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AACH,MAAM,UAAU,qBAAqB,CAAwC,EAC3E,cAAc,EACd,QAAQ,EACR,WAAW,GAAG,EAAE,EAChB,QAAQ,GAAG,iBAAiB,EAC5B,WAAW,GAAG,CAAC,EACf,OAAO,GAAG,IAAI,EACd,SAAS,EACT,MAAM,EACN,eAAe,EACf,MAAM,GACuB;IAC7B,MAAM,EAAE,GAAG,cAAc,EAAE,CAAC;IAC5B,MAAM,WAAW,GAAG,cAAc,EAAE,CAAC;IAErC,MAAM,YAAY,GAAG,IAAI,CAAC,SAAS,CAAC,QAAQ,CAAC,CAAC;IAC9C,MAAM,eAAe,GAAG,IAAI,CAAC,SAAS,CAAC,WAAW,CAAC,CAAC;IACpD,MAAM,QAAQ,GAAG,IAAI,CAAC,SAAS,CAAC,CAAC,cAAc,EAAE,YAAY,EAAE,eAAe,EAAE,QAAQ,CAAC,CAAC,CAAC;IAC3F,MAAM,SAAS,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,IAAI,CAAC,KAAK,CAAC,WAAW,CAAC,CAAC,CAAC;IAEvD,MAAM,CAAC,SAAS,EAAE,YAAY,CAAC,GAAG,QAAQ,CAAC,GAAG,EAAE,CAAC,CAAC,EAAE,QAAQ,EAAE,IAAI,EAAE,SAAS,EAAE,CAAC,CAAC,CAAC;IAClF,MAAM,IAAI,GAAG,SAAS,CAAC,QAAQ,KAAK,QAAQ,CAAC,CAAC,CAAC,SAAS,CAAC,IAAI,CAAC,CAAC,CAAC,SAAS,CAAC;IAE1E,MAAM,SAAS,GAAG,MAAM,CAAC,MAAM,CAAC,CAAC;IACjC,SAAS,CAAC,OAAO,GAAG,MAAM,CAAC;IAE3B,MAAM,SAAS,GAAG,WAAW,CAC3B,KAAK,EAAE,CAAS,EAAqC,EAAE;QACrD,IAAI,MAAM,GAA4B,IAAI,CAAC;QAC3C,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC;YACV,MAAM,QAAQ,GAAG,MAAM,WAAW,CAAC,eAAe,CAA2B;gBAC3E,QAAQ,EAAE,gBAAgB,CAAC,QAAQ,EAAE,CAAC,GAAG,CAAC,CAAC;gBAC3C,OAAO,EAAE,GAAG,EAAE,CAAC,SAAS,CAAC,CAAC,GAAG,CAAC,CAAC;aAChC,CAAC,CAAC;YACH,IAAI,CAAC,QAAQ,CAAC,OAAO,IAAI,CAAC,QAAQ,CAAC,MAAM,EAAE,CAAC;gBAC1C,OAAO,EAAE,KAAK,EAAE,EAAE,EAAE,MAAM,EAAE,IAAI,EAAE,OAAO,EAAE,KAAK,EAAE,KAAK,EAAE,QAAQ,CAAC,MAAM,EAAE,EAAE,IAAI,IAAI,EAAE,CAAC;YACzF,CAAC;YACD,MAAM,GAAG,QAAQ,CAAC,MAAM,CAAC;QAC3B,CAAC;QACD,MAAM,CAAC,GAAG,KAAK,CACb,UAAU,CAAC,EAAE,EAAE,cAAc,CAAC,EAC9B,GAAG,WAAW,EACd,GAAG,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,UAAU,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,EACvC,KAAK,CAAC,QAAQ,GAAG,CAAC,CAAC,CACpB,CAAC;QACF,MAAM,QAAQ,GAAG,MAAM,OAAO,CAAC,CAAC,CAAC,CAAC;QAClC,MAAM,QAAQ,GAAG,QAAQ,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,EAAE,QAAQ,CAAC,CAAC;QAClD,OAAO;YACL,KAAK,EAAE,QAAQ,CAAC,GAAG,CAAC,CAAC,OAAO,EAAE,EAAE;gBAC9B,MAAM,UAAU,GAAG,EAAE,EAAE,EAAE,OAAO,CAAC,EAAE,EAAE,GAAG,OAAO,CAAC,IAAI,EAAE,EAAE,CAAC;gBACzD,MAAM,MAAM,GAAG,SAAS,CAAC,OAAO,CAAC,CAAC,CAAC,SAAS,CAAC,OAAO,CAAC,UAAU,CAAC,CAAC,CAAC,CAAC,UAAU,CAAC;gBAC9E,OAAO,MAAmB,CAAC;YAC7B,CAAC,CAAC;YACF,MAAM,EAAE,QAAQ,CAAC,QAAQ,CAAC,MAAM,GAAG,CAAC,CAAC,IAAI,IAAI;YAC7C,OAAO,EAAE,QAAQ,CAAC,IAAI,CAAC,MAAM,GAAG,QAAQ;YACxC,KAAK,EAAE,MAAM,EAAE,EAAE,IAAI,IAAI;SAC1B,CAAC;IACJ,CAAC;IACD,2EAA2E;IAC3E,uDAAuD;IACvD,CAAC,EAAE,EAAE,WAAW,EAAE,QAAQ,EAAE,SAAS,CAAC,CACvC,CAAC;IAEF,MAAM,WAAW,GAAG,QAAQ,CAAC;QAC3B,QAAQ,EAAE,gBAAgB,CAAC,QAAQ,EAAE,IAAI,CAAC;QAC1C,OAAO,EAAE,GAAG,EAAE,CAAC,SAAS,CAAC,IAAI,CAAC;QAC9B,OAAO;QACP,SAAS;QACT,MAAM;QACN,eAAe;KAChB,CAAC,CAAC;IAEH,wFAAwF;IACxF,4FAA4F;IAC5F,mEAAmE;IACnE,MAAM,YAAY,GAAG,QAAQ,CAAC;QAC5B,QAAQ,EAAE,gBAAgB,CAAC,QAAQ,EAAE,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,IAAI,GAAG,CAAC,CAAC,CAAC;QAC3D,OAAO,EAAE,GAAG,EAAE,CAAC,SAAS,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,IAAI,GAAG,CAAC,CAAC,CAAC;QAC/C,OAAO,EAAE,KAAK;KACf,CAAC,CAAC;IACH,MAAM,WAAW,GAAG,WAAW,CAAC,IAAI,CAAC;IACrC,MAAM,UAAU,GACd,IAAI,GAAG,CAAC;QACR,WAAW,KAAK,SAAS;QACzB,CAAC,YAAY,CAAC,IAAI,KAAK,SAAS,IAAI,WAAW,CAAC,KAAK,KAAK,CAAC,YAAY,CAAC,IAAI,CAAC,MAAM,EAAE,EAAE,IAAI,IAAI,CAAC,CAAC,CAAC;IACpG,MAAM,EAAE,OAAO,EAAE,UAAU,EAAE,GAAG,WAAW,CAAC;IAC5C,SAAS,CAAC,GAAG,EAAE;QACb,IAAI,OAAO,IAAI,UAAU,IAAI,CAAC,UAAU;YAAE,KAAK,OAAO,EAAE,CAAC;IAC3D,CAAC,EAAE,CAAC,OAAO,EAAE,UAAU,EAAE,UAAU,EAAE,OAAO,CAAC,CAAC,CAAC;IAE/C,MAAM,KAAK,GAAG,UAAU,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,WAAW,CAAC;IACnD,MAAM,WAAW,GAAG,OAAO,CAAC,KAAK,EAAE,OAAO,IAAI,KAAK,CAAC,MAAM,CAAC,CAAC;IAE5D,MAAM,OAAO,GAAG,WAAW,CACzB,CAAC,MAAc,EAAE,EAAE;QACjB,MAAM,IAAI,GAAG,IAAI,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC;QAChC,IAAI,IAAI,GAAG,CAAC,IAAI,IAAI,KAAK,IAAI;YAAE,OAAO;QACtC,IAAI,IAAI,GAAG,IAAI,GAAG,CAAC,EAAE,CAAC;YACpB,MAAM,MAAM,GAAG,WAAW,CAAC,YAAY,CACrC,gBAAgB,CAAC,QAAQ,EAAE,IAAI,GAAG,CAAC,CAAC,CACrC,CAAC;YACF,IAAI,CAAC,MAAM,EAAE,OAAO;gBAAE,OAAO;QAC/B,CAAC;QACD,IAAI,IAAI,KAAK,IAAI,GAAG,CAAC,IAAI,CAAC,WAAW;YAAE,OAAO;QAC9C,YAAY,CAAC,EAAE,QAAQ,EAAE,IAAI,EAAE,IAAI,EAAE,CAAC,CAAC;IACzC,CAAC;IACD,wDAAwD;IACxD,uDAAuD;IACvD,CAAC,IAAI,EAAE,WAAW,EAAE,QAAQ,EAAE,WAAW,CAAC,CAC3C,CAAC;IAEF,MAAM,QAAQ,GAAG,WAAW,CAAC,GAAG,EAAE,CAAC,OAAO,CAAC,IAAI,GAAG,CAAC,CAAC,EAAE,CAAC,IAAI,EAAE,OAAO,CAAC,CAAC,CAAC;IACvE,MAAM,QAAQ,GAAG,WAAW,CAAC,GAAG,EAAE,CAAC,OAAO,CAAC,IAAI,GAAG,CAAC,CAAC,EAAE,CAAC,IAAI,EAAE,OAAO,CAAC,CAAC,CAAC;IAEvE,OAAO;QACL,GAAG,WAAW;QACd,GAAG,CAAC,UAAU,CAAC,CAAC,CAAC,EAAE,SAAS,EAAE,IAAI,EAAE,SAAS,EAAE,IAAI,EAAE,SAAS,EAAE,KAAK,EAAE,MAAM,EAAE,SAAkB,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;QACzG,IAAI,EAAE,KAAK,EAAE,KAAK;QAClB,IAAI;QACJ,QAAQ;QACR,WAAW;QACX,WAAW,EAAE,IAAI,GAAG,CAAC;QACrB,OAAO;QACP,QAAQ;QACR,QAAQ;KACyB,CAAC;AACtC,CAAC","sourcesContent":["'use client';\n\nimport { useState, useCallback, useEffect, useRef } from 'react';\nimport { useQuery, useQueryClient, type UseQueryResult } from '@tanstack/react-query';\nimport {\n collection,\n query,\n getDocs,\n limit,\n startAfter,\n type DocumentData,\n type DocumentSnapshot,\n} from 'firebase/firestore';\nimport { useFirestoreDb } from './context.js';\nimport { paginatedPageKey } from '../../cache-helpers.js';\nimport type { FirestorePaginatedOptions, PaginatedPage, WithId } from '../../firestore/types.js';\n\nconst DEFAULT_PAGE_SIZE = 10;\n\n/**\n * Return type for useFirestorePaginated hook. `data` is the displayed page's rows; the\n * page's cache entry (rows, cursor, has-more) lives at `paginatedPageKey(queryKey, page)`.\n */\nexport type UseFirestorePaginatedResult<T> = Omit<UseQueryResult<PaginatedPage<WithId<T>>, Error>, 'data'> & {\n data: WithId<T>[] | undefined;\n page: number;\n pageSize: number;\n hasNextPage: boolean;\n hasPrevPage: boolean;\n setPage: (page: number) => void;\n nextPage: () => void;\n prevPage: () => void;\n};\n\n/**\n * Page-based pagination for Firestore collections (Previous / Next, one page at a time).\n *\n * Each page is ONE cache entry at `[...queryKey, 'page', n]` holding its rows, its last\n * row as the next page's cursor, and whether a further page exists — read with a one-row\n * look-ahead, so `hasNextPage` is false exactly when nothing follows the displayed page.\n * Page n reads page n-1's cursor from the cache (loading the chain first if it is\n * missing) and records it as `after`. A displayed page whose `after` no longer matches\n * page n-1's cached cursor (page n-1 was re-read and its last row moved) is re-read before\n * it is shown, so every instance, remount, and refetch of a key agrees on where a page\n * starts. The page number belongs to the query identity (path, key, constraints, page\n * size): a new identity renders `initialPage` in its first render.\n *\n * `queryKey` must encode everything that changes the read — path, filters, ordering, and\n * page size — because the cache entries are shared by key.\n *\n * @example\n * ```tsx\n * const { data, page, nextPage, prevPage, hasNextPage, hasPrevPage } = useFirestorePaginated<Task>({\n * collectionPath: 'tasks',\n * queryKey: ['tasks', 'active', { pageSize: 10 }],\n * constraints: [where('status', '==', 'active'), orderBy('createdAt', 'desc')],\n * pageSize: 10,\n * });\n * ```\n */\nexport function useFirestorePaginated<T extends DocumentData = DocumentData>({\n collectionPath,\n queryKey,\n constraints = [],\n pageSize = DEFAULT_PAGE_SIZE,\n initialPage = 1,\n enabled = true,\n staleTime,\n gcTime,\n refetchInterval,\n select,\n}: FirestorePaginatedOptions<T>): UseFirestorePaginatedResult<T> {\n const db = useFirestoreDb();\n const queryClient = useQueryClient();\n\n const queryKeyMemo = JSON.stringify(queryKey);\n const constraintsMemo = JSON.stringify(constraints);\n const identity = JSON.stringify([collectionPath, queryKeyMemo, constraintsMemo, pageSize]);\n const firstPage = Math.max(1, Math.floor(initialPage));\n\n const [pageState, setPageState] = useState(() => ({ identity, page: firstPage }));\n const page = pageState.identity === identity ? pageState.page : firstPage;\n\n const selectRef = useRef(select);\n selectRef.current = select;\n\n const fetchPage = useCallback(\n async (n: number): Promise<PaginatedPage<WithId<T>>> => {\n let cursor: DocumentSnapshot | null = null;\n if (n > 1) {\n const previous = await queryClient.ensureQueryData<PaginatedPage<WithId<T>>>({\n queryKey: paginatedPageKey(queryKey, n - 1),\n queryFn: () => fetchPage(n - 1),\n });\n if (!previous.hasMore || !previous.cursor) {\n return { items: [], cursor: null, hasMore: false, after: previous.cursor?.id ?? null };\n }\n cursor = previous.cursor;\n }\n const q = query(\n collection(db, collectionPath),\n ...constraints,\n ...(cursor ? [startAfter(cursor)] : []),\n limit(pageSize + 1),\n );\n const snapshot = await getDocs(q);\n const pageDocs = snapshot.docs.slice(0, pageSize);\n return {\n items: pageDocs.map((docSnap) => {\n const dataWithId = { id: docSnap.id, ...docSnap.data() };\n const mapped = selectRef.current ? selectRef.current(dataWithId) : dataWithId;\n return mapped as WithId<T>;\n }),\n cursor: pageDocs[pageDocs.length - 1] ?? null,\n hasMore: snapshot.docs.length > pageSize,\n after: cursor?.id ?? null,\n };\n },\n // queryKey and constraints are captured through their serialized identity.\n // eslint-disable-next-line react-hooks/exhaustive-deps\n [db, queryClient, identity, selectRef],\n );\n\n const queryResult = useQuery({\n queryKey: paginatedPageKey(queryKey, page),\n queryFn: () => fetchPage(page),\n enabled,\n staleTime,\n gcTime,\n refetchInterval,\n });\n\n // The page before the displayed one, observed from the cache only (never fetched here).\n // When it is re-read and its last row moves, the displayed entry no longer starts where the\n // chain says it must: it is re-read before it is shown as current.\n const previousPage = useQuery({\n queryKey: paginatedPageKey(queryKey, Math.max(1, page - 1)),\n queryFn: () => fetchPage(Math.max(1, page - 1)),\n enabled: false,\n });\n const cachedEntry = queryResult.data;\n const outOfChain =\n page > 1 &&\n cachedEntry !== undefined &&\n (previousPage.data === undefined || cachedEntry.after !== (previousPage.data.cursor?.id ?? null));\n const { refetch, isFetching } = queryResult;\n useEffect(() => {\n if (enabled && outOfChain && !isFetching) void refetch();\n }, [enabled, outOfChain, isFetching, refetch]);\n\n const entry = outOfChain ? undefined : cachedEntry;\n const hasNextPage = Boolean(entry?.hasMore && entry.cursor);\n\n const setPage = useCallback(\n (target: number) => {\n const next = Math.floor(target);\n if (next < 1 || next === page) return;\n if (next > page + 1) {\n const before = queryClient.getQueryData<PaginatedPage<unknown>>(\n paginatedPageKey(queryKey, next - 1),\n );\n if (!before?.hasMore) return;\n }\n if (next === page + 1 && !hasNextPage) return;\n setPageState({ identity, page: next });\n },\n // queryKey is captured through its serialized identity.\n // eslint-disable-next-line react-hooks/exhaustive-deps\n [page, hasNextPage, identity, queryClient],\n );\n\n const nextPage = useCallback(() => setPage(page + 1), [page, setPage]);\n const prevPage = useCallback(() => setPage(page - 1), [page, setPage]);\n\n return {\n ...queryResult,\n ...(outOfChain ? { isLoading: true, isPending: true, isSuccess: false, status: 'pending' as const } : {}),\n data: entry?.items,\n page,\n pageSize,\n hasNextPage,\n hasPrevPage: page > 1,\n setPage,\n nextPage,\n prevPage,\n } as UseFirestorePaginatedResult<T>;\n}\n"]}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { Firestore } from 'firebase/firestore';
|
|
2
|
+
import type { BatchFirestoreDocOutcome } from '../../firestore/types.js';
|
|
2
3
|
/** Transport used to resolve an id in one-shot mode. */
|
|
3
4
|
export type BatchFirestoreDocsTransport = 'batch' | 'get';
|
|
4
5
|
export type BatchFirestoreDocsOptions = {
|
|
@@ -97,6 +98,14 @@ export type BatchFirestoreDocsResult<T> = {
|
|
|
97
98
|
* First error across the requested ids, if any
|
|
98
99
|
*/
|
|
99
100
|
error: Error | null;
|
|
101
|
+
/**
|
|
102
|
+
* One entry per requested id (empty ids and duplicates dropped): `loading`, `present` with
|
|
103
|
+
* its data, `absent`, or `failed` with that id's own error. A multi-id consumer reads which
|
|
104
|
+
* ids are missing and which failed here — `data` / `isError` / `error` are the aggregate
|
|
105
|
+
* view and cannot tell them apart. The map, and each unchanged entry, keep their identity
|
|
106
|
+
* across renders.
|
|
107
|
+
*/
|
|
108
|
+
outcomes: Record<string, BatchFirestoreDocOutcome<T>>;
|
|
100
109
|
/**
|
|
101
110
|
* Refetch every requested id, cached ones included. A no-op in subscribe mode.
|
|
102
111
|
*/
|
|
@@ -115,6 +124,9 @@ export type BatchFirestoreDocsResult<T> = {
|
|
|
115
124
|
* an absent id is then re-read on a bounded ladder (see `absentRetryDelaysMs`) so a doc
|
|
116
125
|
* that appears shortly afterwards shows up without an invalidation.
|
|
117
126
|
*
|
|
127
|
+
* Each id's own answer — loading, present, absent, or failed with its own error — is in
|
|
128
|
+
* `outcomes`; the aggregate fields cannot tell an absent id from a failed one.
|
|
129
|
+
*
|
|
118
130
|
* @example
|
|
119
131
|
* ```typescript
|
|
120
132
|
* const { data: users, isLoading } = useBatchFirestoreDocs<PublicUser>({
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"useBatchFirestoreDocs.d.ts","sourceRoot":"","sources":["../../../src/react/firestore/useBatchFirestoreDocs.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,oBAAoB,CAAC;
|
|
1
|
+
{"version":3,"file":"useBatchFirestoreDocs.d.ts","sourceRoot":"","sources":["../../../src/react/firestore/useBatchFirestoreDocs.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,oBAAoB,CAAC;AAIpD,OAAO,KAAK,EAAE,wBAAwB,EAAE,MAAM,0BAA0B,CAAC;AAEzE,wDAAwD;AACxD,MAAM,MAAM,2BAA2B,GAAG,OAAO,GAAG,KAAK,CAAC;AAE1D,MAAM,MAAM,yBAAyB,GAAG;IACtC;;OAEG;IACH,EAAE,EAAE,SAAS,CAAC;IAEd;;OAEG;IACH,cAAc,EAAE,MAAM,CAAC;IAEvB;;OAEG;IACH,GAAG,EAAE,MAAM,EAAE,CAAC;IAEd;;;;OAIG;IACH,cAAc,EAAE,MAAM,CAAC;IAEvB;;;OAGG;IACH,SAAS,CAAC,EAAE,MAAM,CAAC;IAEnB;;OAEG;IACH,MAAM,CAAC,EAAE,MAAM,CAAC;IAEhB;;OAEG;IACH,OAAO,CAAC,EAAE,OAAO,CAAC;IAElB;;;;;;;;OAQG;IACH,SAAS,CAAC,EAAE,OAAO,CAAC;IAEpB;;;;;;;;;;;OAWG;IACH,SAAS,CAAC,EAAE,2BAA2B,CAAC;IAExC;;;;;OAKG;IACH,eAAe,CAAC,EAAE,MAAM,CAAC;IAEzB;;;;;;;;;;;OAWG;IACH,mBAAmB,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;CACzC,CAAC;AAEF,MAAM,MAAM,wBAAwB,CAAC,CAAC,IAAI;IACxC;;;;;OAKG;IACH,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,CAAC,CAAC,CAAC;IAExB;;OAEG;IACH,SAAS,EAAE,OAAO,CAAC;IAEnB;;OAEG;IACH,OAAO,EAAE,OAAO,CAAC;IAEjB;;OAEG;IACH,KAAK,EAAE,KAAK,GAAG,IAAI,CAAC;IAEpB;;;;;;OAMG;IACH,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,wBAAwB,CAAC,CAAC,CAAC,CAAC,CAAC;IAEtD;;OAEG;IACH,OAAO,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;CAC9B,CAAC;AAqBF;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AACH,wBAAgB,qBAAqB,CAAC,CAAC,SAAS,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,EAAE,EACnE,EAAE,EACF,cAAc,EACd,GAAG,EACH,cAAc,EACd,SAA0B,EAC1B,MAAuB,EACvB,OAAc,EACd,SAAiB,EACjB,SAAmB,EACnB,eAA2B,EAC3B,mBAA4C,GAC7C,EAAE,yBAAyB,GAAG,wBAAwB,CAAC,CAAC,CAAC,CAoLzD"}
|