astro-tractstack 2.0.0-rc.45 → 2.0.0-rc.46

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "astro-tractstack",
3
- "version": "2.0.0-rc.45",
3
+ "version": "2.0.0-rc.46",
4
4
  "description": "Astro integration for TractStack - redeeming the web from boring experiences",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -205,26 +205,22 @@ export default function SearchModal({
205
205
  )}
206
206
 
207
207
  {!showResults && (
208
- <div className="relative">
209
- <div className="relative w-full border-none bg-transparent px-6 py-2 text-xl">
210
- {/* Background layer with completion text */}
211
- {showCompletion && (
212
- <div className="pointer-events-none absolute inset-0 px-6 py-2 text-xl text-gray-400">
213
- {bestCompletion}
214
- </div>
215
- )}
216
- {/* Foreground input */}
217
- <input
218
- ref={inputRef}
219
- type="text"
220
- value={query}
221
- onChange={handleInputChange}
222
- onKeyDown={handleKeyDown}
223
- placeholder="Search content..."
224
- className="text-mydarkgrey relative z-10 w-full border-none bg-transparent text-xl placeholder-gray-500 outline-none"
225
- style={{ background: 'transparent' }}
226
- />
227
- </div>
208
+ <div className="relative w-full px-6 py-2">
209
+ {showCompletion && (
210
+ <div className="pointer-events-none absolute left-0 top-0 flex h-full w-full items-center px-6 py-2 text-xl text-gray-400">
211
+ {bestCompletion}
212
+ </div>
213
+ )}
214
+ <input
215
+ ref={inputRef}
216
+ type="text"
217
+ value={query}
218
+ onChange={handleInputChange}
219
+ onKeyDown={handleKeyDown}
220
+ placeholder="Search content..."
221
+ className="text-mydarkgrey relative z-10 w-full border-none bg-transparent text-xl placeholder-gray-500 outline-none"
222
+ style={{ background: 'transparent', padding: '0' }}
223
+ />
228
224
  </div>
229
225
  )}
230
226
  <button
@@ -342,7 +338,6 @@ export default function SearchModal({
342
338
  <SearchResults
343
339
  results={searchResults}
344
340
  contentMap={contentMap}
345
- onResultClick={handleClose}
346
341
  />
347
342
  )}
348
343
  </div>
@@ -14,7 +14,6 @@ const VERBOSE = false;
14
14
  interface SearchResultsProps {
15
15
  results: CategorizedResults;
16
16
  contentMap: FullContentMapItem[];
17
- onResultClick: () => void;
18
17
  }
19
18
 
20
19
  interface ResultItem {
@@ -36,7 +35,6 @@ const ITEMS_PER_PAGE = 10;
36
35
  export default function SearchResults({
37
36
  results,
38
37
  contentMap,
39
- onResultClick,
40
38
  }: SearchResultsProps) {
41
39
  const [currentPage, setCurrentPage] = useState(1);
42
40
 
@@ -238,7 +236,7 @@ export default function SearchResults({
238
236
  key={item.id}
239
237
  className="rounded-lg border border-gray-200 p-4 transition-colors hover:bg-gray-100"
240
238
  >
241
- <a href={item.url} onClick={onResultClick} className="group block">
239
+ <a href={item.url} className="group block">
242
240
  <div className="flex flex-col md:flex-row md:items-start md:gap-4">
243
241
  {/* Mobile: Full width image with overlay badge */}
244
242
  <div
@@ -39,10 +39,12 @@ export function useSearch(): UseSearchReturn {
39
39
  const [isRetrieving, setIsRetrieving] = useState(false);
40
40
  const [retrieveError, setRetrieveError] = useState<string | null>(null);
41
41
 
42
+ // Race condition protection
42
43
  const debounceRef = useRef<NodeJS.Timeout>();
43
44
  const lastSearchTimeRef = useRef<number>(0);
44
45
  const pendingQueryRef = useRef<string | null>(null);
45
46
  const throttleTimeoutRef = useRef<NodeJS.Timeout>();
47
+ const inflightQueryRef = useRef<string | null>(null);
46
48
  const api = useMemo(() => new TractStackAPI(), []);
47
49
 
48
50
  const performDiscovery = useCallback(
@@ -52,26 +54,43 @@ export function useSearch(): UseSearchReturn {
52
54
  return;
53
55
  }
54
56
 
57
+ // Check if this exact query is already inflight
58
+ if (inflightQueryRef.current === query.trim()) {
59
+ return;
60
+ }
61
+
62
+ // Mark this query as inflight
63
+ inflightQueryRef.current = query.trim();
64
+
55
65
  setIsDiscovering(true);
56
66
  setDiscoverError(null);
57
- lastSearchTimeRef.current = Date.now();
58
67
 
59
68
  try {
60
69
  const response = await api.discover(query.trim());
61
70
 
62
- if (response.success && response.data) {
63
- setSuggestions(response.data.suggestions);
64
- } else {
65
- setDiscoverError(response.error || 'Discovery failed');
66
- setSuggestions([]);
71
+ // Only process response if this is still the current inflight query
72
+ if (inflightQueryRef.current === query.trim()) {
73
+ if (response.success && response.data) {
74
+ setSuggestions(response.data.suggestions);
75
+ } else {
76
+ setDiscoverError(response.error || 'Discovery failed');
77
+ setSuggestions([]);
78
+ }
67
79
  }
68
80
  } catch (err) {
69
- setDiscoverError(
70
- err instanceof Error ? err.message : 'Discovery failed'
71
- );
72
- setSuggestions([]);
81
+ // Only process error if this is still the current inflight query
82
+ if (inflightQueryRef.current === query.trim()) {
83
+ setDiscoverError(
84
+ err instanceof Error ? err.message : 'Discovery failed'
85
+ );
86
+ setSuggestions([]);
87
+ }
73
88
  } finally {
74
- setIsDiscovering(false);
89
+ // Clear inflight tracking only if this is still the current query
90
+ if (inflightQueryRef.current === query.trim()) {
91
+ inflightQueryRef.current = null;
92
+ setIsDiscovering(false);
93
+ }
75
94
  }
76
95
  },
77
96
  [api]
@@ -107,6 +126,10 @@ export function useSearch(): UseSearchReturn {
107
126
  if (pendingQueryRef.current) {
108
127
  const queryToExecute = pendingQueryRef.current;
109
128
  pendingQueryRef.current = null;
129
+
130
+ // Update timestamp immediately when scheduling request, not when executing
131
+ lastSearchTimeRef.current = Date.now();
132
+
110
133
  performDiscovery(queryToExecute);
111
134
  }
112
135
  }, [performDiscovery]);
@@ -133,6 +156,7 @@ export function useSearch(): UseSearchReturn {
133
156
  setDiscoverError(null);
134
157
  setIsDiscovering(false);
135
158
  pendingQueryRef.current = null;
159
+ inflightQueryRef.current = null;
136
160
  return;
137
161
  }
138
162
 
@@ -199,7 +223,7 @@ export function useSearch(): UseSearchReturn {
199
223
  clearTimeout(throttleTimeoutRef.current);
200
224
  }
201
225
 
202
- // Reset all state
226
+ // Reset all state including race condition protection
203
227
  setSuggestions([]);
204
228
  setIsDiscovering(false);
205
229
  setDiscoverError(null);
@@ -207,6 +231,7 @@ export function useSearch(): UseSearchReturn {
207
231
  setIsRetrieving(false);
208
232
  setRetrieveError(null);
209
233
  pendingQueryRef.current = null;
234
+ inflightQueryRef.current = null;
210
235
  }, []);
211
236
 
212
237
  return {
@@ -273,8 +273,8 @@ const enableBunny = import.meta.env.PUBLIC_ENABLE_BUNNY === 'true';
273
273
  }
274
274
  <div
275
275
  id="loading-indicator"
276
- class="bg-myorange fixed left-0 top-0 z-50 h-1 w-full scale-x-0 transform transition-transform duration-300 ease-out"
277
- style="opacity: 0.5; filter: blur(0.5px);"
276
+ class="bg-myorange fixed left-0 top-0 h-1 w-full scale-x-0 transform transition-transform duration-300 ease-out"
277
+ style="opacity: 0.5; filter: blur(0.5px); z-index: 10070;"
278
278
  >
279
279
  </div>
280
280