@ttt-productions/query-core 0.14.7 → 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.
- package/dist/firestore/types.d.ts +1 -5
- package/dist/firestore/types.d.ts.map +1 -1
- package/dist/firestore/types.js.map +1 -1
- package/dist/react/firestore/batch-doc-loader.d.ts +1 -3
- package/dist/react/firestore/batch-doc-loader.d.ts.map +1 -1
- package/dist/react/firestore/batch-doc-loader.js +4 -6
- package/dist/react/firestore/batch-doc-loader.js.map +1 -1
- package/dist/react/firestore/subscription-status.d.ts +26 -2
- package/dist/react/firestore/subscription-status.d.ts.map +1 -1
- package/dist/react/firestore/subscription-status.js +145 -9
- package/dist/react/firestore/subscription-status.js.map +1 -1
- package/dist/react/firestore/use-firestore-collection.d.ts.map +1 -1
- package/dist/react/firestore/use-firestore-collection.js +7 -22
- package/dist/react/firestore/use-firestore-collection.js.map +1 -1
- package/dist/react/firestore/use-firestore-doc.d.ts.map +1 -1
- package/dist/react/firestore/use-firestore-doc.js +7 -22
- package/dist/react/firestore/use-firestore-doc.js.map +1 -1
- package/dist/react/firestore/use-firestore-live-infinite.d.ts +8 -5
- package/dist/react/firestore/use-firestore-live-infinite.d.ts.map +1 -1
- package/dist/react/firestore/use-firestore-live-infinite.js +21 -24
- package/dist/react/firestore/use-firestore-live-infinite.js.map +1 -1
- package/dist/react/firestore/useBatchFirestoreDocs.d.ts +1 -1
- package/dist/react/firestore/useBatchFirestoreDocs.js.map +1 -1
- 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
|
-
/**
|
|
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":"AAAA,OAAO,KAAK,EACV,YAAY,EACZ,gBAAgB,EAChB,qBAAqB,EACrB,eAAe,EAChB,MAAM,oBAAoB,CAAC;AAE5B;;GAEG;AACH,MAAM,MAAM,MAAM,CAAC,CAAC,IAAI,CAAC,GAAG;IAAE,EAAE,EAAE,MAAM,CAAA;CAAE,CAAC;AAE3C;;;;;;;;;;;;GAYG;AACH,MAAM,MAAM,oBAAoB,GAAG,YAAY,GAAG,MAAM,GAAG,SAAS,GAAG,OAAO,CAAC;AAE/E,yFAAyF;AACzF,MAAM,MAAM,eAAe,CAAC,CAAC,IAAI,CAAC,GAAG;IAAE,WAAW,EAAE,oBAAoB,CAAA;CAAE,CAAC;AAE3E;;;;;;;;;GASG;AACH,MAAM,MAAM,wBAAwB,CAAC,CAAC,IAClC;IAAE,MAAM,EAAE,SAAS,CAAA;CAAE,GACrB;IAAE,MAAM,EAAE,SAAS,CAAC;IAAC,IAAI,EAAE,CAAC,CAAA;CAAE,GAC9B;IAAE,MAAM,EAAE,QAAQ,CAAA;CAAE,GACpB;IAAE,MAAM,EAAE,QAAQ,CAAC;IAAC,KAAK,EAAE,KAAK,CAAA;CAAE,CAAC;AAEvC;;GAEG;AACH,wBAAgB,SAAS,CAAC,CAAC,SAAS,YAAY,EAC9C,IAAI,EAAE,qBAAqB,CAAC,CAAC,CAAC,GAC7B,MAAM,CAAC,CAAC,CAAC,CAEX;AAED;;GAEG;AACH,MAAM,WAAW,oBAAoB;IACnC,4BAA4B;IAC5B,QAAQ,EAAE,SAAS,OAAO,EAAE,CAAC;IAC7B,+BAA+B;IAC/B,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,+BAA+B;IAC/B,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,4CAA4C;IAC5C,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED;;GAEG;AACH,MAAM,WAAW,mBAAmB,CAAC,CAAC,CAAE,SAAQ,oBAAoB;IAClE,gDAAgD;IAChD,OAAO,EAAE,MAAM,CAAC;IAChB,8DAA8D;IAC9D,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,mFAAmF;IACnF,MAAM,CAAC,EAAE,CAAC,IAAI,EAAE,YAAY,GAAG;QAAE,EAAE,EAAE,MAAM,CAAA;KAAE,KAAK,CAAC,CAAC;CACrD;AAED;;GAEG;AACH,MAAM,WAAW,0BAA0B,CAAC,CAAC,CAAE,SAAQ,oBAAoB;IACzE,gEAAgE;IAChE,cAAc,EAAE,MAAM,CAAC;IACvB,yDAAyD;IACzD,WAAW,CAAC,EAAE,eAAe,EAAE,CAAC;IAChC,8DAA8D;IAC9D,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,mFAAmF;IACnF,MAAM,CAAC,EAAE,CAAC,IAAI,EAAE,YAAY,GAAG;QAAE,EAAE,EAAE,MAAM,CAAA;KAAE,KAAK,CAAC,CAAC;CACrD;AAED;;GAEG;AACH,MAAM,WAAW,wBAAwB,CAAC,CAAC,CAAE,SAAQ,oBAAoB;IACvE,sBAAsB;IACtB,cAAc,EAAE,MAAM,CAAC;IACvB,6EAA6E;IAC7E,WAAW,CAAC,EAAE,eAAe,EAAE,CAAC;IAChC,6CAA6C;IAC7C,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,mFAAmF;IACnF,MAAM,CAAC,EAAE,CAAC,IAAI,EAAE,YAAY,GAAG;QAAE,EAAE,EAAE,MAAM,CAAA;KAAE,KAAK,CAAC,CAAC;CACrD;AAED;;GAEG;AACH,MAAM,WAAW,yBAAyB,CAAC,CAAC,CAAE,SAAQ,oBAAoB;IACxE,sBAAsB;IACtB,cAAc,EAAE,MAAM,CAAC;IACvB,kCAAkC;IAClC,WAAW,CAAC,EAAE,eAAe,EAAE,CAAC;IAChC,6CAA6C;IAC7C,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,uCAAuC;IACvC,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,iGAAiG;IACjG,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,mFAAmF;IACnF,MAAM,CAAC,EAAE,CAAC,IAAI,EAAE,YAAY,GAAG;QAAE,EAAE,EAAE,MAAM,CAAA;KAAE,KAAK,CAAC,CAAC;CACrD;AAED;;GAEG;AACH,MAAM,WAAW,qBAAsB,SAAQ,oBAAoB;IACjE,sBAAsB;IACtB,cAAc,EAAE,MAAM,CAAC;IACvB,wEAAwE;IACxE,WAAW,CAAC,EAAE,eAAe,EAAE,CAAC;IAChC,mDAAmD;IACnD,eAAe,CAAC,EAAE,MAAM,CAAC;CAC1B;AAED;;;;;GAKG;AACH,MAAM,WAAW,4BAA4B,CAAC,CAAC,GAAG,YAAY,GAAG;IAAE,EAAE,EAAE,MAAM,CAAA;CAAE;IAC7E,iFAAiF;IACjF,cAAc,EAAE,MAAM,CAAC;IACvB,4FAA4F;IAC5F,QAAQ,EAAE,SAAS,OAAO,EAAE,CAAC;IAC7B,uFAAuF;IACvF,YAAY,EAAE,MAAM,CAAC;IACrB,+FAA+F;IAC/F,WAAW,CAAC,EAAE,eAAe,EAAE,CAAC;IAChC,qEAAqE;IACrE,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,kEAAkE;IAClE,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,oFAAoF;IACpF,MAAM,CAAC,EAAE,CAAC,IAAI,EAAE,YAAY,GAAG;QAAE,EAAE,EAAE,MAAM,CAAA;KAAE,KAAK,CAAC,CAAC;IACpD,qHAAqH;IACrH,IAAI,CAAC,EAAE,KAAK,GAAG,MAAM,CAAC;CACvB;AAED;;GAEG;AACH,MAAM,WAAW,YAAY,CAAC,CAAC;IAC7B,KAAK,EAAE,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC;IACnB,OAAO,EAAE,gBAAgB,GAAG,IAAI,CAAC;IACjC,IAAI,EAAE,MAAM,CAAC;CACd;AAED;;;;;GAKG;AACH,MAAM,WAAW,aAAa,CAAC,CAAC;IAC9B,KAAK,EAAE,CAAC,EAAE,CAAC;IACX,MAAM,EAAE,gBAAgB,GAAG,IAAI,CAAC;IAChC,OAAO,EAAE,OAAO,CAAC;IACjB,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;CACtB;AAED;;GAEG;AACH,MAAM,MAAM,iBAAiB,GACzB;IAAE,IAAI,EAAE,KAAK,CAAC;IAAC,OAAO,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,YAAY,CAAC;IAAC,KAAK,CAAC,EAAE,OAAO,CAAA;CAAE,GACrE;IAAE,IAAI,EAAE,QAAQ,CAAC;IAAC,OAAO,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,YAAY,CAAA;CAAE,GACvD;IAAE,IAAI,EAAE,QAAQ,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,CAAC;AAExC;;GAEG;AACH,MAAM,WAAW,wBAAwB,CAAC,CAAC;IACzC,6CAA6C;IAC7C,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,0CAA0C;IAC1C,cAAc,CAAC,EAAE,SAAS,OAAO,EAAE,EAAE,CAAC;IACtC,gCAAgC;IAChC,UAAU,CAAC,EAAE;QACX,uCAAuC;QACvC,QAAQ,EAAE,SAAS,OAAO,EAAE,CAAC;QAC7B,oCAAoC;QACpC,OAAO,EAAE,CAAC,OAAO,EAAE,CAAC,GAAG,SAAS,EAAE,OAAO,EAAE,OAAO,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC;KAC7D,CAAC;CACH;AAED;;GAEG;AACH,MAAM,WAAW,qBAAqB;IACpC,0CAA0C;IAC1C,cAAc,CAAC,EAAE,SAAS,OAAO,EAAE,EAAE,CAAC;IACtC,8CAA8C;IAC9C,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB"}
|
|
@@ -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 /**
|
|
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"]}
|
|
@@ -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
|
|
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;
|
|
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
|
|
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
|
|
104
|
-
|
|
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 {
|
|
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;
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
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,
|
|
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
|
|
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
|
-
|
|
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
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"use-firestore-collection.js","sourceRoot":"","sources":["../../../src/react/firestore/use-firestore-collection.ts"],"names":[],"mappings":"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","sourcesContent":["'use client';\n\nimport { useEffect } from 'react';\nimport { useQuery, useQueryClient, type UseQueryResult } from '@tanstack/react-query';\nimport {\n collection,\n query,\n getDocs,\n onSnapshot,\n type DocumentData,\n} from 'firebase/firestore';\nimport { useFirestoreDb } from './context.js';\nimport { RESUBSCRIBE_DELAYS_MS, isPermissionDeniedError } from './resubscribe.js';\nimport { useSubscriptionStatus } from './subscription-status.js';\nimport type {\n FirestoreCollectionOptions,\n WithId,\n WithSourceState,\n} from '../../firestore/types.js';\n\n/**\n * Fetch all documents from a Firestore collection with optional realtime updates.\n * Use this for small collections where you need all documents at once.\n * For large collections, use useFirestoreInfinite or useFirestorePaginated.\n * * @example\n * ```tsx\n * // Fetch all channels in an entity\n * const { data: channels } = useFirestoreCollection<Channel>({\n * collectionPath: `entities/${entityId}/channels`,\n * queryKey: ['channels', entityId],\n * constraints: [orderBy('createdAt', 'asc')],\n * });\n * * // With realtime updates\n * const { data: members } = useFirestoreCollection<Member>({\n * collectionPath: `entities/${entityId}/members`,\n * queryKey: ['members', entityId],\n * subscribe: true,\n * });\n * ```\n */\nexport function useFirestoreCollection<T extends DocumentData = DocumentData>({\n collectionPath,\n queryKey,\n constraints = [],\n enabled = true,\n subscribe = false,\n staleTime,\n gcTime,\n select,\n}: FirestoreCollectionOptions<T>): WithSourceState<UseQueryResult<WithId<T>[], Error>> {\n const db = useFirestoreDb();\n const queryClient = useQueryClient();\n\n // STABILIZE DEPENDENCIES:\n // We stringify these because [where('a', '==', 'b')] !== [where('a', '==', 'b')] in JS.\n const constraintsMemo = JSON.stringify(constraints);\n const queryKeyMemo = JSON.stringify(queryKey);\n\n // Listener errors can't surface through useQuery (the queryFn is disabled while\n // subscribed), so they're tracked per subscription identity and merged into the result.\n const {\n error: subscriptionError,\n sourceState,\n reset: resetStatus,\n update: updateStatus,\n } = useSubscriptionStatus(\n JSON.stringify([collectionPath, queryKeyMemo, constraintsMemo, enabled, subscribe]),\n );\n\n useEffect(() => {\n if (!subscribe || !enabled) return;\n\n resetStatus();\n\n // Bounded resubscribe: a transient `permission-denied` (e.g. a failed mid-session\n // App Check token refresh) is retried on the RESUBSCRIBE_DELAYS_MS ladder before the\n // error is surfaced. `attempt` resets on any healthy snapshot; `disposed`/timers are\n // cleaned up on unmount so a pending retry never fires after teardown.\n let currentUnsubscribe: (() => void) | null = null;\n let retryTimer: ReturnType<typeof setTimeout> | null = null;\n let attempt = 0;\n let disposed = false;\n\n const start = () => {\n if (disposed) return;\n const collectionRef = collection(db, collectionPath);\n const q = constraints.length > 0\n ? query(collectionRef, ...constraints)\n : collectionRef;\n\n currentUnsubscribe = onSnapshot(\n q,\n { includeMetadataChanges: true },\n (snapshot) => {\n attempt = 0;\n updateStatus({ error: null });\n const items = snapshot.docs.map((docSnap) => {\n const rawData = docSnap.data();\n const dataWithId = { id: docSnap.id, ...rawData };\n const data = select ? select(dataWithId) : dataWithId;\n return data as WithId<T>;\n });\n queryClient.setQueryData(queryKey, items);\n const fromCache = snapshot.metadata?.fromCache ?? false;\n updateStatus((prev) => ({ sourceState: fromCache ? (prev.sourceState === 'connecting' ? 'connecting' : 'offline') : 'live' }));\n },\n (error) => {\n // Heal a transient permission-denied by resubscribing on the backoff ladder,\n // keeping the last-known data visible. Only surface the error once the ladder\n // is exhausted; any other error surfaces immediately, as before.\n if (isPermissionDeniedError(error) && attempt < RESUBSCRIBE_DELAYS_MS.length) {\n currentUnsubscribe?.();\n currentUnsubscribe = null;\n const delay = RESUBSCRIBE_DELAYS_MS[attempt];\n attempt += 1;\n updateStatus({ sourceState: 'connecting' });\n retryTimer = setTimeout(start, delay);\n return;\n }\n console.error('[useFirestoreCollection] Subscription error:', error);\n updateStatus({ error, sourceState: 'error' });\n queryClient.setQueryData(queryKey, undefined);\n }\n );\n };\n\n start();\n\n return () => {\n disposed = true;\n if (retryTimer) clearTimeout(retryTimer);\n currentUnsubscribe?.();\n };\n // queryKey & constraints are tracked via their stringified forms above; select is intentionally\n // excluded so an inline caller function doesn't force a re-subscribe on every render.\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [db, collectionPath, queryKeyMemo, constraintsMemo, enabled, subscribe, queryClient, resetStatus, updateStatus]);\n\n const queryResult = useQuery({\n queryKey,\n queryFn: async (): Promise<WithId<T>[]> => {\n const collectionRef = collection(db, collectionPath);\n const q = constraints.length > 0\n ? query(collectionRef, ...constraints)\n : collectionRef;\n\n const snapshot = await getDocs(q);\n return snapshot.docs.map((docSnap) => {\n const rawData = docSnap.data();\n // Include id in data passed to select, so it can be renamed/transformed\n const dataWithId = { id: docSnap.id, ...rawData };\n const data = select ? select(dataWithId) : dataWithId;\n return data as WithId<T>;\n });\n },\n enabled: enabled && !subscribe,\n staleTime: subscribe ? Infinity : staleTime,\n gcTime,\n });\n\n // Merge a listener error into the result so subscribed consumers see a truthful\n // isError/error/status instead of a forever-pending disabled query.\n if (subscribe && subscriptionError) {\n return {\n ...queryResult,\n data: undefined,\n error: subscriptionError,\n isError: true,\n isLoadingError: true,\n isSuccess: false,\n isPending: false,\n isLoading: false,\n status: 'error',\n sourceState,\n } as WithSourceState<UseQueryResult<WithId<T>[], Error>>;\n }\n\n return { ...queryResult, sourceState } as WithSourceState<UseQueryResult<WithId<T>[], Error>>;\n}\n"]}
|
|
1
|
+
{"version":3,"file":"use-firestore-collection.js","sourceRoot":"","sources":["../../../src/react/firestore/use-firestore-collection.ts"],"names":[],"mappings":"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","sourcesContent":["'use client';\n\nimport { useEffect } from 'react';\nimport { useQuery, useQueryClient, type UseQueryResult } from '@tanstack/react-query';\nimport {\n collection,\n query,\n getDocs,\n onSnapshot,\n type DocumentData,\n} from 'firebase/firestore';\nimport { useFirestoreDb } from './context.js';\nimport { RESUBSCRIBE_DELAYS_MS, isPermissionDeniedError } from './resubscribe.js';\nimport { useSubscribedResult, useSubscriptionStatus } from './subscription-status.js';\nimport type {\n FirestoreCollectionOptions,\n WithId,\n WithSourceState,\n} from '../../firestore/types.js';\n\n/**\n * Fetch all documents from a Firestore collection with optional realtime updates.\n * Use this for small collections where you need all documents at once.\n * For large collections, use useFirestoreInfinite or useFirestorePaginated.\n * * @example\n * ```tsx\n * // Fetch all channels in an entity\n * const { data: channels } = useFirestoreCollection<Channel>({\n * collectionPath: `entities/${entityId}/channels`,\n * queryKey: ['channels', entityId],\n * constraints: [orderBy('createdAt', 'asc')],\n * });\n * * // With realtime updates\n * const { data: members } = useFirestoreCollection<Member>({\n * collectionPath: `entities/${entityId}/members`,\n * queryKey: ['members', entityId],\n * subscribe: true,\n * });\n * ```\n */\nexport function useFirestoreCollection<T extends DocumentData = DocumentData>({\n collectionPath,\n queryKey,\n constraints = [],\n enabled = true,\n subscribe = false,\n staleTime,\n gcTime,\n select,\n}: FirestoreCollectionOptions<T>): WithSourceState<UseQueryResult<WithId<T>[], Error>> {\n const db = useFirestoreDb();\n const queryClient = useQueryClient();\n\n // STABILIZE DEPENDENCIES:\n // We stringify these because [where('a', '==', 'b')] !== [where('a', '==', 'b')] in JS.\n const constraintsMemo = JSON.stringify(constraints);\n const queryKeyMemo = JSON.stringify(queryKey);\n\n // Listener errors can't surface through useQuery (the queryFn is disabled while\n // subscribed), so they're tracked per subscription identity and merged into the result.\n const subscription = useSubscriptionStatus(\n JSON.stringify([collectionPath, queryKeyMemo, constraintsMemo, enabled, subscribe]),\n );\n const { reset: resetStatus, update: updateStatus, restarts } = subscription;\n\n useEffect(() => {\n if (!subscribe || !enabled) return;\n\n resetStatus();\n\n // Bounded resubscribe: a transient `permission-denied` (e.g. a failed mid-session\n // App Check token refresh) is retried on the RESUBSCRIBE_DELAYS_MS ladder before the\n // error is surfaced. `attempt` resets on any healthy snapshot; `disposed`/timers are\n // cleaned up on unmount so a pending retry never fires after teardown.\n let currentUnsubscribe: (() => void) | null = null;\n let retryTimer: ReturnType<typeof setTimeout> | null = null;\n let attempt = 0;\n let disposed = false;\n\n const start = () => {\n if (disposed) return;\n const collectionRef = collection(db, collectionPath);\n const q = constraints.length > 0\n ? query(collectionRef, ...constraints)\n : collectionRef;\n\n currentUnsubscribe = onSnapshot(\n q,\n { includeMetadataChanges: true },\n (snapshot) => {\n attempt = 0;\n updateStatus({ error: null, restart: null });\n const items = snapshot.docs.map((docSnap) => {\n const rawData = docSnap.data();\n const dataWithId = { id: docSnap.id, ...rawData };\n const data = select ? select(dataWithId) : dataWithId;\n return data as WithId<T>;\n });\n queryClient.setQueryData(queryKey, items);\n const fromCache = snapshot.metadata?.fromCache ?? false;\n updateStatus((prev) => ({ sourceState: fromCache ? (prev.sourceState === 'connecting' ? 'connecting' : 'offline') : 'live' }));\n },\n (error) => {\n // Heal a transient permission-denied by resubscribing on the backoff ladder,\n // keeping the last-known data visible. Only surface the error once the ladder\n // is exhausted; any other error surfaces immediately, as before.\n if (isPermissionDeniedError(error) && attempt < RESUBSCRIBE_DELAYS_MS.length) {\n currentUnsubscribe?.();\n currentUnsubscribe = null;\n const delay = RESUBSCRIBE_DELAYS_MS[attempt];\n attempt += 1;\n updateStatus({ sourceState: 'connecting' });\n retryTimer = setTimeout(start, delay);\n return;\n }\n console.error('[useFirestoreCollection] Subscription error:', error);\n updateStatus({ error, sourceState: 'error', restart: null });\n queryClient.setQueryData(queryKey, undefined);\n }\n );\n };\n\n start();\n\n return () => {\n disposed = true;\n if (retryTimer) clearTimeout(retryTimer);\n currentUnsubscribe?.();\n };\n // queryKey & constraints are tracked via their stringified forms above; select is intentionally\n // excluded so an inline caller function doesn't force a re-subscribe on every render.\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [db, collectionPath, queryKeyMemo, constraintsMemo, enabled, subscribe, restarts, queryClient, resetStatus, updateStatus]);\n\n const queryResult = useQuery({\n queryKey,\n queryFn: async (): Promise<WithId<T>[]> => {\n const collectionRef = collection(db, collectionPath);\n const q = constraints.length > 0\n ? query(collectionRef, ...constraints)\n : collectionRef;\n\n const snapshot = await getDocs(q);\n return snapshot.docs.map((docSnap) => {\n const rawData = docSnap.data();\n // Include id in data passed to select, so it can be renamed/transformed\n const dataWithId = { id: docSnap.id, ...rawData };\n const data = select ? select(dataWithId) : dataWithId;\n return data as WithId<T>;\n });\n },\n enabled: enabled && !subscribe,\n staleTime: subscribe ? Infinity : staleTime,\n gcTime,\n });\n\n return useSubscribedResult(queryResult, subscribe, enabled, subscription);\n}\n"]}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"use-firestore-doc.d.ts","sourceRoot":"","sources":["../../../src/react/firestore/use-firestore-doc.ts"],"names":[],"mappings":"AAGA,OAAO,EAA4B,KAAK,cAAc,EAAE,MAAM,uBAAuB,CAAC;AACtF,OAAO,EAA2B,KAAK,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAIhF,OAAO,KAAK,EACV,mBAAmB,EACnB,MAAM,EACN,eAAe,EAChB,MAAM,0BAA0B,CAAC;AAElC;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,wBAAgB,eAAe,CAAC,CAAC,SAAS,YAAY,GAAG,YAAY,EAAE,EACrE,OAAO,EACP,QAAQ,EACR,OAAc,EACd,SAAiB,EACjB,SAAS,EACT,MAAM,EACN,MAAM,GACP,EAAE,mBAAmB,CAAC,CAAC,CAAC,GAAG,eAAe,CAAC,cAAc,CAAC,MAAM,CAAC,CAAC,CAAC,GAAG,IAAI,EAAE,KAAK,CAAC,CAAC,
|
|
1
|
+
{"version":3,"file":"use-firestore-doc.d.ts","sourceRoot":"","sources":["../../../src/react/firestore/use-firestore-doc.ts"],"names":[],"mappings":"AAGA,OAAO,EAA4B,KAAK,cAAc,EAAE,MAAM,uBAAuB,CAAC;AACtF,OAAO,EAA2B,KAAK,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAIhF,OAAO,KAAK,EACV,mBAAmB,EACnB,MAAM,EACN,eAAe,EAChB,MAAM,0BAA0B,CAAC;AAElC;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,wBAAgB,eAAe,CAAC,CAAC,SAAS,YAAY,GAAG,YAAY,EAAE,EACrE,OAAO,EACP,QAAQ,EACR,OAAc,EACd,SAAiB,EACjB,SAAS,EACT,MAAM,EACN,MAAM,GACP,EAAE,mBAAmB,CAAC,CAAC,CAAC,GAAG,eAAe,CAAC,cAAc,CAAC,MAAM,CAAC,CAAC,CAAC,GAAG,IAAI,EAAE,KAAK,CAAC,CAAC,CAoGnF"}
|