@ttt-productions/query-core 0.14.5 → 0.14.7

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.
Files changed (38) hide show
  1. package/dist/cache-helpers.d.ts +17 -0
  2. package/dist/cache-helpers.d.ts.map +1 -1
  3. package/dist/cache-helpers.js +48 -0
  4. package/dist/cache-helpers.js.map +1 -1
  5. package/dist/firestore/types.d.ts +10 -4
  6. package/dist/firestore/types.d.ts.map +1 -1
  7. package/dist/firestore/types.js.map +1 -1
  8. package/dist/index.d.ts +3 -2
  9. package/dist/index.d.ts.map +1 -1
  10. package/dist/index.js +2 -1
  11. package/dist/index.js.map +1 -1
  12. package/dist/react/firestore/subscription-status.d.ts +21 -0
  13. package/dist/react/firestore/subscription-status.d.ts.map +1 -0
  14. package/dist/react/firestore/subscription-status.js +29 -0
  15. package/dist/react/firestore/subscription-status.js.map +1 -0
  16. package/dist/react/firestore/use-firestore-collection.d.ts.map +1 -1
  17. package/dist/react/firestore/use-firestore-collection.js +10 -13
  18. package/dist/react/firestore/use-firestore-collection.js.map +1 -1
  19. package/dist/react/firestore/use-firestore-doc.d.ts.map +1 -1
  20. package/dist/react/firestore/use-firestore-doc.js +10 -13
  21. package/dist/react/firestore/use-firestore-doc.js.map +1 -1
  22. package/dist/react/firestore/use-firestore-live-infinite.d.ts +31 -14
  23. package/dist/react/firestore/use-firestore-live-infinite.d.ts.map +1 -1
  24. package/dist/react/firestore/use-firestore-live-infinite.js +192 -45
  25. package/dist/react/firestore/use-firestore-live-infinite.js.map +1 -1
  26. package/dist/react/firestore/use-firestore-paginated.d.ts +27 -24
  27. package/dist/react/firestore/use-firestore-paginated.d.ts.map +1 -1
  28. package/dist/react/firestore/use-firestore-paginated.js +102 -92
  29. package/dist/react/firestore/use-firestore-paginated.js.map +1 -1
  30. package/dist/react/search/use-firestore-search.d.ts +3 -1
  31. package/dist/react/search/use-firestore-search.d.ts.map +1 -1
  32. package/dist/react/search/use-firestore-search.js +50 -5
  33. package/dist/react/search/use-firestore-search.js.map +1 -1
  34. package/dist/search/rule.d.ts +10 -0
  35. package/dist/search/rule.d.ts.map +1 -0
  36. package/dist/search/rule.js +14 -0
  37. package/dist/search/rule.js.map +1 -0
  38. package/package.json +1 -1
@@ -1,27 +1,44 @@
1
1
  import { type DocumentData } from 'firebase/firestore';
2
- import type { FirestoreLiveInfiniteOptions } from '../../firestore/types.js';
2
+ import type { FirestoreLiveInfiniteOptions, FirestoreSourceState } from '../../firestore/types.js';
3
3
  export interface FirestoreLiveInfiniteResult<T> {
4
- /** Merged + deduped live-window and older items, sorted per `sort` (default asc). */
4
+ /** The current subscription identity's rows — live set plus loaded older pages, sorted per `sort`. */
5
5
  items: T[];
6
- /** True until the live window's first snapshot arrives. */
6
+ /** True until the current identity's first snapshot arrives, unless its listener has failed. */
7
7
  isInitialLoading: boolean;
8
- /** Load the next older page. */
8
+ /** Liveness of the current identity's listener; `connecting` from that identity's first render. */
9
+ sourceState: FirestoreSourceState;
10
+ /** The listener's failure for the current identity; `null` while connecting, live, or offline. */
11
+ error: Error | null;
12
+ /** Load the next older page. Never rejects — a failed read lands in `olderError`. */
9
13
  fetchOlder: () => Promise<void>;
10
- /** Whether more older pages remain. */
14
+ /** Whether older rows exist that are not loaded yet (a one-doc look-ahead, never a full-page guess). */
11
15
  hasOlder: boolean;
12
- /** Whether an older page is currently being fetched. */
16
+ /** Whether an older page is being read (including a `retry` of one). */
13
17
  isFetchingOlder: boolean;
18
+ /** The last older-page read failed; `null` once an older read succeeds. */
19
+ olderError: Error | null;
20
+ /** Re-run what failed: resubscribe a failed listener, re-read a failed older page. */
21
+ retry: () => void;
14
22
  }
15
23
  /**
16
- * A live newest-window plus cursor-bridged older pagination, merged into one
17
- * ordered list. The newest page is a realtime `onSnapshot` window (orderBy
18
- * descending, limited to `pageSize`); older pages load on demand via `getDocs`,
19
- * each seeded with `startAfter(previousOldestDoc)` — the first older page starts
20
- * after the live window's oldest document, so there is no gap and no re-read of
21
- * pages already in memory.
24
+ * A live list (newest rows realtime) with older rows paged in on demand, merged into
25
+ * one ordered list.
22
26
  *
23
- * Generic enough for any live feed (chat threads, live comments, activity
24
- * feeds). `db` comes from `<FirestoreProvider>`.
27
+ * Until the first server-confirmed snapshot that has rows, the listener reads the newest
28
+ * `pageSize` rows plus one look-ahead row. That snapshot fixes an anchor — the oldest row
29
+ * of the window — and from then on the listener covers anchor → newest with no limit, so
30
+ * it grows only with rows that arrive while the list is open, and every older page starts
31
+ * after the anchor. Nothing can fall between the live set and the older pages, however
32
+ * many rows arrive. Older pages carry the same one-row look-ahead, so `hasOlder` is false
33
+ * exactly when nothing older exists.
34
+ *
35
+ * Everything read is tagged to its subscription identity (path, key, ordering,
36
+ * constraints, page size, enabled): a new identity renders empty and connecting in its
37
+ * first render, and a retired identity's late callback or older page never lands.
38
+ *
39
+ * A `permission-denied` listener error resubscribes on the `RESUBSCRIBE_DELAYS_MS` ladder
40
+ * before surfacing; any other listener error surfaces at once. Rows already shown are
41
+ * kept beside the error. `db` comes from `<FirestoreProvider>`.
25
42
  */
26
43
  export declare function useFirestoreLiveInfinite<T = DocumentData & {
27
44
  id: string;
@@ -1 +1 @@
1
- {"version":3,"file":"use-firestore-live-infinite.d.ts","sourceRoot":"","sources":["../../../src/react/firestore/use-firestore-live-infinite.ts"],"names":[],"mappings":"AAIA,OAAO,EAQL,KAAK,YAAY,EAElB,MAAM,oBAAoB,CAAC;AAE5B,OAAO,KAAK,EAAE,4BAA4B,EAAE,MAAM,0BAA0B,CAAC;AAI7E,MAAM,WAAW,2BAA2B,CAAC,CAAC;IAC5C,qFAAqF;IACrF,KAAK,EAAE,CAAC,EAAE,CAAC;IACX,2DAA2D;IAC3D,gBAAgB,EAAE,OAAO,CAAC;IAC1B,gCAAgC;IAChC,UAAU,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;IAChC,uCAAuC;IACvC,QAAQ,EAAE,OAAO,CAAC;IAClB,wDAAwD;IACxD,eAAe,EAAE,OAAO,CAAC;CAC1B;AAID;;;;;;;;;;GAUG;AACH,wBAAgB,wBAAwB,CAAC,CAAC,GAAG,YAAY,GAAG;IAAE,EAAE,EAAE,MAAM,CAAA;CAAE,EAAE,EAC1E,cAAc,EACd,QAAQ,EACR,YAAY,EACZ,WAAgB,EAChB,QAAQ,EAAE,iBAAsB,EAChC,OAAc,EACd,MAAM,EACN,YAAY,EACZ,IAAY,GACb,EAAE,4BAA4B,CAAC,CAAC,CAAC,GAAG,2BAA2B,CAAC,CAAC,CAAC,CA6GlE"}
1
+ {"version":3,"file":"use-firestore-live-infinite.d.ts","sourceRoot":"","sources":["../../../src/react/firestore/use-firestore-live-infinite.ts"],"names":[],"mappings":"AAIA,OAAO,EASL,KAAK,YAAY,EAElB,MAAM,oBAAoB,CAAC;AAG5B,OAAO,KAAK,EAAE,4BAA4B,EAAE,oBAAoB,EAAE,MAAM,0BAA0B,CAAC;AAInG,MAAM,WAAW,2BAA2B,CAAC,CAAC;IAC5C,sGAAsG;IACtG,KAAK,EAAE,CAAC,EAAE,CAAC;IACX,gGAAgG;IAChG,gBAAgB,EAAE,OAAO,CAAC;IAC1B,mGAAmG;IACnG,WAAW,EAAE,oBAAoB,CAAC;IAClC,kGAAkG;IAClG,KAAK,EAAE,KAAK,GAAG,IAAI,CAAC;IACpB,qFAAqF;IACrF,UAAU,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;IAChC,wGAAwG;IACxG,QAAQ,EAAE,OAAO,CAAC;IAClB,wEAAwE;IACxE,eAAe,EAAE,OAAO,CAAC;IACzB,2EAA2E;IAC3E,UAAU,EAAE,KAAK,GAAG,IAAI,CAAC;IACzB,sFAAsF;IACtF,KAAK,EAAE,MAAM,IAAI,CAAC;CACnB;AA2BD;;;;;;;;;;;;;;;;;;;GAmBG;AACH,wBAAgB,wBAAwB,CAAC,CAAC,GAAG,YAAY,GAAG;IAAE,EAAE,EAAE,MAAM,CAAA;CAAE,EAAE,EAC1E,cAAc,EACd,QAAQ,EACR,YAAY,EACZ,WAAgB,EAChB,QAAQ,EAAE,iBAAsB,EAChC,OAAc,EACd,MAAM,EACN,YAAY,EACZ,IAAY,GACb,EAAE,4BAA4B,CAAC,CAAC,CAAC,GAAG,2BAA2B,CAAC,CAAC,CAAC,CA4PlE"}
@@ -1,19 +1,40 @@
1
1
  'use client';
2
2
  import * as React from 'react';
3
3
  import { useInfiniteQuery } from '@tanstack/react-query';
4
- import { collection, query, orderBy, limit, startAfter, getDocs, onSnapshot, } from 'firebase/firestore';
4
+ import { collection, query, orderBy, limit, startAfter, endAt, getDocs, onSnapshot, } from 'firebase/firestore';
5
5
  import { useFirestoreDb } from './context.js';
6
+ import { RESUBSCRIBE_DELAYS_MS, isPermissionDeniedError } from './resubscribe.js';
6
7
  const MAX_PAGE_SIZE = 100;
8
+ function freshWindow(identity) {
9
+ return {
10
+ identity,
11
+ ready: false,
12
+ entries: [],
13
+ anchor: null,
14
+ olderExists: false,
15
+ sourceState: 'connecting',
16
+ error: null,
17
+ };
18
+ }
7
19
  /**
8
- * A live newest-window plus cursor-bridged older pagination, merged into one
9
- * ordered list. The newest page is a realtime `onSnapshot` window (orderBy
10
- * descending, limited to `pageSize`); older pages load on demand via `getDocs`,
11
- * each seeded with `startAfter(previousOldestDoc)` — the first older page starts
12
- * after the live window's oldest document, so there is no gap and no re-read of
13
- * pages already in memory.
20
+ * A live list (newest rows realtime) with older rows paged in on demand, merged into
21
+ * one ordered list.
22
+ *
23
+ * Until the first server-confirmed snapshot that has rows, the listener reads the newest
24
+ * `pageSize` rows plus one look-ahead row. That snapshot fixes an anchor — the oldest row
25
+ * of the window — and from then on the listener covers anchor → newest with no limit, so
26
+ * it grows only with rows that arrive while the list is open, and every older page starts
27
+ * after the anchor. Nothing can fall between the live set and the older pages, however
28
+ * many rows arrive. Older pages carry the same one-row look-ahead, so `hasOlder` is false
29
+ * exactly when nothing older exists.
14
30
  *
15
- * Generic enough for any live feed (chat threads, live comments, activity
16
- * feeds). `db` comes from `<FirestoreProvider>`.
31
+ * Everything read is tagged to its subscription identity (path, key, ordering,
32
+ * constraints, page size, enabled): a new identity renders empty and connecting in its
33
+ * first render, and a retired identity's late callback or older page never lands.
34
+ *
35
+ * A `permission-denied` listener error resubscribes on the `RESUBSCRIBE_DELAYS_MS` ladder
36
+ * before surfacing; any other listener error surfaces at once. Rows already shown are
37
+ * kept beside the error. `db` comes from `<FirestoreProvider>`.
17
38
  */
18
39
  export function useFirestoreLiveInfinite({ collectionPath, queryKey, orderByField, constraints = [], pageSize: requestedPageSize = 20, enabled = true, select, getSortValue, sort = 'asc', }) {
19
40
  const db = useFirestoreDb();
@@ -32,77 +53,203 @@ export function useFirestoreLiveInfinite({ collectionPath, queryKey, orderByFiel
32
53
  : dataWithId[orderByField];
33
54
  return { docId: snap.id, sortValue, item };
34
55
  }, [orderByField]);
35
- // Stable string deps for the subscription effect (values are captured via the
36
- // queryKey, which the caller varies per collection/thread).
56
+ // Constraint objects are fresh every render; their serialized form is the identity.
37
57
  const queryKeyMemo = JSON.stringify(queryKey);
38
58
  const constraintsMemo = JSON.stringify(constraints);
39
- const [windowState, setWindowState] = React.useState({ ready: false, entries: [], oldestDoc: null });
59
+ const identity = JSON.stringify([
60
+ collectionPath,
61
+ queryKeyMemo,
62
+ orderByField,
63
+ constraintsMemo,
64
+ pageSize,
65
+ enabled,
66
+ ]);
67
+ const [windowState, setWindowState] = React.useState(() => freshWindow(identity));
68
+ const windowRef = React.useRef(windowState);
69
+ windowRef.current = windowState;
70
+ const [retryNonce, setRetryNonce] = React.useState(0);
71
+ const current = windowState.identity === identity ? windowState : freshWindow(identity);
40
72
  React.useEffect(() => {
41
73
  if (!enabled) {
42
- setWindowState({ ready: false, entries: [], oldestDoc: null });
74
+ // Retire the enabled identity's rows, so re-enabling starts afresh rather than resuming.
75
+ setWindowState(freshWindow(identity));
43
76
  return;
44
77
  }
45
- const colRef = collection(db, collectionPath);
46
- const q = query(colRef, ...constraints, orderBy(orderByField, 'desc'), limit(pageSize));
47
- const unsub = onSnapshot(q, (snap) => {
48
- const docs = snap.docs;
49
- setWindowState({
50
- ready: true,
51
- entries: docs.map(toEntry),
52
- oldestDoc: docs[docs.length - 1] ?? null,
78
+ // A retry of the same identity resumes from its anchor and keeps its rows.
79
+ const prior = windowRef.current;
80
+ const resumed = prior.identity === identity;
81
+ setWindowState((prev) => prev.identity === identity
82
+ ? { ...prev, error: null, sourceState: 'connecting' }
83
+ : freshWindow(identity));
84
+ let disposed = false;
85
+ let unsubscribe = null;
86
+ let retryTimer = null;
87
+ let attempt = 0;
88
+ let currentAnchor = resumed ? prior.anchor : null;
89
+ const update = (fn) => {
90
+ if (disposed)
91
+ return;
92
+ setWindowState((prev) => (prev.identity === identity ? fn(prev) : prev));
93
+ };
94
+ const listen = (anchor) => {
95
+ if (disposed)
96
+ return;
97
+ currentAnchor = anchor;
98
+ const colRef = collection(db, collectionPath);
99
+ const q = anchor
100
+ ? query(colRef, ...constraints, orderBy(orderByField, 'desc'), endAt(anchor))
101
+ : query(colRef, ...constraints, orderBy(orderByField, 'desc'), limit(pageSize + 1));
102
+ let serverConfirmed = false;
103
+ unsubscribe = onSnapshot(q, { includeMetadataChanges: true }, (snap) => {
104
+ if (disposed)
105
+ return;
106
+ attempt = 0;
107
+ const docs = snap.docs;
108
+ const fromCache = snap.metadata?.fromCache ?? false;
109
+ const nextSource = (prev) => {
110
+ if (!fromCache)
111
+ return 'live';
112
+ if (!serverConfirmed)
113
+ return prev === 'error' ? 'connecting' : prev;
114
+ return 'offline';
115
+ };
116
+ if (!fromCache)
117
+ serverConfirmed = true;
118
+ if (anchor) {
119
+ update((prev) => ({
120
+ ...prev,
121
+ ready: true,
122
+ entries: docs.map(toEntry),
123
+ sourceState: nextSource(prev.sourceState),
124
+ error: null,
125
+ }));
126
+ return;
127
+ }
128
+ const windowDocs = docs.slice(0, pageSize);
129
+ if (fromCache || windowDocs.length === 0) {
130
+ update((prev) => ({
131
+ ...prev,
132
+ ready: true,
133
+ entries: windowDocs.map(toEntry),
134
+ sourceState: nextSource(prev.sourceState),
135
+ error: null,
136
+ }));
137
+ return;
138
+ }
139
+ const newAnchor = windowDocs[windowDocs.length - 1];
140
+ update((prev) => ({
141
+ ...prev,
142
+ ready: true,
143
+ entries: windowDocs.map(toEntry),
144
+ anchor: newAnchor,
145
+ olderExists: docs.length > pageSize,
146
+ sourceState: 'live',
147
+ error: null,
148
+ }));
149
+ unsubscribe?.();
150
+ unsubscribe = null;
151
+ listen(newAnchor);
152
+ }, (error) => {
153
+ if (disposed)
154
+ return;
155
+ unsubscribe?.();
156
+ unsubscribe = null;
157
+ if (isPermissionDeniedError(error) && attempt < RESUBSCRIBE_DELAYS_MS.length) {
158
+ const delay = RESUBSCRIBE_DELAYS_MS[attempt];
159
+ attempt += 1;
160
+ update((prev) => ({ ...prev, sourceState: 'connecting' }));
161
+ retryTimer = setTimeout(() => listen(currentAnchor), delay);
162
+ return;
163
+ }
164
+ console.error('[useFirestoreLiveInfinite] Subscription error:', error);
165
+ update((prev) => ({ ...prev, sourceState: 'error', error }));
53
166
  });
54
- });
55
- return () => unsub();
56
- // constraints captured via constraintsMemo; queryKey via queryKeyMemo; toEntry is stable.
167
+ };
168
+ listen(currentAnchor);
169
+ return () => {
170
+ disposed = true;
171
+ if (retryTimer)
172
+ clearTimeout(retryTimer);
173
+ unsubscribe?.();
174
+ };
175
+ // `identity` carries collectionPath, queryKey, orderByField, constraints, pageSize, and
176
+ // enabled; constraints are read from the render that produced it.
57
177
  // eslint-disable-next-line react-hooks/exhaustive-deps
58
- }, [db, collectionPath, orderByField, pageSize, enabled, queryKeyMemo, constraintsMemo, toEntry]);
178
+ }, [db, identity, retryNonce, toEntry]);
179
+ const anchor = enabled ? current.anchor : null;
180
+ const olderEnabled = anchor !== null && current.olderExists;
59
181
  const older = useInfiniteQuery({
60
- queryKey: [...queryKey, 'older'],
61
- enabled: enabled && windowState.ready,
182
+ queryKey: [...queryKey, 'older', identity, anchor?.id ?? null],
183
+ enabled: olderEnabled,
62
184
  initialPageParam: undefined,
63
185
  queryFn: async ({ pageParam }) => {
64
- const cursor = pageParam ?? windowState.oldestDoc ?? undefined;
186
+ const cursor = pageParam ?? anchor;
65
187
  if (!cursor) {
66
- return {
67
- entries: [],
68
- nextCursor: undefined,
69
- };
188
+ return { entries: [], nextCursor: undefined };
70
189
  }
71
190
  const colRef = collection(db, collectionPath);
72
- const q = query(colRef, ...constraints, orderBy(orderByField, 'desc'), startAfter(cursor), limit(pageSize));
191
+ const q = query(colRef, ...constraints, orderBy(orderByField, 'desc'), startAfter(cursor), limit(pageSize + 1));
73
192
  const snap = await getDocs(q);
74
193
  const docs = snap.docs;
194
+ const page = docs.slice(0, pageSize);
75
195
  return {
76
- entries: docs.map(toEntry),
77
- nextCursor: docs.length === pageSize ? docs[docs.length - 1] : undefined,
196
+ entries: page.map(toEntry),
197
+ nextCursor: docs.length > pageSize ? page[page.length - 1] : undefined,
78
198
  };
79
199
  },
80
200
  getNextPageParam: (last) => last.nextCursor,
81
201
  });
82
- const { fetchNextPage } = older;
202
+ const { data: olderData, isFetching: olderFetching, isError: olderIsError, error: olderErr, hasNextPage: olderHasNext, fetchNextPage, refetch: refetchOlder, } = older;
203
+ const olderPages = olderEnabled ? olderData?.pages : undefined;
204
+ const hasOlder = olderEnabled && (olderPages ? Boolean(olderHasNext) : true);
205
+ const olderError = olderEnabled && olderIsError ? olderErr : null;
83
206
  const fetchOlder = React.useCallback(async () => {
84
- await fetchNextPage();
85
- }, [fetchNextPage]);
207
+ if (!olderEnabled || olderFetching)
208
+ return;
209
+ if (!olderPages) {
210
+ await refetchOlder();
211
+ return;
212
+ }
213
+ if (olderHasNext)
214
+ await fetchNextPage();
215
+ }, [olderEnabled, olderFetching, olderPages, olderHasNext, refetchOlder, fetchNextPage]);
216
+ const listenerFailed = enabled && current.error !== null;
217
+ const retry = React.useCallback(() => {
218
+ if (listenerFailed) {
219
+ setWindowState((prev) => prev.identity === identity ? { ...prev, error: null, sourceState: 'connecting' } : prev);
220
+ setRetryNonce((n) => n + 1);
221
+ }
222
+ if (olderError && !olderFetching) {
223
+ if (olderPages)
224
+ void fetchNextPage();
225
+ else
226
+ void refetchOlder();
227
+ }
228
+ }, [listenerFailed, identity, olderError, olderFetching, olderPages, fetchNextPage, refetchOlder]);
86
229
  const items = React.useMemo(() => {
87
230
  if (!enabled)
88
231
  return [];
89
- const olderEntries = (older.data?.pages ?? []).flatMap((p) => p.entries);
90
- // Window entries win on overlap (they are the freshest, live copies).
232
+ const olderEntries = (olderPages ?? []).flatMap((p) => p.entries);
233
+ // Live entries win on overlap (they are the freshest copies).
91
234
  const merged = new Map();
92
- for (const entry of [...windowState.entries, ...olderEntries]) {
235
+ for (const entry of [...current.entries, ...olderEntries]) {
93
236
  if (!merged.has(entry.docId))
94
237
  merged.set(entry.docId, entry);
95
238
  }
96
239
  const arr = Array.from(merged.values());
97
240
  arr.sort((a, b) => (sort === 'asc' ? a.sortValue - b.sortValue : b.sortValue - a.sortValue));
98
241
  return arr.map((e) => e.item);
99
- }, [enabled, older.data, windowState.entries, sort]);
242
+ }, [enabled, olderPages, current.entries, sort]);
100
243
  return {
101
244
  items,
102
- isInitialLoading: enabled ? !windowState.ready : false,
245
+ isInitialLoading: enabled ? !current.ready && current.error === null : false,
246
+ sourceState: current.sourceState,
247
+ error: enabled ? current.error : null,
103
248
  fetchOlder,
104
- hasOlder: Boolean(older.hasNextPage),
105
- isFetchingOlder: older.isFetchingNextPage,
249
+ hasOlder,
250
+ isFetchingOlder: olderEnabled && olderFetching,
251
+ olderError,
252
+ retry,
106
253
  };
107
254
  }
108
255
  //# sourceMappingURL=use-firestore-live-infinite.js.map
@@ -1 +1 @@
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 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>[], Error> & {
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
- * Supports navigating to specific pages (1, 2, 3...) rather than infinite scroll.
20
- * * @example
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
- * data,
24
- * page,
25
- * setPage,
26
- * hasNextPage,
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;AAE5B,OAAO,KAAK,EAAE,yBAAyB,EAAE,MAAM,EAAE,MAAM,0BAA0B,CAAC;AAKlF;;GAEG;AACH,MAAM,MAAM,2BAA2B,CAAC,CAAC,IAAI,cAAc,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,KAAK,CAAC,GAAG;IAChF,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;IACrB,gBAAgB,EAAE,MAAM,IAAI,CAAC;CAC9B,CAAC;AAEF;;;;;;;;;;;;;;;;;;;;;;;GAuBG;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,MAAM,GACP,EAAE,yBAAyB,CAAC,CAAC,CAAC,GAAG,2BAA2B,CAAC,CAAC,CAAC,CA2G/D"}
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"}