@ttt-productions/query-core 0.14.5 → 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 +10 -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/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/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
|
-
/**
|
|
4
|
+
/** The current subscription identity's rows — live set plus loaded older pages, sorted per `sort`. */
|
|
5
5
|
items: T[];
|
|
6
|
-
/** True until the
|
|
6
|
+
/** True until the current identity's first snapshot arrives, unless its listener has failed. */
|
|
7
7
|
isInitialLoading: boolean;
|
|
8
|
-
/**
|
|
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
|
|
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
|
|
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
|
|
17
|
-
* ordered list.
|
|
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
|
-
*
|
|
24
|
-
*
|
|
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,
|
|
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
|
|
9
|
-
* ordered list.
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
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
|
-
*
|
|
16
|
-
*
|
|
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
|
-
//
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
46
|
-
const
|
|
47
|
-
const
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
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
|
-
|
|
56
|
-
|
|
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,
|
|
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:
|
|
182
|
+
queryKey: [...queryKey, 'older', identity, anchor?.id ?? null],
|
|
183
|
+
enabled: olderEnabled,
|
|
62
184
|
initialPageParam: undefined,
|
|
63
185
|
queryFn: async ({ pageParam }) => {
|
|
64
|
-
const cursor = pageParam ??
|
|
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:
|
|
77
|
-
nextCursor: docs.length
|
|
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
|
-
|
|
85
|
-
|
|
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 = (
|
|
90
|
-
//
|
|
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 [...
|
|
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,
|
|
242
|
+
}, [enabled, olderPages, current.entries, sort]);
|
|
100
243
|
return {
|
|
101
244
|
items,
|
|
102
|
-
isInitialLoading: enabled ? !
|
|
245
|
+
isInitialLoading: enabled ? !current.ready && current.error === null : false,
|
|
246
|
+
sourceState: current.sourceState,
|
|
247
|
+
error: enabled ? current.error : null,
|
|
103
248
|
fetchOlder,
|
|
104
|
-
hasOlder
|
|
105
|
-
isFetchingOlder:
|
|
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
|
|
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"}
|