@ttt-productions/query-core 0.14.6 → 0.14.8

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 (36) hide show
  1. package/dist/firestore/types.d.ts +1 -5
  2. package/dist/firestore/types.d.ts.map +1 -1
  3. package/dist/firestore/types.js.map +1 -1
  4. package/dist/index.d.ts +1 -0
  5. package/dist/index.d.ts.map +1 -1
  6. package/dist/index.js +1 -0
  7. package/dist/index.js.map +1 -1
  8. package/dist/react/firestore/batch-doc-loader.d.ts +1 -3
  9. package/dist/react/firestore/batch-doc-loader.d.ts.map +1 -1
  10. package/dist/react/firestore/batch-doc-loader.js +4 -6
  11. package/dist/react/firestore/batch-doc-loader.js.map +1 -1
  12. package/dist/react/firestore/subscription-status.d.ts +26 -2
  13. package/dist/react/firestore/subscription-status.d.ts.map +1 -1
  14. package/dist/react/firestore/subscription-status.js +145 -9
  15. package/dist/react/firestore/subscription-status.js.map +1 -1
  16. package/dist/react/firestore/use-firestore-collection.d.ts.map +1 -1
  17. package/dist/react/firestore/use-firestore-collection.js +7 -22
  18. package/dist/react/firestore/use-firestore-collection.js.map +1 -1
  19. package/dist/react/firestore/use-firestore-doc.d.ts.map +1 -1
  20. package/dist/react/firestore/use-firestore-doc.js +7 -22
  21. package/dist/react/firestore/use-firestore-doc.js.map +1 -1
  22. package/dist/react/firestore/use-firestore-live-infinite.d.ts +8 -5
  23. package/dist/react/firestore/use-firestore-live-infinite.d.ts.map +1 -1
  24. package/dist/react/firestore/use-firestore-live-infinite.js +21 -24
  25. package/dist/react/firestore/use-firestore-live-infinite.js.map +1 -1
  26. package/dist/react/firestore/useBatchFirestoreDocs.d.ts +1 -1
  27. package/dist/react/firestore/useBatchFirestoreDocs.js.map +1 -1
  28. package/dist/react/search/use-firestore-search.d.ts +3 -1
  29. package/dist/react/search/use-firestore-search.d.ts.map +1 -1
  30. package/dist/react/search/use-firestore-search.js +50 -5
  31. package/dist/react/search/use-firestore-search.js.map +1 -1
  32. package/dist/search/rule.d.ts +10 -0
  33. package/dist/search/rule.d.ts.map +1 -0
  34. package/dist/search/rule.js +14 -0
  35. package/dist/search/rule.js.map +1 -0
  36. package/package.json +67 -64
@@ -159,11 +159,7 @@ 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';
168
164
  }
169
165
  /**
@@ -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\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
@@ -4,6 +4,7 @@ export type { WithId, FirestoreSourceState, WithSourceState, BatchFirestoreDocOu
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
@@ -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,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;AAG3B,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"}
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
@@ -4,6 +4,7 @@ export { STALE_TIMES } from './defaults.js';
4
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,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;AASzC,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';\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"]}
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"]}
@@ -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,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"}
@@ -1,20 +1,24 @@
1
1
  'use client';
2
- import { useCallback, useState } from 'react';
2
+ import { useCallback, useEffect, useRef, useState } from 'react';
3
+ function freshStatus(identity) {
4
+ return { identity, error: null, sourceState: 'connecting', restart: null, restarts: 0 };
5
+ }
3
6
  /**
4
7
  * A realtime listener's source state and error, tagged to the subscription identity that
5
8
  * produced them. A new identity reads `connecting` with no error from its first render;
6
9
  * writes made for a retired identity are dropped once the new identity's listener has
7
10
  * started (it calls `reset`).
11
+ *
12
+ * `restart()` clears the error and bumps `restarts`; the listener effect re-opens on the bump,
13
+ * and its first snapshot or next surfaced error ends the restart (`settled` in the patch).
8
14
  */
9
15
  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 [status, setStatus] = useState(() => freshStatus(identity));
17
+ const current = status.identity === identity ? status : freshStatus(identity);
16
18
  const reset = useCallback(() => {
17
- setStatus({ identity, error: null, sourceState: 'connecting' });
19
+ setStatus((prev) => prev.identity === identity
20
+ ? { ...prev, error: null, sourceState: 'connecting' }
21
+ : freshStatus(identity));
18
22
  }, [identity]);
19
23
  const update = useCallback((patch) => {
20
24
  setStatus((prev) => {
@@ -24,6 +28,138 @@ export function useSubscriptionStatus(identity) {
24
28
  return { ...prev, ...next };
25
29
  });
26
30
  }, [identity]);
27
- return { error: current.error, sourceState: current.sourceState, reset, update };
31
+ // The identity the hook renders now. A `restart` captured under an earlier identity must not
32
+ // touch the current one's status, or every write the current listener makes would be dropped.
33
+ const renderedIdentity = useRef(identity);
34
+ useEffect(() => {
35
+ renderedIdentity.current = identity;
36
+ }, [identity]);
37
+ /** Re-opens the listener; false (and nothing changes) when this identity is no longer rendered. */
38
+ const restart = useCallback(() => {
39
+ if (renderedIdentity.current !== identity)
40
+ return false;
41
+ setStatus((prev) => {
42
+ const base = prev.identity === identity ? prev : freshStatus(identity);
43
+ return {
44
+ ...base,
45
+ error: null,
46
+ sourceState: 'connecting',
47
+ restart: { fromError: base.error !== null || base.restart?.fromError === true },
48
+ restarts: base.restarts + 1,
49
+ };
50
+ });
51
+ return true;
52
+ }, [identity]);
53
+ return {
54
+ error: current.error,
55
+ sourceState: current.sourceState,
56
+ pendingRestart: current.restart,
57
+ restarts: current.restarts,
58
+ reset,
59
+ update,
60
+ restart,
61
+ };
62
+ }
63
+ /**
64
+ * The subscribed hooks' result: the query's own result, with the listener's error and source
65
+ * state merged in. In subscribe mode `refetch()` re-opens the listener (clearing its error, with a
66
+ * fresh resubscribe ladder) and resolves once the re-opened listener delivers its first snapshot or
67
+ * surfaces its next error; until then the result reports fetching. A restart after a failure shows
68
+ * no data until that snapshot: the failed listener's last rows are not presented as current.
69
+ */
70
+ export function useSubscribedResult(query, subscribe, enabled, status) {
71
+ const { error, sourceState, pendingRestart, restart } = status;
72
+ const waiters = useRef([]);
73
+ const latest = useRef(query);
74
+ const mounted = useRef(true);
75
+ const active = subscribe && enabled;
76
+ const refetch = useCallback((options) => {
77
+ // An unmounted hook, or a refetch captured under an identity the hook has left, re-opens
78
+ // nothing: it settles at once rather than waiting on a listener that will never report.
79
+ if (!active || !mounted.current)
80
+ return Promise.resolve(latest.current);
81
+ return new Promise((resolve, reject) => {
82
+ if (!restart()) {
83
+ resolve(latest.current);
84
+ return;
85
+ }
86
+ waiters.current.push({ resolve, reject, throwOnError: options?.throwOnError === true });
87
+ });
88
+ }, [active, restart]);
89
+ let result;
90
+ if (!subscribe) {
91
+ result = query;
92
+ }
93
+ else if (error) {
94
+ result = {
95
+ ...query,
96
+ data: undefined,
97
+ error,
98
+ isError: true,
99
+ isLoadingError: true,
100
+ isSuccess: false,
101
+ isPending: false,
102
+ isLoading: false,
103
+ status: 'error',
104
+ refetch,
105
+ };
106
+ }
107
+ else if (pendingRestart?.fromError) {
108
+ result = {
109
+ ...query,
110
+ data: undefined,
111
+ error: null,
112
+ isError: false,
113
+ isLoadingError: false,
114
+ isRefetchError: false,
115
+ isSuccess: false,
116
+ isPending: true,
117
+ isLoading: true,
118
+ isFetching: true,
119
+ isRefetching: false,
120
+ status: 'pending',
121
+ fetchStatus: 'fetching',
122
+ refetch,
123
+ };
124
+ }
125
+ else if (pendingRestart) {
126
+ result = {
127
+ ...query,
128
+ isFetching: true,
129
+ isLoading: query.isPending,
130
+ isRefetching: !query.isPending,
131
+ fetchStatus: 'fetching',
132
+ refetch,
133
+ };
134
+ }
135
+ else {
136
+ result = { ...query, refetch };
137
+ }
138
+ const restartOpen = pendingRestart !== null;
139
+ useEffect(() => {
140
+ latest.current = result;
141
+ if (restartOpen || waiters.current.length === 0)
142
+ return;
143
+ const settled = waiters.current;
144
+ waiters.current = [];
145
+ for (const waiter of settled) {
146
+ if (waiter.throwOnError && result.error)
147
+ waiter.reject(result.error);
148
+ else
149
+ waiter.resolve(result);
150
+ }
151
+ });
152
+ // A refetch still open when the hook unmounts resolves with the last result instead of hanging.
153
+ useEffect(() => {
154
+ mounted.current = true;
155
+ return () => {
156
+ mounted.current = false;
157
+ const settled = waiters.current;
158
+ waiters.current = [];
159
+ for (const waiter of settled)
160
+ waiter.resolve(latest.current);
161
+ };
162
+ }, []);
163
+ return { ...result, sourceState };
28
164
  }
29
165
  //# sourceMappingURL=subscription-status.js.map
@@ -1 +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
+ {"version":3,"file":"subscription-status.js","sourceRoot":"","sources":["../../../src/react/firestore/subscription-status.ts"],"names":[],"mappings":"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","sourcesContent":["'use client';\n\nimport { useCallback, useEffect, useRef, useState } from 'react';\nimport type { QueryObserverResult, RefetchOptions, UseQueryResult } from '@tanstack/react-query';\nimport type { FirestoreSourceState, WithSourceState } from '../../firestore/types.js';\n\n/** A restart a subscribed result's `refetch()` asked for, open until the listener's first snapshot or its next error. */\ninterface PendingRestart {\n /** The listener had failed: its last data is not shown as current while the restart is open. */\n fromError: boolean;\n}\n\ninterface SubscriptionStatus {\n identity: string;\n error: Error | null;\n sourceState: FirestoreSourceState;\n restart: PendingRestart | null;\n /** Bumped by every restart; the listener effect depends on it, so a bump re-opens the listener. */\n restarts: number;\n}\n\ntype StatusPatch = Partial<Omit<SubscriptionStatus, 'identity' | 'restarts'>>;\n\nfunction freshStatus(identity: string): SubscriptionStatus {\n return { identity, error: null, sourceState: 'connecting', restart: null, restarts: 0 };\n}\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 *\n * `restart()` clears the error and bumps `restarts`; the listener effect re-opens on the bump,\n * and its first snapshot or next surfaced error ends the restart (`settled` in the patch).\n */\nexport function useSubscriptionStatus(identity: string) {\n const [status, setStatus] = useState<SubscriptionStatus>(() => freshStatus(identity));\n\n const current: SubscriptionStatus = status.identity === identity ? status : freshStatus(identity);\n\n const reset = useCallback(() => {\n setStatus((prev) =>\n prev.identity === identity\n ? { ...prev, error: null, sourceState: 'connecting' }\n : freshStatus(identity),\n );\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 // The identity the hook renders now. A `restart` captured under an earlier identity must not\n // touch the current one's status, or every write the current listener makes would be dropped.\n const renderedIdentity = useRef(identity);\n useEffect(() => {\n renderedIdentity.current = identity;\n }, [identity]);\n\n /** Re-opens the listener; false (and nothing changes) when this identity is no longer rendered. */\n const restart = useCallback((): boolean => {\n if (renderedIdentity.current !== identity) return false;\n setStatus((prev) => {\n const base = prev.identity === identity ? prev : freshStatus(identity);\n return {\n ...base,\n error: null,\n sourceState: 'connecting',\n restart: { fromError: base.error !== null || base.restart?.fromError === true },\n restarts: base.restarts + 1,\n };\n });\n return true;\n }, [identity]);\n\n return {\n error: current.error,\n sourceState: current.sourceState,\n pendingRestart: current.restart,\n restarts: current.restarts,\n reset,\n update,\n restart,\n };\n}\n\ntype SubscriptionStatusApi = ReturnType<typeof useSubscriptionStatus>;\n\ninterface RefetchWaiter<TData> {\n resolve: (result: QueryObserverResult<TData, Error>) => void;\n reject: (error: Error) => void;\n throwOnError: boolean;\n}\n\n/**\n * The subscribed hooks' result: the query's own result, with the listener's error and source\n * state merged in. In subscribe mode `refetch()` re-opens the listener (clearing its error, with a\n * fresh resubscribe ladder) and resolves once the re-opened listener delivers its first snapshot or\n * surfaces its next error; until then the result reports fetching. A restart after a failure shows\n * no data until that snapshot: the failed listener's last rows are not presented as current.\n */\nexport function useSubscribedResult<TData>(\n query: UseQueryResult<TData, Error>,\n subscribe: boolean,\n enabled: boolean,\n status: SubscriptionStatusApi,\n): WithSourceState<UseQueryResult<TData, Error>> {\n const { error, sourceState, pendingRestart, restart } = status;\n const waiters = useRef<RefetchWaiter<TData>[]>([]);\n const latest = useRef(query as QueryObserverResult<TData, Error>);\n const mounted = useRef(true);\n const active = subscribe && enabled;\n\n const refetch = useCallback(\n (options?: RefetchOptions): Promise<QueryObserverResult<TData, Error>> => {\n // An unmounted hook, or a refetch captured under an identity the hook has left, re-opens\n // nothing: it settles at once rather than waiting on a listener that will never report.\n if (!active || !mounted.current) return Promise.resolve(latest.current);\n return new Promise((resolve, reject) => {\n if (!restart()) {\n resolve(latest.current);\n return;\n }\n waiters.current.push({ resolve, reject, throwOnError: options?.throwOnError === true });\n });\n },\n [active, restart],\n );\n\n let result: UseQueryResult<TData, Error>;\n if (!subscribe) {\n result = query;\n } else if (error) {\n result = {\n ...query,\n data: undefined,\n error,\n isError: true,\n isLoadingError: true,\n isSuccess: false,\n isPending: false,\n isLoading: false,\n status: 'error',\n refetch,\n } as UseQueryResult<TData, Error>;\n } else if (pendingRestart?.fromError) {\n result = {\n ...query,\n data: undefined,\n error: null,\n isError: false,\n isLoadingError: false,\n isRefetchError: false,\n isSuccess: false,\n isPending: true,\n isLoading: true,\n isFetching: true,\n isRefetching: false,\n status: 'pending',\n fetchStatus: 'fetching',\n refetch,\n } as UseQueryResult<TData, Error>;\n } else if (pendingRestart) {\n result = {\n ...query,\n isFetching: true,\n isLoading: query.isPending,\n isRefetching: !query.isPending,\n fetchStatus: 'fetching',\n refetch,\n } as UseQueryResult<TData, Error>;\n } else {\n result = { ...query, refetch } as UseQueryResult<TData, Error>;\n }\n\n const restartOpen = pendingRestart !== null;\n useEffect(() => {\n latest.current = result as QueryObserverResult<TData, Error>;\n if (restartOpen || waiters.current.length === 0) return;\n const settled = waiters.current;\n waiters.current = [];\n for (const waiter of settled) {\n if (waiter.throwOnError && result.error) waiter.reject(result.error);\n else waiter.resolve(result as QueryObserverResult<TData, Error>);\n }\n });\n\n // A refetch still open when the hook unmounts resolves with the last result instead of hanging.\n useEffect(() => {\n mounted.current = true;\n return () => {\n mounted.current = false;\n const settled = waiters.current;\n waiters.current = [];\n for (const waiter of settled) waiter.resolve(latest.current);\n };\n }, []);\n\n return { ...result, sourceState } as WithSourceState<UseQueryResult<TData, Error>>;\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;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
+ {"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,CA4GrF"}
@@ -4,7 +4,7 @@ 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
+ import { useSubscribedResult, useSubscriptionStatus } from './subscription-status.js';
8
8
  /**
9
9
  * Fetch all documents from a Firestore collection with optional realtime updates.
10
10
  * Use this for small collections where you need all documents at once.
@@ -34,7 +34,8 @@ export function useFirestoreCollection({ collectionPath, queryKey, constraints =
34
34
  const queryKeyMemo = JSON.stringify(queryKey);
35
35
  // Listener errors can't surface through useQuery (the queryFn is disabled while
36
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]));
37
+ const subscription = useSubscriptionStatus(JSON.stringify([collectionPath, queryKeyMemo, constraintsMemo, enabled, subscribe]));
38
+ const { reset: resetStatus, update: updateStatus, restarts } = subscription;
38
39
  useEffect(() => {
39
40
  if (!subscribe || !enabled)
40
41
  return;
@@ -56,7 +57,7 @@ export function useFirestoreCollection({ collectionPath, queryKey, constraints =
56
57
  : collectionRef;
57
58
  currentUnsubscribe = onSnapshot(q, { includeMetadataChanges: true }, (snapshot) => {
58
59
  attempt = 0;
59
- updateStatus({ error: null });
60
+ updateStatus({ error: null, restart: null });
60
61
  const items = snapshot.docs.map((docSnap) => {
61
62
  const rawData = docSnap.data();
62
63
  const dataWithId = { id: docSnap.id, ...rawData };
@@ -80,7 +81,7 @@ export function useFirestoreCollection({ collectionPath, queryKey, constraints =
80
81
  return;
81
82
  }
82
83
  console.error('[useFirestoreCollection] Subscription error:', error);
83
- updateStatus({ error, sourceState: 'error' });
84
+ updateStatus({ error, sourceState: 'error', restart: null });
84
85
  queryClient.setQueryData(queryKey, undefined);
85
86
  });
86
87
  };
@@ -94,7 +95,7 @@ export function useFirestoreCollection({ collectionPath, queryKey, constraints =
94
95
  // queryKey & constraints are tracked via their stringified forms above; select is intentionally
95
96
  // excluded so an inline caller function doesn't force a re-subscribe on every render.
96
97
  // eslint-disable-next-line react-hooks/exhaustive-deps
97
- }, [db, collectionPath, queryKeyMemo, constraintsMemo, enabled, subscribe, queryClient, resetStatus, updateStatus]);
98
+ }, [db, collectionPath, queryKeyMemo, constraintsMemo, enabled, subscribe, restarts, queryClient, resetStatus, updateStatus]);
98
99
  const queryResult = useQuery({
99
100
  queryKey,
100
101
  queryFn: async () => {
@@ -115,22 +116,6 @@ export function useFirestoreCollection({ collectionPath, queryKey, constraints =
115
116
  staleTime: subscribe ? Infinity : staleTime,
116
117
  gcTime,
117
118
  });
118
- // Merge a listener error into the result so subscribed consumers see a truthful
119
- // isError/error/status instead of a forever-pending disabled query.
120
- if (subscribe && subscriptionError) {
121
- return {
122
- ...queryResult,
123
- data: undefined,
124
- error: subscriptionError,
125
- isError: true,
126
- isLoadingError: true,
127
- isSuccess: false,
128
- isPending: false,
129
- isLoading: false,
130
- status: 'error',
131
- sourceState,
132
- };
133
- }
134
- return { ...queryResult, sourceState };
119
+ return useSubscribedResult(queryResult, subscribe, enabled, subscription);
135
120
  }
136
121
  //# sourceMappingURL=use-firestore-collection.js.map