@ttt-productions/query-core 0.14.7 → 0.14.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (37) hide show
  1. package/dist/firestore/types.d.ts +8 -5
  2. package/dist/firestore/types.d.ts.map +1 -1
  3. package/dist/firestore/types.js.map +1 -1
  4. package/dist/react/firestore/batch-doc-loader.d.ts +1 -3
  5. package/dist/react/firestore/batch-doc-loader.d.ts.map +1 -1
  6. package/dist/react/firestore/batch-doc-loader.js +4 -6
  7. package/dist/react/firestore/batch-doc-loader.js.map +1 -1
  8. package/dist/react/firestore/context.d.ts +29 -1
  9. package/dist/react/firestore/context.d.ts.map +1 -1
  10. package/dist/react/firestore/context.js +34 -9
  11. package/dist/react/firestore/context.js.map +1 -1
  12. package/dist/react/firestore/doc-subscription-registry.d.ts +6 -0
  13. package/dist/react/firestore/doc-subscription-registry.d.ts.map +1 -1
  14. package/dist/react/firestore/doc-subscription-registry.js +2 -1
  15. package/dist/react/firestore/doc-subscription-registry.js.map +1 -1
  16. package/dist/react/firestore/subscription-status.d.ts +26 -2
  17. package/dist/react/firestore/subscription-status.d.ts.map +1 -1
  18. package/dist/react/firestore/subscription-status.js +145 -9
  19. package/dist/react/firestore/subscription-status.js.map +1 -1
  20. package/dist/react/firestore/use-firestore-collection.d.ts.map +1 -1
  21. package/dist/react/firestore/use-firestore-collection.js +10 -24
  22. package/dist/react/firestore/use-firestore-collection.js.map +1 -1
  23. package/dist/react/firestore/use-firestore-doc.d.ts.map +1 -1
  24. package/dist/react/firestore/use-firestore-doc.js +10 -24
  25. package/dist/react/firestore/use-firestore-doc.js.map +1 -1
  26. package/dist/react/firestore/use-firestore-live-infinite.d.ts +8 -5
  27. package/dist/react/firestore/use-firestore-live-infinite.d.ts.map +1 -1
  28. package/dist/react/firestore/use-firestore-live-infinite.js +27 -27
  29. package/dist/react/firestore/use-firestore-live-infinite.js.map +1 -1
  30. package/dist/react/firestore/useBatchFirestoreDocs.d.ts +1 -1
  31. package/dist/react/firestore/useBatchFirestoreDocs.d.ts.map +1 -1
  32. package/dist/react/firestore/useBatchFirestoreDocs.js +8 -1
  33. package/dist/react/firestore/useBatchFirestoreDocs.js.map +1 -1
  34. package/dist/react/index.d.ts +1 -1
  35. package/dist/react/index.d.ts.map +1 -1
  36. package/dist/react/index.js.map +1 -1
  37. package/package.json +67 -64
@@ -159,12 +159,15 @@ export interface FirestoreLiveInfiniteOptions<T = DocumentData & {
159
159
  select?: (data: DocumentData & {
160
160
  id: string;
161
161
  }) => T;
162
- /** Numeric sort key from a raw doc (id included). Default: reads `orderByField` as a number. */
163
- getSortValue?: (data: DocumentData & {
164
- id: string;
165
- }) => number;
166
- /** Output ordering of the merged list. Default 'asc' (oldest → newest). */
162
+ /** Output ordering of the merged list, by `orderByField` as Firestore orders it. Default 'asc' (oldest → newest). */
167
163
  sort?: 'asc' | 'desc';
164
+ /**
165
+ * How long the loaded older pages count as fresh (ms). The live window is a listener and is
166
+ * always current; this is the older pages' freshness tier. Omit for the query client's default.
167
+ */
168
+ staleTime?: number;
169
+ /** How long the older pages stay cached once no list shows them (ms). Omit for the query client's default. */
170
+ gcTime?: number;
168
171
  }
169
172
  /**
170
173
  * Result page for infinite queries.
@@ -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 /** 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"]}
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 /** Output ordering of the merged list, by `orderByField` as Firestore orders it. Default 'asc' (oldest → newest). */\n sort?: 'asc' | 'desc';\n /**\n * How long the loaded older pages count as fresh (ms). The live window is a listener and is\n * always current; this is the older pages' freshness tier. Omit for the query client's default.\n */\n staleTime?: number;\n /** How long the older pages stay cached once no list shows them (ms). Omit for the query client's default. */\n gcTime?: number;\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"]}
@@ -5,7 +5,7 @@ import type { QueryClient } from '@tanstack/react-query';
5
5
  *
6
6
  * Every id is its own observed React Query query, so without coalescing N ids would issue
7
7
  * N Firestore round-trips. The loader collects the ids enqueued in a single microtask and
8
- * dispatches them as `where(documentId(), 'in', chunk)` queries of at most 30 ids each
8
+ * dispatches them as `where(documentId(), 'in', chunk)` queries of at most `FIRESTORE_IN_FILTER_LIMIT` ids each
9
9
  * (`'batch'` transport), or as concurrent per-document gets (`'get'` transport — required
10
10
  * for collections whose rules gate reads on `resource.data`, where an unconstrained list
11
11
  * query is denied wholesale but per-document gets are evaluated per doc).
@@ -26,8 +26,6 @@ import type { QueryClient } from '@tanstack/react-query';
26
26
  * instance is part of the scope on purpose: two `db` instances sharing a collection path
27
27
  * must never share a request.
28
28
  */
29
- /** Firestore's hard cap on the number of values in an `in` filter. */
30
- export declare const FIRESTORE_IN_LIMIT = 30;
31
29
  /** Maximum `in`-query requests in flight at once, per loader scope. */
32
30
  export declare const MAX_CONCURRENT_BATCH_REQUESTS = 4;
33
31
  /** Maximum per-document gets in flight at once, per loader scope. */
@@ -1 +1 @@
1
- {"version":3,"file":"batch-doc-loader.d.ts","sourceRoot":"","sources":["../../../src/react/firestore/batch-doc-loader.ts"],"names":[],"mappings":"AAEA,OAAO,EAQL,KAAK,SAAS,EACf,MAAM,oBAAoB,CAAC;AAC5B,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,uBAAuB,CAAC;AAEzD;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AAEH,sEAAsE;AACtE,eAAO,MAAM,kBAAkB,KAAK,CAAC;AAErC,uEAAuE;AACvE,eAAO,MAAM,6BAA6B,IAAI,CAAC;AAE/C,qEAAqE;AACrE,eAAO,MAAM,uBAAuB,KAAK,CAAC;AAE1C,MAAM,MAAM,SAAS,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG;IAAE,EAAE,EAAE,MAAM,CAAA;CAAE,CAAC;AAOjE,MAAM,MAAM,SAAS,GAAG;IACtB,gEAAgE;IAChE,WAAW,CAAC,EAAE,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,WAAW,GAAG,OAAO,CAAC,SAAS,GAAG,IAAI,CAAC,CAAC;IACzE,qFAAqF;IACrF,OAAO,CAAC,EAAE,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,WAAW,GAAG,OAAO,CAAC,SAAS,GAAG,IAAI,CAAC,CAAC;CACtE,CAAC;AAoJF,wBAAgB,YAAY,CAC1B,WAAW,EAAE,WAAW,EACxB,EAAE,EAAE,SAAS,EACb,cAAc,EAAE,MAAM,GACrB,SAAS,CAiBX"}
1
+ {"version":3,"file":"batch-doc-loader.d.ts","sourceRoot":"","sources":["../../../src/react/firestore/batch-doc-loader.ts"],"names":[],"mappings":"AAEA,OAAO,EAQL,KAAK,SAAS,EACf,MAAM,oBAAoB,CAAC;AAC5B,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,uBAAuB,CAAC;AAGzD;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AAEH,uEAAuE;AACvE,eAAO,MAAM,6BAA6B,IAAI,CAAC;AAE/C,qEAAqE;AACrE,eAAO,MAAM,uBAAuB,KAAK,CAAC;AAE1C,MAAM,MAAM,SAAS,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG;IAAE,EAAE,EAAE,MAAM,CAAA;CAAE,CAAC;AAOjE,MAAM,MAAM,SAAS,GAAG;IACtB,gEAAgE;IAChE,WAAW,CAAC,EAAE,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,WAAW,GAAG,OAAO,CAAC,SAAS,GAAG,IAAI,CAAC,CAAC;IACzE,qFAAqF;IACrF,OAAO,CAAC,EAAE,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,WAAW,GAAG,OAAO,CAAC,SAAS,GAAG,IAAI,CAAC,CAAC;CACtE,CAAC;AAmJF,wBAAgB,YAAY,CAC1B,WAAW,EAAE,WAAW,EACxB,EAAE,EAAE,SAAS,EACb,cAAc,EAAE,MAAM,GACrB,SAAS,CAiBX"}
@@ -1,11 +1,12 @@
1
1
  'use client';
2
2
  import { collection, doc, documentId, getDoc, getDocs, query, where, } from 'firebase/firestore';
3
+ import { chunk, FIRESTORE_IN_FILTER_LIMIT } from '@ttt-productions/firebase-helpers';
3
4
  /**
4
5
  * Coalescing document loader behind {@link useBatchFirestoreDocs}'s one-shot mode.
5
6
  *
6
7
  * Every id is its own observed React Query query, so without coalescing N ids would issue
7
8
  * N Firestore round-trips. The loader collects the ids enqueued in a single microtask and
8
- * dispatches them as `where(documentId(), 'in', chunk)` queries of at most 30 ids each
9
+ * dispatches them as `where(documentId(), 'in', chunk)` queries of at most `FIRESTORE_IN_FILTER_LIMIT` ids each
9
10
  * (`'batch'` transport), or as concurrent per-document gets (`'get'` transport — required
10
11
  * for collections whose rules gate reads on `resource.data`, where an unconstrained list
11
12
  * query is denied wholesale but per-document gets are evaluated per doc).
@@ -26,8 +27,6 @@ import { collection, doc, documentId, getDoc, getDocs, query, where, } from 'fir
26
27
  * instance is part of the scope on purpose: two `db` instances sharing a collection path
27
28
  * must never share a request.
28
29
  */
29
- /** Firestore's hard cap on the number of values in an `in` filter. */
30
- export const FIRESTORE_IN_LIMIT = 30;
31
30
  /** Maximum `in`-query requests in flight at once, per loader scope. */
32
31
  export const MAX_CONCURRENT_BATCH_REQUESTS = 4;
33
32
  /** Maximum per-document gets in flight at once, per loader scope. */
@@ -100,9 +99,8 @@ function createDocLoader(db, collectionPath) {
100
99
  openBatch = null;
101
100
  if (!batch || batch.size === 0)
102
101
  return;
103
- const ids = [...batch.keys()];
104
- for (let i = 0; i < ids.length; i += FIRESTORE_IN_LIMIT) {
105
- void runChunk(ids.slice(i, i + FIRESTORE_IN_LIMIT), batch);
102
+ for (const ids of chunk([...batch.keys()], FIRESTORE_IN_FILTER_LIMIT)) {
103
+ void runChunk(ids, batch);
106
104
  }
107
105
  };
108
106
  const loadBatched = (id, signal) => new Promise((resolve, reject) => {
@@ -1 +1 @@
1
- {"version":3,"file":"batch-doc-loader.js","sourceRoot":"","sources":["../../../src/react/firestore/batch-doc-loader.ts"],"names":[],"mappings":"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","sourcesContent":["'use client';\n\nimport {\n collection,\n doc,\n documentId,\n getDoc,\n getDocs,\n query,\n where,\n type Firestore,\n} from 'firebase/firestore';\nimport type { QueryClient } from '@tanstack/react-query';\n\n/**\n * Coalescing document loader behind {@link useBatchFirestoreDocs}'s one-shot mode.\n *\n * Every id is its own observed React Query query, so without coalescing N ids would issue\n * N Firestore round-trips. The loader collects the ids enqueued in a single microtask and\n * dispatches them as `where(documentId(), 'in', chunk)` queries of at most 30 ids each\n * (`'batch'` transport), or as concurrent per-document gets (`'get'` transport — required\n * for collections whose rules gate reads on `resource.data`, where an unconstrained list\n * query is denied wholesale but per-document gets are evaluated per doc).\n *\n * Two properties are load-bearing:\n *\n * - **The loader resolves promises only — it NEVER writes the query cache.** TanStack's\n * per-query fetch identity is therefore the sole guard against a late batch response\n * overwriting a newer per-id result.\n * - **A dispatched batch is closed.** A later enqueue of the same id (a refetch after a\n * cancellation, say) always joins a NEW batch, never the in-flight one — so a cancelled\n * read can never be served by the request it was cancelled out of.\n *\n * Cancelling one per-id query detaches only that waiter; the shared request still completes\n * for its batch-mates.\n *\n * Loaders are scoped per `(QueryClient, Firestore instance, collectionPath)`. The Firestore\n * instance is part of the scope on purpose: two `db` instances sharing a collection path\n * must never share a request.\n */\n\n/** Firestore's hard cap on the number of values in an `in` filter. */\nexport const FIRESTORE_IN_LIMIT = 30;\n\n/** Maximum `in`-query requests in flight at once, per loader scope. */\nexport const MAX_CONCURRENT_BATCH_REQUESTS = 4;\n\n/** Maximum per-document gets in flight at once, per loader scope. */\nexport const MAX_CONCURRENT_DOC_GETS = 12;\n\nexport type LoadedDoc = Record<string, unknown> & { id: string };\n\ntype Waiter = {\n resolve: (value: LoadedDoc | null) => void;\n reject: (error: unknown) => void;\n};\n\nexport type DocLoader = {\n /** Resolve one id through the coalescing `in`-query batcher. */\n loadBatched(id: string, signal?: AbortSignal): Promise<LoadedDoc | null>;\n /** Resolve one id through its own `getDoc`, bounded by the get concurrency limit. */\n loadOne(id: string, signal?: AbortSignal): Promise<LoadedDoc | null>;\n};\n\n/** Runs at most `max` tasks concurrently; a slot is released when its task settles. */\nfunction createConcurrencyLimiter(max: number) {\n let active = 0;\n const waiting: (() => void)[] = [];\n\n const pump = () => {\n while (active < max && waiting.length > 0) {\n const start = waiting.shift();\n active += 1;\n start?.();\n }\n };\n\n const release = () => {\n active -= 1;\n pump();\n };\n\n return <R>(task: () => Promise<R>): Promise<R> =>\n new Promise<R>((resolve, reject) => {\n waiting.push(() => {\n task().then(\n (value) => {\n release();\n resolve(value);\n },\n (error) => {\n release();\n reject(error);\n },\n );\n });\n pump();\n });\n}\n\nfunction abortReason(signal: AbortSignal): unknown {\n return signal.reason ?? new Error('Document read was cancelled');\n}\n\nfunction createDocLoader(db: Firestore, collectionPath: string): DocLoader {\n const runBatchRequest = createConcurrencyLimiter(MAX_CONCURRENT_BATCH_REQUESTS);\n const runDocGet = createConcurrencyLimiter(MAX_CONCURRENT_DOC_GETS);\n\n // Ids enqueued in the CURRENT microtask. Nulled out at dispatch so the dispatched batch\n // is closed and a later enqueue opens a fresh one.\n let openBatch: Map<string, Waiter[]> | null = null;\n\n const runChunk = (chunk: string[], batch: Map<string, Waiter[]>) =>\n runBatchRequest(async () => {\n let found: Map<string, LoadedDoc> | null = null;\n let failure: unknown;\n try {\n const snapshot = await getDocs(\n query(collection(db, collectionPath), where(documentId(), 'in', chunk)),\n );\n found = new Map<string, LoadedDoc>();\n snapshot.forEach((snap) => {\n found!.set(snap.id, { id: snap.id, ...snap.data() } as LoadedDoc);\n });\n } catch (error) {\n failure = error;\n }\n\n for (const id of chunk) {\n const waiters = batch.get(id);\n batch.delete(id);\n if (!waiters) continue;\n for (const waiter of [...waiters]) {\n if (found) waiter.resolve(found.get(id) ?? null);\n else waiter.reject(failure);\n }\n }\n });\n\n const dispatch = () => {\n const batch = openBatch;\n openBatch = null;\n if (!batch || batch.size === 0) return;\n const ids = [...batch.keys()];\n for (let i = 0; i < ids.length; i += FIRESTORE_IN_LIMIT) {\n void runChunk(ids.slice(i, i + FIRESTORE_IN_LIMIT), batch);\n }\n };\n\n const loadBatched = (id: string, signal?: AbortSignal): Promise<LoadedDoc | null> =>\n new Promise<LoadedDoc | null>((resolve, reject) => {\n if (signal?.aborted) {\n reject(abortReason(signal));\n return;\n }\n\n if (!openBatch) {\n openBatch = new Map();\n queueMicrotask(dispatch);\n }\n\n const batch = openBatch;\n let slot = batch.get(id);\n if (!slot) {\n slot = [];\n batch.set(id, slot);\n }\n const waiters = slot;\n\n let settled = false;\n let onAbort: (() => void) | undefined;\n\n const settle = (run: () => void) => {\n if (settled) return;\n settled = true;\n const index = waiters.indexOf(waiter);\n if (index >= 0) waiters.splice(index, 1);\n // An id nobody is waiting on any more must not ride into a billed `in` query.\n if (waiters.length === 0 && batch.get(id) === waiters) batch.delete(id);\n if (onAbort) signal?.removeEventListener('abort', onAbort);\n run();\n };\n\n const waiter: Waiter = {\n resolve: (value) => settle(() => resolve(value)),\n reject: (error) => settle(() => reject(error)),\n };\n\n waiters.push(waiter);\n\n if (signal) {\n onAbort = () => waiter.reject(abortReason(signal));\n signal.addEventListener('abort', onAbort, { once: true });\n }\n });\n\n const loadOne = (id: string, signal?: AbortSignal): Promise<LoadedDoc | null> =>\n runDocGet(async () => {\n if (signal?.aborted) throw abortReason(signal);\n const snapshot = await getDoc(doc(db, collectionPath, id));\n return snapshot.exists() ? ({ id: snapshot.id, ...snapshot.data() } as LoadedDoc) : null;\n });\n\n return { loadBatched, loadOne };\n}\n\n// Per-QueryClient → per-Firestore-instance → per-collectionPath. WeakMaps so a discarded\n// client or db instance takes its loaders with it.\nconst loaderScopes = new WeakMap<QueryClient, WeakMap<Firestore, Map<string, DocLoader>>>();\n\nexport function getDocLoader(\n queryClient: QueryClient,\n db: Firestore,\n collectionPath: string,\n): DocLoader {\n let byDb = loaderScopes.get(queryClient);\n if (!byDb) {\n byDb = new WeakMap();\n loaderScopes.set(queryClient, byDb);\n }\n let byPath = byDb.get(db);\n if (!byPath) {\n byPath = new Map();\n byDb.set(db, byPath);\n }\n let loader = byPath.get(collectionPath);\n if (!loader) {\n loader = createDocLoader(db, collectionPath);\n byPath.set(collectionPath, loader);\n }\n return loader;\n}\n"]}
1
+ {"version":3,"file":"batch-doc-loader.js","sourceRoot":"","sources":["../../../src/react/firestore/batch-doc-loader.ts"],"names":[],"mappings":"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","sourcesContent":["'use client';\n\nimport {\n collection,\n doc,\n documentId,\n getDoc,\n getDocs,\n query,\n where,\n type Firestore,\n} from 'firebase/firestore';\nimport type { QueryClient } from '@tanstack/react-query';\nimport { chunk, FIRESTORE_IN_FILTER_LIMIT } from '@ttt-productions/firebase-helpers';\n\n/**\n * Coalescing document loader behind {@link useBatchFirestoreDocs}'s one-shot mode.\n *\n * Every id is its own observed React Query query, so without coalescing N ids would issue\n * N Firestore round-trips. The loader collects the ids enqueued in a single microtask and\n * dispatches them as `where(documentId(), 'in', chunk)` queries of at most `FIRESTORE_IN_FILTER_LIMIT` ids each\n * (`'batch'` transport), or as concurrent per-document gets (`'get'` transport — required\n * for collections whose rules gate reads on `resource.data`, where an unconstrained list\n * query is denied wholesale but per-document gets are evaluated per doc).\n *\n * Two properties are load-bearing:\n *\n * - **The loader resolves promises only — it NEVER writes the query cache.** TanStack's\n * per-query fetch identity is therefore the sole guard against a late batch response\n * overwriting a newer per-id result.\n * - **A dispatched batch is closed.** A later enqueue of the same id (a refetch after a\n * cancellation, say) always joins a NEW batch, never the in-flight one — so a cancelled\n * read can never be served by the request it was cancelled out of.\n *\n * Cancelling one per-id query detaches only that waiter; the shared request still completes\n * for its batch-mates.\n *\n * Loaders are scoped per `(QueryClient, Firestore instance, collectionPath)`. The Firestore\n * instance is part of the scope on purpose: two `db` instances sharing a collection path\n * must never share a request.\n */\n\n/** Maximum `in`-query requests in flight at once, per loader scope. */\nexport const MAX_CONCURRENT_BATCH_REQUESTS = 4;\n\n/** Maximum per-document gets in flight at once, per loader scope. */\nexport const MAX_CONCURRENT_DOC_GETS = 12;\n\nexport type LoadedDoc = Record<string, unknown> & { id: string };\n\ntype Waiter = {\n resolve: (value: LoadedDoc | null) => void;\n reject: (error: unknown) => void;\n};\n\nexport type DocLoader = {\n /** Resolve one id through the coalescing `in`-query batcher. */\n loadBatched(id: string, signal?: AbortSignal): Promise<LoadedDoc | null>;\n /** Resolve one id through its own `getDoc`, bounded by the get concurrency limit. */\n loadOne(id: string, signal?: AbortSignal): Promise<LoadedDoc | null>;\n};\n\n/** Runs at most `max` tasks concurrently; a slot is released when its task settles. */\nfunction createConcurrencyLimiter(max: number) {\n let active = 0;\n const waiting: (() => void)[] = [];\n\n const pump = () => {\n while (active < max && waiting.length > 0) {\n const start = waiting.shift();\n active += 1;\n start?.();\n }\n };\n\n const release = () => {\n active -= 1;\n pump();\n };\n\n return <R>(task: () => Promise<R>): Promise<R> =>\n new Promise<R>((resolve, reject) => {\n waiting.push(() => {\n task().then(\n (value) => {\n release();\n resolve(value);\n },\n (error) => {\n release();\n reject(error);\n },\n );\n });\n pump();\n });\n}\n\nfunction abortReason(signal: AbortSignal): unknown {\n return signal.reason ?? new Error('Document read was cancelled');\n}\n\nfunction createDocLoader(db: Firestore, collectionPath: string): DocLoader {\n const runBatchRequest = createConcurrencyLimiter(MAX_CONCURRENT_BATCH_REQUESTS);\n const runDocGet = createConcurrencyLimiter(MAX_CONCURRENT_DOC_GETS);\n\n // Ids enqueued in the CURRENT microtask. Nulled out at dispatch so the dispatched batch\n // is closed and a later enqueue opens a fresh one.\n let openBatch: Map<string, Waiter[]> | null = null;\n\n const runChunk = (chunk: string[], batch: Map<string, Waiter[]>) =>\n runBatchRequest(async () => {\n let found: Map<string, LoadedDoc> | null = null;\n let failure: unknown;\n try {\n const snapshot = await getDocs(\n query(collection(db, collectionPath), where(documentId(), 'in', chunk)),\n );\n found = new Map<string, LoadedDoc>();\n snapshot.forEach((snap) => {\n found!.set(snap.id, { id: snap.id, ...snap.data() } as LoadedDoc);\n });\n } catch (error) {\n failure = error;\n }\n\n for (const id of chunk) {\n const waiters = batch.get(id);\n batch.delete(id);\n if (!waiters) continue;\n for (const waiter of [...waiters]) {\n if (found) waiter.resolve(found.get(id) ?? null);\n else waiter.reject(failure);\n }\n }\n });\n\n const dispatch = () => {\n const batch = openBatch;\n openBatch = null;\n if (!batch || batch.size === 0) return;\n for (const ids of chunk([...batch.keys()], FIRESTORE_IN_FILTER_LIMIT)) {\n void runChunk(ids, batch);\n }\n };\n\n const loadBatched = (id: string, signal?: AbortSignal): Promise<LoadedDoc | null> =>\n new Promise<LoadedDoc | null>((resolve, reject) => {\n if (signal?.aborted) {\n reject(abortReason(signal));\n return;\n }\n\n if (!openBatch) {\n openBatch = new Map();\n queueMicrotask(dispatch);\n }\n\n const batch = openBatch;\n let slot = batch.get(id);\n if (!slot) {\n slot = [];\n batch.set(id, slot);\n }\n const waiters = slot;\n\n let settled = false;\n let onAbort: (() => void) | undefined;\n\n const settle = (run: () => void) => {\n if (settled) return;\n settled = true;\n const index = waiters.indexOf(waiter);\n if (index >= 0) waiters.splice(index, 1);\n // An id nobody is waiting on any more must not ride into a billed `in` query.\n if (waiters.length === 0 && batch.get(id) === waiters) batch.delete(id);\n if (onAbort) signal?.removeEventListener('abort', onAbort);\n run();\n };\n\n const waiter: Waiter = {\n resolve: (value) => settle(() => resolve(value)),\n reject: (error) => settle(() => reject(error)),\n };\n\n waiters.push(waiter);\n\n if (signal) {\n onAbort = () => waiter.reject(abortReason(signal));\n signal.addEventListener('abort', onAbort, { once: true });\n }\n });\n\n const loadOne = (id: string, signal?: AbortSignal): Promise<LoadedDoc | null> =>\n runDocGet(async () => {\n if (signal?.aborted) throw abortReason(signal);\n const snapshot = await getDoc(doc(db, collectionPath, id));\n return snapshot.exists() ? ({ id: snapshot.id, ...snapshot.data() } as LoadedDoc) : null;\n });\n\n return { loadBatched, loadOne };\n}\n\n// Per-QueryClient → per-Firestore-instance → per-collectionPath. WeakMaps so a discarded\n// client or db instance takes its loaders with it.\nconst loaderScopes = new WeakMap<QueryClient, WeakMap<Firestore, Map<string, DocLoader>>>();\n\nexport function getDocLoader(\n queryClient: QueryClient,\n db: Firestore,\n collectionPath: string,\n): DocLoader {\n let byDb = loaderScopes.get(queryClient);\n if (!byDb) {\n byDb = new WeakMap();\n loaderScopes.set(queryClient, byDb);\n }\n let byPath = byDb.get(db);\n if (!byPath) {\n byPath = new Map();\n byDb.set(db, byPath);\n }\n let loader = byPath.get(collectionPath);\n if (!loader) {\n loader = createDocLoader(db, collectionPath);\n byPath.set(collectionPath, loader);\n }\n return loader;\n}\n"]}
@@ -1,7 +1,28 @@
1
1
  import { type ReactNode } from 'react';
2
2
  import type { Firestore } from 'firebase/firestore';
3
+ /** The realtime read whose listener failed, as a provider-level listener-error handler receives it. */
4
+ export interface FirestoreListenerErrorDetails {
5
+ /** The hook whose listener failed. */
6
+ hook: 'useFirestoreDoc' | 'useFirestoreCollection' | 'useFirestoreLiveInfinite' | 'useBatchFirestoreDocs';
7
+ /** The read's React Query key, as the caller passed it. */
8
+ queryKey: readonly unknown[];
9
+ /** The document or collection path the listener read. */
10
+ path: string;
11
+ }
12
+ /**
13
+ * Receives each error a realtime listener surfaces — once per surfaced error, after a
14
+ * `permission-denied` has spent its resubscribe ladder (any other code at once). The read keeps
15
+ * the error in its own result; this is the app's one place to capture it.
16
+ */
17
+ export type FirestoreListenerErrorHandler = (error: Error, details: FirestoreListenerErrorDetails) => void;
3
18
  export interface FirestoreProviderProps {
4
19
  db: Firestore;
20
+ /**
21
+ * The app's capture for a realtime listener's surfaced error. A subscribed read never lands its
22
+ * listener error in the query cache, so a reporter that watches the cache does not see it; this
23
+ * hands it over instead. Without it the hooks write the error to `console.error`.
24
+ */
25
+ onListenerError?: FirestoreListenerErrorHandler;
5
26
  children: ReactNode;
6
27
  }
7
28
  /**
@@ -19,10 +40,17 @@ export interface FirestoreProviderProps {
19
40
  * </QueryProvider>
20
41
  * ```
21
42
  */
22
- export declare function FirestoreProvider({ db, children }: FirestoreProviderProps): import("react").JSX.Element;
43
+ export declare function FirestoreProvider({ db, onListenerError, children }: FirestoreProviderProps): import("react").JSX.Element;
23
44
  /**
24
45
  * Access the Firestore instance from context.
25
46
  * Throws if used outside of FirestoreProvider.
26
47
  */
27
48
  export declare function useFirestoreDb(): Firestore;
49
+ /**
50
+ * The function a realtime hook calls with its listener's surfaced error: the provider's
51
+ * `onListenerError` as it stands when the error arrives, or `console.error` when the app supplied
52
+ * none. The returned function keeps one identity, so a listener effect that depends on it is never
53
+ * re-opened by a provider handler written inline.
54
+ */
55
+ export declare function useListenerErrorReporter(): (error: Error, details: FirestoreListenerErrorDetails) => void;
28
56
  //# sourceMappingURL=context.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"context.d.ts","sourceRoot":"","sources":["../../../src/react/firestore/context.tsx"],"names":[],"mappings":"AAEA,OAAO,EAA6B,KAAK,SAAS,EAAE,MAAM,OAAO,CAAC;AAClE,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,oBAAoB,CAAC;AAQpD,MAAM,WAAW,sBAAsB;IACrC,EAAE,EAAE,SAAS,CAAC;IACd,QAAQ,EAAE,SAAS,CAAC;CACrB;AAED;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,iBAAiB,CAAC,EAAE,EAAE,EAAE,QAAQ,EAAE,EAAE,sBAAsB,+BAMzE;AAED;;;GAGG;AACH,wBAAgB,cAAc,IAAI,SAAS,CAS1C"}
1
+ {"version":3,"file":"context.d.ts","sourceRoot":"","sources":["../../../src/react/firestore/context.tsx"],"names":[],"mappings":"AAEA,OAAO,EAAsE,KAAK,SAAS,EAAE,MAAM,OAAO,CAAC;AAC3G,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,oBAAoB,CAAC;AAEpD,uGAAuG;AACvG,MAAM,WAAW,6BAA6B;IAC5C,sCAAsC;IACtC,IAAI,EAAE,iBAAiB,GAAG,wBAAwB,GAAG,0BAA0B,GAAG,uBAAuB,CAAC;IAC1G,2DAA2D;IAC3D,QAAQ,EAAE,SAAS,OAAO,EAAE,CAAC;IAC7B,yDAAyD;IACzD,IAAI,EAAE,MAAM,CAAC;CACd;AAED;;;;GAIG;AACH,MAAM,MAAM,6BAA6B,GAAG,CAAC,KAAK,EAAE,KAAK,EAAE,OAAO,EAAE,6BAA6B,KAAK,IAAI,CAAC;AAS3G,MAAM,WAAW,sBAAsB;IACrC,EAAE,EAAE,SAAS,CAAC;IACd;;;;OAIG;IACH,eAAe,CAAC,EAAE,6BAA6B,CAAC;IAChD,QAAQ,EAAE,SAAS,CAAC;CACrB;AAED;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,iBAAiB,CAAC,EAAE,EAAE,EAAE,eAAe,EAAE,QAAQ,EAAE,EAAE,sBAAsB,+BAO1F;AAaD;;;GAGG;AACH,wBAAgB,cAAc,IAAI,SAAS,CAE1C;AAED;;;;;GAKG;AACH,wBAAgB,wBAAwB,IAAI,CAAC,KAAK,EAAE,KAAK,EAAE,OAAO,EAAE,6BAA6B,KAAK,IAAI,CAYzG"}
@@ -1,6 +1,6 @@
1
1
  'use client';
2
2
  import { jsx as _jsx } from "react/jsx-runtime";
3
- import { createContext, useContext } from 'react';
3
+ import { createContext, useCallback, useContext, useEffect, useMemo, useRef } from 'react';
4
4
  const FirestoreContext = createContext(null);
5
5
  /**
6
6
  * Provides Firestore instance to all useFirestore* hooks.
@@ -17,19 +17,44 @@ const FirestoreContext = createContext(null);
17
17
  * </QueryProvider>
18
18
  * ```
19
19
  */
20
- export function FirestoreProvider({ db, children }) {
21
- return (_jsx(FirestoreContext.Provider, { value: { db }, children: children }));
20
+ export function FirestoreProvider({ db, onListenerError, children }) {
21
+ const value = useMemo(() => ({ db, onListenerError }), [db, onListenerError]);
22
+ return (_jsx(FirestoreContext.Provider, { value: value, children: children }));
22
23
  }
23
- /**
24
- * Access the Firestore instance from context.
25
- * Throws if used outside of FirestoreProvider.
26
- */
27
- export function useFirestoreDb() {
24
+ function useFirestoreContext() {
28
25
  const context = useContext(FirestoreContext);
29
26
  if (!context) {
30
27
  throw new Error('useFirestoreDb must be used within a FirestoreProvider. ' +
31
28
  'Wrap your app with <FirestoreProvider db={db}>.');
32
29
  }
33
- return context.db;
30
+ return context;
31
+ }
32
+ /**
33
+ * Access the Firestore instance from context.
34
+ * Throws if used outside of FirestoreProvider.
35
+ */
36
+ export function useFirestoreDb() {
37
+ return useFirestoreContext().db;
38
+ }
39
+ /**
40
+ * The function a realtime hook calls with its listener's surfaced error: the provider's
41
+ * `onListenerError` as it stands when the error arrives, or `console.error` when the app supplied
42
+ * none. The returned function keeps one identity, so a listener effect that depends on it is never
43
+ * re-opened by a provider handler written inline.
44
+ */
45
+ export function useListenerErrorReporter() {
46
+ // Read without requiring the provider: `useBatchFirestoreDocs` takes its `db` as an option and
47
+ // may run outside one, where the error goes to the console.
48
+ const onListenerError = useContext(FirestoreContext)?.onListenerError;
49
+ const handler = useRef(onListenerError);
50
+ useEffect(() => {
51
+ handler.current = onListenerError;
52
+ }, [onListenerError]);
53
+ return useCallback((error, details) => {
54
+ if (handler.current)
55
+ handler.current(error, details);
56
+ else
57
+ console.error(`[${details.hook}] Subscription error:`, error);
58
+ }, []);
34
59
  }
35
60
  //# sourceMappingURL=context.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"context.js","sourceRoot":"","sources":["../../../src/react/firestore/context.tsx"],"names":[],"mappings":"AAAA,YAAY,CAAC;;AAEb,OAAO,EAAE,aAAa,EAAE,UAAU,EAAkB,MAAM,OAAO,CAAC;AAOlE,MAAM,gBAAgB,GAAG,aAAa,CAA+B,IAAI,CAAC,CAAC;AAO3E;;;;;;;;;;;;;;GAcG;AACH,MAAM,UAAU,iBAAiB,CAAC,EAAE,EAAE,EAAE,QAAQ,EAA0B;IACxE,OAAO,CACL,KAAC,gBAAgB,CAAC,QAAQ,IAAC,KAAK,EAAE,EAAE,EAAE,EAAE,YACrC,QAAQ,GACiB,CAC7B,CAAC;AACJ,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,cAAc;IAC5B,MAAM,OAAO,GAAG,UAAU,CAAC,gBAAgB,CAAC,CAAC;IAC7C,IAAI,CAAC,OAAO,EAAE,CAAC;QACb,MAAM,IAAI,KAAK,CACb,0DAA0D;YAC1D,iDAAiD,CAClD,CAAC;IACJ,CAAC;IACD,OAAO,OAAO,CAAC,EAAE,CAAC;AACpB,CAAC","sourcesContent":["'use client';\n\nimport { createContext, useContext, type ReactNode } from 'react';\nimport type { Firestore } from 'firebase/firestore';\n\ninterface FirestoreContextValue {\n db: Firestore;\n}\n\nconst FirestoreContext = createContext<FirestoreContextValue | null>(null);\n\nexport interface FirestoreProviderProps {\n db: Firestore;\n children: ReactNode;\n}\n\n/**\n * Provides Firestore instance to all useFirestore* hooks.\n * Wrap your app with this provider alongside QueryProvider.\n *\n * @example\n * ```tsx\n * import { db } from '@/lib/firebase';\n *\n * <QueryProvider>\n * <FirestoreProvider db={db}>\n * {children}\n * </FirestoreProvider>\n * </QueryProvider>\n * ```\n */\nexport function FirestoreProvider({ db, children }: FirestoreProviderProps) {\n return (\n <FirestoreContext.Provider value={{ db }}>\n {children}\n </FirestoreContext.Provider>\n );\n}\n\n/**\n * Access the Firestore instance from context.\n * Throws if used outside of FirestoreProvider.\n */\nexport function useFirestoreDb(): Firestore {\n const context = useContext(FirestoreContext);\n if (!context) {\n throw new Error(\n 'useFirestoreDb must be used within a FirestoreProvider. ' +\n 'Wrap your app with <FirestoreProvider db={db}>.'\n );\n }\n return context.db;\n}\n"]}
1
+ {"version":3,"file":"context.js","sourceRoot":"","sources":["../../../src/react/firestore/context.tsx"],"names":[],"mappings":"AAAA,YAAY,CAAC;;AAEb,OAAO,EAAE,aAAa,EAAE,WAAW,EAAE,UAAU,EAAE,SAAS,EAAE,OAAO,EAAE,MAAM,EAAkB,MAAM,OAAO,CAAC;AAyB3G,MAAM,gBAAgB,GAAG,aAAa,CAA+B,IAAI,CAAC,CAAC;AAa3E;;;;;;;;;;;;;;GAcG;AACH,MAAM,UAAU,iBAAiB,CAAC,EAAE,EAAE,EAAE,eAAe,EAAE,QAAQ,EAA0B;IACzF,MAAM,KAAK,GAAG,OAAO,CAAC,GAAG,EAAE,CAAC,CAAC,EAAE,EAAE,EAAE,eAAe,EAAE,CAAC,EAAE,CAAC,EAAE,EAAE,eAAe,CAAC,CAAC,CAAC;IAC9E,OAAO,CACL,KAAC,gBAAgB,CAAC,QAAQ,IAAC,KAAK,EAAE,KAAK,YACpC,QAAQ,GACiB,CAC7B,CAAC;AACJ,CAAC;AAED,SAAS,mBAAmB;IAC1B,MAAM,OAAO,GAAG,UAAU,CAAC,gBAAgB,CAAC,CAAC;IAC7C,IAAI,CAAC,OAAO,EAAE,CAAC;QACb,MAAM,IAAI,KAAK,CACb,0DAA0D;YAC1D,iDAAiD,CAClD,CAAC;IACJ,CAAC;IACD,OAAO,OAAO,CAAC;AACjB,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,cAAc;IAC5B,OAAO,mBAAmB,EAAE,CAAC,EAAE,CAAC;AAClC,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,wBAAwB;IACtC,+FAA+F;IAC/F,4DAA4D;IAC5D,MAAM,eAAe,GAAG,UAAU,CAAC,gBAAgB,CAAC,EAAE,eAAe,CAAC;IACtE,MAAM,OAAO,GAAG,MAAM,CAAC,eAAe,CAAC,CAAC;IACxC,SAAS,CAAC,GAAG,EAAE;QACb,OAAO,CAAC,OAAO,GAAG,eAAe,CAAC;IACpC,CAAC,EAAE,CAAC,eAAe,CAAC,CAAC,CAAC;IACtB,OAAO,WAAW,CAAC,CAAC,KAAY,EAAE,OAAsC,EAAE,EAAE;QAC1E,IAAI,OAAO,CAAC,OAAO;YAAE,OAAO,CAAC,OAAO,CAAC,KAAK,EAAE,OAAO,CAAC,CAAC;;YAChD,OAAO,CAAC,KAAK,CAAC,IAAI,OAAO,CAAC,IAAI,uBAAuB,EAAE,KAAK,CAAC,CAAC;IACrE,CAAC,EAAE,EAAE,CAAC,CAAC;AACT,CAAC","sourcesContent":["'use client';\n\nimport { createContext, useCallback, useContext, useEffect, useMemo, useRef, type ReactNode } from 'react';\nimport type { Firestore } from 'firebase/firestore';\n\n/** The realtime read whose listener failed, as a provider-level listener-error handler receives it. */\nexport interface FirestoreListenerErrorDetails {\n /** The hook whose listener failed. */\n hook: 'useFirestoreDoc' | 'useFirestoreCollection' | 'useFirestoreLiveInfinite' | 'useBatchFirestoreDocs';\n /** The read's React Query key, as the caller passed it. */\n queryKey: readonly unknown[];\n /** The document or collection path the listener read. */\n path: string;\n}\n\n/**\n * Receives each error a realtime listener surfaces — once per surfaced error, after a\n * `permission-denied` has spent its resubscribe ladder (any other code at once). The read keeps\n * the error in its own result; this is the app's one place to capture it.\n */\nexport type FirestoreListenerErrorHandler = (error: Error, details: FirestoreListenerErrorDetails) => void;\n\ninterface FirestoreContextValue {\n db: Firestore;\n onListenerError?: FirestoreListenerErrorHandler;\n}\n\nconst FirestoreContext = createContext<FirestoreContextValue | null>(null);\n\nexport interface FirestoreProviderProps {\n db: Firestore;\n /**\n * The app's capture for a realtime listener's surfaced error. A subscribed read never lands its\n * listener error in the query cache, so a reporter that watches the cache does not see it; this\n * hands it over instead. Without it the hooks write the error to `console.error`.\n */\n onListenerError?: FirestoreListenerErrorHandler;\n children: ReactNode;\n}\n\n/**\n * Provides Firestore instance to all useFirestore* hooks.\n * Wrap your app with this provider alongside QueryProvider.\n *\n * @example\n * ```tsx\n * import { db } from '@/lib/firebase';\n *\n * <QueryProvider>\n * <FirestoreProvider db={db}>\n * {children}\n * </FirestoreProvider>\n * </QueryProvider>\n * ```\n */\nexport function FirestoreProvider({ db, onListenerError, children }: FirestoreProviderProps) {\n const value = useMemo(() => ({ db, onListenerError }), [db, onListenerError]);\n return (\n <FirestoreContext.Provider value={value}>\n {children}\n </FirestoreContext.Provider>\n );\n}\n\nfunction useFirestoreContext(): FirestoreContextValue {\n const context = useContext(FirestoreContext);\n if (!context) {\n throw new Error(\n 'useFirestoreDb must be used within a FirestoreProvider. ' +\n 'Wrap your app with <FirestoreProvider db={db}>.'\n );\n }\n return context;\n}\n\n/**\n * Access the Firestore instance from context.\n * Throws if used outside of FirestoreProvider.\n */\nexport function useFirestoreDb(): Firestore {\n return useFirestoreContext().db;\n}\n\n/**\n * The function a realtime hook calls with its listener's surfaced error: the provider's\n * `onListenerError` as it stands when the error arrives, or `console.error` when the app supplied\n * none. The returned function keeps one identity, so a listener effect that depends on it is never\n * re-opened by a provider handler written inline.\n */\nexport function useListenerErrorReporter(): (error: Error, details: FirestoreListenerErrorDetails) => void {\n // Read without requiring the provider: `useBatchFirestoreDocs` takes its `db` as an option and\n // may run outside one, where the error goes to the console.\n const onListenerError = useContext(FirestoreContext)?.onListenerError;\n const handler = useRef(onListenerError);\n useEffect(() => {\n handler.current = onListenerError;\n }, [onListenerError]);\n return useCallback((error: Error, details: FirestoreListenerErrorDetails) => {\n if (handler.current) handler.current(error, details);\n else console.error(`[${details.hook}] Subscription error:`, error);\n }, []);\n}\n"]}
@@ -27,6 +27,12 @@ export interface SubscribeDocParams {
27
27
  onUpdate: () => void;
28
28
  /** Called on listener error — and at once on joining a listener that has already errored. */
29
29
  onError?: (error: Error) => void;
30
+ /**
31
+ * The app's capture for the shared listener's error. Only the subscriber that opens the listener
32
+ * supplies it, and it is called once per error the listener surfaces — never again for a
33
+ * subscriber that joins the failed listener.
34
+ */
35
+ reportListenerError?: (error: Error) => void;
30
36
  }
31
37
  /**
32
38
  * Subscribe to a single document via a shared, reference-counted listener.
@@ -1 +1 @@
1
- {"version":3,"file":"doc-subscription-registry.d.ts","sourceRoot":"","sources":["../../../src/react/firestore/doc-subscription-registry.ts"],"names":[],"mappings":"AAEA,OAAO,EAAmB,KAAK,SAAS,EAAE,MAAM,oBAAoB,CAAC;AACrE,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,uBAAuB,CAAC;AA8CzD;;;;;;;GAOG;AACH,wBAAgB,eAAe,CAC7B,WAAW,EAAE,WAAW,EACxB,cAAc,EAAE,MAAM,EACtB,EAAE,EAAE,MAAM,GACT,OAAO,CAET;AAED;;;;GAIG;AACH,wBAAgB,eAAe,CAC7B,WAAW,EAAE,WAAW,EACxB,cAAc,EAAE,MAAM,EACtB,EAAE,EAAE,MAAM,GACT,KAAK,GAAG,IAAI,CAEd;AAWD,MAAM,WAAW,kBAAkB;IACjC,WAAW,EAAE,WAAW,CAAC;IACzB,EAAE,EAAE,SAAS,CAAC;IACd,4EAA4E;IAC5E,cAAc,EAAE,MAAM,CAAC;IACvB,qEAAqE;IACrE,cAAc,EAAE,MAAM,CAAC;IACvB,EAAE,EAAE,MAAM,CAAC;IACX,uFAAuF;IACvF,QAAQ,EAAE,MAAM,IAAI,CAAC;IACrB,6FAA6F;IAC7F,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,KAAK,KAAK,IAAI,CAAC;CAClC;AAED;;;;GAIG;AACH,wBAAgB,YAAY,CAAC,MAAM,EAAE,kBAAkB,GAAG,MAAM,IAAI,CAwDnE;AAED,sEAAsE;AACtE,wBAAgB,qBAAqB,CAAC,WAAW,EAAE,WAAW,GAAG,MAAM,CAEtE"}
1
+ {"version":3,"file":"doc-subscription-registry.d.ts","sourceRoot":"","sources":["../../../src/react/firestore/doc-subscription-registry.ts"],"names":[],"mappings":"AAEA,OAAO,EAAmB,KAAK,SAAS,EAAE,MAAM,oBAAoB,CAAC;AACrE,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,uBAAuB,CAAC;AA8CzD;;;;;;;GAOG;AACH,wBAAgB,eAAe,CAC7B,WAAW,EAAE,WAAW,EACxB,cAAc,EAAE,MAAM,EACtB,EAAE,EAAE,MAAM,GACT,OAAO,CAET;AAED;;;;GAIG;AACH,wBAAgB,eAAe,CAC7B,WAAW,EAAE,WAAW,EACxB,cAAc,EAAE,MAAM,EACtB,EAAE,EAAE,MAAM,GACT,KAAK,GAAG,IAAI,CAEd;AAWD,MAAM,WAAW,kBAAkB;IACjC,WAAW,EAAE,WAAW,CAAC;IACzB,EAAE,EAAE,SAAS,CAAC;IACd,4EAA4E;IAC5E,cAAc,EAAE,MAAM,CAAC;IACvB,qEAAqE;IACrE,cAAc,EAAE,MAAM,CAAC;IACvB,EAAE,EAAE,MAAM,CAAC;IACX,uFAAuF;IACvF,QAAQ,EAAE,MAAM,IAAI,CAAC;IACrB,6FAA6F;IAC7F,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,KAAK,KAAK,IAAI,CAAC;IACjC;;;;OAIG;IACH,mBAAmB,CAAC,EAAE,CAAC,KAAK,EAAE,KAAK,KAAK,IAAI,CAAC;CAC9C;AAED;;;;GAIG;AACH,wBAAgB,YAAY,CAAC,MAAM,EAAE,kBAAkB,GAAG,MAAM,IAAI,CAyDnE;AAED,sEAAsE;AACtE,wBAAgB,qBAAqB,CAAC,WAAW,EAAE,WAAW,GAAG,MAAM,CAEtE"}
@@ -38,7 +38,7 @@ function getRegistry(queryClient) {
38
38
  * detached only when the last subscriber for that id unsubscribes.
39
39
  */
40
40
  export function subscribeDoc(params) {
41
- const { queryClient, db, collectionPath, queryKeyPrefix, id, onUpdate, onError } = params;
41
+ const { queryClient, db, collectionPath, queryKeyPrefix, id, onUpdate, onError, reportListenerError } = params;
42
42
  const registry = getRegistry(queryClient);
43
43
  const key = registryKey(queryKeyPrefix, id);
44
44
  let entry = registry.get(key);
@@ -60,6 +60,7 @@ export function subscribeDoc(params) {
60
60
  queryClient.setQueryData([queryKeyPrefix, id], null, { updatedAt: Date.now() });
61
61
  for (const sub of [...subscribers])
62
62
  sub.onError?.(error);
63
+ reportListenerError?.(error);
63
64
  });
64
65
  entry = created;
65
66
  registry.set(key, entry);
@@ -1 +1 @@
1
- {"version":3,"file":"doc-subscription-registry.js","sourceRoot":"","sources":["../../../src/react/firestore/doc-subscription-registry.ts"],"names":[],"mappings":"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","sourcesContent":["'use client';\n\nimport { doc, onSnapshot, type Firestore } from 'firebase/firestore';\nimport type { QueryClient } from '@tanstack/react-query';\n\n/**\n * Reference-counted, real-time document subscription registry.\n *\n * Backs the `subscribe` mode of {@link useBatchFirestoreDocs}. The problem it solves:\n * many components on a page resolve the SAME document id (e.g. the same author rendered\n * in a feed card, a comment, and a mention). Without sharing, each would open its own\n * `onSnapshot` listener. This registry keeps exactly ONE Firestore listener per\n * `(queryClient, queryKeyPrefix, id)` and fans its updates out to every subscriber, so N\n * components showing the same id cost one listener.\n *\n * The single listener writes the document straight into the React Query cache at\n * `[queryKeyPrefix, id]` — `{ id, ...data }` when the doc exists, or `null` when it does\n * not (negative caching, so a missing/lagging doc resolves the instant it appears instead\n * of staying blank until staleTime). A listener ERROR also negative-caches the id: a\n * rules-denied doc (hidden, or non-existent when the rule references `resource.data`) is\n * resolved, not forever-loading — consumers see `data: null` plus the error instead\n * of an eternal spinner. Firestore ends a listener that errors, so the entry keeps that error\n * for as long as the entry lives (until its last subscriber leaves; a later snapshot, were one\n * to arrive, clears it): a subscriber that joins an errored listener receives the error too,\n * and a one-shot reader of the same key reads it through `listenerErrorOf`, so neither takes\n * the cached `null` as a missing document. The registry is keyed per `QueryClient` (a WeakMap),\n * which in practice means per browser tab, so each tab listens independently and stays\n * consistent without cross-tab plumbing.\n */\n\ntype Subscriber = {\n onUpdate: () => void;\n onError?: (error: Error) => void;\n};\n\ntype Entry = {\n unsubscribe: () => void;\n subscribers: Set<Subscriber>;\n /** The listener's last error, cleared by the next snapshot. */\n error: Error | null;\n};\n\n// Per-QueryClient registry. WeakMap so a discarded QueryClient (and its entries) is GC'd.\nconst registries = new WeakMap<QueryClient, Map<string, Entry>>();\n\nfunction registryKey(queryKeyPrefix: string, id: string): string {\n return `${queryKeyPrefix} ${id}`;\n}\n\n/**\n * True while a shared listener owns `[queryKeyPrefix, id]` for this client.\n *\n * A one-shot read that was already in flight when a consumer entered subscribe mode would\n * otherwise resolve AFTER the listener's first snapshot and overwrite it — and `onSnapshot`\n * does not re-emit until the document actually changes, so the stale value would stick.\n * The one-shot `queryFn` consults this after its read and yields to the listener's value.\n */\nexport function isListenerOwned(\n queryClient: QueryClient,\n queryKeyPrefix: string,\n id: string,\n): boolean {\n return registries.get(queryClient)?.has(registryKey(queryKeyPrefix, id)) ?? false;\n}\n\n/**\n * The error of the shared listener that owns `[queryKeyPrefix, id]`, or `null` when no listener\n * owns the key or it has not errored. The listener writes `null` to the key when it errors, so a\n * one-shot reader of the same key consults this before taking that `null` as a missing document.\n */\nexport function listenerErrorOf(\n queryClient: QueryClient,\n queryKeyPrefix: string,\n id: string,\n): Error | null {\n return registries.get(queryClient)?.get(registryKey(queryKeyPrefix, id))?.error ?? null;\n}\n\nfunction getRegistry(queryClient: QueryClient): Map<string, Entry> {\n let registry = registries.get(queryClient);\n if (!registry) {\n registry = new Map();\n registries.set(queryClient, registry);\n }\n return registry;\n}\n\nexport interface SubscribeDocParams {\n queryClient: QueryClient;\n db: Firestore;\n /** Top-level/full collection path the doc lives in (e.g. 'publicUsers'). */\n collectionPath: string;\n /** Cache key prefix; the doc is cached at `[queryKeyPrefix, id]`. */\n queryKeyPrefix: string;\n id: string;\n /** Called whenever a snapshot (data or missing) lands, so the caller can re-render. */\n onUpdate: () => void;\n /** Called on listener error — and at once on joining a listener that has already errored. */\n onError?: (error: Error) => void;\n}\n\n/**\n * Subscribe to a single document via a shared, reference-counted listener.\n * Returns an idempotent unsubscribe function; the underlying Firestore listener is\n * detached only when the last subscriber for that id unsubscribes.\n */\nexport function subscribeDoc(params: SubscribeDocParams): () => void {\n const { queryClient, db, collectionPath, queryKeyPrefix, id, onUpdate, onError } = params;\n const registry = getRegistry(queryClient);\n const key = registryKey(queryKeyPrefix, id);\n\n let entry = registry.get(key);\n if (!entry) {\n const created: Entry = { unsubscribe: () => {}, subscribers: new Set<Subscriber>(), error: null };\n const subscribers = created.subscribers;\n const ref = doc(db, collectionPath, id);\n created.unsubscribe = onSnapshot(\n ref,\n (snapshot) => {\n created.error = null;\n queryClient.setQueryData(\n [queryKeyPrefix, id],\n snapshot.exists() ? { id: snapshot.id, ...snapshot.data() } : null,\n { updatedAt: Date.now() },\n );\n // Copy before iterating: an onUpdate could (re-entrantly) unsubscribe.\n for (const sub of [...subscribers]) sub.onUpdate();\n },\n (error) => {\n // Write `null` so the id resolves — as a failure, through the kept error — instead\n // of loading forever: subscribe-mode loading (\"no cache entry yet\") would otherwise\n // stay true for ids whose listener can never land a snapshot.\n created.error = error;\n queryClient.setQueryData([queryKeyPrefix, id], null, { updatedAt: Date.now() });\n for (const sub of [...subscribers]) sub.onError?.(error);\n },\n );\n entry = created;\n registry.set(key, entry);\n }\n\n const subscriber: Subscriber = { onUpdate, onError };\n entry.subscribers.add(subscriber);\n if (entry.error) onError?.(entry.error);\n\n let released = false;\n return () => {\n if (released) return;\n released = true;\n const current = registry.get(key);\n if (!current) return;\n current.subscribers.delete(subscriber);\n if (current.subscribers.size === 0) {\n current.unsubscribe();\n registry.delete(key);\n // A failed listener's `null` is a placeholder, not a read: once nothing owns the key,\n // mark it stale so a remaining one-shot reader reads the document for itself.\n if (current.error) {\n void queryClient.invalidateQueries({ queryKey: [queryKeyPrefix, id], exact: true });\n }\n }\n };\n}\n\n/** Test-only: number of active shared listeners for a QueryClient. */\nexport function __activeListenerCount(queryClient: QueryClient): number {\n return registries.get(queryClient)?.size ?? 0;\n}\n"]}
1
+ {"version":3,"file":"doc-subscription-registry.js","sourceRoot":"","sources":["../../../src/react/firestore/doc-subscription-registry.ts"],"names":[],"mappings":"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","sourcesContent":["'use client';\n\nimport { doc, onSnapshot, type Firestore } from 'firebase/firestore';\nimport type { QueryClient } from '@tanstack/react-query';\n\n/**\n * Reference-counted, real-time document subscription registry.\n *\n * Backs the `subscribe` mode of {@link useBatchFirestoreDocs}. The problem it solves:\n * many components on a page resolve the SAME document id (e.g. the same author rendered\n * in a feed card, a comment, and a mention). Without sharing, each would open its own\n * `onSnapshot` listener. This registry keeps exactly ONE Firestore listener per\n * `(queryClient, queryKeyPrefix, id)` and fans its updates out to every subscriber, so N\n * components showing the same id cost one listener.\n *\n * The single listener writes the document straight into the React Query cache at\n * `[queryKeyPrefix, id]` — `{ id, ...data }` when the doc exists, or `null` when it does\n * not (negative caching, so a missing/lagging doc resolves the instant it appears instead\n * of staying blank until staleTime). A listener ERROR also negative-caches the id: a\n * rules-denied doc (hidden, or non-existent when the rule references `resource.data`) is\n * resolved, not forever-loading — consumers see `data: null` plus the error instead\n * of an eternal spinner. Firestore ends a listener that errors, so the entry keeps that error\n * for as long as the entry lives (until its last subscriber leaves; a later snapshot, were one\n * to arrive, clears it): a subscriber that joins an errored listener receives the error too,\n * and a one-shot reader of the same key reads it through `listenerErrorOf`, so neither takes\n * the cached `null` as a missing document. The registry is keyed per `QueryClient` (a WeakMap),\n * which in practice means per browser tab, so each tab listens independently and stays\n * consistent without cross-tab plumbing.\n */\n\ntype Subscriber = {\n onUpdate: () => void;\n onError?: (error: Error) => void;\n};\n\ntype Entry = {\n unsubscribe: () => void;\n subscribers: Set<Subscriber>;\n /** The listener's last error, cleared by the next snapshot. */\n error: Error | null;\n};\n\n// Per-QueryClient registry. WeakMap so a discarded QueryClient (and its entries) is GC'd.\nconst registries = new WeakMap<QueryClient, Map<string, Entry>>();\n\nfunction registryKey(queryKeyPrefix: string, id: string): string {\n return `${queryKeyPrefix} ${id}`;\n}\n\n/**\n * True while a shared listener owns `[queryKeyPrefix, id]` for this client.\n *\n * A one-shot read that was already in flight when a consumer entered subscribe mode would\n * otherwise resolve AFTER the listener's first snapshot and overwrite it — and `onSnapshot`\n * does not re-emit until the document actually changes, so the stale value would stick.\n * The one-shot `queryFn` consults this after its read and yields to the listener's value.\n */\nexport function isListenerOwned(\n queryClient: QueryClient,\n queryKeyPrefix: string,\n id: string,\n): boolean {\n return registries.get(queryClient)?.has(registryKey(queryKeyPrefix, id)) ?? false;\n}\n\n/**\n * The error of the shared listener that owns `[queryKeyPrefix, id]`, or `null` when no listener\n * owns the key or it has not errored. The listener writes `null` to the key when it errors, so a\n * one-shot reader of the same key consults this before taking that `null` as a missing document.\n */\nexport function listenerErrorOf(\n queryClient: QueryClient,\n queryKeyPrefix: string,\n id: string,\n): Error | null {\n return registries.get(queryClient)?.get(registryKey(queryKeyPrefix, id))?.error ?? null;\n}\n\nfunction getRegistry(queryClient: QueryClient): Map<string, Entry> {\n let registry = registries.get(queryClient);\n if (!registry) {\n registry = new Map();\n registries.set(queryClient, registry);\n }\n return registry;\n}\n\nexport interface SubscribeDocParams {\n queryClient: QueryClient;\n db: Firestore;\n /** Top-level/full collection path the doc lives in (e.g. 'publicUsers'). */\n collectionPath: string;\n /** Cache key prefix; the doc is cached at `[queryKeyPrefix, id]`. */\n queryKeyPrefix: string;\n id: string;\n /** Called whenever a snapshot (data or missing) lands, so the caller can re-render. */\n onUpdate: () => void;\n /** Called on listener error — and at once on joining a listener that has already errored. */\n onError?: (error: Error) => void;\n /**\n * The app's capture for the shared listener's error. Only the subscriber that opens the listener\n * supplies it, and it is called once per error the listener surfaces — never again for a\n * subscriber that joins the failed listener.\n */\n reportListenerError?: (error: Error) => void;\n}\n\n/**\n * Subscribe to a single document via a shared, reference-counted listener.\n * Returns an idempotent unsubscribe function; the underlying Firestore listener is\n * detached only when the last subscriber for that id unsubscribes.\n */\nexport function subscribeDoc(params: SubscribeDocParams): () => void {\n const { queryClient, db, collectionPath, queryKeyPrefix, id, onUpdate, onError, reportListenerError } = params;\n const registry = getRegistry(queryClient);\n const key = registryKey(queryKeyPrefix, id);\n\n let entry = registry.get(key);\n if (!entry) {\n const created: Entry = { unsubscribe: () => {}, subscribers: new Set<Subscriber>(), error: null };\n const subscribers = created.subscribers;\n const ref = doc(db, collectionPath, id);\n created.unsubscribe = onSnapshot(\n ref,\n (snapshot) => {\n created.error = null;\n queryClient.setQueryData(\n [queryKeyPrefix, id],\n snapshot.exists() ? { id: snapshot.id, ...snapshot.data() } : null,\n { updatedAt: Date.now() },\n );\n // Copy before iterating: an onUpdate could (re-entrantly) unsubscribe.\n for (const sub of [...subscribers]) sub.onUpdate();\n },\n (error) => {\n // Write `null` so the id resolves — as a failure, through the kept error — instead\n // of loading forever: subscribe-mode loading (\"no cache entry yet\") would otherwise\n // stay true for ids whose listener can never land a snapshot.\n created.error = error;\n queryClient.setQueryData([queryKeyPrefix, id], null, { updatedAt: Date.now() });\n for (const sub of [...subscribers]) sub.onError?.(error);\n reportListenerError?.(error);\n },\n );\n entry = created;\n registry.set(key, entry);\n }\n\n const subscriber: Subscriber = { onUpdate, onError };\n entry.subscribers.add(subscriber);\n if (entry.error) onError?.(entry.error);\n\n let released = false;\n return () => {\n if (released) return;\n released = true;\n const current = registry.get(key);\n if (!current) return;\n current.subscribers.delete(subscriber);\n if (current.subscribers.size === 0) {\n current.unsubscribe();\n registry.delete(key);\n // A failed listener's `null` is a placeholder, not a read: once nothing owns the key,\n // mark it stale so a remaining one-shot reader reads the document for itself.\n if (current.error) {\n void queryClient.invalidateQueries({ queryKey: [queryKeyPrefix, id], exact: true });\n }\n }\n };\n}\n\n/** Test-only: number of active shared listeners for a QueryClient. */\nexport function __activeListenerCount(queryClient: QueryClient): number {\n return registries.get(queryClient)?.size ?? 0;\n}\n"]}
@@ -1,21 +1,45 @@
1
- import type { FirestoreSourceState } from '../../firestore/types.js';
1
+ import type { UseQueryResult } from '@tanstack/react-query';
2
+ import type { FirestoreSourceState, WithSourceState } from '../../firestore/types.js';
3
+ /** A restart a subscribed result's `refetch()` asked for, open until the listener's first snapshot or its next error. */
4
+ interface PendingRestart {
5
+ /** The listener had failed: its last data is not shown as current while the restart is open. */
6
+ fromError: boolean;
7
+ }
2
8
  interface SubscriptionStatus {
3
9
  identity: string;
4
10
  error: Error | null;
5
11
  sourceState: FirestoreSourceState;
12
+ restart: PendingRestart | null;
13
+ /** Bumped by every restart; the listener effect depends on it, so a bump re-opens the listener. */
14
+ restarts: number;
6
15
  }
7
- type StatusPatch = Partial<Omit<SubscriptionStatus, 'identity'>>;
16
+ type StatusPatch = Partial<Omit<SubscriptionStatus, 'identity' | 'restarts'>>;
8
17
  /**
9
18
  * A realtime listener's source state and error, tagged to the subscription identity that
10
19
  * produced them. A new identity reads `connecting` with no error from its first render;
11
20
  * writes made for a retired identity are dropped once the new identity's listener has
12
21
  * started (it calls `reset`).
22
+ *
23
+ * `restart()` clears the error and bumps `restarts`; the listener effect re-opens on the bump,
24
+ * and its first snapshot or next surfaced error ends the restart (`settled` in the patch).
13
25
  */
14
26
  export declare function useSubscriptionStatus(identity: string): {
15
27
  error: Error | null;
16
28
  sourceState: FirestoreSourceState;
29
+ pendingRestart: PendingRestart | null;
30
+ restarts: number;
17
31
  reset: () => void;
18
32
  update: (patch: StatusPatch | ((prev: SubscriptionStatus) => StatusPatch)) => void;
33
+ restart: () => boolean;
19
34
  };
35
+ type SubscriptionStatusApi = ReturnType<typeof useSubscriptionStatus>;
36
+ /**
37
+ * The subscribed hooks' result: the query's own result, with the listener's error and source
38
+ * state merged in. In subscribe mode `refetch()` re-opens the listener (clearing its error, with a
39
+ * fresh resubscribe ladder) and resolves once the re-opened listener delivers its first snapshot or
40
+ * surfaces its next error; until then the result reports fetching. A restart after a failure shows
41
+ * no data until that snapshot: the failed listener's last rows are not presented as current.
42
+ */
43
+ export declare function useSubscribedResult<TData>(query: UseQueryResult<TData, Error>, subscribe: boolean, enabled: boolean, status: SubscriptionStatusApi): WithSourceState<UseQueryResult<TData, Error>>;
20
44
  export {};
21
45
  //# sourceMappingURL=subscription-status.d.ts.map
@@ -1 +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"}
1
+ {"version":3,"file":"subscription-status.d.ts","sourceRoot":"","sources":["../../../src/react/firestore/subscription-status.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAuC,cAAc,EAAE,MAAM,uBAAuB,CAAC;AACjG,OAAO,KAAK,EAAE,oBAAoB,EAAE,eAAe,EAAE,MAAM,0BAA0B,CAAC;AAEtF,yHAAyH;AACzH,UAAU,cAAc;IACtB,gGAAgG;IAChG,SAAS,EAAE,OAAO,CAAC;CACpB;AAED,UAAU,kBAAkB;IAC1B,QAAQ,EAAE,MAAM,CAAC;IACjB,KAAK,EAAE,KAAK,GAAG,IAAI,CAAC;IACpB,WAAW,EAAE,oBAAoB,CAAC;IAClC,OAAO,EAAE,cAAc,GAAG,IAAI,CAAC;IAC/B,mGAAmG;IACnG,QAAQ,EAAE,MAAM,CAAC;CAClB;AAED,KAAK,WAAW,GAAG,OAAO,CAAC,IAAI,CAAC,kBAAkB,EAAE,UAAU,GAAG,UAAU,CAAC,CAAC,CAAC;AAM9E;;;;;;;;GAQG;AACH,wBAAgB,qBAAqB,CAAC,QAAQ,EAAE,MAAM;;;;;;oBAc1C,WAAW,GAAG,CAAC,CAAC,IAAI,EAAE,kBAAkB,KAAK,WAAW,CAAC;mBAkBnC,OAAO;EAwBxC;AAED,KAAK,qBAAqB,GAAG,UAAU,CAAC,OAAO,qBAAqB,CAAC,CAAC;AAQtE;;;;;;GAMG;AACH,wBAAgB,mBAAmB,CAAC,KAAK,EACvC,KAAK,EAAE,cAAc,CAAC,KAAK,EAAE,KAAK,CAAC,EACnC,SAAS,EAAE,OAAO,EAClB,OAAO,EAAE,OAAO,EAChB,MAAM,EAAE,qBAAqB,GAC5B,eAAe,CAAC,cAAc,CAAC,KAAK,EAAE,KAAK,CAAC,CAAC,CA6F/C"}