@etoile-dev/react 1.0.0 → 1.0.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/README.md CHANGED
@@ -481,10 +481,13 @@ const { results, isLoading } = useEtoileSearch({
481
481
  apiKey: process.env.ETOILE_API_KEY!,
482
482
  collections: ["paintings"],
483
483
  query,
484
+ shouldRetryOnError: true,
485
+ errorRetryCount: 2,
486
+ errorRetryInterval: 1000,
484
487
  });
485
488
  ```
486
489
 
487
- Returns `results`, `isLoading`, `error`, `appliedFilters`, and `refinedQuery`.
490
+ Returns `results`, `isLoading`, `error`, `isError`, `appliedFilters`, and `refinedQuery`.
488
491
 
489
492
  ---
490
493
 
@@ -12,6 +12,15 @@ export type UseEtoileSearchOptions = {
12
12
  offset?: number;
13
13
  /** Debounce delay in ms before firing the request (default: 100) */
14
14
  debounceMs?: number;
15
+ /**
16
+ * Whether failed requests should retry automatically (default: true).
17
+ * Uses `errorRetryCount` and `errorRetryInterval`.
18
+ */
19
+ shouldRetryOnError?: boolean;
20
+ /** Maximum number of retries after a failed request (default: 2). */
21
+ errorRetryCount?: number;
22
+ /** Delay between retries in ms (default: 1000). */
23
+ errorRetryInterval?: number;
15
24
  /**
16
25
  * Explicit metadata filters applied to results.
17
26
  * Mutually exclusive with `autoFilters`.
@@ -48,6 +57,8 @@ export type UseEtoileSearchReturn = {
48
57
  isLoading: boolean;
49
58
  /** Set when the last request failed; null otherwise. */
50
59
  error: unknown;
60
+ /** True when `error` is set. */
61
+ isError: boolean;
51
62
  /** Filters that were applied. Populated when `filters` or `autoFilters` is used. */
52
63
  appliedFilters?: SearchFilter[];
53
64
  /**
@@ -69,9 +80,12 @@ export type UseEtoileSearchReturn = {
69
80
  * @param options.limit - Maximum results to return (default: `10`).
70
81
  * @param options.offset - Number of results to skip for pagination (default: `0`).
71
82
  * @param options.debounceMs - Debounce delay in ms before firing the request (default: `100`).
83
+ * @param options.shouldRetryOnError - Retry failed requests automatically (default: `true`).
84
+ * @param options.errorRetryCount - Maximum retries after a failed request (default: `2`).
85
+ * @param options.errorRetryInterval - Delay between retries in ms (default: `1000`).
72
86
  * @param options.filters - Explicit metadata filters. Mutually exclusive with `autoFilters`.
73
87
  * @param options.autoFilters - When `true`, the AI extracts filters from the query automatically.
74
- * @returns Current search state: `results`, `isLoading`, `error`, `appliedFilters`, `refinedQuery`.
88
+ * @returns Current search state: `results`, `isLoading`, `error`, `isError`, `appliedFilters`, `refinedQuery`.
75
89
  *
76
90
  * @example Basic usage
77
91
  * ```tsx
@@ -106,7 +120,7 @@ export type UseEtoileSearchReturn = {
106
120
  * });
107
121
  * ```
108
122
  */
109
- export declare function useEtoileSearch({ apiKey, collections, query, limit, offset, debounceMs, filters, autoFilters, baseUrl, }: UseEtoileSearchOptions): UseEtoileSearchReturn;
123
+ export declare function useEtoileSearch({ apiKey, collections, query, limit, offset, debounceMs, shouldRetryOnError, errorRetryCount, errorRetryInterval, filters, autoFilters, baseUrl, }: UseEtoileSearchOptions): UseEtoileSearchReturn;
110
124
  /**
111
125
  * Alias of `useEtoileSearch` for ergonomic imports in examples.
112
126
  *
@@ -13,9 +13,12 @@ import { Etoile } from "@etoile-dev/client";
13
13
  * @param options.limit - Maximum results to return (default: `10`).
14
14
  * @param options.offset - Number of results to skip for pagination (default: `0`).
15
15
  * @param options.debounceMs - Debounce delay in ms before firing the request (default: `100`).
16
+ * @param options.shouldRetryOnError - Retry failed requests automatically (default: `true`).
17
+ * @param options.errorRetryCount - Maximum retries after a failed request (default: `2`).
18
+ * @param options.errorRetryInterval - Delay between retries in ms (default: `1000`).
16
19
  * @param options.filters - Explicit metadata filters. Mutually exclusive with `autoFilters`.
17
20
  * @param options.autoFilters - When `true`, the AI extracts filters from the query automatically.
18
- * @returns Current search state: `results`, `isLoading`, `error`, `appliedFilters`, `refinedQuery`.
21
+ * @returns Current search state: `results`, `isLoading`, `error`, `isError`, `appliedFilters`, `refinedQuery`.
19
22
  *
20
23
  * @example Basic usage
21
24
  * ```tsx
@@ -50,7 +53,7 @@ import { Etoile } from "@etoile-dev/client";
50
53
  * });
51
54
  * ```
52
55
  */
53
- export function useEtoileSearch({ apiKey, collections, query, limit = 10, offset, debounceMs = 100, filters, autoFilters, baseUrl, }) {
56
+ export function useEtoileSearch({ apiKey, collections, query, limit = 10, offset, debounceMs = 100, shouldRetryOnError = true, errorRetryCount = 2, errorRetryInterval = 1000, filters, autoFilters, baseUrl, }) {
54
57
  const [results, setResults] = React.useState([]);
55
58
  const [isLoading, setIsLoading] = React.useState(false);
56
59
  const [error, setError] = React.useState(null);
@@ -62,6 +65,18 @@ export function useEtoileSearch({ apiKey, collections, query, limit = 10, offset
62
65
  filtersRef.current = filters;
63
66
  const autoFiltersRef = React.useRef(autoFilters);
64
67
  autoFiltersRef.current = autoFilters;
68
+ const collectionsRef = React.useRef(collections);
69
+ collectionsRef.current = collections;
70
+ // Compare value-based inputs by value so inline objects/arrays don't retrigger endlessly.
71
+ const collectionsKey = React.useMemo(() => collections.join("\u001f"), [collections]);
72
+ const filtersKey = React.useMemo(() => JSON.stringify(filters ?? null), [filters]);
73
+ const autoFiltersKey = autoFilters === undefined ? "unset" : autoFilters ? "true" : "false";
74
+ const retryCount = Number.isFinite(errorRetryCount)
75
+ ? Math.max(0, Math.floor(errorRetryCount))
76
+ : 0;
77
+ const retryInterval = Number.isFinite(errorRetryInterval)
78
+ ? Math.max(0, errorRetryInterval)
79
+ : 0;
65
80
  // Debounce the raw query
66
81
  const [debouncedQuery, setDebouncedQuery] = React.useState(query);
67
82
  React.useEffect(() => {
@@ -77,10 +92,10 @@ export function useEtoileSearch({ apiKey, collections, query, limit = 10, offset
77
92
  setIsLoading(false);
78
93
  }
79
94
  }, [query]);
80
- // Fetch on debounced query / filter change
95
+ // Fetch on debounced query / collection / filter / retry option change
81
96
  React.useEffect(() => {
82
97
  if (debouncedQuery.trim() === "") {
83
- setResults([]);
98
+ setResults((prev) => (prev.length === 0 ? prev : []));
84
99
  setAppliedFilters(undefined);
85
100
  setRefinedQuery(undefined);
86
101
  setIsLoading(false);
@@ -88,40 +103,74 @@ export function useEtoileSearch({ apiKey, collections, query, limit = 10, offset
88
103
  return;
89
104
  }
90
105
  let active = true;
91
- setError(null);
92
- client
93
- .search({
94
- collections,
95
- query: debouncedQuery,
96
- limit,
97
- ...(offset !== undefined && { offset }),
98
- ...(filtersRef.current !== undefined && { filters: filtersRef.current }),
99
- ...(autoFiltersRef.current !== undefined && { autoFilters: autoFiltersRef.current }),
100
- })
101
- .then((res) => {
102
- if (!active)
103
- return;
104
- setResults(Array.isArray(res.results) ? res.results : []);
105
- setAppliedFilters(res.appliedFilters);
106
- setRefinedQuery(res.refinedQuery);
107
- })
108
- .catch((err) => {
106
+ let retryTimer;
107
+ let retryAttempt = 0;
108
+ const runSearch = () => {
109
109
  if (!active)
110
110
  return;
111
- setResults([]);
112
- setAppliedFilters(undefined);
113
- setRefinedQuery(undefined);
114
- setError(err);
115
- })
116
- .finally(() => {
117
- if (active)
111
+ setIsLoading(true);
112
+ client
113
+ .search({
114
+ collections: collectionsRef.current,
115
+ query: debouncedQuery,
116
+ limit,
117
+ ...(offset !== undefined && { offset }),
118
+ ...(filtersRef.current !== undefined && { filters: filtersRef.current }),
119
+ ...(autoFiltersRef.current !== undefined && { autoFilters: autoFiltersRef.current }),
120
+ })
121
+ .then((res) => {
122
+ if (!active)
123
+ return;
124
+ setResults(Array.isArray(res.results) ? res.results : []);
125
+ setAppliedFilters(res.appliedFilters);
126
+ setRefinedQuery(res.refinedQuery);
127
+ setError(null);
128
+ setIsLoading(false);
129
+ })
130
+ .catch((err) => {
131
+ if (!active)
132
+ return;
133
+ const canRetry = shouldRetryOnError && retryAttempt < retryCount;
134
+ if (canRetry) {
135
+ retryAttempt += 1;
136
+ retryTimer = setTimeout(runSearch, retryInterval);
137
+ return;
138
+ }
139
+ setResults([]);
140
+ setAppliedFilters(undefined);
141
+ setRefinedQuery(undefined);
142
+ setError(err);
118
143
  setIsLoading(false);
119
- });
144
+ });
145
+ };
146
+ setError(null);
147
+ runSearch();
120
148
  return () => {
121
149
  active = false;
150
+ if (retryTimer) {
151
+ clearTimeout(retryTimer);
152
+ }
122
153
  };
123
- }, [client, collections, debouncedQuery, limit, offset]);
124
- return { results, isLoading, error, appliedFilters, refinedQuery };
154
+ }, [
155
+ client,
156
+ collectionsKey,
157
+ filtersKey,
158
+ autoFiltersKey,
159
+ debouncedQuery,
160
+ limit,
161
+ offset,
162
+ shouldRetryOnError,
163
+ retryCount,
164
+ retryInterval,
165
+ ]);
166
+ return {
167
+ results,
168
+ isLoading,
169
+ error,
170
+ isError: error != null,
171
+ appliedFilters,
172
+ refinedQuery,
173
+ };
125
174
  }
126
175
  /**
127
176
  * Alias of `useEtoileSearch` for ergonomic imports in examples.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@etoile-dev/react",
3
- "version": "1.0.0",
3
+ "version": "1.0.1",
4
4
  "description": "Official React primitives for Etoile - Headless, composable search components",
5
5
  "keywords": [
6
6
  "etoile",