@buoy-gg/react-query 3.0.1 → 4.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.
Files changed (68) hide show
  1. package/lib/commonjs/index.js +20 -0
  2. package/lib/commonjs/preset.js +23 -0
  3. package/lib/commonjs/react-query/components/QueryFilterViewV3.js +5 -2
  4. package/lib/commonjs/react-query/components/QuerySelector.js +2 -1
  5. package/lib/commonjs/react-query/components/modals/MutationBrowserFooter.js +4 -2
  6. package/lib/commonjs/react-query/components/modals/QueryBrowserFooter.js +5 -2
  7. package/lib/commonjs/react-query/components/modals/ReactQueryModalHeader.js +24 -1
  8. package/lib/commonjs/react-query/components/modals/SwipeIndicator.js +2 -2
  9. package/lib/commonjs/react-query/components/query-browser/MutationStatusCount.js +10 -5
  10. package/lib/commonjs/react-query/components/query-browser/QueryRow.js +3 -0
  11. package/lib/commonjs/react-query/components/query-browser/QueryStatus.js +30 -5
  12. package/lib/commonjs/react-query/components/query-browser/QueryStatusCount.js +12 -1
  13. package/lib/commonjs/react-query/hooks/useQueryStatusCounts.js +6 -2
  14. package/lib/commonjs/react-query/sync/dehydrate.js +56 -0
  15. package/lib/commonjs/react-query/sync/reactQuerySyncAdapter.js +132 -0
  16. package/lib/commonjs/react-query/sync/useReactQuerySyncAdapter.js +26 -0
  17. package/lib/commonjs/react-query/utils/getQueryStatusColor.js +1 -1
  18. package/lib/commonjs/react-query/utils/getQueryStatusLabel.js +8 -2
  19. package/lib/module/index.js +5 -0
  20. package/lib/module/preset.js +23 -0
  21. package/lib/module/react-query/components/QueryFilterViewV3.js +5 -2
  22. package/lib/module/react-query/components/QuerySelector.js +2 -1
  23. package/lib/module/react-query/components/modals/MutationBrowserFooter.js +4 -2
  24. package/lib/module/react-query/components/modals/QueryBrowserFooter.js +5 -2
  25. package/lib/module/react-query/components/modals/ReactQueryModalHeader.js +24 -1
  26. package/lib/module/react-query/components/modals/SwipeIndicator.js +2 -1
  27. package/lib/module/react-query/components/query-browser/MutationStatusCount.js +10 -5
  28. package/lib/module/react-query/components/query-browser/QueryRow.js +3 -0
  29. package/lib/module/react-query/components/query-browser/QueryStatus.js +30 -5
  30. package/lib/module/react-query/components/query-browser/QueryStatusCount.js +12 -1
  31. package/lib/module/react-query/hooks/useQueryStatusCounts.js +6 -2
  32. package/lib/module/react-query/sync/dehydrate.js +52 -0
  33. package/lib/module/react-query/sync/reactQuerySyncAdapter.js +127 -0
  34. package/lib/module/react-query/sync/useReactQuerySyncAdapter.js +23 -0
  35. package/lib/module/react-query/utils/getQueryStatusColor.js +1 -1
  36. package/lib/module/react-query/utils/getQueryStatusLabel.js +8 -2
  37. package/lib/typescript/index.d.ts +3 -0
  38. package/lib/typescript/index.d.ts.map +1 -1
  39. package/lib/typescript/preset.d.ts.map +1 -1
  40. package/lib/typescript/react-query/components/QueryFilterViewV3.d.ts.map +1 -1
  41. package/lib/typescript/react-query/components/QuerySelector.d.ts.map +1 -1
  42. package/lib/typescript/react-query/components/modals/MutationBrowserFooter.d.ts +3 -1
  43. package/lib/typescript/react-query/components/modals/MutationBrowserFooter.d.ts.map +1 -1
  44. package/lib/typescript/react-query/components/modals/QueryBrowserFooter.d.ts +4 -1
  45. package/lib/typescript/react-query/components/modals/QueryBrowserFooter.d.ts.map +1 -1
  46. package/lib/typescript/react-query/components/modals/ReactQueryModalHeader.d.ts +6 -1
  47. package/lib/typescript/react-query/components/modals/ReactQueryModalHeader.d.ts.map +1 -1
  48. package/lib/typescript/react-query/components/modals/SwipeIndicator.d.ts.map +1 -1
  49. package/lib/typescript/react-query/components/query-browser/MutationStatusCount.d.ts +2 -0
  50. package/lib/typescript/react-query/components/query-browser/MutationStatusCount.d.ts.map +1 -1
  51. package/lib/typescript/react-query/components/query-browser/QueryRow.d.ts.map +1 -1
  52. package/lib/typescript/react-query/components/query-browser/QueryStatus.d.ts +5 -0
  53. package/lib/typescript/react-query/components/query-browser/QueryStatus.d.ts.map +1 -1
  54. package/lib/typescript/react-query/components/query-browser/QueryStatusCount.d.ts +2 -0
  55. package/lib/typescript/react-query/components/query-browser/QueryStatusCount.d.ts.map +1 -1
  56. package/lib/typescript/react-query/hooks/useQueryStatusCounts.d.ts +2 -0
  57. package/lib/typescript/react-query/hooks/useQueryStatusCounts.d.ts.map +1 -1
  58. package/lib/typescript/react-query/sync/dehydrate.d.ts +30 -0
  59. package/lib/typescript/react-query/sync/dehydrate.d.ts.map +1 -0
  60. package/lib/typescript/react-query/sync/reactQuerySyncAdapter.d.ts +34 -0
  61. package/lib/typescript/react-query/sync/reactQuerySyncAdapter.d.ts.map +1 -0
  62. package/lib/typescript/react-query/sync/useReactQuerySyncAdapter.d.ts +9 -0
  63. package/lib/typescript/react-query/sync/useReactQuerySyncAdapter.d.ts.map +1 -0
  64. package/lib/typescript/react-query/utils/getQueryStatusColor.d.ts +1 -1
  65. package/lib/typescript/react-query/utils/getQueryStatusColor.d.ts.map +1 -1
  66. package/lib/typescript/react-query/utils/getQueryStatusLabel.d.ts +7 -1
  67. package/lib/typescript/react-query/utils/getQueryStatusLabel.d.ts.map +1 -1
  68. package/package.json +3 -3
@@ -6,12 +6,18 @@ Object.defineProperty(exports, "__esModule", {
6
6
  exports.getQueryStatusLabel = getQueryStatusLabel;
7
7
  /**
8
8
  * Converts a query object into a human-friendly status string for badge rendering.
9
- * Priority order: disabled > fetching > inactive > paused > stale > fresh
9
+ * Priority order: disabled > fetching > error > inactive > paused > stale > fresh
10
+ *
11
+ * "error" sits just below "fetching" so a hard-failed query (status === "error",
12
+ * i.e. failed with no cached data) surfaces as an error instead of hiding under
13
+ * "inactive"/"stale", while an in-flight retry still reads as "fetching". This
14
+ * is the single source of truth every consumer (cards, dots, counts, the Error
15
+ * filter) reads, and it matches the dashboard sidebar's error dot exactly.
10
16
  */
11
17
  function getQueryStatusLabel(query) {
12
18
  // Check disabled first - disabled queries won't automatically fetch
13
19
  if (query.isDisabled()) {
14
20
  return "disabled";
15
21
  }
16
- return query.state.fetchStatus === "fetching" ? "fetching" : !query.getObserversCount() ? "inactive" : query.state.fetchStatus === "paused" ? "paused" : query.isStale() ? "stale" : "fresh";
22
+ return query.state.fetchStatus === "fetching" ? "fetching" : query.state.status === "error" ? "error" : !query.getObserversCount() ? "inactive" : query.state.fetchStatus === "paused" ? "paused" : query.isStale() ? "stale" : "fresh";
17
23
  }
@@ -27,6 +27,11 @@ export { reactQueryToolPreset, createReactQueryTool, wifiTogglePreset, createWif
27
27
  // =============================================================================
28
28
  export { setQueryClient, disconnectQueryClient } from "./react-query/stores/reactQueryEventStore";
29
29
 
30
+ // =============================================================================
31
+ // EXTERNAL SYNC (Adapter factory for @buoy-gg/external-sync's useExternalSync)
32
+ // =============================================================================
33
+ export { createReactQuerySyncAdapter } from "./react-query/sync/reactQuerySyncAdapter";
34
+ export { useReactQuerySyncAdapter } from "./react-query/sync/useReactQuerySyncAdapter";
30
35
  // =============================================================================
31
36
  // COMPONENTS (For custom UI implementations)
32
37
  // =============================================================================
@@ -34,6 +34,29 @@ const saveWifiState = async enabled => {
34
34
  // Failed to save WiFi state
35
35
  }
36
36
  };
37
+
38
+ /**
39
+ * Restore the persisted WiFi state into onlineManager on app load.
40
+ *
41
+ * The toggle is rendered as a `toggle-only` tool whose state lives in
42
+ * `onlineManager`, not in a mounted React component. The `useWifiState` hook
43
+ * only restores when its component (the modal) mounts, so without this the
44
+ * persisted offline state is lost on reload and defaults back to online.
45
+ * Running this at module load ensures the saved state is applied eagerly.
46
+ */
47
+ const restoreWifiState = async () => {
48
+ try {
49
+ const savedState = await persistentStorage.getItem(devToolsStorageKeys.settings.wifiEnabled());
50
+ if (savedState !== null) {
51
+ onlineManager.setOnline(savedState === "true");
52
+ }
53
+ } catch (error) {
54
+ // Failed to restore WiFi state
55
+ }
56
+ };
57
+
58
+ // Eagerly restore persisted WiFi state on import so offline mode survives reloads.
59
+ restoreWifiState();
37
60
  import { useState, useEffect } from "react";
38
61
 
39
62
  /**
@@ -31,9 +31,12 @@ export function QueryFilterViewV3({
31
31
  error: 0
32
32
  };
33
33
  queries.forEach(query => {
34
+ // Count by the status LABEL (single source of truth) so the Error chip's
35
+ // count matches exactly what the cards show and what the Error filter
36
+ // returns. (Previously error was counted off `state.error`, a different
37
+ // axis than the label, so the count, cards, and filter could disagree.)
34
38
  const status = getQueryStatusLabel(query);
35
- if (status === "disabled") counts.disabled++;else if (status === "fresh") counts.fresh++;else if (status === "stale") counts.stale++;else if (status === "fetching") counts.fetching++;else if (status === "paused") counts.paused++;else if (status === "inactive") counts.inactive++;
36
- if (query.state.error) counts.error++;
39
+ if (status === "disabled") counts.disabled++;else if (status === "error") counts.error++;else if (status === "fresh") counts.fresh++;else if (status === "stale") counts.stale++;else if (status === "fetching") counts.fetching++;else if (status === "paused") counts.paused++;else if (status === "inactive") counts.inactive++;
37
40
  });
38
41
  return counts;
39
42
  }, [queries]);
@@ -76,7 +76,8 @@ export function QuerySelector({
76
76
  gray: "#6B7280",
77
77
  purple: "#8B5CF6",
78
78
  yellow: "#F59E0B",
79
- green: "#10B981"
79
+ green: "#10B981",
80
+ red: "#EF4444"
80
81
  };
81
82
  const statusColor = colorMap[statusColorName] || "#6B7280";
82
83
  const isSelected = query === selectedQuery;
@@ -16,7 +16,8 @@ import { jsx as _jsx } from "react/jsx-runtime";
16
16
  export function MutationBrowserFooter({
17
17
  activeFilter,
18
18
  onFilterChange,
19
- modalMode
19
+ modalMode,
20
+ size = "default"
20
21
  }) {
21
22
  const isFloatingMode = modalMode === "floating";
22
23
  const insets = useSafeAreaInsets({
@@ -38,7 +39,8 @@ export function MutationBrowserFooter({
38
39
  style: styles.filterContainer,
39
40
  children: /*#__PURE__*/_jsx(MutationStatusCount, {
40
41
  activeFilter: activeFilter,
41
- onFilterChange: onFilterChange
42
+ onFilterChange: onFilterChange,
43
+ size: size
42
44
  })
43
45
  })
44
46
  }, `footer-${isFloatingMode ? "floating" : "docked"}`);
@@ -15,7 +15,9 @@ import { jsx as _jsx } from "react/jsx-runtime";
15
15
  export function QueryBrowserFooter({
16
16
  activeFilter,
17
17
  onFilterChange,
18
- isFloatingMode = true // Default to floating mode if not specified
18
+ isFloatingMode = true,
19
+ // Default to floating mode if not specified
20
+ size = "default"
19
21
  }) {
20
22
  // Use safe area insets with a minimum bottom padding of 16 for docked mode
21
23
  // This ensures proper spacing even on resized simulators or devices without home indicator
@@ -32,7 +34,8 @@ export function QueryBrowserFooter({
32
34
  style: styles.filterContainer,
33
35
  children: /*#__PURE__*/_jsx(QueryStatusCount, {
34
36
  activeFilter: activeFilter,
35
- onFilterChange: onFilterChange
37
+ onFilterChange: onFilterChange,
38
+ size: size
36
39
  })
37
40
  })
38
41
  });
@@ -3,6 +3,7 @@
3
3
  import { ModalHeader, TabSelector, Search, X, Filter, Trash, buoyColors } from "@buoy-gg/shared-ui";
4
4
  import { useState, useRef, useEffect } from "react";
5
5
  import { View, TextInput, TouchableOpacity, StyleSheet } from "react-native";
6
+ import { WifiToggle } from "../WifiToggle";
6
7
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
7
8
  /**
8
9
  * Shared header for all React Query modals. Handles tab switching when browsing and presents
@@ -18,7 +19,8 @@ export function ReactQueryModalHeader({
18
19
  onSearchChange,
19
20
  onFilterPress,
20
21
  hasActiveFilters = false,
21
- onClearCache
22
+ onClearCache,
23
+ showWifiToggle = false
22
24
  }) {
23
25
  const [isSearchActive, setIsSearchActive] = useState(false);
24
26
  const searchInputRef = useRef(null);
@@ -108,6 +110,18 @@ export function ReactQueryModalHeader({
108
110
  color: buoyColors.textSecondary
109
111
  })
110
112
  }) : null]
113
+ }) : showWifiToggle ? /*#__PURE__*/_jsxs(View, {
114
+ style: styles.tabRow,
115
+ children: [/*#__PURE__*/_jsx(View, {
116
+ style: styles.tabSelectorWrap,
117
+ children: /*#__PURE__*/_jsx(TabSelector, {
118
+ tabs: tabs,
119
+ activeTab: activeTab,
120
+ onTabChange: tab => onTabChange(tab)
121
+ })
122
+ }), /*#__PURE__*/_jsx(WifiToggle, {
123
+ size: 16
124
+ })]
111
125
  }) : /*#__PURE__*/_jsx(TabSelector, {
112
126
  tabs: tabs,
113
127
  activeTab: activeTab,
@@ -144,6 +158,15 @@ export function ReactQueryModalHeader({
144
158
  });
145
159
  }
146
160
  const styles = StyleSheet.create({
161
+ tabRow: {
162
+ flex: 1,
163
+ flexDirection: "row",
164
+ alignItems: "center",
165
+ gap: 12
166
+ },
167
+ tabSelectorWrap: {
168
+ flex: 1
169
+ },
147
170
  headerSearchContainer: {
148
171
  flexDirection: "row",
149
172
  alignItems: "center",
@@ -1,6 +1,7 @@
1
1
  "use strict";
2
2
 
3
3
  import { View, StyleSheet, Animated } from "react-native";
4
+ import { absoluteFill } from "@buoy-gg/shared-ui";
4
5
  import { useMemo } from "react";
5
6
  import { ChevronLeft, ChevronRight, buoyColors } from "@buoy-gg/shared-ui";
6
7
 
@@ -184,7 +185,7 @@ export function SwipeIndicator({
184
185
  }
185
186
  const styles = StyleSheet.create({
186
187
  container: {
187
- ...StyleSheet.absoluteFillObject,
188
+ ...absoluteFill,
188
189
  zIndex: 1000,
189
190
  justifyContent: "center",
190
191
  alignItems: "center"
@@ -11,7 +11,8 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
11
11
  */
12
12
  const MutationStatusCount = ({
13
13
  activeFilter,
14
- onFilterChange
14
+ onFilterChange,
15
+ size = "default"
15
16
  }) => {
16
17
  const {
17
18
  pending,
@@ -57,7 +58,8 @@ const MutationStatusCount = ({
57
58
  isActive: activeFilter === "pending",
58
59
  onPress: event => handleFilterClick("pending", event),
59
60
  onTouchStart: handleTouchStart,
60
- showLabel: true
61
+ showLabel: true,
62
+ size: size
61
63
  }), /*#__PURE__*/_jsx(QueryStatus, {
62
64
  label: "Success",
63
65
  color: "green",
@@ -65,7 +67,8 @@ const MutationStatusCount = ({
65
67
  isActive: activeFilter === "success",
66
68
  onPress: event => handleFilterClick("success", event),
67
69
  onTouchStart: handleTouchStart,
68
- showLabel: true
70
+ showLabel: true,
71
+ size: size
69
72
  }), /*#__PURE__*/_jsx(QueryStatus, {
70
73
  label: "Error",
71
74
  color: "red",
@@ -73,7 +76,8 @@ const MutationStatusCount = ({
73
76
  isActive: activeFilter === "error",
74
77
  onPress: event => handleFilterClick("error", event),
75
78
  onTouchStart: handleTouchStart,
76
- showLabel: true
79
+ showLabel: true,
80
+ size: size
77
81
  }), /*#__PURE__*/_jsx(QueryStatus, {
78
82
  label: "Paused",
79
83
  color: "purple",
@@ -81,7 +85,8 @@ const MutationStatusCount = ({
81
85
  isActive: activeFilter === "paused",
82
86
  onPress: event => handleFilterClick("paused", event),
83
87
  onTouchStart: handleTouchStart,
84
- showLabel: true
88
+ showLabel: true,
89
+ size: size
85
90
  })]
86
91
  })
87
92
  });
@@ -22,6 +22,9 @@ const QueryRow = ({
22
22
  case "disabled":
23
23
  return buoyColors.textMuted + "80";
24
24
  // More muted for disabled
25
+ case "error":
26
+ return buoyColors.error;
27
+ // red — failed with no cached data
25
28
  case "fresh":
26
29
  return buoyColors.success;
27
30
  case "stale":
@@ -13,8 +13,10 @@ const QueryStatus = ({
13
13
  showLabel = true,
14
14
  isActive = false,
15
15
  onPress,
16
- onTouchStart
16
+ onTouchStart,
17
+ size = "default"
17
18
  }) => {
19
+ const isLarge = size === "large";
18
20
  // Buoy theme color mapping for status indicators
19
21
  const getStatusColors = colorName => {
20
22
  const colorMap = {
@@ -60,7 +62,7 @@ const QueryStatus = ({
60
62
  const statusColors = getStatusColors(color);
61
63
  return /*#__PURE__*/_jsxs(TouchableOpacity, {
62
64
  "sentry-label": "ignore devtools query status",
63
- style: [styles.queryStatusTag, isActive && {
65
+ style: [styles.queryStatusTag, isLarge && styles.queryStatusTagLarge, isActive && {
64
66
  backgroundColor: statusColors.dot + "15",
65
67
  borderColor: statusColors.dot + "40"
66
68
  }],
@@ -69,16 +71,16 @@ const QueryStatus = ({
69
71
  onPressIn: onTouchStart,
70
72
  activeOpacity: 0.7,
71
73
  children: [/*#__PURE__*/_jsx(View, {
72
- style: [styles.dot, {
74
+ style: [styles.dot, isLarge && styles.dotLarge, {
73
75
  backgroundColor: statusColors.dot
74
76
  }]
75
77
  }), showLabel && /*#__PURE__*/_jsx(Text, {
76
- style: [styles.label],
78
+ style: [styles.label, isLarge && styles.labelLarge],
77
79
  numberOfLines: 1,
78
80
  ellipsizeMode: "tail",
79
81
  children: label
80
82
  }), count > 0 && /*#__PURE__*/_jsx(Text, {
81
- style: [styles.count, {
83
+ style: [styles.count, isLarge && styles.countLarge, {
82
84
  color: statusColors.dot
83
85
  }],
84
86
  numberOfLines: 1,
@@ -99,23 +101,46 @@ const styles = StyleSheet.create({
99
101
  borderWidth: 1,
100
102
  borderColor: buoyColors.textMuted + "20"
101
103
  },
104
+ queryStatusTagLarge: {
105
+ borderRadius: 16,
106
+ paddingHorizontal: 13,
107
+ paddingVertical: 6,
108
+ height: 32,
109
+ gap: 7
110
+ },
102
111
  dot: {
103
112
  width: 6,
104
113
  height: 6,
105
114
  borderRadius: 3
106
115
  },
116
+ dotLarge: {
117
+ width: 8,
118
+ height: 8,
119
+ borderRadius: 4
120
+ },
107
121
  label: {
108
122
  fontSize: 11,
109
123
  fontWeight: "500",
110
124
  color: buoyColors.textSecondary,
111
125
  fontFamily: "system"
112
126
  },
127
+ labelLarge: {
128
+ fontSize: 13,
129
+ fontWeight: "600",
130
+ letterSpacing: 0.2,
131
+ fontFamily: '"SF Pro Display", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif'
132
+ },
113
133
  count: {
114
134
  fontSize: 11,
115
135
  fontVariant: ["tabular-nums"],
116
136
  fontWeight: "600",
117
137
  marginLeft: "auto",
118
138
  fontFamily: "system"
139
+ },
140
+ countLarge: {
141
+ fontSize: 13,
142
+ fontWeight: "700",
143
+ fontFamily: '"SF Pro Display", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif'
119
144
  }
120
145
  });
121
146
  export default QueryStatus;
@@ -10,7 +10,8 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
10
10
  */
11
11
  const QueryStatusCount = ({
12
12
  activeFilter,
13
- onFilterChange
13
+ onFilterChange,
14
+ size = "default"
14
15
  }) => {
15
16
  const {
16
17
  fresh,
@@ -64,6 +65,8 @@ const QueryStatusCount = ({
64
65
  onPress: event => handleFilterClick("fresh", event),
65
66
  onTouchStart: handleTouchStart,
66
67
  showLabel: true // Always show labels now
68
+ ,
69
+ size: size
67
70
  }), /*#__PURE__*/_jsx(QueryStatus, {
68
71
  label: "Loading",
69
72
  color: "blue",
@@ -72,6 +75,8 @@ const QueryStatusCount = ({
72
75
  onPress: event => handleFilterClick("fetching", event),
73
76
  onTouchStart: handleTouchStart,
74
77
  showLabel: true // Always show labels now
78
+ ,
79
+ size: size
75
80
  }), /*#__PURE__*/_jsx(QueryStatus, {
76
81
  label: "Paused",
77
82
  color: "purple",
@@ -80,6 +85,8 @@ const QueryStatusCount = ({
80
85
  onPress: event => handleFilterClick("paused", event),
81
86
  onTouchStart: handleTouchStart,
82
87
  showLabel: true // Always show labels now
88
+ ,
89
+ size: size
83
90
  }), /*#__PURE__*/_jsx(QueryStatus, {
84
91
  label: "Stale",
85
92
  color: "yellow",
@@ -88,6 +95,8 @@ const QueryStatusCount = ({
88
95
  onPress: event => handleFilterClick("stale", event),
89
96
  onTouchStart: handleTouchStart,
90
97
  showLabel: true // Always show labels now
98
+ ,
99
+ size: size
91
100
  }), /*#__PURE__*/_jsx(QueryStatus, {
92
101
  label: "Idle",
93
102
  color: "gray",
@@ -96,6 +105,8 @@ const QueryStatusCount = ({
96
105
  onPress: event => handleFilterClick("inactive", event),
97
106
  onTouchStart: handleTouchStart,
98
107
  showLabel: true // Always show labels now
108
+ ,
109
+ size: size
99
110
  })]
100
111
  })
101
112
  });
@@ -14,7 +14,9 @@ function useQueryStatusCounts() {
14
14
  stale: 0,
15
15
  fetching: 0,
16
16
  paused: 0,
17
- inactive: 0
17
+ inactive: 0,
18
+ error: 0,
19
+ disabled: 0
18
20
  });
19
21
  useEffect(() => {
20
22
  const updateCounts = () => {
@@ -28,7 +30,9 @@ function useQueryStatusCounts() {
28
30
  stale: 0,
29
31
  fetching: 0,
30
32
  paused: 0,
31
- inactive: 0
33
+ inactive: 0,
34
+ error: 0,
35
+ disabled: 0
32
36
  });
33
37
  setTimeout(() => setCounts(newCounts), 0);
34
38
  };
@@ -0,0 +1,52 @@
1
+ "use strict";
2
+
3
+ /**
4
+ * Dehydration for external sync — ported from buoy-desktop's
5
+ * react-query-external-sync/hydration.ts so the desktop dashboard can
6
+ * hydrate an identical mirror of the device's QueryClient.
7
+ */
8
+
9
+ export function dehydrateQueryClient(client) {
10
+ const mutations = client.getMutationCache().getAll().map(mutation => dehydrateMutation(mutation));
11
+ const queries = client.getQueryCache().getAll().map(query => dehydrateQuery(query));
12
+ return {
13
+ mutations,
14
+ queries
15
+ };
16
+ }
17
+ function dehydrateMutation(mutation) {
18
+ return {
19
+ mutationId: mutation.mutationId,
20
+ mutationKey: mutation.options.mutationKey,
21
+ state: mutation.state,
22
+ ...(mutation.options.scope && {
23
+ scope: mutation.options.scope
24
+ }),
25
+ ...(mutation.meta && {
26
+ meta: mutation.meta
27
+ })
28
+ };
29
+ }
30
+ function dehydrateQuery(query) {
31
+ const observerStates = query.observers.map(observer => ({
32
+ queryHash: query.queryHash,
33
+ options: observer.options,
34
+ // Remove queryFn from observer options so the dashboard can't accidentally
35
+ // run device fetch functions (they aren't serializable anyway)
36
+ queryFn: undefined
37
+ }));
38
+ return {
39
+ state: {
40
+ ...query.state,
41
+ ...(query.state.data !== undefined && {
42
+ data: query.state.data
43
+ })
44
+ },
45
+ queryKey: query.queryKey,
46
+ queryHash: query.queryHash,
47
+ ...(query.meta && {
48
+ meta: query.meta
49
+ }),
50
+ observers: observerStates
51
+ };
52
+ }
@@ -0,0 +1,127 @@
1
+ "use strict";
2
+
3
+ /**
4
+ * Sync adapter for the React Query tool, consumed by @buoy-gg/external-sync's
5
+ * `useExternalSync` (structurally matches its ToolSyncAdapter interface so
6
+ * this package doesn't need a dependency on it).
7
+ *
8
+ * Action handlers are ported from buoy-desktop's react-query-external-sync
9
+ * (useSyncQueries.ts) so the dashboard keeps identical behavior, including
10
+ * the trigger/restore loading and error simulations used by the TanStack
11
+ * devtools UI.
12
+ */
13
+ import { onlineManager } from "@tanstack/react-query";
14
+ import { dehydrateQueryClient } from "./dehydrate";
15
+ export function createReactQuerySyncAdapter(queryClient) {
16
+ const getQuery = params => {
17
+ const {
18
+ queryHash
19
+ } = params;
20
+ const query = queryClient.getQueryCache().get(queryHash);
21
+ if (!query) {
22
+ throw new Error(`Query with hash "${queryHash}" not found`);
23
+ }
24
+ return query;
25
+ };
26
+ return {
27
+ version: 1,
28
+ getSnapshot: () => ({
29
+ dehydratedState: dehydrateQueryClient(queryClient),
30
+ isOnlineManagerOnline: onlineManager.isOnline()
31
+ }),
32
+ subscribe: onChange => {
33
+ const unsubQueries = queryClient.getQueryCache().subscribe(onChange);
34
+ const unsubMutations = queryClient.getMutationCache().subscribe(onChange);
35
+ const unsubOnline = onlineManager.subscribe(onChange);
36
+ return () => {
37
+ unsubQueries();
38
+ unsubMutations();
39
+ unsubOnline();
40
+ };
41
+ },
42
+ actions: {
43
+ refetch: params => {
44
+ const query = getQuery(params);
45
+ // Swallow fetch rejections — the resulting error state syncs anyway
46
+ return query.fetch().catch(() => undefined);
47
+ },
48
+ invalidate: params => queryClient.invalidateQueries(getQuery(params)),
49
+ reset: params => queryClient.resetQueries(getQuery(params)),
50
+ remove: params => {
51
+ queryClient.removeQueries(getQuery(params));
52
+ },
53
+ setQueryData: params => {
54
+ const {
55
+ queryKey,
56
+ data
57
+ } = params;
58
+ queryClient.setQueryData(queryKey, data, {
59
+ updatedAt: Date.now()
60
+ });
61
+ },
62
+ triggerError: params => {
63
+ const query = getQuery(params);
64
+ const __previousQueryOptions = query.options;
65
+ query.setState({
66
+ status: "error",
67
+ error: new Error("Unknown error from devtools"),
68
+ fetchMeta: {
69
+ ...query.state.fetchMeta,
70
+ // @ts-expect-error This does exist
71
+ __previousQueryOptions
72
+ }
73
+ });
74
+ },
75
+ restoreError: params => queryClient.resetQueries(getQuery(params)),
76
+ triggerLoading: params => {
77
+ const query = getQuery(params);
78
+ const __previousQueryOptions = query.options;
79
+ // Trigger a fetch in order to trigger suspense as well
80
+ query.fetch({
81
+ ...__previousQueryOptions,
82
+ queryFn: () => new Promise(() => {
83
+ // Never resolve — simulates perpetual loading
84
+ }),
85
+ gcTime: -1
86
+ }).catch(() => undefined);
87
+ query.setState({
88
+ data: undefined,
89
+ status: "pending",
90
+ fetchMeta: {
91
+ ...query.state.fetchMeta,
92
+ // @ts-expect-error This does exist
93
+ __previousQueryOptions
94
+ }
95
+ });
96
+ },
97
+ restoreLoading: params => {
98
+ const query = getQuery(params);
99
+ const previousState = query.state;
100
+ const previousOptions = query.state.fetchMeta ? query.state.fetchMeta.__previousQueryOptions : null;
101
+ query.cancel({
102
+ silent: true
103
+ });
104
+ query.setState({
105
+ ...previousState,
106
+ fetchStatus: "idle",
107
+ fetchMeta: null
108
+ });
109
+ if (previousOptions) {
110
+ query.fetch(previousOptions).catch(() => undefined);
111
+ }
112
+ },
113
+ clearQueryCache: () => {
114
+ queryClient.getQueryCache().clear();
115
+ },
116
+ clearMutationCache: () => {
117
+ queryClient.getMutationCache().clear();
118
+ },
119
+ setOnline: params => {
120
+ const {
121
+ online
122
+ } = params;
123
+ onlineManager.setOnline(online);
124
+ }
125
+ }
126
+ };
127
+ }
@@ -0,0 +1,23 @@
1
+ "use strict";
2
+
3
+ import { useMemo } from "react";
4
+ import { useQueryClient } from "@tanstack/react-query";
5
+ import { createReactQuerySyncAdapter } from "./reactQuerySyncAdapter";
6
+
7
+ /**
8
+ * Context-based variant of createReactQuerySyncAdapter for zero-config
9
+ * wiring (FloatingDevTools' auto external sync): reads the QueryClient from
10
+ * the surrounding QueryClientProvider. Returns null when rendered outside a
11
+ * provider so callers can skip registering the query tool.
12
+ */
13
+ export function useReactQuerySyncAdapter() {
14
+ let queryClient;
15
+ try {
16
+ // Just a context read — throws only when there is no provider above us,
17
+ // which never changes for a mounted tree, so hook order stays stable.
18
+ queryClient = useQueryClient();
19
+ } catch {
20
+ queryClient = null;
21
+ }
22
+ return useMemo(() => queryClient ? createReactQuerySyncAdapter(queryClient) : null, [queryClient]);
23
+ }
@@ -8,5 +8,5 @@ export function getQueryStatusColor({
8
8
  observerCount,
9
9
  isStale
10
10
  }) {
11
- return queryState.fetchStatus === "fetching" ? "blue" : !observerCount ? "gray" : queryState.fetchStatus === "paused" ? "purple" : isStale ? "yellow" : "green";
11
+ return queryState.fetchStatus === "fetching" ? "blue" : queryState.status === "error" ? "red" : !observerCount ? "gray" : queryState.fetchStatus === "paused" ? "purple" : isStale ? "yellow" : "green";
12
12
  }
@@ -2,12 +2,18 @@
2
2
 
3
3
  /**
4
4
  * Converts a query object into a human-friendly status string for badge rendering.
5
- * Priority order: disabled > fetching > inactive > paused > stale > fresh
5
+ * Priority order: disabled > fetching > error > inactive > paused > stale > fresh
6
+ *
7
+ * "error" sits just below "fetching" so a hard-failed query (status === "error",
8
+ * i.e. failed with no cached data) surfaces as an error instead of hiding under
9
+ * "inactive"/"stale", while an in-flight retry still reads as "fetching". This
10
+ * is the single source of truth every consumer (cards, dots, counts, the Error
11
+ * filter) reads, and it matches the dashboard sidebar's error dot exactly.
6
12
  */
7
13
  export function getQueryStatusLabel(query) {
8
14
  // Check disabled first - disabled queries won't automatically fetch
9
15
  if (query.isDisabled()) {
10
16
  return "disabled";
11
17
  }
12
- return query.state.fetchStatus === "fetching" ? "fetching" : !query.getObserversCount() ? "inactive" : query.state.fetchStatus === "paused" ? "paused" : query.isStale() ? "stale" : "fresh";
18
+ return query.state.fetchStatus === "fetching" ? "fetching" : query.state.status === "error" ? "error" : !query.getObserversCount() ? "inactive" : query.state.fetchStatus === "paused" ? "paused" : query.isStale() ? "stale" : "fresh";
13
19
  }
@@ -9,6 +9,9 @@
9
9
  */
10
10
  export { reactQueryToolPreset, createReactQueryTool, wifiTogglePreset, createWifiToggleTool, } from "./preset";
11
11
  export { setQueryClient, disconnectQueryClient, type ReactQueryEvent, type ReactQueryEventType, } from "./react-query/stores/reactQueryEventStore";
12
+ export { createReactQuerySyncAdapter } from "./react-query/sync/reactQuerySyncAdapter";
13
+ export { useReactQuerySyncAdapter } from "./react-query/sync/useReactQuerySyncAdapter";
14
+ export type { DehydratedState, DehydratedQuery, DehydratedMutation, ObserverState, } from "./react-query/sync/dehydrate";
12
15
  export { ReactQueryModal } from "./react-query/components/modals/ReactQueryModal";
13
16
  export { ReactQueryModalHeader } from "./react-query/components/modals/ReactQueryModalHeader";
14
17
  export { QueryBrowserModal } from "./react-query/components/modals/QueryBrowserModal";