@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.
- 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 +3 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -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/dist/react/search/use-firestore-search.d.ts +3 -1
- package/dist/react/search/use-firestore-search.d.ts.map +1 -1
- package/dist/react/search/use-firestore-search.js +50 -5
- package/dist/react/search/use-firestore-search.js.map +1 -1
- package/dist/search/rule.d.ts +10 -0
- package/dist/search/rule.d.ts.map +1 -0
- package/dist/search/rule.js +14 -0
- package/dist/search/rule.js.map +1 -0
- package/package.json +1 -1
package/dist/cache-helpers.d.ts
CHANGED
|
@@ -8,4 +8,21 @@ export declare function removeByPrefix(client: QueryClient, prefix: QueryKey): v
|
|
|
8
8
|
* Useful for optimistic updates without baking in business logic.
|
|
9
9
|
*/
|
|
10
10
|
export declare function updateQueryData<T>(client: QueryClient, key: QueryKey, updater: (prev: T | undefined) => T): void;
|
|
11
|
+
/** The cache key of page `page` of a `useFirestorePaginated` list keyed `queryKey`. */
|
|
12
|
+
export declare function paginatedPageKey(queryKey: QueryKey, page: number): QueryKey;
|
|
13
|
+
/**
|
|
14
|
+
* Patch rows in place across every cached page of a `useFirestorePaginated` list
|
|
15
|
+
* (`queryKey` is the base key the hook was given). Return the same row to leave it
|
|
16
|
+
* unchanged; a page with no changed row keeps its identity. Cursors and has-more are
|
|
17
|
+
* untouched, so paging stays aligned with the server.
|
|
18
|
+
*/
|
|
19
|
+
export declare function mapPaginatedItems<T>(client: QueryClient, queryKey: QueryKey, mapItem: (item: T) => T): void;
|
|
20
|
+
/**
|
|
21
|
+
* Put a just-created row at the top of page 1 of a `useFirestorePaginated` list, only
|
|
22
|
+
* when page 1 is cached. Use it only for a list ordered newest-first. Page 1 keeps its
|
|
23
|
+
* cursor, so it shows one extra row and page 2 keeps its place until page 1 is next read;
|
|
24
|
+
* that read moves page 1's cursor, and the hook then re-reads page 2 from the new cursor
|
|
25
|
+
* before showing it.
|
|
26
|
+
*/
|
|
27
|
+
export declare function prependToFirstPaginatedPage<T>(client: QueryClient, queryKey: QueryKey, item: T): void;
|
|
11
28
|
//# sourceMappingURL=cache-helpers.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"cache-helpers.d.ts","sourceRoot":"","sources":["../src/cache-helpers.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,QAAQ,EAAE,MAAM,uBAAuB,CAAC;
|
|
1
|
+
{"version":3,"file":"cache-helpers.d.ts","sourceRoot":"","sources":["../src/cache-helpers.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,QAAQ,EAAE,MAAM,uBAAuB,CAAC;AAGnE,uDAAuD;AACvD,wBAAsB,kBAAkB,CAAC,MAAM,EAAE,WAAW,EAAE,MAAM,EAAE,QAAQ,iBAE7E;AAED,uEAAuE;AACvE,wBAAgB,cAAc,CAAC,MAAM,EAAE,WAAW,EAAE,MAAM,EAAE,QAAQ,QAEnE;AAED;;;GAGG;AACH,wBAAgB,eAAe,CAAC,CAAC,EAC/B,MAAM,EAAE,WAAW,EACnB,GAAG,EAAE,QAAQ,EACb,OAAO,EAAE,CAAC,IAAI,EAAE,CAAC,GAAG,SAAS,KAAK,CAAC,QAGpC;AAED,uFAAuF;AACvF,wBAAgB,gBAAgB,CAAC,QAAQ,EAAE,QAAQ,EAAE,IAAI,EAAE,MAAM,GAAG,QAAQ,CAE3E;AAeD;;;;;GAKG;AACH,wBAAgB,iBAAiB,CAAC,CAAC,EACjC,MAAM,EAAE,WAAW,EACnB,QAAQ,EAAE,QAAQ,EAClB,OAAO,EAAE,CAAC,IAAI,EAAE,CAAC,KAAK,CAAC,GACtB,IAAI,CAWN;AAED;;;;;;GAMG;AACH,wBAAgB,2BAA2B,CAAC,CAAC,EAC3C,MAAM,EAAE,WAAW,EACnB,QAAQ,EAAE,QAAQ,EAClB,IAAI,EAAE,CAAC,GACN,IAAI,CAKN"}
|
package/dist/cache-helpers.js
CHANGED
|
@@ -13,4 +13,52 @@ export function removeByPrefix(client, prefix) {
|
|
|
13
13
|
export function updateQueryData(client, key, updater) {
|
|
14
14
|
client.setQueryData(key, (prev) => updater(prev));
|
|
15
15
|
}
|
|
16
|
+
/** The cache key of page `page` of a `useFirestorePaginated` list keyed `queryKey`. */
|
|
17
|
+
export function paginatedPageKey(queryKey, page) {
|
|
18
|
+
return [...queryKey, 'page', page];
|
|
19
|
+
}
|
|
20
|
+
function isPaginatedPage(value) {
|
|
21
|
+
return (typeof value === 'object' &&
|
|
22
|
+
value !== null &&
|
|
23
|
+
Array.isArray(value.items) &&
|
|
24
|
+
typeof value.hasMore === 'boolean');
|
|
25
|
+
}
|
|
26
|
+
function isPageKey(baseKey, key) {
|
|
27
|
+
return key.length === baseKey.length + 2 && key[baseKey.length] === 'page';
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Patch rows in place across every cached page of a `useFirestorePaginated` list
|
|
31
|
+
* (`queryKey` is the base key the hook was given). Return the same row to leave it
|
|
32
|
+
* unchanged; a page with no changed row keeps its identity. Cursors and has-more are
|
|
33
|
+
* untouched, so paging stays aligned with the server.
|
|
34
|
+
*/
|
|
35
|
+
export function mapPaginatedItems(client, queryKey, mapItem) {
|
|
36
|
+
for (const [key, value] of client.getQueriesData({ queryKey })) {
|
|
37
|
+
if (!isPageKey(queryKey, key) || !isPaginatedPage(value))
|
|
38
|
+
continue;
|
|
39
|
+
let changed = false;
|
|
40
|
+
const items = value.items.map((item) => {
|
|
41
|
+
const next = mapItem(item);
|
|
42
|
+
if (next !== item)
|
|
43
|
+
changed = true;
|
|
44
|
+
return next;
|
|
45
|
+
});
|
|
46
|
+
if (changed)
|
|
47
|
+
client.setQueryData(key, { ...value, items });
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Put a just-created row at the top of page 1 of a `useFirestorePaginated` list, only
|
|
52
|
+
* when page 1 is cached. Use it only for a list ordered newest-first. Page 1 keeps its
|
|
53
|
+
* cursor, so it shows one extra row and page 2 keeps its place until page 1 is next read;
|
|
54
|
+
* that read moves page 1's cursor, and the hook then re-reads page 2 from the new cursor
|
|
55
|
+
* before showing it.
|
|
56
|
+
*/
|
|
57
|
+
export function prependToFirstPaginatedPage(client, queryKey, item) {
|
|
58
|
+
const key = paginatedPageKey(queryKey, 1);
|
|
59
|
+
const value = client.getQueryData(key);
|
|
60
|
+
if (!isPaginatedPage(value))
|
|
61
|
+
return;
|
|
62
|
+
client.setQueryData(key, { ...value, items: [item, ...value.items] });
|
|
63
|
+
}
|
|
16
64
|
//# sourceMappingURL=cache-helpers.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"cache-helpers.js","sourceRoot":"","sources":["../src/cache-helpers.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"cache-helpers.js","sourceRoot":"","sources":["../src/cache-helpers.ts"],"names":[],"mappings":"AAGA,uDAAuD;AACvD,MAAM,CAAC,KAAK,UAAU,kBAAkB,CAAC,MAAmB,EAAE,MAAgB;IAC5E,OAAO,MAAM,CAAC,iBAAiB,CAAC,EAAE,QAAQ,EAAE,MAAM,EAAE,KAAK,EAAE,KAAK,EAAE,CAAC,CAAC;AACtE,CAAC;AAED,uEAAuE;AACvE,MAAM,UAAU,cAAc,CAAC,MAAmB,EAAE,MAAgB;IAClE,OAAO,MAAM,CAAC,aAAa,CAAC,EAAE,QAAQ,EAAE,MAAM,EAAE,KAAK,EAAE,KAAK,EAAE,CAAC,CAAC;AAClE,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,eAAe,CAC7B,MAAmB,EACnB,GAAa,EACb,OAAmC;IAEnC,MAAM,CAAC,YAAY,CAAI,GAAG,EAAE,CAAC,IAAI,EAAE,EAAE,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC;AACvD,CAAC;AAED,uFAAuF;AACvF,MAAM,UAAU,gBAAgB,CAAC,QAAkB,EAAE,IAAY;IAC/D,OAAO,CAAC,GAAG,QAAQ,EAAE,MAAM,EAAE,IAAI,CAAC,CAAC;AACrC,CAAC;AAED,SAAS,eAAe,CAAC,KAAc;IACrC,OAAO,CACL,OAAO,KAAK,KAAK,QAAQ;QACzB,KAAK,KAAK,IAAI;QACd,KAAK,CAAC,OAAO,CAAE,KAA6B,CAAC,KAAK,CAAC;QACnD,OAAQ,KAA+B,CAAC,OAAO,KAAK,SAAS,CAC9D,CAAC;AACJ,CAAC;AAED,SAAS,SAAS,CAAC,OAAiB,EAAE,GAAa;IACjD,OAAO,GAAG,CAAC,MAAM,KAAK,OAAO,CAAC,MAAM,GAAG,CAAC,IAAI,GAAG,CAAC,OAAO,CAAC,MAAM,CAAC,KAAK,MAAM,CAAC;AAC7E,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,iBAAiB,CAC/B,MAAmB,EACnB,QAAkB,EAClB,OAAuB;IAEvB,KAAK,MAAM,CAAC,GAAG,EAAE,KAAK,CAAC,IAAI,MAAM,CAAC,cAAc,CAAmB,EAAE,QAAQ,EAAE,CAAC,EAAE,CAAC;QACjF,IAAI,CAAC,SAAS,CAAC,QAAQ,EAAE,GAAG,CAAC,IAAI,CAAC,eAAe,CAAC,KAAK,CAAC;YAAE,SAAS;QACnE,IAAI,OAAO,GAAG,KAAK,CAAC;QACpB,MAAM,KAAK,GAAG,KAAK,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE;YACrC,MAAM,IAAI,GAAG,OAAO,CAAC,IAAS,CAAC,CAAC;YAChC,IAAI,IAAI,KAAK,IAAI;gBAAE,OAAO,GAAG,IAAI,CAAC;YAClC,OAAO,IAAI,CAAC;QACd,CAAC,CAAC,CAAC;QACH,IAAI,OAAO;YAAE,MAAM,CAAC,YAAY,CAAmB,GAAG,EAAE,EAAE,GAAG,KAAK,EAAE,KAAK,EAAE,CAAC,CAAC;IAC/E,CAAC;AACH,CAAC;AAED;;;;;;GAMG;AACH,MAAM,UAAU,2BAA2B,CACzC,MAAmB,EACnB,QAAkB,EAClB,IAAO;IAEP,MAAM,GAAG,GAAG,gBAAgB,CAAC,QAAQ,EAAE,CAAC,CAAC,CAAC;IAC1C,MAAM,KAAK,GAAG,MAAM,CAAC,YAAY,CAAmB,GAAG,CAAC,CAAC;IACzD,IAAI,CAAC,eAAe,CAAC,KAAK,CAAC;QAAE,OAAO;IACpC,MAAM,CAAC,YAAY,CAAmB,GAAG,EAAE,EAAE,GAAG,KAAK,EAAE,KAAK,EAAE,CAAC,IAAI,EAAE,GAAG,KAAK,CAAC,KAAK,CAAC,EAAE,CAAC,CAAC;AAC1F,CAAC","sourcesContent":["import type { QueryClient, QueryKey } from '@tanstack/react-query';\nimport type { PaginatedPage } from './firestore/types.js';\n\n/** Invalidate everything that matches a key prefix. */\nexport async function invalidateByPrefix(client: QueryClient, prefix: QueryKey) {\n return client.invalidateQueries({ queryKey: prefix, exact: false });\n}\n\n/** Remove everything that matches a key prefix (drops cached data). */\nexport function removeByPrefix(client: QueryClient, prefix: QueryKey) {\n return client.removeQueries({ queryKey: prefix, exact: false });\n}\n\n/**\n * Generic cache update helper.\n * Useful for optimistic updates without baking in business logic.\n */\nexport function updateQueryData<T>(\n client: QueryClient,\n key: QueryKey,\n updater: (prev: T | undefined) => T\n) {\n client.setQueryData<T>(key, (prev) => updater(prev));\n}\n\n/** The cache key of page `page` of a `useFirestorePaginated` list keyed `queryKey`. */\nexport function paginatedPageKey(queryKey: QueryKey, page: number): QueryKey {\n return [...queryKey, 'page', page];\n}\n\nfunction isPaginatedPage(value: unknown): value is PaginatedPage<unknown> {\n return (\n typeof value === 'object' &&\n value !== null &&\n Array.isArray((value as { items?: unknown }).items) &&\n typeof (value as { hasMore?: unknown }).hasMore === 'boolean'\n );\n}\n\nfunction isPageKey(baseKey: QueryKey, key: QueryKey): boolean {\n return key.length === baseKey.length + 2 && key[baseKey.length] === 'page';\n}\n\n/**\n * Patch rows in place across every cached page of a `useFirestorePaginated` list\n * (`queryKey` is the base key the hook was given). Return the same row to leave it\n * unchanged; a page with no changed row keeps its identity. Cursors and has-more are\n * untouched, so paging stays aligned with the server.\n */\nexport function mapPaginatedItems<T>(\n client: QueryClient,\n queryKey: QueryKey,\n mapItem: (item: T) => T,\n): void {\n for (const [key, value] of client.getQueriesData<PaginatedPage<T>>({ queryKey })) {\n if (!isPageKey(queryKey, key) || !isPaginatedPage(value)) continue;\n let changed = false;\n const items = value.items.map((item) => {\n const next = mapItem(item as T);\n if (next !== item) changed = true;\n return next;\n });\n if (changed) client.setQueryData<PaginatedPage<T>>(key, { ...value, items });\n }\n}\n\n/**\n * Put a just-created row at the top of page 1 of a `useFirestorePaginated` list, only\n * when page 1 is cached. Use it only for a list ordered newest-first. Page 1 keeps its\n * cursor, so it shows one extra row and page 2 keeps its place until page 1 is next read;\n * that read moves page 1's cursor, and the hook then re-reads page 2 from the new cursor\n * before showing it.\n */\nexport function prependToFirstPaginatedPage<T>(\n client: QueryClient,\n queryKey: QueryKey,\n item: T,\n): void {\n const key = paginatedPageKey(queryKey, 1);\n const value = client.getQueryData<PaginatedPage<T>>(key);\n if (!isPaginatedPage(value)) return;\n client.setQueryData<PaginatedPage<T>>(key, { ...value, items: [item, ...value.items] });\n}\n"]}
|
|
@@ -116,6 +116,8 @@ export interface FirestorePaginatedOptions<T> extends FirestoreBaseOptions {
|
|
|
116
116
|
pageSize?: number;
|
|
117
117
|
/** Initial page number (default: 1) */
|
|
118
118
|
initialPage?: number;
|
|
119
|
+
/** Re-read the displayed page every `refetchInterval` ms while it is mounted (omit for none). */
|
|
120
|
+
refetchInterval?: number;
|
|
119
121
|
/** Transform function applied to each document. Receives data WITH id included. */
|
|
120
122
|
select?: (data: DocumentData & {
|
|
121
123
|
id: string;
|
|
@@ -173,12 +175,16 @@ export interface InfinitePage<T> {
|
|
|
173
175
|
size: number;
|
|
174
176
|
}
|
|
175
177
|
/**
|
|
176
|
-
*
|
|
178
|
+
* One page's cache entry for `useFirestorePaginated`, stored at `[...queryKey, 'page', n]`:
|
|
179
|
+
* the page's rows, its last row (the next page's cursor; `null` for an empty page), and
|
|
180
|
+
* whether a further page exists (read with a one-row look-ahead), and the id of the row it
|
|
181
|
+
* was read after (`null` for page 1), checked against the previous page's cursor.
|
|
177
182
|
*/
|
|
178
|
-
export interface
|
|
179
|
-
items:
|
|
180
|
-
|
|
183
|
+
export interface PaginatedPage<T> {
|
|
184
|
+
items: T[];
|
|
185
|
+
cursor: DocumentSnapshot | null;
|
|
181
186
|
hasMore: boolean;
|
|
187
|
+
after: string | null;
|
|
182
188
|
}
|
|
183
189
|
/**
|
|
184
190
|
* Mutation operation types for batch mutations.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/firestore/types.ts"],"names":[],"mappings":"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
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/firestore/types.ts"],"names":[],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.js","sourceRoot":"","sources":["../../src/firestore/types.ts"],"names":[],"mappings":"AA8CA;;GAEG;AACH,MAAM,UAAU,SAAS,CACvB,IAA8B;IAE9B,OAAO,EAAE,EAAE,EAAE,IAAI,CAAC,EAAE,EAAE,GAAG,IAAI,CAAC,IAAI,EAAE,EAAE,CAAC;AACzC,CAAC","sourcesContent":["import type {\n DocumentData,\n DocumentSnapshot,\n QueryDocumentSnapshot,\n QueryConstraint,\n} from 'firebase/firestore';\n\n/**\n * Document with ID included in the data object.\n */\nexport type WithId<T> = T & { id: string };\n\n/**\n * Metadata-derived liveness of a realtime subscription. Firestore serves cached\n * snapshots offline WITHOUT firing the error callback, so error-only detection\n * renders a stale cached empty result as \"live\". This four-state is derived from\n * `snapshot.metadata.fromCache` (server-confirmed vs cache) plus the listener\n * error callback:\n * - `connecting` — no server-confirmed snapshot yet (initial, or only cache-first data).\n * - `live` — at least one server-confirmed snapshot (`fromCache === false`).\n * - `offline` — only cached data after having been live (connectivity loss).\n * - `error` — the listener error callback fired (e.g. permission loss).\n *\n * Meaningful only for `subscribe: true`; a non-subscribed hook stays `connecting`.\n */\nexport type FirestoreSourceState = 'connecting' | 'live' | 'offline' | 'error';\n\n/** A query-hook result augmented with the metadata-derived subscription source state. */\nexport type WithSourceState<R> = R & { sourceState: FirestoreSourceState };\n\n/**\n * One requested id's answer from `useBatchFirestoreDocs`:\n * - `loading` — no answer yet (nothing cached for the id, or the hook is disabled).\n * - `present` — the document exists and is readable; `data` is it.\n * - `absent` — the read succeeded and the document does not exist.\n * - `failed` — the read (or, in subscribe mode, the listener) errored; `error` is the cause.\n * A failure is never reported as `absent`, and a document cached before the failure is\n * not reported as `present`. What a failure means for a given collection (a rules denial\n * that stands for \"not there for this reader\", say) is the consumer's to decide from `error`.\n */\nexport type BatchFirestoreDocOutcome<T> =\n | { status: 'loading' }\n | { status: 'present'; data: T }\n | { status: 'absent' }\n | { status: 'failed'; error: Error };\n\n/**\n * Extract document data with ID from a QueryDocumentSnapshot.\n */\nexport function docWithId<T extends DocumentData>(\n snap: QueryDocumentSnapshot<T>\n): WithId<T> {\n return { id: snap.id, ...snap.data() };\n}\n\n/**\n * Base options shared across all Firestore hooks.\n */\nexport interface FirestoreBaseOptions {\n /** React Query cache key */\n queryKey: readonly unknown[];\n /** Enable/disable the query */\n enabled?: boolean;\n /** Stale time override (ms) */\n staleTime?: number;\n /** Garbage collection time override (ms) */\n gcTime?: number;\n}\n\n/**\n * Options for single document queries.\n */\nexport interface FirestoreDocOptions<T> extends FirestoreBaseOptions {\n /** Full document path (e.g., 'users/abc123') */\n docPath: string;\n /** Enable realtime updates via onSnapshot (default: false) */\n subscribe?: boolean;\n /** Transform function applied to document data. Receives data WITH id included. */\n select?: (data: DocumentData & { id: string }) => T;\n}\n\n/**\n * Options for collection queries.\n */\nexport interface FirestoreCollectionOptions<T> extends FirestoreBaseOptions {\n /** Collection path (e.g., 'users' or 'entities/abc/members') */\n collectionPath: string;\n /** Firestore query constraints (where, orderBy, etc.) */\n constraints?: QueryConstraint[];\n /** Enable realtime updates via onSnapshot (default: false) */\n subscribe?: boolean;\n /** Transform function applied to each document. Receives data WITH id included. */\n select?: (data: DocumentData & { id: string }) => T;\n}\n\n/**\n * Options for infinite scroll queries.\n */\nexport interface FirestoreInfiniteOptions<T> extends FirestoreBaseOptions {\n /** Collection path */\n collectionPath: string;\n /** Firestore query constraints (where, orderBy - required for pagination) */\n constraints?: QueryConstraint[];\n /** Number of items per page (default: 20) */\n pageSize?: number;\n /** Transform function applied to each document. Receives data WITH id included. */\n select?: (data: DocumentData & { id: string }) => T;\n}\n\n/**\n * Options for paginated queries with page numbers.\n */\nexport interface FirestorePaginatedOptions<T> extends FirestoreBaseOptions {\n /** Collection path */\n collectionPath: string;\n /** Firestore query constraints */\n constraints?: QueryConstraint[];\n /** Number of items per page (default: 10) */\n pageSize?: number;\n /** Initial page number (default: 1) */\n initialPage?: number;\n /** Transform function applied to each document. Receives data WITH id included. */\n select?: (data: DocumentData & { id: string }) => T;\n}\n\n/**\n * Options for count() aggregation queries.\n */\nexport interface FirestoreCountOptions extends FirestoreBaseOptions {\n /** Collection path */\n collectionPath: string;\n /** Firestore query constraints (where, etc.) applied before counting */\n constraints?: QueryConstraint[];\n /** Polling interval in ms (omit for no polling) */\n refetchInterval?: number;\n}\n\n/**\n * Options for a live newest-window + cursor-bridged older-pages list\n * (e.g. chat messages, live comment threads, live activity feeds). The newest\n * page is a realtime onSnapshot window; older pages load on demand via getDocs,\n * seeded from the window's oldest document so there is no gap or re-read.\n */\nexport interface FirestoreLiveInfiniteOptions<T = DocumentData & { id: string }> {\n /** Collection path (slash-joined for nested paths, e.g. 'chats/t1/messages'). */\n collectionPath: string;\n /** React Query cache key. The older-pages query is keyed under `[...queryKey, 'older']`. */\n queryKey: readonly unknown[];\n /** Field both the live window and older pages order by (descending under the hood). */\n orderByField: string;\n /** Equality/filter constraints. Do NOT pass orderBy/limit/startAfter — the hook adds those. */\n constraints?: QueryConstraint[];\n /** Items per page / live-window size (default 20, capped at 100). */\n pageSize?: number;\n /** When false, no listener is opened and no pages are fetched. */\n enabled?: boolean;\n /** Map each raw doc (id included) to the item shape. Default: identity (WithId). */\n select?: (data: DocumentData & { id: string }) => T;\n /** Numeric sort key from a raw doc (id included). Default: reads `orderByField` as a number. */\n getSortValue?: (data: DocumentData & { id: string }) => number;\n /** Output ordering of the merged list. Default 'asc' (oldest → newest). */\n sort?: 'asc' | 'desc';\n}\n\n/**\n * Result page for infinite queries.\n */\nexport interface InfinitePage<T> {\n items: WithId<T>[];\n lastDoc: DocumentSnapshot | null;\n size: number;\n}\n\n/**\n *
|
|
1
|
+
{"version":3,"file":"types.js","sourceRoot":"","sources":["../../src/firestore/types.ts"],"names":[],"mappings":"AA8CA;;GAEG;AACH,MAAM,UAAU,SAAS,CACvB,IAA8B;IAE9B,OAAO,EAAE,EAAE,EAAE,IAAI,CAAC,EAAE,EAAE,GAAG,IAAI,CAAC,IAAI,EAAE,EAAE,CAAC;AACzC,CAAC","sourcesContent":["import type {\n DocumentData,\n DocumentSnapshot,\n QueryDocumentSnapshot,\n QueryConstraint,\n} from 'firebase/firestore';\n\n/**\n * Document with ID included in the data object.\n */\nexport type WithId<T> = T & { id: string };\n\n/**\n * Metadata-derived liveness of a realtime subscription. Firestore serves cached\n * snapshots offline WITHOUT firing the error callback, so error-only detection\n * renders a stale cached empty result as \"live\". This four-state is derived from\n * `snapshot.metadata.fromCache` (server-confirmed vs cache) plus the listener\n * error callback:\n * - `connecting` — no server-confirmed snapshot yet (initial, or only cache-first data).\n * - `live` — at least one server-confirmed snapshot (`fromCache === false`).\n * - `offline` — only cached data after having been live (connectivity loss).\n * - `error` — the listener error callback fired (e.g. permission loss).\n *\n * Meaningful only for `subscribe: true`; a non-subscribed hook stays `connecting`.\n */\nexport type FirestoreSourceState = 'connecting' | 'live' | 'offline' | 'error';\n\n/** A query-hook result augmented with the metadata-derived subscription source state. */\nexport type WithSourceState<R> = R & { sourceState: FirestoreSourceState };\n\n/**\n * One requested id's answer from `useBatchFirestoreDocs`:\n * - `loading` — no answer yet (nothing cached for the id, or the hook is disabled).\n * - `present` — the document exists and is readable; `data` is it.\n * - `absent` — the read succeeded and the document does not exist.\n * - `failed` — the read (or, in subscribe mode, the listener) errored; `error` is the cause.\n * A failure is never reported as `absent`, and a document cached before the failure is\n * not reported as `present`. What a failure means for a given collection (a rules denial\n * that stands for \"not there for this reader\", say) is the consumer's to decide from `error`.\n */\nexport type BatchFirestoreDocOutcome<T> =\n | { status: 'loading' }\n | { status: 'present'; data: T }\n | { status: 'absent' }\n | { status: 'failed'; error: Error };\n\n/**\n * Extract document data with ID from a QueryDocumentSnapshot.\n */\nexport function docWithId<T extends DocumentData>(\n snap: QueryDocumentSnapshot<T>\n): WithId<T> {\n return { id: snap.id, ...snap.data() };\n}\n\n/**\n * Base options shared across all Firestore hooks.\n */\nexport interface FirestoreBaseOptions {\n /** React Query cache key */\n queryKey: readonly unknown[];\n /** Enable/disable the query */\n enabled?: boolean;\n /** Stale time override (ms) */\n staleTime?: number;\n /** Garbage collection time override (ms) */\n gcTime?: number;\n}\n\n/**\n * Options for single document queries.\n */\nexport interface FirestoreDocOptions<T> extends FirestoreBaseOptions {\n /** Full document path (e.g., 'users/abc123') */\n docPath: string;\n /** Enable realtime updates via onSnapshot (default: false) */\n subscribe?: boolean;\n /** Transform function applied to document data. Receives data WITH id included. */\n select?: (data: DocumentData & { id: string }) => T;\n}\n\n/**\n * Options for collection queries.\n */\nexport interface FirestoreCollectionOptions<T> extends FirestoreBaseOptions {\n /** Collection path (e.g., 'users' or 'entities/abc/members') */\n collectionPath: string;\n /** Firestore query constraints (where, orderBy, etc.) */\n constraints?: QueryConstraint[];\n /** Enable realtime updates via onSnapshot (default: false) */\n subscribe?: boolean;\n /** Transform function applied to each document. Receives data WITH id included. */\n select?: (data: DocumentData & { id: string }) => T;\n}\n\n/**\n * Options for infinite scroll queries.\n */\nexport interface FirestoreInfiniteOptions<T> extends FirestoreBaseOptions {\n /** Collection path */\n collectionPath: string;\n /** Firestore query constraints (where, orderBy - required for pagination) */\n constraints?: QueryConstraint[];\n /** Number of items per page (default: 20) */\n pageSize?: number;\n /** Transform function applied to each document. Receives data WITH id included. */\n select?: (data: DocumentData & { id: string }) => T;\n}\n\n/**\n * Options for paginated queries with page numbers.\n */\nexport interface FirestorePaginatedOptions<T> extends FirestoreBaseOptions {\n /** Collection path */\n collectionPath: string;\n /** Firestore query constraints */\n constraints?: QueryConstraint[];\n /** Number of items per page (default: 10) */\n pageSize?: number;\n /** Initial page number (default: 1) */\n initialPage?: number;\n /** Re-read the displayed page every `refetchInterval` ms while it is mounted (omit for none). */\n refetchInterval?: number;\n /** Transform function applied to each document. Receives data WITH id included. */\n select?: (data: DocumentData & { id: string }) => T;\n}\n\n/**\n * Options for count() aggregation queries.\n */\nexport interface FirestoreCountOptions extends FirestoreBaseOptions {\n /** Collection path */\n collectionPath: string;\n /** Firestore query constraints (where, etc.) applied before counting */\n constraints?: QueryConstraint[];\n /** Polling interval in ms (omit for no polling) */\n refetchInterval?: number;\n}\n\n/**\n * Options for a live newest-window + cursor-bridged older-pages list\n * (e.g. chat messages, live comment threads, live activity feeds). The newest\n * page is a realtime onSnapshot window; older pages load on demand via getDocs,\n * seeded from the window's oldest document so there is no gap or re-read.\n */\nexport interface FirestoreLiveInfiniteOptions<T = DocumentData & { id: string }> {\n /** Collection path (slash-joined for nested paths, e.g. 'chats/t1/messages'). */\n collectionPath: string;\n /** React Query cache key. The older-pages query is keyed under `[...queryKey, 'older']`. */\n queryKey: readonly unknown[];\n /** Field both the live window and older pages order by (descending under the hood). */\n orderByField: string;\n /** Equality/filter constraints. Do NOT pass orderBy/limit/startAfter — the hook adds those. */\n constraints?: QueryConstraint[];\n /** Items per page / live-window size (default 20, capped at 100). */\n pageSize?: number;\n /** When false, no listener is opened and no pages are fetched. */\n enabled?: boolean;\n /** Map each raw doc (id included) to the item shape. Default: identity (WithId). */\n select?: (data: DocumentData & { id: string }) => T;\n /** Numeric sort key from a raw doc (id included). Default: reads `orderByField` as a number. */\n getSortValue?: (data: DocumentData & { id: string }) => number;\n /** Output ordering of the merged list. Default 'asc' (oldest → newest). */\n sort?: 'asc' | 'desc';\n}\n\n/**\n * Result page for infinite queries.\n */\nexport interface InfinitePage<T> {\n items: WithId<T>[];\n lastDoc: DocumentSnapshot | null;\n size: number;\n}\n\n/**\n * One page's cache entry for `useFirestorePaginated`, stored at `[...queryKey, 'page', n]`:\n * the page's rows, its last row (the next page's cursor; `null` for an empty page), and\n * whether a further page exists (read with a one-row look-ahead), and the id of the row it\n * was read after (`null` for page 1), checked against the previous page's cursor.\n */\nexport interface PaginatedPage<T> {\n items: T[];\n cursor: DocumentSnapshot | null;\n hasMore: boolean;\n after: string | null;\n}\n\n/**\n * Mutation operation types for batch mutations.\n */\nexport type MutationOperation =\n | { type: 'set'; docPath: string; data: DocumentData; merge?: boolean }\n | { type: 'update'; docPath: string; data: DocumentData }\n | { type: 'delete'; docPath: string };\n\n/**\n * Options for document mutations.\n */\nexport interface FirestoreMutationOptions<T> {\n /** Document path for single-doc mutations */\n docPath?: string;\n /** Query keys to invalidate on success */\n invalidateKeys?: readonly unknown[][];\n /** Enable optimistic updates */\n optimistic?: {\n /** Query key of the cache to update */\n queryKey: readonly unknown[];\n /** How to update the cached data */\n updater: (oldData: T | undefined, newData: Partial<T>) => T;\n };\n}\n\n/**\n * Options for batch mutations.\n */\nexport interface FirestoreBatchOptions {\n /** Query keys to invalidate on success */\n invalidateKeys?: readonly unknown[][];\n /** Max operations per batch (default: 450) */\n batchSize?: number;\n}\n"]}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
export { STALE_TIMES } from './defaults.js';
|
|
2
|
-
export { invalidateByPrefix, removeByPrefix, updateQueryData } from './cache-helpers.js';
|
|
3
|
-
export type { WithId, FirestoreSourceState, WithSourceState, BatchFirestoreDocOutcome, FirestoreBaseOptions, FirestoreDocOptions, FirestoreCollectionOptions, FirestoreInfiniteOptions, FirestorePaginatedOptions, InfinitePage,
|
|
2
|
+
export { invalidateByPrefix, removeByPrefix, updateQueryData, paginatedPageKey, mapPaginatedItems, prependToFirstPaginatedPage, } from './cache-helpers.js';
|
|
3
|
+
export type { WithId, FirestoreSourceState, WithSourceState, BatchFirestoreDocOutcome, FirestoreBaseOptions, FirestoreDocOptions, FirestoreCollectionOptions, FirestoreInfiniteOptions, FirestorePaginatedOptions, InfinitePage, PaginatedPage, MutationOperation, FirestoreMutationOptions, FirestoreBatchOptions, } from './firestore/types.js';
|
|
4
4
|
export { docWithId } from './firestore/types.js';
|
|
5
5
|
export { flattenInfiniteData, getInfiniteDataCount, } from './firestore/infinite-helpers.js';
|
|
6
6
|
export type { FirestoreSearchConfig, FirestoreSearchOptions, SearchEqualityFilter, } from './search/types.js';
|
|
7
|
+
export { FIRESTORE_SEARCH_MIN_LENGTH, isSearchableText } from './search/rule.js';
|
|
7
8
|
export { createDomainEventInvalidator, exact, prefix, predicate, serializeInvalidation, applyInvalidations, } from "./domain-events/index.js";
|
|
8
9
|
export type { DomainEventInvalidator, DomainEventInvalidationRegistry, CacheInvalidation, RefetchType, } from "./domain-events/index.js";
|
|
9
10
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,WAAW,EAAE,MAAM,eAAe,CAAC;AAG5C,OAAO,
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,WAAW,EAAE,MAAM,eAAe,CAAC;AAG5C,OAAO,EACL,kBAAkB,EAClB,cAAc,EACd,eAAe,EACf,gBAAgB,EAChB,iBAAiB,EACjB,2BAA2B,GAC5B,MAAM,oBAAoB,CAAC;AAG5B,YAAY,EACV,MAAM,EACN,oBAAoB,EACpB,eAAe,EACf,wBAAwB,EACxB,oBAAoB,EACpB,mBAAmB,EACnB,0BAA0B,EAC1B,wBAAwB,EACxB,yBAAyB,EACzB,YAAY,EACZ,aAAa,EACb,iBAAiB,EACjB,wBAAwB,EACxB,qBAAqB,GACtB,MAAM,sBAAsB,CAAC;AAC9B,OAAO,EAAE,SAAS,EAAE,MAAM,sBAAsB,CAAC;AACjD,OAAO,EACL,mBAAmB,EACnB,oBAAoB,GACrB,MAAM,iCAAiC,CAAC;AAGzC,YAAY,EACV,qBAAqB,EACrB,sBAAsB,EACtB,oBAAoB,GACrB,MAAM,mBAAmB,CAAC;AAC3B,OAAO,EAAE,2BAA2B,EAAE,gBAAgB,EAAE,MAAM,kBAAkB,CAAC;AAGjF,OAAO,EACL,4BAA4B,EAC5B,KAAK,EACL,MAAM,EACN,SAAS,EACT,qBAAqB,EACrB,kBAAkB,GACnB,MAAM,0BAA0B,CAAC;AAClC,YAAY,EACV,sBAAsB,EACtB,+BAA+B,EAC/B,iBAAiB,EACjB,WAAW,GACZ,MAAM,0BAA0B,CAAC"}
|
package/dist/index.js
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
// Query defaults & staleTime presets (server-safe — plain constants, no React runtime)
|
|
2
2
|
export { STALE_TIMES } from './defaults.js';
|
|
3
3
|
// Cache Helpers
|
|
4
|
-
export { invalidateByPrefix, removeByPrefix, updateQueryData } from './cache-helpers.js';
|
|
4
|
+
export { invalidateByPrefix, removeByPrefix, updateQueryData, paginatedPageKey, mapPaginatedItems, prependToFirstPaginatedPage, } from './cache-helpers.js';
|
|
5
5
|
export { docWithId } from './firestore/types.js';
|
|
6
6
|
export { flattenInfiniteData, getInfiniteDataCount, } from './firestore/infinite-helpers.js';
|
|
7
|
+
export { FIRESTORE_SEARCH_MIN_LENGTH, isSearchableText } from './search/rule.js';
|
|
7
8
|
// Domain-event invalidator (mechanism — consumers register their event registry)
|
|
8
9
|
export { createDomainEventInvalidator, exact, prefix, predicate, serializeInvalidation, applyInvalidations, } from "./domain-events/index.js";
|
|
9
10
|
//# sourceMappingURL=index.js.map
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,uFAAuF;AACvF,OAAO,EAAE,WAAW,EAAE,MAAM,eAAe,CAAC;AAE5C,gBAAgB;AAChB,OAAO,
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,uFAAuF;AACvF,OAAO,EAAE,WAAW,EAAE,MAAM,eAAe,CAAC;AAE5C,gBAAgB;AAChB,OAAO,EACL,kBAAkB,EAClB,cAAc,EACd,eAAe,EACf,gBAAgB,EAChB,iBAAiB,EACjB,2BAA2B,GAC5B,MAAM,oBAAoB,CAAC;AAmB5B,OAAO,EAAE,SAAS,EAAE,MAAM,sBAAsB,CAAC;AACjD,OAAO,EACL,mBAAmB,EACnB,oBAAoB,GACrB,MAAM,iCAAiC,CAAC;AAQzC,OAAO,EAAE,2BAA2B,EAAE,gBAAgB,EAAE,MAAM,kBAAkB,CAAC;AAEjF,iFAAiF;AACjF,OAAO,EACL,4BAA4B,EAC5B,KAAK,EACL,MAAM,EACN,SAAS,EACT,qBAAqB,EACrB,kBAAkB,GACnB,MAAM,0BAA0B,CAAC","sourcesContent":["// Query defaults & staleTime presets (server-safe — plain constants, no React runtime)\nexport { STALE_TIMES } from './defaults.js';\n\n// Cache Helpers\nexport {\n invalidateByPrefix,\n removeByPrefix,\n updateQueryData,\n paginatedPageKey,\n mapPaginatedItems,\n prependToFirstPaginatedPage,\n} from './cache-helpers.js';\n\n// Firestore Types & Pure Helpers\nexport type {\n WithId,\n FirestoreSourceState,\n WithSourceState,\n BatchFirestoreDocOutcome,\n FirestoreBaseOptions,\n FirestoreDocOptions,\n FirestoreCollectionOptions,\n FirestoreInfiniteOptions,\n FirestorePaginatedOptions,\n InfinitePage,\n PaginatedPage,\n MutationOperation,\n FirestoreMutationOptions,\n FirestoreBatchOptions,\n} from './firestore/types.js';\nexport { docWithId } from './firestore/types.js';\nexport {\n flattenInfiniteData,\n getInfiniteDataCount,\n} from './firestore/infinite-helpers.js';\n\n// Search Types\nexport type {\n FirestoreSearchConfig,\n FirestoreSearchOptions,\n SearchEqualityFilter,\n} from './search/types.js';\nexport { FIRESTORE_SEARCH_MIN_LENGTH, isSearchableText } from './search/rule.js';\n\n// Domain-event invalidator (mechanism — consumers register their event registry)\nexport {\n createDomainEventInvalidator,\n exact,\n prefix,\n predicate,\n serializeInvalidation,\n applyInvalidations,\n} from \"./domain-events/index.js\";\nexport type {\n DomainEventInvalidator,\n DomainEventInvalidationRegistry,\n CacheInvalidation,\n RefetchType,\n} from \"./domain-events/index.js\";\n"]}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { FirestoreSourceState } from '../../firestore/types.js';
|
|
2
|
+
interface SubscriptionStatus {
|
|
3
|
+
identity: string;
|
|
4
|
+
error: Error | null;
|
|
5
|
+
sourceState: FirestoreSourceState;
|
|
6
|
+
}
|
|
7
|
+
type StatusPatch = Partial<Omit<SubscriptionStatus, 'identity'>>;
|
|
8
|
+
/**
|
|
9
|
+
* A realtime listener's source state and error, tagged to the subscription identity that
|
|
10
|
+
* produced them. A new identity reads `connecting` with no error from its first render;
|
|
11
|
+
* writes made for a retired identity are dropped once the new identity's listener has
|
|
12
|
+
* started (it calls `reset`).
|
|
13
|
+
*/
|
|
14
|
+
export declare function useSubscriptionStatus(identity: string): {
|
|
15
|
+
error: Error | null;
|
|
16
|
+
sourceState: FirestoreSourceState;
|
|
17
|
+
reset: () => void;
|
|
18
|
+
update: (patch: StatusPatch | ((prev: SubscriptionStatus) => StatusPatch)) => void;
|
|
19
|
+
};
|
|
20
|
+
export {};
|
|
21
|
+
//# sourceMappingURL=subscription-status.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"subscription-status.d.ts","sourceRoot":"","sources":["../../../src/react/firestore/subscription-status.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,0BAA0B,CAAC;AAErE,UAAU,kBAAkB;IAC1B,QAAQ,EAAE,MAAM,CAAC;IACjB,KAAK,EAAE,KAAK,GAAG,IAAI,CAAC;IACpB,WAAW,EAAE,oBAAoB,CAAC;CACnC;AAED,KAAK,WAAW,GAAG,OAAO,CAAC,IAAI,CAAC,kBAAkB,EAAE,UAAU,CAAC,CAAC,CAAC;AAEjE;;;;;GAKG;AACH,wBAAgB,qBAAqB,CAAC,QAAQ,EAAE,MAAM;;;;oBAe1C,WAAW,GAAG,CAAC,CAAC,IAAI,EAAE,kBAAkB,KAAK,WAAW,CAAC;EAWpE"}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { useCallback, useState } from 'react';
|
|
3
|
+
/**
|
|
4
|
+
* A realtime listener's source state and error, tagged to the subscription identity that
|
|
5
|
+
* produced them. A new identity reads `connecting` with no error from its first render;
|
|
6
|
+
* writes made for a retired identity are dropped once the new identity's listener has
|
|
7
|
+
* started (it calls `reset`).
|
|
8
|
+
*/
|
|
9
|
+
export function useSubscriptionStatus(identity) {
|
|
10
|
+
const [status, setStatus] = useState(() => ({
|
|
11
|
+
identity,
|
|
12
|
+
error: null,
|
|
13
|
+
sourceState: 'connecting',
|
|
14
|
+
}));
|
|
15
|
+
const current = status.identity === identity ? status : { identity, error: null, sourceState: 'connecting' };
|
|
16
|
+
const reset = useCallback(() => {
|
|
17
|
+
setStatus({ identity, error: null, sourceState: 'connecting' });
|
|
18
|
+
}, [identity]);
|
|
19
|
+
const update = useCallback((patch) => {
|
|
20
|
+
setStatus((prev) => {
|
|
21
|
+
if (prev.identity !== identity)
|
|
22
|
+
return prev;
|
|
23
|
+
const next = typeof patch === 'function' ? patch(prev) : patch;
|
|
24
|
+
return { ...prev, ...next };
|
|
25
|
+
});
|
|
26
|
+
}, [identity]);
|
|
27
|
+
return { error: current.error, sourceState: current.sourceState, reset, update };
|
|
28
|
+
}
|
|
29
|
+
//# sourceMappingURL=subscription-status.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"subscription-status.js","sourceRoot":"","sources":["../../../src/react/firestore/subscription-status.ts"],"names":[],"mappings":"AAAA,YAAY,CAAC;AAEb,OAAO,EAAE,WAAW,EAAE,QAAQ,EAAE,MAAM,OAAO,CAAC;AAW9C;;;;;GAKG;AACH,MAAM,UAAU,qBAAqB,CAAC,QAAgB;IACpD,MAAM,CAAC,MAAM,EAAE,SAAS,CAAC,GAAG,QAAQ,CAAqB,GAAG,EAAE,CAAC,CAAC;QAC9D,QAAQ;QACR,KAAK,EAAE,IAAI;QACX,WAAW,EAAE,YAAY;KAC1B,CAAC,CAAC,CAAC;IAEJ,MAAM,OAAO,GACX,MAAM,CAAC,QAAQ,KAAK,QAAQ,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,QAAQ,EAAE,KAAK,EAAE,IAAI,EAAE,WAAW,EAAE,YAAY,EAAE,CAAC;IAE/F,MAAM,KAAK,GAAG,WAAW,CAAC,GAAG,EAAE;QAC7B,SAAS,CAAC,EAAE,QAAQ,EAAE,KAAK,EAAE,IAAI,EAAE,WAAW,EAAE,YAAY,EAAE,CAAC,CAAC;IAClE,CAAC,EAAE,CAAC,QAAQ,CAAC,CAAC,CAAC;IAEf,MAAM,MAAM,GAAG,WAAW,CACxB,CAAC,KAAgE,EAAE,EAAE;QACnE,SAAS,CAAC,CAAC,IAAI,EAAE,EAAE;YACjB,IAAI,IAAI,CAAC,QAAQ,KAAK,QAAQ;gBAAE,OAAO,IAAI,CAAC;YAC5C,MAAM,IAAI,GAAG,OAAO,KAAK,KAAK,UAAU,CAAC,CAAC,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC;YAC/D,OAAO,EAAE,GAAG,IAAI,EAAE,GAAG,IAAI,EAAE,CAAC;QAC9B,CAAC,CAAC,CAAC;IACL,CAAC,EACD,CAAC,QAAQ,CAAC,CACX,CAAC;IAEF,OAAO,EAAE,KAAK,EAAE,OAAO,CAAC,KAAK,EAAE,WAAW,EAAE,OAAO,CAAC,WAAW,EAAE,KAAK,EAAE,MAAM,EAAE,CAAC;AACnF,CAAC","sourcesContent":["'use client';\n\nimport { useCallback, useState } from 'react';\nimport type { FirestoreSourceState } from '../../firestore/types.js';\n\ninterface SubscriptionStatus {\n identity: string;\n error: Error | null;\n sourceState: FirestoreSourceState;\n}\n\ntype StatusPatch = Partial<Omit<SubscriptionStatus, 'identity'>>;\n\n/**\n * A realtime listener's source state and error, tagged to the subscription identity that\n * produced them. A new identity reads `connecting` with no error from its first render;\n * writes made for a retired identity are dropped once the new identity's listener has\n * started (it calls `reset`).\n */\nexport function useSubscriptionStatus(identity: string) {\n const [status, setStatus] = useState<SubscriptionStatus>(() => ({\n identity,\n error: null,\n sourceState: 'connecting',\n }));\n\n const current: SubscriptionStatus =\n status.identity === identity ? status : { identity, error: null, sourceState: 'connecting' };\n\n const reset = useCallback(() => {\n setStatus({ identity, error: null, sourceState: 'connecting' });\n }, [identity]);\n\n const update = useCallback(\n (patch: StatusPatch | ((prev: SubscriptionStatus) => StatusPatch)) => {\n setStatus((prev) => {\n if (prev.identity !== identity) return prev;\n const next = typeof patch === 'function' ? patch(prev) : patch;\n return { ...prev, ...next };\n });\n },\n [identity],\n );\n\n return { error: current.error, sourceState: current.sourceState, reset, update };\n}\n"]}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"use-firestore-collection.d.ts","sourceRoot":"","sources":["../../../src/react/firestore/use-firestore-collection.ts"],"names":[],"mappings":"AAGA,OAAO,EAA4B,KAAK,cAAc,EAAE,MAAM,uBAAuB,CAAC;AACtF,OAAO,EAKL,KAAK,YAAY,EAClB,MAAM,oBAAoB,CAAC;
|
|
1
|
+
{"version":3,"file":"use-firestore-collection.d.ts","sourceRoot":"","sources":["../../../src/react/firestore/use-firestore-collection.ts"],"names":[],"mappings":"AAGA,OAAO,EAA4B,KAAK,cAAc,EAAE,MAAM,uBAAuB,CAAC;AACtF,OAAO,EAKL,KAAK,YAAY,EAClB,MAAM,oBAAoB,CAAC;AAI5B,OAAO,KAAK,EACV,0BAA0B,EAC1B,MAAM,EACN,eAAe,EAChB,MAAM,0BAA0B,CAAC;AAElC;;;;;;;;;;;;;;;;;;;GAmBG;AACH,wBAAgB,sBAAsB,CAAC,CAAC,SAAS,YAAY,GAAG,YAAY,EAAE,EAC5E,cAAc,EACd,QAAQ,EACR,WAAgB,EAChB,OAAc,EACd,SAAiB,EACjB,SAAS,EACT,MAAM,EACN,MAAM,GACP,EAAE,0BAA0B,CAAC,CAAC,CAAC,GAAG,eAAe,CAAC,cAAc,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,KAAK,CAAC,CAAC,CAiIrF"}
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
'use client';
|
|
2
|
-
import { useEffect
|
|
2
|
+
import { useEffect } from 'react';
|
|
3
3
|
import { useQuery, useQueryClient } from '@tanstack/react-query';
|
|
4
4
|
import { collection, query, getDocs, onSnapshot, } from 'firebase/firestore';
|
|
5
5
|
import { useFirestoreDb } from './context.js';
|
|
6
6
|
import { RESUBSCRIBE_DELAYS_MS, isPermissionDeniedError } from './resubscribe.js';
|
|
7
|
+
import { useSubscriptionStatus } from './subscription-status.js';
|
|
7
8
|
/**
|
|
8
9
|
* Fetch all documents from a Firestore collection with optional realtime updates.
|
|
9
10
|
* Use this for small collections where you need all documents at once.
|
|
@@ -32,15 +33,12 @@ export function useFirestoreCollection({ collectionPath, queryKey, constraints =
|
|
|
32
33
|
const constraintsMemo = JSON.stringify(constraints);
|
|
33
34
|
const queryKeyMemo = JSON.stringify(queryKey);
|
|
34
35
|
// Listener errors can't surface through useQuery (the queryFn is disabled while
|
|
35
|
-
// subscribed), so they're tracked
|
|
36
|
-
const
|
|
37
|
-
// Metadata-derived source state — reset whenever the subscription identity changes.
|
|
38
|
-
const [sourceState, setSourceState] = useState('connecting');
|
|
36
|
+
// subscribed), so they're tracked per subscription identity and merged into the result.
|
|
37
|
+
const { error: subscriptionError, sourceState, reset: resetStatus, update: updateStatus, } = useSubscriptionStatus(JSON.stringify([collectionPath, queryKeyMemo, constraintsMemo, enabled, subscribe]));
|
|
39
38
|
useEffect(() => {
|
|
40
39
|
if (!subscribe || !enabled)
|
|
41
40
|
return;
|
|
42
|
-
|
|
43
|
-
setSourceState('connecting');
|
|
41
|
+
resetStatus();
|
|
44
42
|
// Bounded resubscribe: a transient `permission-denied` (e.g. a failed mid-session
|
|
45
43
|
// App Check token refresh) is retried on the RESUBSCRIBE_DELAYS_MS ladder before the
|
|
46
44
|
// error is surfaced. `attempt` resets on any healthy snapshot; `disposed`/timers are
|
|
@@ -58,7 +56,7 @@ export function useFirestoreCollection({ collectionPath, queryKey, constraints =
|
|
|
58
56
|
: collectionRef;
|
|
59
57
|
currentUnsubscribe = onSnapshot(q, { includeMetadataChanges: true }, (snapshot) => {
|
|
60
58
|
attempt = 0;
|
|
61
|
-
|
|
59
|
+
updateStatus({ error: null });
|
|
62
60
|
const items = snapshot.docs.map((docSnap) => {
|
|
63
61
|
const rawData = docSnap.data();
|
|
64
62
|
const dataWithId = { id: docSnap.id, ...rawData };
|
|
@@ -67,7 +65,7 @@ export function useFirestoreCollection({ collectionPath, queryKey, constraints =
|
|
|
67
65
|
});
|
|
68
66
|
queryClient.setQueryData(queryKey, items);
|
|
69
67
|
const fromCache = snapshot.metadata?.fromCache ?? false;
|
|
70
|
-
|
|
68
|
+
updateStatus((prev) => ({ sourceState: fromCache ? (prev.sourceState === 'connecting' ? 'connecting' : 'offline') : 'live' }));
|
|
71
69
|
}, (error) => {
|
|
72
70
|
// Heal a transient permission-denied by resubscribing on the backoff ladder,
|
|
73
71
|
// keeping the last-known data visible. Only surface the error once the ladder
|
|
@@ -77,13 +75,12 @@ export function useFirestoreCollection({ collectionPath, queryKey, constraints =
|
|
|
77
75
|
currentUnsubscribe = null;
|
|
78
76
|
const delay = RESUBSCRIBE_DELAYS_MS[attempt];
|
|
79
77
|
attempt += 1;
|
|
80
|
-
|
|
78
|
+
updateStatus({ sourceState: 'connecting' });
|
|
81
79
|
retryTimer = setTimeout(start, delay);
|
|
82
80
|
return;
|
|
83
81
|
}
|
|
84
82
|
console.error('[useFirestoreCollection] Subscription error:', error);
|
|
85
|
-
|
|
86
|
-
setSourceState('error');
|
|
83
|
+
updateStatus({ error, sourceState: 'error' });
|
|
87
84
|
queryClient.setQueryData(queryKey, undefined);
|
|
88
85
|
});
|
|
89
86
|
};
|
|
@@ -97,7 +94,7 @@ export function useFirestoreCollection({ collectionPath, queryKey, constraints =
|
|
|
97
94
|
// queryKey & constraints are tracked via their stringified forms above; select is intentionally
|
|
98
95
|
// excluded so an inline caller function doesn't force a re-subscribe on every render.
|
|
99
96
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
100
|
-
}, [db, collectionPath, queryKeyMemo, constraintsMemo, enabled, subscribe, queryClient]);
|
|
97
|
+
}, [db, collectionPath, queryKeyMemo, constraintsMemo, enabled, subscribe, queryClient, resetStatus, updateStatus]);
|
|
101
98
|
const queryResult = useQuery({
|
|
102
99
|
queryKey,
|
|
103
100
|
queryFn: async () => {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"use-firestore-collection.js","sourceRoot":"","sources":["../../../src/react/firestore/use-firestore-collection.ts"],"names":[],"mappings":"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","sourcesContent":["'use client';\n\nimport { useEffect, useState } from 'react';\nimport { useQuery, useQueryClient, type UseQueryResult } from '@tanstack/react-query';\nimport {\n collection,\n query,\n getDocs,\n onSnapshot,\n type DocumentData,\n} from 'firebase/firestore';\nimport { useFirestoreDb } from './context.js';\nimport { RESUBSCRIBE_DELAYS_MS, isPermissionDeniedError } from './resubscribe.js';\nimport type {\n FirestoreCollectionOptions,\n FirestoreSourceState,\n WithId,\n WithSourceState,\n} from '../../firestore/types.js';\n\n/**\n * Fetch all documents from a Firestore collection with optional realtime updates.\n * Use this for small collections where you need all documents at once.\n * For large collections, use useFirestoreInfinite or useFirestorePaginated.\n * * @example\n * ```tsx\n * // Fetch all channels in an entity\n * const { data: channels } = useFirestoreCollection<Channel>({\n * collectionPath: `entities/${entityId}/channels`,\n * queryKey: ['channels', entityId],\n * constraints: [orderBy('createdAt', 'asc')],\n * });\n * * // With realtime updates\n * const { data: members } = useFirestoreCollection<Member>({\n * collectionPath: `entities/${entityId}/members`,\n * queryKey: ['members', entityId],\n * subscribe: true,\n * });\n * ```\n */\nexport function useFirestoreCollection<T extends DocumentData = DocumentData>({\n collectionPath,\n queryKey,\n constraints = [],\n enabled = true,\n subscribe = false,\n staleTime,\n gcTime,\n select,\n}: FirestoreCollectionOptions<T>): WithSourceState<UseQueryResult<WithId<T>[], Error>> {\n const db = useFirestoreDb();\n const queryClient = useQueryClient();\n\n // STABILIZE DEPENDENCIES:\n // We stringify these because [where('a', '==', 'b')] !== [where('a', '==', 'b')] in JS.\n const constraintsMemo = JSON.stringify(constraints);\n const queryKeyMemo = JSON.stringify(queryKey);\n\n // Listener errors can't surface through useQuery (the queryFn is disabled while\n // subscribed), so they're tracked here and merged into the returned result.\n const [subscriptionError, setSubscriptionError] = useState<Error | null>(null);\n // Metadata-derived source state — reset whenever the subscription identity changes.\n const [sourceState, setSourceState] = useState<FirestoreSourceState>('connecting');\n\n useEffect(() => {\n if (!subscribe || !enabled) return;\n\n setSubscriptionError(null);\n setSourceState('connecting');\n\n // Bounded resubscribe: a transient `permission-denied` (e.g. a failed mid-session\n // App Check token refresh) is retried on the RESUBSCRIBE_DELAYS_MS ladder before the\n // error is surfaced. `attempt` resets on any healthy snapshot; `disposed`/timers are\n // cleaned up on unmount so a pending retry never fires after teardown.\n let currentUnsubscribe: (() => void) | null = null;\n let retryTimer: ReturnType<typeof setTimeout> | null = null;\n let attempt = 0;\n let disposed = false;\n\n const start = () => {\n if (disposed) return;\n const collectionRef = collection(db, collectionPath);\n const q = constraints.length > 0\n ? query(collectionRef, ...constraints)\n : collectionRef;\n\n currentUnsubscribe = onSnapshot(\n q,\n { includeMetadataChanges: true },\n (snapshot) => {\n attempt = 0;\n setSubscriptionError(null);\n const items = snapshot.docs.map((docSnap) => {\n const rawData = docSnap.data();\n const dataWithId = { id: docSnap.id, ...rawData };\n const data = select ? select(dataWithId) : dataWithId;\n return data as WithId<T>;\n });\n queryClient.setQueryData(queryKey, items);\n const fromCache = snapshot.metadata?.fromCache ?? false;\n setSourceState((prev) => (fromCache ? (prev === 'connecting' ? 'connecting' : 'offline') : 'live'));\n },\n (error) => {\n // Heal a transient permission-denied by resubscribing on the backoff ladder,\n // keeping the last-known data visible. Only surface the error once the ladder\n // is exhausted; any other error surfaces immediately, as before.\n if (isPermissionDeniedError(error) && attempt < RESUBSCRIBE_DELAYS_MS.length) {\n currentUnsubscribe?.();\n currentUnsubscribe = null;\n const delay = RESUBSCRIBE_DELAYS_MS[attempt];\n attempt += 1;\n setSourceState('connecting');\n retryTimer = setTimeout(start, delay);\n return;\n }\n console.error('[useFirestoreCollection] Subscription error:', error);\n setSubscriptionError(error);\n setSourceState('error');\n queryClient.setQueryData(queryKey, undefined);\n }\n );\n };\n\n start();\n\n return () => {\n disposed = true;\n if (retryTimer) clearTimeout(retryTimer);\n currentUnsubscribe?.();\n };\n // queryKey & constraints are tracked via their stringified forms above; select is intentionally\n // excluded so an inline caller function doesn't force a re-subscribe on every render.\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [db, collectionPath, queryKeyMemo, constraintsMemo, enabled, subscribe, queryClient]);\n\n const queryResult = useQuery({\n queryKey,\n queryFn: async (): Promise<WithId<T>[]> => {\n const collectionRef = collection(db, collectionPath);\n const q = constraints.length > 0\n ? query(collectionRef, ...constraints)\n : collectionRef;\n\n const snapshot = await getDocs(q);\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: enabled && !subscribe,\n staleTime: subscribe ? Infinity : staleTime,\n gcTime,\n });\n\n // Merge a listener error into the result so subscribed consumers see a truthful\n // isError/error/status instead of a forever-pending disabled query.\n if (subscribe && subscriptionError) {\n return {\n ...queryResult,\n data: undefined,\n error: subscriptionError,\n isError: true,\n isLoadingError: true,\n isSuccess: false,\n isPending: false,\n isLoading: false,\n status: 'error',\n sourceState,\n } as WithSourceState<UseQueryResult<WithId<T>[], Error>>;\n }\n\n return { ...queryResult, sourceState } as WithSourceState<UseQueryResult<WithId<T>[], Error>>;\n}\n"]}
|
|
1
|
+
{"version":3,"file":"use-firestore-collection.js","sourceRoot":"","sources":["../../../src/react/firestore/use-firestore-collection.ts"],"names":[],"mappings":"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","sourcesContent":["'use client';\n\nimport { useEffect } from 'react';\nimport { useQuery, useQueryClient, type UseQueryResult } from '@tanstack/react-query';\nimport {\n collection,\n query,\n getDocs,\n onSnapshot,\n type DocumentData,\n} from 'firebase/firestore';\nimport { useFirestoreDb } from './context.js';\nimport { RESUBSCRIBE_DELAYS_MS, isPermissionDeniedError } from './resubscribe.js';\nimport { useSubscriptionStatus } from './subscription-status.js';\nimport type {\n FirestoreCollectionOptions,\n WithId,\n WithSourceState,\n} from '../../firestore/types.js';\n\n/**\n * Fetch all documents from a Firestore collection with optional realtime updates.\n * Use this for small collections where you need all documents at once.\n * For large collections, use useFirestoreInfinite or useFirestorePaginated.\n * * @example\n * ```tsx\n * // Fetch all channels in an entity\n * const { data: channels } = useFirestoreCollection<Channel>({\n * collectionPath: `entities/${entityId}/channels`,\n * queryKey: ['channels', entityId],\n * constraints: [orderBy('createdAt', 'asc')],\n * });\n * * // With realtime updates\n * const { data: members } = useFirestoreCollection<Member>({\n * collectionPath: `entities/${entityId}/members`,\n * queryKey: ['members', entityId],\n * subscribe: true,\n * });\n * ```\n */\nexport function useFirestoreCollection<T extends DocumentData = DocumentData>({\n collectionPath,\n queryKey,\n constraints = [],\n enabled = true,\n subscribe = false,\n staleTime,\n gcTime,\n select,\n}: FirestoreCollectionOptions<T>): WithSourceState<UseQueryResult<WithId<T>[], Error>> {\n const db = useFirestoreDb();\n const queryClient = useQueryClient();\n\n // STABILIZE DEPENDENCIES:\n // We stringify these because [where('a', '==', 'b')] !== [where('a', '==', 'b')] in JS.\n const constraintsMemo = JSON.stringify(constraints);\n const queryKeyMemo = JSON.stringify(queryKey);\n\n // Listener errors can't surface through useQuery (the queryFn is disabled while\n // subscribed), so they're tracked per subscription identity and merged into the result.\n const {\n error: subscriptionError,\n sourceState,\n reset: resetStatus,\n update: updateStatus,\n } = useSubscriptionStatus(\n JSON.stringify([collectionPath, queryKeyMemo, constraintsMemo, enabled, subscribe]),\n );\n\n useEffect(() => {\n if (!subscribe || !enabled) return;\n\n resetStatus();\n\n // Bounded resubscribe: a transient `permission-denied` (e.g. a failed mid-session\n // App Check token refresh) is retried on the RESUBSCRIBE_DELAYS_MS ladder before the\n // error is surfaced. `attempt` resets on any healthy snapshot; `disposed`/timers are\n // cleaned up on unmount so a pending retry never fires after teardown.\n let currentUnsubscribe: (() => void) | null = null;\n let retryTimer: ReturnType<typeof setTimeout> | null = null;\n let attempt = 0;\n let disposed = false;\n\n const start = () => {\n if (disposed) return;\n const collectionRef = collection(db, collectionPath);\n const q = constraints.length > 0\n ? query(collectionRef, ...constraints)\n : collectionRef;\n\n currentUnsubscribe = onSnapshot(\n q,\n { includeMetadataChanges: true },\n (snapshot) => {\n attempt = 0;\n updateStatus({ error: null });\n const items = snapshot.docs.map((docSnap) => {\n const rawData = docSnap.data();\n const dataWithId = { id: docSnap.id, ...rawData };\n const data = select ? select(dataWithId) : dataWithId;\n return data as WithId<T>;\n });\n queryClient.setQueryData(queryKey, items);\n const fromCache = snapshot.metadata?.fromCache ?? false;\n updateStatus((prev) => ({ sourceState: fromCache ? (prev.sourceState === 'connecting' ? 'connecting' : 'offline') : 'live' }));\n },\n (error) => {\n // Heal a transient permission-denied by resubscribing on the backoff ladder,\n // keeping the last-known data visible. Only surface the error once the ladder\n // is exhausted; any other error surfaces immediately, as before.\n if (isPermissionDeniedError(error) && attempt < RESUBSCRIBE_DELAYS_MS.length) {\n currentUnsubscribe?.();\n currentUnsubscribe = null;\n const delay = RESUBSCRIBE_DELAYS_MS[attempt];\n attempt += 1;\n updateStatus({ sourceState: 'connecting' });\n retryTimer = setTimeout(start, delay);\n return;\n }\n console.error('[useFirestoreCollection] Subscription error:', error);\n updateStatus({ error, sourceState: 'error' });\n queryClient.setQueryData(queryKey, undefined);\n }\n );\n };\n\n start();\n\n return () => {\n disposed = true;\n if (retryTimer) clearTimeout(retryTimer);\n currentUnsubscribe?.();\n };\n // queryKey & constraints are tracked via their stringified forms above; select is intentionally\n // excluded so an inline caller function doesn't force a re-subscribe on every render.\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [db, collectionPath, queryKeyMemo, constraintsMemo, enabled, subscribe, queryClient, resetStatus, updateStatus]);\n\n const queryResult = useQuery({\n queryKey,\n queryFn: async (): Promise<WithId<T>[]> => {\n const collectionRef = collection(db, collectionPath);\n const q = constraints.length > 0\n ? query(collectionRef, ...constraints)\n : collectionRef;\n\n const snapshot = await getDocs(q);\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: enabled && !subscribe,\n staleTime: subscribe ? Infinity : staleTime,\n gcTime,\n });\n\n // Merge a listener error into the result so subscribed consumers see a truthful\n // isError/error/status instead of a forever-pending disabled query.\n if (subscribe && subscriptionError) {\n return {\n ...queryResult,\n data: undefined,\n error: subscriptionError,\n isError: true,\n isLoadingError: true,\n isSuccess: false,\n isPending: false,\n isLoading: false,\n status: 'error',\n sourceState,\n } as WithSourceState<UseQueryResult<WithId<T>[], Error>>;\n }\n\n return { ...queryResult, sourceState } as WithSourceState<UseQueryResult<WithId<T>[], Error>>;\n}\n"]}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"use-firestore-doc.d.ts","sourceRoot":"","sources":["../../../src/react/firestore/use-firestore-doc.ts"],"names":[],"mappings":"AAGA,OAAO,EAA4B,KAAK,cAAc,EAAE,MAAM,uBAAuB,CAAC;AACtF,OAAO,EAA2B,KAAK,YAAY,EAAE,MAAM,oBAAoB,CAAC;
|
|
1
|
+
{"version":3,"file":"use-firestore-doc.d.ts","sourceRoot":"","sources":["../../../src/react/firestore/use-firestore-doc.ts"],"names":[],"mappings":"AAGA,OAAO,EAA4B,KAAK,cAAc,EAAE,MAAM,uBAAuB,CAAC;AACtF,OAAO,EAA2B,KAAK,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAIhF,OAAO,KAAK,EACV,mBAAmB,EACnB,MAAM,EACN,eAAe,EAChB,MAAM,0BAA0B,CAAC;AAElC;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,wBAAgB,eAAe,CAAC,CAAC,SAAS,YAAY,GAAG,YAAY,EAAE,EACrE,OAAO,EACP,QAAQ,EACR,OAAc,EACd,SAAiB,EACjB,SAAS,EACT,MAAM,EACN,MAAM,GACP,EAAE,mBAAmB,CAAC,CAAC,CAAC,GAAG,eAAe,CAAC,cAAc,CAAC,MAAM,CAAC,CAAC,CAAC,GAAG,IAAI,EAAE,KAAK,CAAC,CAAC,CAyHnF"}
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
'use client';
|
|
2
|
-
import { useEffect
|
|
2
|
+
import { useEffect } from 'react';
|
|
3
3
|
import { useQuery, useQueryClient } from '@tanstack/react-query';
|
|
4
4
|
import { doc, getDoc, onSnapshot } from 'firebase/firestore';
|
|
5
5
|
import { useFirestoreDb } from './context.js';
|
|
6
6
|
import { RESUBSCRIBE_DELAYS_MS, isPermissionDeniedError } from './resubscribe.js';
|
|
7
|
+
import { useSubscriptionStatus } from './subscription-status.js';
|
|
7
8
|
/**
|
|
8
9
|
* Fetch a single Firestore document with optional realtime updates.
|
|
9
10
|
* * @example
|
|
@@ -32,16 +33,13 @@ export function useFirestoreDoc({ docPath, queryKey, enabled = true, subscribe =
|
|
|
32
33
|
const queryClient = useQueryClient();
|
|
33
34
|
const queryKeyMemo = JSON.stringify(queryKey);
|
|
34
35
|
// Listener errors can't surface through useQuery (the queryFn is disabled while
|
|
35
|
-
// subscribed), so they're tracked
|
|
36
|
-
const
|
|
37
|
-
// Metadata-derived source state — reset whenever the subscription identity changes.
|
|
38
|
-
const [sourceState, setSourceState] = useState('connecting');
|
|
36
|
+
// subscribed), so they're tracked per subscription identity and merged into the result.
|
|
37
|
+
const { error: subscriptionError, sourceState, reset: resetStatus, update: updateStatus, } = useSubscriptionStatus(JSON.stringify([docPath, queryKeyMemo, enabled, subscribe]));
|
|
39
38
|
// Set up realtime subscription
|
|
40
39
|
useEffect(() => {
|
|
41
40
|
if (!subscribe || !enabled)
|
|
42
41
|
return;
|
|
43
|
-
|
|
44
|
-
setSourceState('connecting');
|
|
42
|
+
resetStatus();
|
|
45
43
|
// Bounded resubscribe: a transient `permission-denied` (e.g. a failed mid-session
|
|
46
44
|
// App Check token refresh) is retried on the RESUBSCRIBE_DELAYS_MS ladder before the
|
|
47
45
|
// error is surfaced. `attempt` resets on any healthy snapshot; `disposed`/timers are
|
|
@@ -56,7 +54,7 @@ export function useFirestoreDoc({ docPath, queryKey, enabled = true, subscribe =
|
|
|
56
54
|
const docRef = doc(db, docPath);
|
|
57
55
|
currentUnsubscribe = onSnapshot(docRef, { includeMetadataChanges: true }, (snapshot) => {
|
|
58
56
|
attempt = 0;
|
|
59
|
-
|
|
57
|
+
updateStatus({ error: null });
|
|
60
58
|
if (snapshot.exists()) {
|
|
61
59
|
const rawData = snapshot.data();
|
|
62
60
|
// Include id in data passed to select, so it can be renamed/transformed
|
|
@@ -68,7 +66,7 @@ export function useFirestoreDoc({ docPath, queryKey, enabled = true, subscribe =
|
|
|
68
66
|
queryClient.setQueryData(queryKey, null);
|
|
69
67
|
}
|
|
70
68
|
const fromCache = snapshot.metadata?.fromCache ?? false;
|
|
71
|
-
|
|
69
|
+
updateStatus((prev) => ({ sourceState: fromCache ? (prev.sourceState === 'connecting' ? 'connecting' : 'offline') : 'live' }));
|
|
72
70
|
}, (error) => {
|
|
73
71
|
// Heal a transient permission-denied by resubscribing on the backoff ladder,
|
|
74
72
|
// keeping the last-known data visible. Only surface the error once the ladder
|
|
@@ -78,13 +76,12 @@ export function useFirestoreDoc({ docPath, queryKey, enabled = true, subscribe =
|
|
|
78
76
|
currentUnsubscribe = null;
|
|
79
77
|
const delay = RESUBSCRIBE_DELAYS_MS[attempt];
|
|
80
78
|
attempt += 1;
|
|
81
|
-
|
|
79
|
+
updateStatus({ sourceState: 'connecting' });
|
|
82
80
|
retryTimer = setTimeout(start, delay);
|
|
83
81
|
return;
|
|
84
82
|
}
|
|
85
83
|
console.error('[useFirestoreDoc] Subscription error:', error);
|
|
86
|
-
|
|
87
|
-
setSourceState('error');
|
|
84
|
+
updateStatus({ error, sourceState: 'error' });
|
|
88
85
|
queryClient.setQueryData(queryKey, undefined);
|
|
89
86
|
});
|
|
90
87
|
};
|
|
@@ -98,7 +95,7 @@ export function useFirestoreDoc({ docPath, queryKey, enabled = true, subscribe =
|
|
|
98
95
|
// Note: select intentionally excluded to prevent re-subscribing on every render if the caller
|
|
99
96
|
// passes an inline function. queryKey is tracked via queryKeyMemo above.
|
|
100
97
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
101
|
-
}, [db, docPath, queryKeyMemo, enabled, subscribe, queryClient]);
|
|
98
|
+
}, [db, docPath, queryKeyMemo, enabled, subscribe, queryClient, resetStatus, updateStatus]);
|
|
102
99
|
const query = useQuery({
|
|
103
100
|
queryKey,
|
|
104
101
|
queryFn: async () => {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"use-firestore-doc.js","sourceRoot":"","sources":["../../../src/react/firestore/use-firestore-doc.ts"],"names":[],"mappings":"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","sourcesContent":["'use client';\n\nimport { useEffect, useState } from 'react';\nimport { useQuery, useQueryClient, type UseQueryResult } from '@tanstack/react-query';\nimport { doc, getDoc, onSnapshot, type DocumentData } from 'firebase/firestore';\nimport { useFirestoreDb } from './context.js';\nimport { RESUBSCRIBE_DELAYS_MS, isPermissionDeniedError } from './resubscribe.js';\nimport type {\n FirestoreDocOptions,\n FirestoreSourceState,\n WithId,\n WithSourceState,\n} from '../../firestore/types.js';\n\n/**\n * Fetch a single Firestore document with optional realtime updates.\n * * @example\n * ```tsx\n * // Simple fetch\n * const { data: user } = useFirestoreDoc<User>({\n * docPath: `users/${userId}`,\n * queryKey: ['user', userId],\n * });\n * * // With realtime updates\n * const { data: entity } = useFirestoreDoc<Entity>({\n * docPath: `entities/${entityId}`,\n * queryKey: ['entity', entityId],\n * subscribe: true,\n * });\n * * // Conditional fetch\n * const { data: profile } = useFirestoreDoc<Profile>({\n * docPath: `profiles/${userId}`,\n * queryKey: ['profile', userId],\n * enabled: !!userId,\n * });\n * ```\n */\nexport function useFirestoreDoc<T extends DocumentData = DocumentData>({\n docPath,\n queryKey,\n enabled = true,\n subscribe = false,\n staleTime,\n gcTime,\n select,\n}: FirestoreDocOptions<T>): WithSourceState<UseQueryResult<WithId<T> | null, Error>> {\n const db = useFirestoreDb();\n const queryClient = useQueryClient();\n const queryKeyMemo = JSON.stringify(queryKey);\n\n // Listener errors can't surface through useQuery (the queryFn is disabled while\n // subscribed), so they're tracked here and merged into the returned result.\n const [subscriptionError, setSubscriptionError] = useState<Error | null>(null);\n // Metadata-derived source state — reset whenever the subscription identity changes.\n const [sourceState, setSourceState] = useState<FirestoreSourceState>('connecting');\n\n // Set up realtime subscription\n useEffect(() => {\n if (!subscribe || !enabled) return;\n\n setSubscriptionError(null);\n setSourceState('connecting');\n\n // Bounded resubscribe: a transient `permission-denied` (e.g. a failed mid-session\n // App Check token refresh) is retried on the RESUBSCRIBE_DELAYS_MS ladder before the\n // error is surfaced. `attempt` resets on any healthy snapshot; `disposed`/timers are\n // cleaned up on unmount so a pending retry never fires after teardown.\n let currentUnsubscribe: (() => void) | null = null;\n let retryTimer: ReturnType<typeof setTimeout> | null = null;\n let attempt = 0;\n let disposed = false;\n\n const start = () => {\n if (disposed) return;\n const docRef = doc(db, docPath);\n currentUnsubscribe = onSnapshot(\n docRef,\n { includeMetadataChanges: true },\n (snapshot) => {\n attempt = 0;\n setSubscriptionError(null);\n if (snapshot.exists()) {\n const rawData = snapshot.data();\n // Include id in data passed to select, so it can be renamed/transformed\n const dataWithId = { id: snapshot.id, ...rawData };\n const data = select ? select(dataWithId) : dataWithId;\n queryClient.setQueryData(queryKey, data as WithId<T>);\n } else {\n queryClient.setQueryData(queryKey, null);\n }\n const fromCache = snapshot.metadata?.fromCache ?? false;\n setSourceState((prev) => (fromCache ? (prev === 'connecting' ? 'connecting' : 'offline') : 'live'));\n },\n (error) => {\n // Heal a transient permission-denied by resubscribing on the backoff ladder,\n // keeping the last-known data visible. Only surface the error once the ladder\n // is exhausted; any other error surfaces immediately, as before.\n if (isPermissionDeniedError(error) && attempt < RESUBSCRIBE_DELAYS_MS.length) {\n currentUnsubscribe?.();\n currentUnsubscribe = null;\n const delay = RESUBSCRIBE_DELAYS_MS[attempt];\n attempt += 1;\n setSourceState('connecting');\n retryTimer = setTimeout(start, delay);\n return;\n }\n console.error('[useFirestoreDoc] Subscription error:', error);\n setSubscriptionError(error);\n setSourceState('error');\n queryClient.setQueryData(queryKey, undefined);\n }\n );\n };\n\n start();\n\n return () => {\n disposed = true;\n if (retryTimer) clearTimeout(retryTimer);\n currentUnsubscribe?.();\n };\n // Note: select intentionally excluded to prevent re-subscribing on every render if the caller\n // passes an inline function. queryKey is tracked via queryKeyMemo above.\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [db, docPath, queryKeyMemo, enabled, subscribe, queryClient]);\n\n const query = useQuery({\n queryKey,\n queryFn: async (): Promise<WithId<T> | null> => {\n const docRef = doc(db, docPath);\n const snapshot = await getDoc(docRef);\n\n if (!snapshot.exists()) {\n return null;\n }\n\n const rawData = snapshot.data();\n // Include id in data passed to select, so it can be renamed/transformed\n const dataWithId = { id: snapshot.id, ...rawData };\n const data = select ? select(dataWithId) : dataWithId;\n return data as WithId<T>;\n },\n enabled: enabled && !subscribe, // Don't fetch if subscribing (snapshot handles it)\n staleTime: subscribe ? Infinity : staleTime, // Realtime data is always fresh\n gcTime,\n });\n\n // Merge a listener error into the result so subscribed consumers see a truthful\n // isError/error/status instead of a forever-pending disabled query.\n if (subscribe && subscriptionError) {\n return {\n ...query,\n data: undefined,\n error: subscriptionError,\n isError: true,\n isLoadingError: true,\n isSuccess: false,\n isPending: false,\n isLoading: false,\n status: 'error',\n sourceState,\n } as WithSourceState<UseQueryResult<WithId<T> | null, Error>>;\n }\n\n return { ...query, sourceState } as WithSourceState<UseQueryResult<WithId<T> | null, Error>>;\n}\n"]}
|
|
1
|
+
{"version":3,"file":"use-firestore-doc.js","sourceRoot":"","sources":["../../../src/react/firestore/use-firestore-doc.ts"],"names":[],"mappings":"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","sourcesContent":["'use client';\n\nimport { useEffect } from 'react';\nimport { useQuery, useQueryClient, type UseQueryResult } from '@tanstack/react-query';\nimport { doc, getDoc, onSnapshot, type DocumentData } from 'firebase/firestore';\nimport { useFirestoreDb } from './context.js';\nimport { RESUBSCRIBE_DELAYS_MS, isPermissionDeniedError } from './resubscribe.js';\nimport { useSubscriptionStatus } from './subscription-status.js';\nimport type {\n FirestoreDocOptions,\n WithId,\n WithSourceState,\n} from '../../firestore/types.js';\n\n/**\n * Fetch a single Firestore document with optional realtime updates.\n * * @example\n * ```tsx\n * // Simple fetch\n * const { data: user } = useFirestoreDoc<User>({\n * docPath: `users/${userId}`,\n * queryKey: ['user', userId],\n * });\n * * // With realtime updates\n * const { data: entity } = useFirestoreDoc<Entity>({\n * docPath: `entities/${entityId}`,\n * queryKey: ['entity', entityId],\n * subscribe: true,\n * });\n * * // Conditional fetch\n * const { data: profile } = useFirestoreDoc<Profile>({\n * docPath: `profiles/${userId}`,\n * queryKey: ['profile', userId],\n * enabled: !!userId,\n * });\n * ```\n */\nexport function useFirestoreDoc<T extends DocumentData = DocumentData>({\n docPath,\n queryKey,\n enabled = true,\n subscribe = false,\n staleTime,\n gcTime,\n select,\n}: FirestoreDocOptions<T>): WithSourceState<UseQueryResult<WithId<T> | null, Error>> {\n const db = useFirestoreDb();\n const queryClient = useQueryClient();\n const queryKeyMemo = JSON.stringify(queryKey);\n\n // Listener errors can't surface through useQuery (the queryFn is disabled while\n // subscribed), so they're tracked per subscription identity and merged into the result.\n const {\n error: subscriptionError,\n sourceState,\n reset: resetStatus,\n update: updateStatus,\n } = useSubscriptionStatus(JSON.stringify([docPath, queryKeyMemo, enabled, subscribe]));\n\n // Set up realtime subscription\n useEffect(() => {\n if (!subscribe || !enabled) return;\n\n resetStatus();\n\n // Bounded resubscribe: a transient `permission-denied` (e.g. a failed mid-session\n // App Check token refresh) is retried on the RESUBSCRIBE_DELAYS_MS ladder before the\n // error is surfaced. `attempt` resets on any healthy snapshot; `disposed`/timers are\n // cleaned up on unmount so a pending retry never fires after teardown.\n let currentUnsubscribe: (() => void) | null = null;\n let retryTimer: ReturnType<typeof setTimeout> | null = null;\n let attempt = 0;\n let disposed = false;\n\n const start = () => {\n if (disposed) return;\n const docRef = doc(db, docPath);\n currentUnsubscribe = onSnapshot(\n docRef,\n { includeMetadataChanges: true },\n (snapshot) => {\n attempt = 0;\n updateStatus({ error: null });\n if (snapshot.exists()) {\n const rawData = snapshot.data();\n // Include id in data passed to select, so it can be renamed/transformed\n const dataWithId = { id: snapshot.id, ...rawData };\n const data = select ? select(dataWithId) : dataWithId;\n queryClient.setQueryData(queryKey, data as WithId<T>);\n } else {\n queryClient.setQueryData(queryKey, null);\n }\n const fromCache = snapshot.metadata?.fromCache ?? false;\n updateStatus((prev) => ({ sourceState: fromCache ? (prev.sourceState === 'connecting' ? 'connecting' : 'offline') : 'live' }));\n },\n (error) => {\n // Heal a transient permission-denied by resubscribing on the backoff ladder,\n // keeping the last-known data visible. Only surface the error once the ladder\n // is exhausted; any other error surfaces immediately, as before.\n if (isPermissionDeniedError(error) && attempt < RESUBSCRIBE_DELAYS_MS.length) {\n currentUnsubscribe?.();\n currentUnsubscribe = null;\n const delay = RESUBSCRIBE_DELAYS_MS[attempt];\n attempt += 1;\n updateStatus({ sourceState: 'connecting' });\n retryTimer = setTimeout(start, delay);\n return;\n }\n console.error('[useFirestoreDoc] Subscription error:', error);\n updateStatus({ error, sourceState: 'error' });\n queryClient.setQueryData(queryKey, undefined);\n }\n );\n };\n\n start();\n\n return () => {\n disposed = true;\n if (retryTimer) clearTimeout(retryTimer);\n currentUnsubscribe?.();\n };\n // Note: select intentionally excluded to prevent re-subscribing on every render if the caller\n // passes an inline function. queryKey is tracked via queryKeyMemo above.\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [db, docPath, queryKeyMemo, enabled, subscribe, queryClient, resetStatus, updateStatus]);\n\n const query = useQuery({\n queryKey,\n queryFn: async (): Promise<WithId<T> | null> => {\n const docRef = doc(db, docPath);\n const snapshot = await getDoc(docRef);\n\n if (!snapshot.exists()) {\n return null;\n }\n\n const rawData = snapshot.data();\n // Include id in data passed to select, so it can be renamed/transformed\n const dataWithId = { id: snapshot.id, ...rawData };\n const data = select ? select(dataWithId) : dataWithId;\n return data as WithId<T>;\n },\n enabled: enabled && !subscribe, // Don't fetch if subscribing (snapshot handles it)\n staleTime: subscribe ? Infinity : staleTime, // Realtime data is always fresh\n gcTime,\n });\n\n // Merge a listener error into the result so subscribed consumers see a truthful\n // isError/error/status instead of a forever-pending disabled query.\n if (subscribe && subscriptionError) {\n return {\n ...query,\n data: undefined,\n error: subscriptionError,\n isError: true,\n isLoadingError: true,\n isSuccess: false,\n isPending: false,\n isLoading: false,\n status: 'error',\n sourceState,\n } as WithSourceState<UseQueryResult<WithId<T> | null, Error>>;\n }\n\n return { ...query, sourceState } as WithSourceState<UseQueryResult<WithId<T> | null, Error>>;\n}\n"]}
|