@topgunbuild/react 0.7.0 → 0.8.1

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/index.d.mts CHANGED
@@ -1,7 +1,7 @@
1
1
  import React, { ReactNode } from 'react';
2
2
  import * as _topgunbuild_client from '@topgunbuild/client';
3
- import { TopGunClient, ChangeEvent, QueryResultItem, QueryFilter, TopicCallback, RegisterResult, ResolverInfo } from '@topgunbuild/client';
4
- import { LWWMap, ORMap, EntryProcessorResult, EntryProcessorDef, JournalEventType, JournalEvent, MergeRejection, ConflictResolverDef } from '@topgunbuild/core';
3
+ import { TopGunClient, ChangeEvent, QueryResultItem, QueryFilter, TopicCallback, RegisterResult, ResolverInfo, SearchResult, HybridResultItem, HybridQueryFilter } from '@topgunbuild/client';
4
+ import { LWWMap, ORMap, EntryProcessorResult, EntryProcessorDef, JournalEventType, JournalEvent, MergeRejection, ConflictResolverDef, SearchOptions } from '@topgunbuild/core';
5
5
 
6
6
  interface TopGunProviderProps {
7
7
  client: TopGunClient;
@@ -607,4 +607,200 @@ interface UseConflictResolverResult {
607
607
  */
608
608
  declare function useConflictResolver(mapName: string, options?: UseConflictResolverOptions): UseConflictResolverResult;
609
609
 
610
- export { TopGunProvider, type TopGunProviderProps, type UseConflictResolverOptions, type UseConflictResolverResult, type UseEntryProcessorOptions, type UseEntryProcessorResult, type UseEventJournalOptions, type UseEventJournalResult, type UseMergeRejectionsOptions, type UseMergeRejectionsResult, type UseMutationResult, type UsePNCounterResult, type UseQueryOptions, type UseQueryResult, useClient, useConflictResolver, useEntryProcessor, useEventJournal, useMap, useMergeRejections, useMutation, useORMap, usePNCounter, useQuery, useTopic };
610
+ /**
611
+ * Extended search options for useSearch hook.
612
+ */
613
+ interface UseSearchOptions extends SearchOptions {
614
+ /**
615
+ * Debounce delay in milliseconds.
616
+ * If specified, query changes will be debounced before sending to the server.
617
+ * Useful for search-as-you-type interfaces.
618
+ */
619
+ debounceMs?: number;
620
+ }
621
+ /**
622
+ * Result type for the useSearch hook.
623
+ */
624
+ interface UseSearchResult<T> {
625
+ /** Current search results sorted by relevance */
626
+ results: SearchResult<T>[];
627
+ /** True while waiting for initial results */
628
+ loading: boolean;
629
+ /** Error if search failed */
630
+ error: Error | null;
631
+ }
632
+ /**
633
+ * React hook for live full-text search with real-time updates.
634
+ *
635
+ * Creates a search subscription that receives delta updates when documents
636
+ * matching the query are added, updated, or removed. Results are automatically
637
+ * sorted by BM25 relevance score.
638
+ *
639
+ * @param mapName - Name of the map to search
640
+ * @param query - Search query text
641
+ * @param options - Search options (limit, minScore, boost, debounceMs)
642
+ * @returns Object containing results, loading state, and error
643
+ *
644
+ * @example Basic usage
645
+ * ```tsx
646
+ * function SearchResults() {
647
+ * const [searchTerm, setSearchTerm] = useState('');
648
+ * const { results, loading } = useSearch<Article>('articles', searchTerm, {
649
+ * limit: 20,
650
+ * boost: { title: 2.0 }
651
+ * });
652
+ *
653
+ * if (loading) return <Spinner />;
654
+ *
655
+ * return (
656
+ * <ul>
657
+ * {results.map(r => (
658
+ * <li key={r.key}>
659
+ * [{r.score.toFixed(2)}] {r.value.title}
660
+ * </li>
661
+ * ))}
662
+ * </ul>
663
+ * );
664
+ * }
665
+ * ```
666
+ *
667
+ * @example With debounce for search-as-you-type
668
+ * ```tsx
669
+ * function SearchInput() {
670
+ * const [input, setInput] = useState('');
671
+ * const { results, loading, error } = useSearch<Product>('products', input, {
672
+ * debounceMs: 300,
673
+ * limit: 10
674
+ * });
675
+ *
676
+ * return (
677
+ * <div>
678
+ * <input
679
+ * value={input}
680
+ * onChange={(e) => setInput(e.target.value)}
681
+ * placeholder="Search products..."
682
+ * />
683
+ * {loading && <span>Searching...</span>}
684
+ * {error && <span className="error">{error.message}</span>}
685
+ * <ul>
686
+ * {results.map(r => (
687
+ * <li key={r.key}>{r.value.name}</li>
688
+ * ))}
689
+ * </ul>
690
+ * </div>
691
+ * );
692
+ * }
693
+ * ```
694
+ */
695
+ declare function useSearch<T = unknown>(mapName: string, query: string, options?: UseSearchOptions): UseSearchResult<T>;
696
+
697
+ /**
698
+ * Extended options for useHybridQuery hook.
699
+ */
700
+ interface UseHybridQueryOptions {
701
+ /**
702
+ * Whether to skip the query (don't execute).
703
+ * Useful for conditional queries.
704
+ */
705
+ skip?: boolean;
706
+ }
707
+ /**
708
+ * Result type for the useHybridQuery hook.
709
+ */
710
+ interface UseHybridQueryResult<T> {
711
+ /** Current query results with _key, value, _score, _matchedTerms */
712
+ results: HybridResultItem<T>[];
713
+ /** True while waiting for initial results */
714
+ loading: boolean;
715
+ /** Error if query failed */
716
+ error: Error | null;
717
+ }
718
+ /**
719
+ * React hook for hybrid queries combining FTS with traditional filters.
720
+ *
721
+ * Creates a subscription that receives live updates when documents
722
+ * matching the query change. Results include relevance scores for FTS predicates.
723
+ *
724
+ * @param mapName - Name of the map to query
725
+ * @param filter - Hybrid query filter with predicate, where, sort, limit, offset
726
+ * @param options - Hook options (skip)
727
+ * @returns Object containing results, loading state, and error
728
+ *
729
+ * @example Basic hybrid query (FTS + filter)
730
+ * ```tsx
731
+ * import { Predicates } from '@topgunbuild/core';
732
+ *
733
+ * function TechArticles() {
734
+ * const { results, loading } = useHybridQuery<Article>('articles', {
735
+ * predicate: Predicates.and(
736
+ * Predicates.match('body', 'machine learning'),
737
+ * Predicates.equal('category', 'tech')
738
+ * ),
739
+ * sort: { _score: 'desc' },
740
+ * limit: 20
741
+ * });
742
+ *
743
+ * if (loading) return <Spinner />;
744
+ *
745
+ * return (
746
+ * <ul>
747
+ * {results.map(r => (
748
+ * <li key={r._key}>
749
+ * [{r._score?.toFixed(2)}] {r.value.title}
750
+ * </li>
751
+ * ))}
752
+ * </ul>
753
+ * );
754
+ * }
755
+ * ```
756
+ *
757
+ * @example With dynamic filter
758
+ * ```tsx
759
+ * function SearchWithFilters() {
760
+ * const [searchTerm, setSearchTerm] = useState('');
761
+ * const [category, setCategory] = useState('all');
762
+ *
763
+ * const filter = useMemo(() => ({
764
+ * predicate: searchTerm
765
+ * ? category !== 'all'
766
+ * ? Predicates.and(
767
+ * Predicates.match('body', searchTerm),
768
+ * Predicates.equal('category', category)
769
+ * )
770
+ * : Predicates.match('body', searchTerm)
771
+ * : category !== 'all'
772
+ * ? Predicates.equal('category', category)
773
+ * : undefined,
774
+ * sort: searchTerm ? { _score: 'desc' } : { createdAt: 'desc' },
775
+ * limit: 20
776
+ * }), [searchTerm, category]);
777
+ *
778
+ * const { results, loading, error } = useHybridQuery<Article>('articles', filter);
779
+ *
780
+ * return (
781
+ * <div>
782
+ * <input
783
+ * value={searchTerm}
784
+ * onChange={(e) => setSearchTerm(e.target.value)}
785
+ * placeholder="Search..."
786
+ * />
787
+ * <select value={category} onChange={(e) => setCategory(e.target.value)}>
788
+ * <option value="all">All</option>
789
+ * <option value="tech">Tech</option>
790
+ * <option value="science">Science</option>
791
+ * </select>
792
+ * {loading && <span>Loading...</span>}
793
+ * {error && <span className="error">{error.message}</span>}
794
+ * <ul>
795
+ * {results.map(r => (
796
+ * <li key={r._key}>{r.value.title}</li>
797
+ * ))}
798
+ * </ul>
799
+ * </div>
800
+ * );
801
+ * }
802
+ * ```
803
+ */
804
+ declare function useHybridQuery<T = unknown>(mapName: string, filter?: HybridQueryFilter, options?: UseHybridQueryOptions): UseHybridQueryResult<T>;
805
+
806
+ export { TopGunProvider, type TopGunProviderProps, type UseConflictResolverOptions, type UseConflictResolverResult, type UseEntryProcessorOptions, type UseEntryProcessorResult, type UseEventJournalOptions, type UseEventJournalResult, type UseHybridQueryOptions, type UseHybridQueryResult, type UseMergeRejectionsOptions, type UseMergeRejectionsResult, type UseMutationResult, type UsePNCounterResult, type UseQueryOptions, type UseQueryResult, type UseSearchOptions, type UseSearchResult, useClient, useConflictResolver, useEntryProcessor, useEventJournal, useHybridQuery, useMap, useMergeRejections, useMutation, useORMap, usePNCounter, useQuery, useSearch, useTopic };
package/dist/index.d.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  import React, { ReactNode } from 'react';
2
2
  import * as _topgunbuild_client from '@topgunbuild/client';
3
- import { TopGunClient, ChangeEvent, QueryResultItem, QueryFilter, TopicCallback, RegisterResult, ResolverInfo } from '@topgunbuild/client';
4
- import { LWWMap, ORMap, EntryProcessorResult, EntryProcessorDef, JournalEventType, JournalEvent, MergeRejection, ConflictResolverDef } from '@topgunbuild/core';
3
+ import { TopGunClient, ChangeEvent, QueryResultItem, QueryFilter, TopicCallback, RegisterResult, ResolverInfo, SearchResult, HybridResultItem, HybridQueryFilter } from '@topgunbuild/client';
4
+ import { LWWMap, ORMap, EntryProcessorResult, EntryProcessorDef, JournalEventType, JournalEvent, MergeRejection, ConflictResolverDef, SearchOptions } from '@topgunbuild/core';
5
5
 
6
6
  interface TopGunProviderProps {
7
7
  client: TopGunClient;
@@ -607,4 +607,200 @@ interface UseConflictResolverResult {
607
607
  */
608
608
  declare function useConflictResolver(mapName: string, options?: UseConflictResolverOptions): UseConflictResolverResult;
609
609
 
610
- export { TopGunProvider, type TopGunProviderProps, type UseConflictResolverOptions, type UseConflictResolverResult, type UseEntryProcessorOptions, type UseEntryProcessorResult, type UseEventJournalOptions, type UseEventJournalResult, type UseMergeRejectionsOptions, type UseMergeRejectionsResult, type UseMutationResult, type UsePNCounterResult, type UseQueryOptions, type UseQueryResult, useClient, useConflictResolver, useEntryProcessor, useEventJournal, useMap, useMergeRejections, useMutation, useORMap, usePNCounter, useQuery, useTopic };
610
+ /**
611
+ * Extended search options for useSearch hook.
612
+ */
613
+ interface UseSearchOptions extends SearchOptions {
614
+ /**
615
+ * Debounce delay in milliseconds.
616
+ * If specified, query changes will be debounced before sending to the server.
617
+ * Useful for search-as-you-type interfaces.
618
+ */
619
+ debounceMs?: number;
620
+ }
621
+ /**
622
+ * Result type for the useSearch hook.
623
+ */
624
+ interface UseSearchResult<T> {
625
+ /** Current search results sorted by relevance */
626
+ results: SearchResult<T>[];
627
+ /** True while waiting for initial results */
628
+ loading: boolean;
629
+ /** Error if search failed */
630
+ error: Error | null;
631
+ }
632
+ /**
633
+ * React hook for live full-text search with real-time updates.
634
+ *
635
+ * Creates a search subscription that receives delta updates when documents
636
+ * matching the query are added, updated, or removed. Results are automatically
637
+ * sorted by BM25 relevance score.
638
+ *
639
+ * @param mapName - Name of the map to search
640
+ * @param query - Search query text
641
+ * @param options - Search options (limit, minScore, boost, debounceMs)
642
+ * @returns Object containing results, loading state, and error
643
+ *
644
+ * @example Basic usage
645
+ * ```tsx
646
+ * function SearchResults() {
647
+ * const [searchTerm, setSearchTerm] = useState('');
648
+ * const { results, loading } = useSearch<Article>('articles', searchTerm, {
649
+ * limit: 20,
650
+ * boost: { title: 2.0 }
651
+ * });
652
+ *
653
+ * if (loading) return <Spinner />;
654
+ *
655
+ * return (
656
+ * <ul>
657
+ * {results.map(r => (
658
+ * <li key={r.key}>
659
+ * [{r.score.toFixed(2)}] {r.value.title}
660
+ * </li>
661
+ * ))}
662
+ * </ul>
663
+ * );
664
+ * }
665
+ * ```
666
+ *
667
+ * @example With debounce for search-as-you-type
668
+ * ```tsx
669
+ * function SearchInput() {
670
+ * const [input, setInput] = useState('');
671
+ * const { results, loading, error } = useSearch<Product>('products', input, {
672
+ * debounceMs: 300,
673
+ * limit: 10
674
+ * });
675
+ *
676
+ * return (
677
+ * <div>
678
+ * <input
679
+ * value={input}
680
+ * onChange={(e) => setInput(e.target.value)}
681
+ * placeholder="Search products..."
682
+ * />
683
+ * {loading && <span>Searching...</span>}
684
+ * {error && <span className="error">{error.message}</span>}
685
+ * <ul>
686
+ * {results.map(r => (
687
+ * <li key={r.key}>{r.value.name}</li>
688
+ * ))}
689
+ * </ul>
690
+ * </div>
691
+ * );
692
+ * }
693
+ * ```
694
+ */
695
+ declare function useSearch<T = unknown>(mapName: string, query: string, options?: UseSearchOptions): UseSearchResult<T>;
696
+
697
+ /**
698
+ * Extended options for useHybridQuery hook.
699
+ */
700
+ interface UseHybridQueryOptions {
701
+ /**
702
+ * Whether to skip the query (don't execute).
703
+ * Useful for conditional queries.
704
+ */
705
+ skip?: boolean;
706
+ }
707
+ /**
708
+ * Result type for the useHybridQuery hook.
709
+ */
710
+ interface UseHybridQueryResult<T> {
711
+ /** Current query results with _key, value, _score, _matchedTerms */
712
+ results: HybridResultItem<T>[];
713
+ /** True while waiting for initial results */
714
+ loading: boolean;
715
+ /** Error if query failed */
716
+ error: Error | null;
717
+ }
718
+ /**
719
+ * React hook for hybrid queries combining FTS with traditional filters.
720
+ *
721
+ * Creates a subscription that receives live updates when documents
722
+ * matching the query change. Results include relevance scores for FTS predicates.
723
+ *
724
+ * @param mapName - Name of the map to query
725
+ * @param filter - Hybrid query filter with predicate, where, sort, limit, offset
726
+ * @param options - Hook options (skip)
727
+ * @returns Object containing results, loading state, and error
728
+ *
729
+ * @example Basic hybrid query (FTS + filter)
730
+ * ```tsx
731
+ * import { Predicates } from '@topgunbuild/core';
732
+ *
733
+ * function TechArticles() {
734
+ * const { results, loading } = useHybridQuery<Article>('articles', {
735
+ * predicate: Predicates.and(
736
+ * Predicates.match('body', 'machine learning'),
737
+ * Predicates.equal('category', 'tech')
738
+ * ),
739
+ * sort: { _score: 'desc' },
740
+ * limit: 20
741
+ * });
742
+ *
743
+ * if (loading) return <Spinner />;
744
+ *
745
+ * return (
746
+ * <ul>
747
+ * {results.map(r => (
748
+ * <li key={r._key}>
749
+ * [{r._score?.toFixed(2)}] {r.value.title}
750
+ * </li>
751
+ * ))}
752
+ * </ul>
753
+ * );
754
+ * }
755
+ * ```
756
+ *
757
+ * @example With dynamic filter
758
+ * ```tsx
759
+ * function SearchWithFilters() {
760
+ * const [searchTerm, setSearchTerm] = useState('');
761
+ * const [category, setCategory] = useState('all');
762
+ *
763
+ * const filter = useMemo(() => ({
764
+ * predicate: searchTerm
765
+ * ? category !== 'all'
766
+ * ? Predicates.and(
767
+ * Predicates.match('body', searchTerm),
768
+ * Predicates.equal('category', category)
769
+ * )
770
+ * : Predicates.match('body', searchTerm)
771
+ * : category !== 'all'
772
+ * ? Predicates.equal('category', category)
773
+ * : undefined,
774
+ * sort: searchTerm ? { _score: 'desc' } : { createdAt: 'desc' },
775
+ * limit: 20
776
+ * }), [searchTerm, category]);
777
+ *
778
+ * const { results, loading, error } = useHybridQuery<Article>('articles', filter);
779
+ *
780
+ * return (
781
+ * <div>
782
+ * <input
783
+ * value={searchTerm}
784
+ * onChange={(e) => setSearchTerm(e.target.value)}
785
+ * placeholder="Search..."
786
+ * />
787
+ * <select value={category} onChange={(e) => setCategory(e.target.value)}>
788
+ * <option value="all">All</option>
789
+ * <option value="tech">Tech</option>
790
+ * <option value="science">Science</option>
791
+ * </select>
792
+ * {loading && <span>Loading...</span>}
793
+ * {error && <span className="error">{error.message}</span>}
794
+ * <ul>
795
+ * {results.map(r => (
796
+ * <li key={r._key}>{r.value.title}</li>
797
+ * ))}
798
+ * </ul>
799
+ * </div>
800
+ * );
801
+ * }
802
+ * ```
803
+ */
804
+ declare function useHybridQuery<T = unknown>(mapName: string, filter?: HybridQueryFilter, options?: UseHybridQueryOptions): UseHybridQueryResult<T>;
805
+
806
+ export { TopGunProvider, type TopGunProviderProps, type UseConflictResolverOptions, type UseConflictResolverResult, type UseEntryProcessorOptions, type UseEntryProcessorResult, type UseEventJournalOptions, type UseEventJournalResult, type UseHybridQueryOptions, type UseHybridQueryResult, type UseMergeRejectionsOptions, type UseMergeRejectionsResult, type UseMutationResult, type UsePNCounterResult, type UseQueryOptions, type UseQueryResult, type UseSearchOptions, type UseSearchResult, useClient, useConflictResolver, useEntryProcessor, useEventJournal, useHybridQuery, useMap, useMergeRejections, useMutation, useORMap, usePNCounter, useQuery, useSearch, useTopic };
package/dist/index.js CHANGED
@@ -25,12 +25,14 @@ __export(index_exports, {
25
25
  useConflictResolver: () => useConflictResolver,
26
26
  useEntryProcessor: () => useEntryProcessor,
27
27
  useEventJournal: () => useEventJournal,
28
+ useHybridQuery: () => useHybridQuery,
28
29
  useMap: () => useMap,
29
30
  useMergeRejections: () => useMergeRejections,
30
31
  useMutation: () => useMutation,
31
32
  useORMap: () => useORMap,
32
33
  usePNCounter: () => usePNCounter,
33
34
  useQuery: () => useQuery,
35
+ useSearch: () => useSearch,
34
36
  useTopic: () => useTopic
35
37
  });
36
38
  module.exports = __toCommonJS(index_exports);
@@ -540,6 +542,179 @@ function useConflictResolver(mapName, options = {}) {
540
542
  registered
541
543
  };
542
544
  }
545
+
546
+ // src/hooks/useSearch.ts
547
+ var import_react12 = require("react");
548
+ function useSearch(mapName, query, options) {
549
+ const client = useClient();
550
+ const [results, setResults] = (0, import_react12.useState)([]);
551
+ const [loading, setLoading] = (0, import_react12.useState)(true);
552
+ const [error, setError] = (0, import_react12.useState)(null);
553
+ const isMounted = (0, import_react12.useRef)(true);
554
+ const handleRef = (0, import_react12.useRef)(null);
555
+ const unsubscribeRef = (0, import_react12.useRef)(null);
556
+ const debounceTimeoutRef = (0, import_react12.useRef)(null);
557
+ const debounceMs = options?.debounceMs;
558
+ const searchOptions = (0, import_react12.useMemo)(() => {
559
+ if (!options) return {};
560
+ const { debounceMs: _, ...opts } = options;
561
+ return opts;
562
+ }, [options?.limit, options?.minScore, options?.boost]);
563
+ const [debouncedQuery, setDebouncedQuery] = (0, import_react12.useState)(query);
564
+ const isFirstQuery = (0, import_react12.useRef)(true);
565
+ (0, import_react12.useEffect)(() => {
566
+ if (debounceMs != null && debounceMs > 0) {
567
+ if (debounceTimeoutRef.current) {
568
+ clearTimeout(debounceTimeoutRef.current);
569
+ }
570
+ debounceTimeoutRef.current = setTimeout(() => {
571
+ if (isMounted.current) {
572
+ setDebouncedQuery(query);
573
+ }
574
+ }, debounceMs);
575
+ return () => {
576
+ if (debounceTimeoutRef.current) {
577
+ clearTimeout(debounceTimeoutRef.current);
578
+ }
579
+ };
580
+ } else {
581
+ setDebouncedQuery(query);
582
+ }
583
+ }, [query, debounceMs]);
584
+ (0, import_react12.useEffect)(() => {
585
+ isMounted.current = true;
586
+ isFirstQuery.current = true;
587
+ return () => {
588
+ isMounted.current = false;
589
+ if (unsubscribeRef.current) {
590
+ unsubscribeRef.current();
591
+ unsubscribeRef.current = null;
592
+ }
593
+ if (handleRef.current) {
594
+ handleRef.current.dispose();
595
+ handleRef.current = null;
596
+ }
597
+ };
598
+ }, [client, mapName]);
599
+ (0, import_react12.useEffect)(() => {
600
+ if (!debouncedQuery.trim()) {
601
+ setResults([]);
602
+ setLoading(false);
603
+ setError(null);
604
+ return;
605
+ }
606
+ setLoading(true);
607
+ setError(null);
608
+ try {
609
+ if (handleRef.current && !isFirstQuery.current) {
610
+ handleRef.current.setQuery(debouncedQuery);
611
+ } else {
612
+ if (handleRef.current) {
613
+ if (unsubscribeRef.current) {
614
+ unsubscribeRef.current();
615
+ }
616
+ handleRef.current.dispose();
617
+ }
618
+ const handle = client.searchSubscribe(mapName, debouncedQuery, searchOptions);
619
+ handleRef.current = handle;
620
+ isFirstQuery.current = false;
621
+ let hasReceivedResults = false;
622
+ unsubscribeRef.current = handle.subscribe((newResults) => {
623
+ if (isMounted.current) {
624
+ setResults(newResults);
625
+ if (!hasReceivedResults) {
626
+ hasReceivedResults = true;
627
+ setLoading(false);
628
+ }
629
+ }
630
+ });
631
+ }
632
+ } catch (err) {
633
+ if (isMounted.current) {
634
+ setError(err instanceof Error ? err : new Error(String(err)));
635
+ setLoading(false);
636
+ }
637
+ }
638
+ }, [client, mapName, debouncedQuery, searchOptions]);
639
+ (0, import_react12.useEffect)(() => {
640
+ if (!handleRef.current || isFirstQuery.current) {
641
+ return;
642
+ }
643
+ handleRef.current.setOptions(searchOptions);
644
+ }, [searchOptions]);
645
+ return (0, import_react12.useMemo)(
646
+ () => ({ results, loading, error }),
647
+ [results, loading, error]
648
+ );
649
+ }
650
+
651
+ // src/hooks/useHybridQuery.ts
652
+ var import_react13 = require("react");
653
+ function useHybridQuery(mapName, filter = {}, options) {
654
+ const client = useClient();
655
+ const [results, setResults] = (0, import_react13.useState)([]);
656
+ const [loading, setLoading] = (0, import_react13.useState)(true);
657
+ const [error, setError] = (0, import_react13.useState)(null);
658
+ const isMounted = (0, import_react13.useRef)(true);
659
+ const handleRef = (0, import_react13.useRef)(null);
660
+ const unsubscribeRef = (0, import_react13.useRef)(null);
661
+ const memoizedFilter = (0, import_react13.useMemo)(() => filter, [
662
+ JSON.stringify(filter.predicate),
663
+ JSON.stringify(filter.where),
664
+ JSON.stringify(filter.sort),
665
+ filter.limit,
666
+ filter.offset
667
+ ]);
668
+ const skip = options?.skip ?? false;
669
+ (0, import_react13.useEffect)(() => {
670
+ isMounted.current = true;
671
+ if (skip) {
672
+ setResults([]);
673
+ setLoading(false);
674
+ setError(null);
675
+ return;
676
+ }
677
+ setLoading(true);
678
+ setError(null);
679
+ try {
680
+ if (handleRef.current) {
681
+ if (unsubscribeRef.current) {
682
+ unsubscribeRef.current();
683
+ unsubscribeRef.current = null;
684
+ }
685
+ }
686
+ const handle = client.hybridQuery(mapName, memoizedFilter);
687
+ handleRef.current = handle;
688
+ let hasReceivedResults = false;
689
+ unsubscribeRef.current = handle.subscribe((newResults) => {
690
+ if (isMounted.current) {
691
+ setResults(newResults);
692
+ if (!hasReceivedResults) {
693
+ hasReceivedResults = true;
694
+ setLoading(false);
695
+ }
696
+ }
697
+ });
698
+ } catch (err) {
699
+ if (isMounted.current) {
700
+ setError(err instanceof Error ? err : new Error(String(err)));
701
+ setLoading(false);
702
+ }
703
+ }
704
+ return () => {
705
+ isMounted.current = false;
706
+ if (unsubscribeRef.current) {
707
+ unsubscribeRef.current();
708
+ unsubscribeRef.current = null;
709
+ }
710
+ handleRef.current = null;
711
+ };
712
+ }, [client, mapName, memoizedFilter, skip]);
713
+ return (0, import_react13.useMemo)(
714
+ () => ({ results, loading, error }),
715
+ [results, loading, error]
716
+ );
717
+ }
543
718
  // Annotate the CommonJS export names for ESM import in node:
544
719
  0 && (module.exports = {
545
720
  TopGunProvider,
@@ -547,12 +722,14 @@ function useConflictResolver(mapName, options = {}) {
547
722
  useConflictResolver,
548
723
  useEntryProcessor,
549
724
  useEventJournal,
725
+ useHybridQuery,
550
726
  useMap,
551
727
  useMergeRejections,
552
728
  useMutation,
553
729
  useORMap,
554
730
  usePNCounter,
555
731
  useQuery,
732
+ useSearch,
556
733
  useTopic
557
734
  });
558
735
  //# sourceMappingURL=index.js.map