@buoy-gg/react-query 7.0.35 → 7.0.38
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 +27 -5
- package/lib/commonjs/react-query/components/query-browser/svgs.js +2 -4
- package/lib/commonjs/react-query/sync/reactQuerySyncAdapter.js +399 -6
- package/lib/module/react-query/components/query-browser/svgs.js +3 -5
- package/lib/module/react-query/sync/reactQuerySyncAdapter.js +400 -6
- package/lib/typescript/react-query/sync/reactQuerySyncAdapter.d.ts +100 -16
- package/lib/typescript/react-query/sync/reactQuerySyncAdapter.d.ts.map +1 -1
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -2,9 +2,9 @@
|
|
|
2
2
|
|
|
3
3
|
[](https://www.npmjs.com/package/@buoy-gg/react-query) [](https://www.npmjs.com/package/@buoy-gg/react-query)
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
Inspect the TanStack Query cache used by your app. Refetch or invalidate a query and simulate states while checking the screen that consumes it.
|
|
6
6
|
|
|
7
|
-
Part of [Buoy](https://github.com/Buoy-gg/buoy)
|
|
7
|
+
Part of [Buoy](https://github.com/Buoy-gg/buoy). Install the package, complete account setup, and follow the integration steps below.
|
|
8
8
|
|
|
9
9
|

|
|
10
10
|
|
|
@@ -16,6 +16,24 @@ npm install @buoy-gg/core @buoy-gg/react-query
|
|
|
16
16
|
|
|
17
17
|
Requires `@tanstack/react-query` as a peer — you already have it if you use React Query.
|
|
18
18
|
|
|
19
|
+
## Before you start
|
|
20
|
+
|
|
21
|
+
Use a development build with `@buoy-gg/core` and a Free or Pro Buoy account key. From your app’s directory, sign in:
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
npx --package=@buoy-gg/core buoy login
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
For Expo, initialize Buoy before rendering the menu:
|
|
28
|
+
|
|
29
|
+
```tsx
|
|
30
|
+
import { Buoy } from "@buoy-gg/core";
|
|
31
|
+
|
|
32
|
+
Buoy.init({ licenseKey: process.env.EXPO_PUBLIC_BUOY_KEY });
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
The login command writes the Expo key to `.env.local`. For React Native CLI, pass the key from your app’s environment configuration; React Native does not load `.env.local` automatically. Mount `FloatingDevTools` inside the same providers as your screens and restart the development server after installation. The [Quick Start](https://buoy.gg/buoy/latest/docs/quick-start) shows the complete root component setup.
|
|
36
|
+
|
|
19
37
|
## Quick start
|
|
20
38
|
|
|
21
39
|
Render `FloatingDevTools` inside your `QueryClientProvider`. The tool picks up your `QueryClient` from context and connects automatically — no extra call needed.
|
|
@@ -38,6 +56,10 @@ export default function App() {
|
|
|
38
56
|
|
|
39
57
|
That's it. The React Query tool (and a WiFi toggle) appear in the floating menu.
|
|
40
58
|
|
|
59
|
+
## Check the integration
|
|
60
|
+
|
|
61
|
+
Open a known query, refetch it, and check the screen that uses it. Restore any simulated state after testing. The offline toggle changes TanStack Query’s online state; query behavior still depends on its `networkMode` and configuration.
|
|
62
|
+
|
|
41
63
|
## What you get
|
|
42
64
|
|
|
43
65
|
- **Cache inspection** — browse every query, drill into cached data with an interactive JSON tree, and edit values in place
|
|
@@ -50,11 +72,11 @@ That's it. The React Query tool (and a WiFi toggle) appear in the floating menu.
|
|
|
50
72
|
|
|
51
73
|
## Desktop & AI
|
|
52
74
|
|
|
53
|
-
|
|
75
|
+
To connect a React Native app to [Buoy Desktop](https://github.com/Buoy-gg/Buoy-Desktop) or MCP, install `@buoy-gg/external-sync` and follow the [Desktop connection guide](https://buoy.gg/buoy/latest/docs/desktop). Sign in to Desktop separately. [MCP setup](https://buoy.gg/buoy/latest/docs/mcp) also requires a process account and Pro access. Available remote actions depend on the tool and app integration.
|
|
54
76
|
|
|
55
|
-
##
|
|
77
|
+
## Account and plan requirements
|
|
56
78
|
|
|
57
|
-
|
|
79
|
+
Use a Free or Pro Buoy account. History limits and paid features vary by tool; see [pricing](https://buoy.gg/pricing). Production access requires Pro where supported. Development-only hooks and actions remain unavailable in release builds.
|
|
58
80
|
|
|
59
81
|
---
|
|
60
82
|
|
|
@@ -14,10 +14,8 @@ var _jsxRuntime = require("react/jsx-runtime");
|
|
|
14
14
|
function TanstackLogo({
|
|
15
15
|
size = 24
|
|
16
16
|
}) {
|
|
17
|
-
return /*#__PURE__*/(0, _jsxRuntime.jsx)(_sharedUi.
|
|
18
|
-
size: size
|
|
19
|
-
colorPreset: "red",
|
|
20
|
-
variant: "circuit"
|
|
17
|
+
return /*#__PURE__*/(0, _jsxRuntime.jsx)(_sharedUi.QueryIcon, {
|
|
18
|
+
size: size
|
|
21
19
|
});
|
|
22
20
|
}
|
|
23
21
|
|
|
@@ -4,6 +4,7 @@ Object.defineProperty(exports, "__esModule", {
|
|
|
4
4
|
value: true
|
|
5
5
|
});
|
|
6
6
|
exports.createReactQuerySyncAdapter = createReactQuerySyncAdapter;
|
|
7
|
+
var _sharedUi = require("@buoy-gg/shared-ui");
|
|
7
8
|
var _reactQuery = require("@tanstack/react-query");
|
|
8
9
|
var _dehydrate = require("./dehydrate");
|
|
9
10
|
var _getQueryStatusLabel = require("../utils/getQueryStatusLabel");
|
|
@@ -21,12 +22,123 @@ var _getQueryStatusLabel = require("../utils/getQueryStatusLabel");
|
|
|
21
22
|
* on-device markers. Dashboards fetch real data via `getQueryData`.
|
|
22
23
|
*/
|
|
23
24
|
|
|
25
|
+
/**
|
|
26
|
+
* Does this patch write into the list at `path`? Only then is an
|
|
27
|
+
* unchecked-shape warning about that list worth saying — a patch that changes
|
|
28
|
+
* `promoCode` should not be told about an empty `results`. Mirrors the same
|
|
29
|
+
* helper in the zustand adapter, which has warned about this since it shipped.
|
|
30
|
+
*/
|
|
31
|
+
function touchesList(patch, path) {
|
|
32
|
+
let cur = patch;
|
|
33
|
+
for (const seg of path.split(".")) {
|
|
34
|
+
if (!cur || typeof cur !== "object" || Array.isArray(cur)) return false;
|
|
35
|
+
if (!(seg in cur)) return false;
|
|
36
|
+
cur = cur[seg];
|
|
37
|
+
}
|
|
38
|
+
// An empty list stays empty; there is no new item to be unsure about.
|
|
39
|
+
return Array.isArray(cur) ? cur.length > 0 : !!cur && typeof cur === "object";
|
|
40
|
+
}
|
|
41
|
+
const isPlainObject = v => !!v && typeof v === "object" && !Array.isArray(v);
|
|
42
|
+
/** The shape category the screen depends on: array vs object vs the rest. */
|
|
43
|
+
function shapeKind(v) {
|
|
44
|
+
if (Array.isArray(v)) return "array";
|
|
45
|
+
if (v === null) return "null";
|
|
46
|
+
if (typeof v === "object") return "object";
|
|
47
|
+
return typeof v;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Objects merge key by key; arrays and primitives are replaced. Two dangers a
|
|
52
|
+
* remote caller (an LLM most of all) hits are collected instead of silently
|
|
53
|
+
* written:
|
|
54
|
+
*
|
|
55
|
+
* - `shrunk`: a non-empty array shorter than the one it replaces — "change
|
|
56
|
+
* item 0" sent as only item 0, dropping the rest.
|
|
57
|
+
* - `breaks`: a field whose SHAPE changes — an array or object overwritten by
|
|
58
|
+
* null or a primitive. This is the crash we actually shipped: a model set
|
|
59
|
+
* the siblings it wasn't changing (`sizes:null, store:null`) thinking it had
|
|
60
|
+
* to name them, and the screen's `sizes.map(s => s.multiplier)` threw. Merge
|
|
61
|
+
* KEEPS untouched fields, so nulling them is never necessary and never safe.
|
|
62
|
+
*
|
|
63
|
+
* The handler refuses either unless forced. An empty array (explicit clear)
|
|
64
|
+
* and null→value / value→value-of-same-kind are fine.
|
|
65
|
+
*/
|
|
66
|
+
function deepMerge(base, patch, path = "", issues = {
|
|
67
|
+
shrunk: [],
|
|
68
|
+
breaks: []
|
|
69
|
+
}) {
|
|
70
|
+
if (Array.isArray(base) && Array.isArray(patch)) {
|
|
71
|
+
if (patch.length > 0 && patch.length < base.length) {
|
|
72
|
+
issues.shrunk.push({
|
|
73
|
+
path: path || "(root)",
|
|
74
|
+
have: base.length,
|
|
75
|
+
sent: patch.length
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
return patch;
|
|
79
|
+
}
|
|
80
|
+
if (!isPlainObject(base) || !isPlainObject(patch)) return patch;
|
|
81
|
+
const out = {
|
|
82
|
+
...base
|
|
83
|
+
};
|
|
84
|
+
for (const [k, v] of Object.entries(patch)) {
|
|
85
|
+
const p = path ? `${path}.${k}` : k;
|
|
86
|
+
if (k in base) {
|
|
87
|
+
const from = shapeKind(base[k]);
|
|
88
|
+
const to = shapeKind(v);
|
|
89
|
+
// A populated array/object being replaced by null or a primitive breaks
|
|
90
|
+
// the shape the screen renders. (object→object and array→array recurse
|
|
91
|
+
// or replace normally; null/primitive→anything is a real value change.)
|
|
92
|
+
if ((from === "array" || from === "object") && from !== to) {
|
|
93
|
+
issues.breaks.push({
|
|
94
|
+
path: p,
|
|
95
|
+
from,
|
|
96
|
+
to
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
out[k] = deepMerge(base[k], v, p, issues);
|
|
100
|
+
} else {
|
|
101
|
+
out[k] = v;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
return out;
|
|
105
|
+
}
|
|
106
|
+
/** Parse a loose, non-hash key spelling into comparable string parts, or null. */
|
|
107
|
+
function looseKeyParts(raw) {
|
|
108
|
+
const trimmed = raw.trim();
|
|
109
|
+
try {
|
|
110
|
+
const parsed = JSON.parse(trimmed);
|
|
111
|
+
if (Array.isArray(parsed)) {
|
|
112
|
+
return parsed.map(k => typeof k === "object" && k !== null ? JSON.stringify(k) : String(k));
|
|
113
|
+
}
|
|
114
|
+
} catch {
|
|
115
|
+
/* not JSON — fall through to comma splitting */
|
|
116
|
+
}
|
|
117
|
+
if (trimmed.includes(",") && !trimmed.startsWith("[")) {
|
|
118
|
+
return trimmed.split(",").map(s => s.trim().replace(/^["']|["']$/g, ""));
|
|
119
|
+
}
|
|
120
|
+
return null;
|
|
121
|
+
}
|
|
24
122
|
function createReactQuerySyncAdapter(queryClient) {
|
|
25
123
|
const getQuery = params => {
|
|
26
124
|
const {
|
|
27
125
|
queryHash
|
|
28
126
|
} = params;
|
|
29
|
-
const
|
|
127
|
+
const cache = queryClient.getQueryCache();
|
|
128
|
+
let query = queryHash ? cache.get(queryHash) : undefined;
|
|
129
|
+
// Tolerate a key that is not an exact hash. Models routinely send a
|
|
130
|
+
// comma-joined key ("pokemon-detail,mewtwo,fetch") or a JSON array where a
|
|
131
|
+
// hash is wanted; resolve it against the live cache by comparing each
|
|
132
|
+
// query's key parts, so one malformed hash does not cost a turn.
|
|
133
|
+
if (!query && typeof queryHash === "string") {
|
|
134
|
+
const wanted = looseKeyParts(queryHash);
|
|
135
|
+
if (wanted) {
|
|
136
|
+
query = cache.getAll().find(q => {
|
|
137
|
+
const parts = q.queryKey.map(k => typeof k === "object" ? JSON.stringify(k) : String(k));
|
|
138
|
+
return parts.length === wanted.length && parts.every((p, i) => p === wanted[i]);
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
}
|
|
30
142
|
if (!query) {
|
|
31
143
|
throw new Error(`Query with hash "${queryHash}" not found`);
|
|
32
144
|
}
|
|
@@ -56,7 +168,8 @@ function createReactQuerySyncAdapter(queryClient) {
|
|
|
56
168
|
*/
|
|
57
169
|
getQueryData: params => {
|
|
58
170
|
const {
|
|
59
|
-
queryHash
|
|
171
|
+
queryHash,
|
|
172
|
+
path
|
|
60
173
|
} = params ?? {};
|
|
61
174
|
if (!queryHash) return {
|
|
62
175
|
found: false,
|
|
@@ -67,9 +180,55 @@ function createReactQuerySyncAdapter(queryClient) {
|
|
|
67
180
|
found: false,
|
|
68
181
|
reason: "unknown queryHash"
|
|
69
182
|
};
|
|
183
|
+
if (path) {
|
|
184
|
+
const at = (0, _sharedUi.readPath)(query.state.data, path);
|
|
185
|
+
if (!at.ok) return {
|
|
186
|
+
found: false,
|
|
187
|
+
reason: at.error
|
|
188
|
+
};
|
|
189
|
+
return {
|
|
190
|
+
found: true,
|
|
191
|
+
queryHash,
|
|
192
|
+
path,
|
|
193
|
+
shape: (0, _sharedUi.shapeSummary)(at.value),
|
|
194
|
+
data: at.value
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
// Before the data, not after: a long payload is sliced at the engine's
|
|
198
|
+
// 24,000-character cap, and the shape is what a caller needs in order to
|
|
199
|
+
// send a correctly NESTED edit back. Ten of eleven measured `query`
|
|
200
|
+
// write failures were one level too shallow — `{name}` for a payload
|
|
201
|
+
// that is `{item: {name}}` — and the sketch is where that is visible in
|
|
202
|
+
// one line even when the data is not.
|
|
203
|
+
// A query with no data is not the same as a query holding nothing, and
|
|
204
|
+
// the two used to be indistinguishable: an errored query, a disabled
|
|
205
|
+
// one and an empty one all came back as a bare `{found:true}`. A reader
|
|
206
|
+
// — an LLM most of all — then reports "the query is empty" when the
|
|
207
|
+
// truth is "it failed with X". `listQueries` has always known; this is
|
|
208
|
+
// the call where anyone actually asks.
|
|
209
|
+
if (query.state.data === undefined) {
|
|
210
|
+
const status = query.state.status;
|
|
211
|
+
const err = query.state.error;
|
|
212
|
+
return {
|
|
213
|
+
found: true,
|
|
214
|
+
queryHash,
|
|
215
|
+
hasData: false,
|
|
216
|
+
status,
|
|
217
|
+
fetchStatus: query.state.fetchStatus,
|
|
218
|
+
...(err ? {
|
|
219
|
+
error: err instanceof Error ? err.message : String(err)
|
|
220
|
+
} : {}),
|
|
221
|
+
reason: status === "error" ? "This query has no data because it FAILED. Report the error, not an empty result." : query.state.fetchStatus === "fetching" ? "No data yet — this query is still fetching." : "This query has never resolved (it may be disabled, or never enabled). It is not empty — it has nothing."
|
|
222
|
+
};
|
|
223
|
+
}
|
|
224
|
+
const shape = (0, _sharedUi.shapeSummary)(query.state.data);
|
|
70
225
|
return {
|
|
71
226
|
found: true,
|
|
72
227
|
queryHash,
|
|
228
|
+
hasData: true,
|
|
229
|
+
...(shape ? {
|
|
230
|
+
shape
|
|
231
|
+
} : {}),
|
|
73
232
|
data: (0, _dehydrate.capQueryDetail)(query.state.data)
|
|
74
233
|
};
|
|
75
234
|
},
|
|
@@ -88,7 +247,17 @@ function createReactQuerySyncAdapter(queryClient) {
|
|
|
88
247
|
} = params ?? {};
|
|
89
248
|
const all = queryClient.getQueryCache().getAll();
|
|
90
249
|
const filtered = staleOnly ? all.filter(q => q.isStale()) : all;
|
|
91
|
-
|
|
250
|
+
// MOUNTED FIRST, then most-recently-updated. `limit` exists to keep
|
|
251
|
+
// this cheap for a remote reader, and sorting by recency alone made it
|
|
252
|
+
// lossy in the one way that matters: a detail screen's query settles
|
|
253
|
+
// once and is then older than every background refetch, so on a cache
|
|
254
|
+
// with more live queries than the limit the query the user is actually
|
|
255
|
+
// LOOKING AT was the first thing dropped. Nothing downstream can undo
|
|
256
|
+
// that — a caller re-sorting the response can only reorder survivors.
|
|
257
|
+
const sorted = [...filtered].sort((a, b) => {
|
|
258
|
+
const mounted = Number(b.getObserversCount() > 0) - Number(a.getObserversCount() > 0);
|
|
259
|
+
return mounted !== 0 ? mounted : (b.state.dataUpdatedAt ?? 0) - (a.state.dataUpdatedAt ?? 0);
|
|
260
|
+
});
|
|
92
261
|
const capped = typeof limit === "number" && limit > 0 ? sorted.slice(0, limit) : sorted;
|
|
93
262
|
const queries = capped.map(q => {
|
|
94
263
|
const err = q.state.error;
|
|
@@ -103,6 +272,12 @@ function createReactQuerySyncAdapter(queryClient) {
|
|
|
103
272
|
error: err ? err.message ?? String(err) : undefined,
|
|
104
273
|
...(includeData ? {
|
|
105
274
|
data: q.state.data
|
|
275
|
+
} : {}),
|
|
276
|
+
// The compact view exists so a caller can survey the cache without
|
|
277
|
+
// pulling payloads; the shape is the part of a payload it actually
|
|
278
|
+
// needs to write a correct edit, and it costs one line.
|
|
279
|
+
...((0, _sharedUi.shapeSummary)(q.state.data) ? {
|
|
280
|
+
shape: (0, _sharedUi.shapeSummary)(q.state.data)
|
|
106
281
|
} : {})
|
|
107
282
|
};
|
|
108
283
|
});
|
|
@@ -120,17 +295,226 @@ function createReactQuerySyncAdapter(queryClient) {
|
|
|
120
295
|
},
|
|
121
296
|
invalidate: params => queryClient.invalidateQueries(getQuery(params)),
|
|
122
297
|
reset: params => queryClient.resetQueries(getQuery(params)),
|
|
298
|
+
/**
|
|
299
|
+
* Dropping a query does NOT re-drive the screen watching it.
|
|
300
|
+
*
|
|
301
|
+
* React Query removes the cache entry; a mounted observer keeps its last
|
|
302
|
+
* result in local state until something makes it fetch again. So the app
|
|
303
|
+
* carries on rendering a value that now exists nowhere — including one
|
|
304
|
+
* the agent itself wrote a moment earlier. Measured on device: `remove`
|
|
305
|
+
* on the live menu query left the storefront showing a single fabricated
|
|
306
|
+
* item, with no skeleton, no refetch and no error, while the cache held
|
|
307
|
+
* no menu at all. Returning a bare null let that be reported as "done".
|
|
308
|
+
*
|
|
309
|
+
* `reset` is the action that actually re-drives an observed query, so
|
|
310
|
+
* the note names it rather than leaving the caller to find out from a
|
|
311
|
+
* screen that looks fine.
|
|
312
|
+
*/
|
|
123
313
|
remove: params => {
|
|
124
|
-
|
|
314
|
+
const target = getQuery(params);
|
|
315
|
+
const watched = queryClient.getQueryCache().findAll(target).some(q => q.getObserversCount() > 0);
|
|
316
|
+
queryClient.removeQueries(target);
|
|
317
|
+
return {
|
|
318
|
+
ok: true,
|
|
319
|
+
...(watched ? {
|
|
320
|
+
note: "Removed — but a screen is still watching this query, and React Query leaves that screen rendering its LAST value (including anything written into the cache before this) until something refetches. Nothing on screen will change on its own. Use `reset` instead when you want the screen to go back to loading and fetch again."
|
|
321
|
+
} : {})
|
|
322
|
+
};
|
|
125
323
|
},
|
|
126
324
|
setQueryData: params => {
|
|
127
325
|
const {
|
|
128
|
-
|
|
129
|
-
|
|
326
|
+
data,
|
|
327
|
+
merge,
|
|
328
|
+
force,
|
|
329
|
+
path,
|
|
330
|
+
value
|
|
130
331
|
} = params;
|
|
332
|
+
/**
|
|
333
|
+
* Address the query the way EVERY OTHER action here does.
|
|
334
|
+
*
|
|
335
|
+
* This action took `queryKey` and silently ignored `queryHash` — while
|
|
336
|
+
* `listQueries` hands out `queryHash` and getQueryData / invalidate /
|
|
337
|
+
* refetch / reset / remove / triggerError / triggerLoading all take it.
|
|
338
|
+
* A caller that used the hash got `getQueryData(undefined)` →
|
|
339
|
+
* `undefined`, and so a refusal about "(root): undefined → object"
|
|
340
|
+
* that named the wrong problem. With `force:true` — which that refusal
|
|
341
|
+
* literally suggests — it wrote under an undefined key and still
|
|
342
|
+
* returned `{ok:true, changedKeys:[…]}`: a success report for a write
|
|
343
|
+
* that touched nothing, which is the one outcome this codebase refuses
|
|
344
|
+
* to ship. Ask Buoy itself was covered because the ENGINE aliases hash
|
|
345
|
+
* → key before dispatch; the MCP server, the desktop dashboard and
|
|
346
|
+
* Scenarios all call straight in here and were not.
|
|
347
|
+
*/
|
|
348
|
+
const {
|
|
349
|
+
queryKey: rawKey,
|
|
350
|
+
queryHash
|
|
351
|
+
} = params;
|
|
352
|
+
let queryKey = rawKey;
|
|
353
|
+
if (queryKey === undefined && typeof queryHash === "string") {
|
|
354
|
+
const found = queryClient.getQueryCache().get(queryHash);
|
|
355
|
+
if (!found) {
|
|
356
|
+
return {
|
|
357
|
+
ok: false,
|
|
358
|
+
error: `Refused: no query with hash ${queryHash} is in the cache. Read listQueries for the current hashes — a query that has been removed or whose key includes changing params (a search term, a cart payload) will not match an older hash.`
|
|
359
|
+
};
|
|
360
|
+
}
|
|
361
|
+
queryKey = found.queryKey;
|
|
362
|
+
}
|
|
363
|
+
if (queryKey === undefined) {
|
|
364
|
+
return {
|
|
365
|
+
ok: false,
|
|
366
|
+
error: `Refused: no query was named. Send \`queryKey\` (the array from listQueries) or \`queryHash\` (its string form).`
|
|
367
|
+
};
|
|
368
|
+
}
|
|
369
|
+
const current = queryClient.getQueryData(queryKey);
|
|
370
|
+
/**
|
|
371
|
+
* There is nothing to merge INTO.
|
|
372
|
+
*
|
|
373
|
+
* The typed-edit guard answers this with "`(root)`: undefined →
|
|
374
|
+
* object", which describes the symptom and not the cause — and it is
|
|
375
|
+
* the same sentence a caller used to get when the query simply could
|
|
376
|
+
* not be found, so it taught nothing. A query with no data is usually
|
|
377
|
+
* one that FAILED or was never enabled, and that is what the caller
|
|
378
|
+
* needs to hear.
|
|
379
|
+
*/
|
|
380
|
+
const existing = queryClient.getQueryCache().find({
|
|
381
|
+
queryKey,
|
|
382
|
+
exact: true
|
|
383
|
+
});
|
|
384
|
+
// Only when the query is THERE and holds nothing. A key that is not in
|
|
385
|
+
// the cache at all is a deliberate seed — `setQueryData(["new"], …)`
|
|
386
|
+
// creates the entry, and that has always worked.
|
|
387
|
+
if (current === undefined && existing && (data !== undefined || path !== undefined)) {
|
|
388
|
+
const q = existing;
|
|
389
|
+
const status = q?.state.status;
|
|
390
|
+
const why = status === "error" ? `it FAILED${q?.state.error ? ` (${q.state.error instanceof Error ? q.state.error.message : String(q.state.error)})` : ""}` : status === "pending" ? "it has never resolved (it may be disabled, or still loading)" : "it holds no data";
|
|
391
|
+
return {
|
|
392
|
+
ok: false,
|
|
393
|
+
error: `Refused: this query has no data to edit — ${why}. There is nothing to merge into. Fix the cause first (restoreError, or let it fetch), or send force:true with the COMPLETE value if you mean to seed the cache from nothing.`,
|
|
394
|
+
status
|
|
395
|
+
};
|
|
396
|
+
}
|
|
397
|
+
// `path` is sugar over a merge, resolved against the data as it is now.
|
|
398
|
+
// It only ever produces a patch; everything below — the keyed expansion,
|
|
399
|
+
// the typed-edit guard, the receipt — then runs unchanged, so the guard
|
|
400
|
+
// stays the single place that decides what a write may do.
|
|
401
|
+
// `data` stopped being schema-required when `path` became an
|
|
402
|
+
// alternative, so the handler has to reject NEITHER — the same hole that
|
|
403
|
+
// let a bare `setState` fall through to a whole-store replace. Here it
|
|
404
|
+
// would write `undefined` over a cached value.
|
|
405
|
+
if (data === undefined && path === undefined) {
|
|
406
|
+
return {
|
|
407
|
+
ok: false,
|
|
408
|
+
error: `Refused: no change was described. Send \`data\` (with merge:true for a partial edit), or \`path\` and \`value\` to set one value.`
|
|
409
|
+
};
|
|
410
|
+
}
|
|
411
|
+
let patch = data;
|
|
412
|
+
if (path !== undefined) {
|
|
413
|
+
if (value === undefined) {
|
|
414
|
+
return {
|
|
415
|
+
ok: false,
|
|
416
|
+
error: `Refused: sent \`path\` without \`value\`. Say what to set "${path}" to; if you mean to clear it, send value:null and the field must already be nullable.`
|
|
417
|
+
};
|
|
418
|
+
}
|
|
419
|
+
const built = (0, _sharedUi.patchFromPath)(current, path, value);
|
|
420
|
+
if (!built.ok) return {
|
|
421
|
+
ok: false,
|
|
422
|
+
error: `Refused: ${built.error}`
|
|
423
|
+
};
|
|
424
|
+
patch = built.patch;
|
|
425
|
+
}
|
|
426
|
+
if (merge || path !== undefined) {
|
|
427
|
+
// Address list items by their own id instead of resending the list:
|
|
428
|
+
// {"results": {"pikachu": {"name": "test123"}}}
|
|
429
|
+
// Expanded BEFORE the guard below, so the typed-edit rule still holds
|
|
430
|
+
// over the result. This is the only form that works when the read was
|
|
431
|
+
// truncated — and a capped read is the normal case here, not the edge
|
|
432
|
+
// one. See packages/shared/src/sync/keyedArrays.ts for the measurement.
|
|
433
|
+
const expandedKeyed = force ? {
|
|
434
|
+
patch,
|
|
435
|
+
notes: [],
|
|
436
|
+
error: undefined
|
|
437
|
+
} : (0, _sharedUi.expandKeyedArrays)(current, patch);
|
|
438
|
+
// An id that contradicts its own address is refused HERE, where we
|
|
439
|
+
// still know both halves; the typed-edit guard downstream would only
|
|
440
|
+
// see the item we picked and could not name the ambiguity.
|
|
441
|
+
if (expandedKeyed.error) {
|
|
442
|
+
return {
|
|
443
|
+
ok: false,
|
|
444
|
+
error: `Refused: ${expandedKeyed.error}`
|
|
445
|
+
};
|
|
446
|
+
}
|
|
447
|
+
const effective = expandedKeyed.patch;
|
|
448
|
+
// Edit like the devtools value editor: only same-type changes to
|
|
449
|
+
// fields that already exist. One rule replaces every ad-hoc guard —
|
|
450
|
+
// no new keys, no type changes, no null-ing a rendered array/object,
|
|
451
|
+
// no resizing a list. This is what makes a crash structurally
|
|
452
|
+
// impossible. `force` drops to a raw write for the rare real case.
|
|
453
|
+
if (!force) {
|
|
454
|
+
const violations = (0, _sharedUi.typedEditViolations)(current, effective, {
|
|
455
|
+
noun: "data"
|
|
456
|
+
});
|
|
457
|
+
if (violations.length > 0) {
|
|
458
|
+
const suggestion = (0, _sharedUi.suggestedPatch)(effective, violations);
|
|
459
|
+
return {
|
|
460
|
+
ok: false,
|
|
461
|
+
error: (0, _sharedUi.refusalText)(violations, "data", suggestion),
|
|
462
|
+
violations,
|
|
463
|
+
...(suggestion ? {
|
|
464
|
+
suggestedData: suggestion
|
|
465
|
+
} : {})
|
|
466
|
+
};
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
const next = deepMerge(current, effective);
|
|
470
|
+
// A list that was EMPTY had no item shape for anything to check the
|
|
471
|
+
// new item against — not the guard above, not the reader, not the
|
|
472
|
+
// app. The zustand tool has always said so; this one stayed silent
|
|
473
|
+
// while the system prompt told the model the note always arrives, so
|
|
474
|
+
// a write of an invented shape into an empty list came back a clean
|
|
475
|
+
// success and got reported as one.
|
|
476
|
+
const unchecked = (0, _sharedUi.unshapedLists)(current).filter(p => touchesList(effective, p));
|
|
477
|
+
queryClient.setQueryData(queryKey, next, {
|
|
478
|
+
updatedAt: Date.now()
|
|
479
|
+
});
|
|
480
|
+
return {
|
|
481
|
+
ok: true,
|
|
482
|
+
mode: "merge",
|
|
483
|
+
...(path !== undefined ? {
|
|
484
|
+
path
|
|
485
|
+
} : {}),
|
|
486
|
+
changedKeys: isPlainObject(effective) ? Object.keys(effective) : undefined,
|
|
487
|
+
...(expandedKeyed.notes.length ? {
|
|
488
|
+
keyed: expandedKeyed.notes
|
|
489
|
+
} : {}),
|
|
490
|
+
...(unchecked.length ? {
|
|
491
|
+
unchecked: unchecked.map(p => `\`${p}\` was empty, so nothing in the app knows what its items look like and the shape of what you added was NOT checked. If the screen renders a field you left out, it will render undefined — read the query back and compare against how the screen uses it.`)
|
|
492
|
+
} : {})
|
|
493
|
+
};
|
|
494
|
+
}
|
|
495
|
+
// THE GUARD. A remote caller — an LLM most of all — reads the cached
|
|
496
|
+
// value through a wire that truncates big payloads, edits one field,
|
|
497
|
+
// and writes the torn object back. The screen then renders
|
|
498
|
+
// `data.stats.map` over a field that no longer exists and the app
|
|
499
|
+
// dies. If the replacement silently drops top-level keys the cache
|
|
500
|
+
// has, that is almost never intended, so refuse and name the fix.
|
|
501
|
+
if (!force && isPlainObject(current) && isPlainObject(data)) {
|
|
502
|
+
const missing = Object.keys(current).filter(k => !(k in data));
|
|
503
|
+
if (missing.length > 0) {
|
|
504
|
+
return {
|
|
505
|
+
ok: false,
|
|
506
|
+
error: `Refused: the new value drops ${missing.length} top-level field(s) the cached value has (${missing.slice(0, 8).join(", ")}${missing.length > 8 ? ", …" : ""}). A screen rendering those fields would crash. To change some fields, send merge:true with ONLY the fields that change. To really replace the whole value, send force:true.`,
|
|
507
|
+
missingKeys: missing
|
|
508
|
+
};
|
|
509
|
+
}
|
|
510
|
+
}
|
|
131
511
|
queryClient.setQueryData(queryKey, data, {
|
|
132
512
|
updatedAt: Date.now()
|
|
133
513
|
});
|
|
514
|
+
return {
|
|
515
|
+
ok: true,
|
|
516
|
+
mode: "replace"
|
|
517
|
+
};
|
|
134
518
|
},
|
|
135
519
|
triggerError: params => {
|
|
136
520
|
const query = getQuery(params);
|
|
@@ -184,7 +568,16 @@ function createReactQuerySyncAdapter(queryClient) {
|
|
|
184
568
|
}
|
|
185
569
|
},
|
|
186
570
|
clearQueryCache: () => {
|
|
571
|
+
// Same trap as `remove`, across every screen at once. See its note.
|
|
572
|
+
const watched = queryClient.getQueryCache().getAll().filter(q => q.getObserversCount() > 0).length;
|
|
187
573
|
queryClient.getQueryCache().clear();
|
|
574
|
+
return {
|
|
575
|
+
ok: true,
|
|
576
|
+
cleared: true,
|
|
577
|
+
...(watched ? {
|
|
578
|
+
note: `Cleared — but ${watched} quer${watched === 1 ? "y is" : "ies are"} still being watched by a screen, and React Query leaves those screens rendering their LAST value until something refetches. The cache is empty; the app on screen is not. Use \`reset\` on a query when you want its screen to reload.`
|
|
579
|
+
} : {})
|
|
580
|
+
};
|
|
188
581
|
},
|
|
189
582
|
clearMutationCache: () => {
|
|
190
583
|
queryClient.getMutationCache().clear();
|
|
@@ -1,15 +1,13 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
|
|
3
|
-
import {
|
|
3
|
+
import { QueryIcon, CheckCircle as SharedCheckCircle, Activity as SharedActivity, Pause as SharedPause, XCircle as SharedXCircle, buoyColors } from "@buoy-gg/shared-ui";
|
|
4
4
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
5
5
|
/** React Query brand mark used across the dev tools UI. */
|
|
6
6
|
export function TanstackLogo({
|
|
7
7
|
size = 24
|
|
8
8
|
}) {
|
|
9
|
-
return /*#__PURE__*/_jsx(
|
|
10
|
-
size: size
|
|
11
|
-
colorPreset: "red",
|
|
12
|
-
variant: "circuit"
|
|
9
|
+
return /*#__PURE__*/_jsx(QueryIcon, {
|
|
10
|
+
size: size
|
|
13
11
|
});
|
|
14
12
|
}
|
|
15
13
|
|
|
@@ -13,15 +13,128 @@
|
|
|
13
13
|
* v2: oversized / unserializable query (and mutation) data is replaced with
|
|
14
14
|
* on-device markers. Dashboards fetch real data via `getQueryData`.
|
|
15
15
|
*/
|
|
16
|
+
import { expandKeyedArrays, patchFromPath, readPath, refusalText, shapeSummary, suggestedPatch, typedEditViolations, unshapedLists } from "@buoy-gg/shared-ui";
|
|
16
17
|
import { onlineManager } from "@tanstack/react-query";
|
|
17
18
|
import { capQueryDetail, dehydrateQueryClient } from "./dehydrate";
|
|
18
19
|
import { getQueryStatusLabel } from "../utils/getQueryStatusLabel";
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Does this patch write into the list at `path`? Only then is an
|
|
23
|
+
* unchecked-shape warning about that list worth saying — a patch that changes
|
|
24
|
+
* `promoCode` should not be told about an empty `results`. Mirrors the same
|
|
25
|
+
* helper in the zustand adapter, which has warned about this since it shipped.
|
|
26
|
+
*/
|
|
27
|
+
function touchesList(patch, path) {
|
|
28
|
+
let cur = patch;
|
|
29
|
+
for (const seg of path.split(".")) {
|
|
30
|
+
if (!cur || typeof cur !== "object" || Array.isArray(cur)) return false;
|
|
31
|
+
if (!(seg in cur)) return false;
|
|
32
|
+
cur = cur[seg];
|
|
33
|
+
}
|
|
34
|
+
// An empty list stays empty; there is no new item to be unsure about.
|
|
35
|
+
return Array.isArray(cur) ? cur.length > 0 : !!cur && typeof cur === "object";
|
|
36
|
+
}
|
|
37
|
+
const isPlainObject = v => !!v && typeof v === "object" && !Array.isArray(v);
|
|
38
|
+
/** The shape category the screen depends on: array vs object vs the rest. */
|
|
39
|
+
function shapeKind(v) {
|
|
40
|
+
if (Array.isArray(v)) return "array";
|
|
41
|
+
if (v === null) return "null";
|
|
42
|
+
if (typeof v === "object") return "object";
|
|
43
|
+
return typeof v;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Objects merge key by key; arrays and primitives are replaced. Two dangers a
|
|
48
|
+
* remote caller (an LLM most of all) hits are collected instead of silently
|
|
49
|
+
* written:
|
|
50
|
+
*
|
|
51
|
+
* - `shrunk`: a non-empty array shorter than the one it replaces — "change
|
|
52
|
+
* item 0" sent as only item 0, dropping the rest.
|
|
53
|
+
* - `breaks`: a field whose SHAPE changes — an array or object overwritten by
|
|
54
|
+
* null or a primitive. This is the crash we actually shipped: a model set
|
|
55
|
+
* the siblings it wasn't changing (`sizes:null, store:null`) thinking it had
|
|
56
|
+
* to name them, and the screen's `sizes.map(s => s.multiplier)` threw. Merge
|
|
57
|
+
* KEEPS untouched fields, so nulling them is never necessary and never safe.
|
|
58
|
+
*
|
|
59
|
+
* The handler refuses either unless forced. An empty array (explicit clear)
|
|
60
|
+
* and null→value / value→value-of-same-kind are fine.
|
|
61
|
+
*/
|
|
62
|
+
function deepMerge(base, patch, path = "", issues = {
|
|
63
|
+
shrunk: [],
|
|
64
|
+
breaks: []
|
|
65
|
+
}) {
|
|
66
|
+
if (Array.isArray(base) && Array.isArray(patch)) {
|
|
67
|
+
if (patch.length > 0 && patch.length < base.length) {
|
|
68
|
+
issues.shrunk.push({
|
|
69
|
+
path: path || "(root)",
|
|
70
|
+
have: base.length,
|
|
71
|
+
sent: patch.length
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
return patch;
|
|
75
|
+
}
|
|
76
|
+
if (!isPlainObject(base) || !isPlainObject(patch)) return patch;
|
|
77
|
+
const out = {
|
|
78
|
+
...base
|
|
79
|
+
};
|
|
80
|
+
for (const [k, v] of Object.entries(patch)) {
|
|
81
|
+
const p = path ? `${path}.${k}` : k;
|
|
82
|
+
if (k in base) {
|
|
83
|
+
const from = shapeKind(base[k]);
|
|
84
|
+
const to = shapeKind(v);
|
|
85
|
+
// A populated array/object being replaced by null or a primitive breaks
|
|
86
|
+
// the shape the screen renders. (object→object and array→array recurse
|
|
87
|
+
// or replace normally; null/primitive→anything is a real value change.)
|
|
88
|
+
if ((from === "array" || from === "object") && from !== to) {
|
|
89
|
+
issues.breaks.push({
|
|
90
|
+
path: p,
|
|
91
|
+
from,
|
|
92
|
+
to
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
out[k] = deepMerge(base[k], v, p, issues);
|
|
96
|
+
} else {
|
|
97
|
+
out[k] = v;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
return out;
|
|
101
|
+
}
|
|
102
|
+
/** Parse a loose, non-hash key spelling into comparable string parts, or null. */
|
|
103
|
+
function looseKeyParts(raw) {
|
|
104
|
+
const trimmed = raw.trim();
|
|
105
|
+
try {
|
|
106
|
+
const parsed = JSON.parse(trimmed);
|
|
107
|
+
if (Array.isArray(parsed)) {
|
|
108
|
+
return parsed.map(k => typeof k === "object" && k !== null ? JSON.stringify(k) : String(k));
|
|
109
|
+
}
|
|
110
|
+
} catch {
|
|
111
|
+
/* not JSON — fall through to comma splitting */
|
|
112
|
+
}
|
|
113
|
+
if (trimmed.includes(",") && !trimmed.startsWith("[")) {
|
|
114
|
+
return trimmed.split(",").map(s => s.trim().replace(/^["']|["']$/g, ""));
|
|
115
|
+
}
|
|
116
|
+
return null;
|
|
117
|
+
}
|
|
19
118
|
export function createReactQuerySyncAdapter(queryClient) {
|
|
20
119
|
const getQuery = params => {
|
|
21
120
|
const {
|
|
22
121
|
queryHash
|
|
23
122
|
} = params;
|
|
24
|
-
const
|
|
123
|
+
const cache = queryClient.getQueryCache();
|
|
124
|
+
let query = queryHash ? cache.get(queryHash) : undefined;
|
|
125
|
+
// Tolerate a key that is not an exact hash. Models routinely send a
|
|
126
|
+
// comma-joined key ("pokemon-detail,mewtwo,fetch") or a JSON array where a
|
|
127
|
+
// hash is wanted; resolve it against the live cache by comparing each
|
|
128
|
+
// query's key parts, so one malformed hash does not cost a turn.
|
|
129
|
+
if (!query && typeof queryHash === "string") {
|
|
130
|
+
const wanted = looseKeyParts(queryHash);
|
|
131
|
+
if (wanted) {
|
|
132
|
+
query = cache.getAll().find(q => {
|
|
133
|
+
const parts = q.queryKey.map(k => typeof k === "object" ? JSON.stringify(k) : String(k));
|
|
134
|
+
return parts.length === wanted.length && parts.every((p, i) => p === wanted[i]);
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
}
|
|
25
138
|
if (!query) {
|
|
26
139
|
throw new Error(`Query with hash "${queryHash}" not found`);
|
|
27
140
|
}
|
|
@@ -51,7 +164,8 @@ export function createReactQuerySyncAdapter(queryClient) {
|
|
|
51
164
|
*/
|
|
52
165
|
getQueryData: params => {
|
|
53
166
|
const {
|
|
54
|
-
queryHash
|
|
167
|
+
queryHash,
|
|
168
|
+
path
|
|
55
169
|
} = params ?? {};
|
|
56
170
|
if (!queryHash) return {
|
|
57
171
|
found: false,
|
|
@@ -62,9 +176,55 @@ export function createReactQuerySyncAdapter(queryClient) {
|
|
|
62
176
|
found: false,
|
|
63
177
|
reason: "unknown queryHash"
|
|
64
178
|
};
|
|
179
|
+
if (path) {
|
|
180
|
+
const at = readPath(query.state.data, path);
|
|
181
|
+
if (!at.ok) return {
|
|
182
|
+
found: false,
|
|
183
|
+
reason: at.error
|
|
184
|
+
};
|
|
185
|
+
return {
|
|
186
|
+
found: true,
|
|
187
|
+
queryHash,
|
|
188
|
+
path,
|
|
189
|
+
shape: shapeSummary(at.value),
|
|
190
|
+
data: at.value
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
// Before the data, not after: a long payload is sliced at the engine's
|
|
194
|
+
// 24,000-character cap, and the shape is what a caller needs in order to
|
|
195
|
+
// send a correctly NESTED edit back. Ten of eleven measured `query`
|
|
196
|
+
// write failures were one level too shallow — `{name}` for a payload
|
|
197
|
+
// that is `{item: {name}}` — and the sketch is where that is visible in
|
|
198
|
+
// one line even when the data is not.
|
|
199
|
+
// A query with no data is not the same as a query holding nothing, and
|
|
200
|
+
// the two used to be indistinguishable: an errored query, a disabled
|
|
201
|
+
// one and an empty one all came back as a bare `{found:true}`. A reader
|
|
202
|
+
// — an LLM most of all — then reports "the query is empty" when the
|
|
203
|
+
// truth is "it failed with X". `listQueries` has always known; this is
|
|
204
|
+
// the call where anyone actually asks.
|
|
205
|
+
if (query.state.data === undefined) {
|
|
206
|
+
const status = query.state.status;
|
|
207
|
+
const err = query.state.error;
|
|
208
|
+
return {
|
|
209
|
+
found: true,
|
|
210
|
+
queryHash,
|
|
211
|
+
hasData: false,
|
|
212
|
+
status,
|
|
213
|
+
fetchStatus: query.state.fetchStatus,
|
|
214
|
+
...(err ? {
|
|
215
|
+
error: err instanceof Error ? err.message : String(err)
|
|
216
|
+
} : {}),
|
|
217
|
+
reason: status === "error" ? "This query has no data because it FAILED. Report the error, not an empty result." : query.state.fetchStatus === "fetching" ? "No data yet — this query is still fetching." : "This query has never resolved (it may be disabled, or never enabled). It is not empty — it has nothing."
|
|
218
|
+
};
|
|
219
|
+
}
|
|
220
|
+
const shape = shapeSummary(query.state.data);
|
|
65
221
|
return {
|
|
66
222
|
found: true,
|
|
67
223
|
queryHash,
|
|
224
|
+
hasData: true,
|
|
225
|
+
...(shape ? {
|
|
226
|
+
shape
|
|
227
|
+
} : {}),
|
|
68
228
|
data: capQueryDetail(query.state.data)
|
|
69
229
|
};
|
|
70
230
|
},
|
|
@@ -83,7 +243,17 @@ export function createReactQuerySyncAdapter(queryClient) {
|
|
|
83
243
|
} = params ?? {};
|
|
84
244
|
const all = queryClient.getQueryCache().getAll();
|
|
85
245
|
const filtered = staleOnly ? all.filter(q => q.isStale()) : all;
|
|
86
|
-
|
|
246
|
+
// MOUNTED FIRST, then most-recently-updated. `limit` exists to keep
|
|
247
|
+
// this cheap for a remote reader, and sorting by recency alone made it
|
|
248
|
+
// lossy in the one way that matters: a detail screen's query settles
|
|
249
|
+
// once and is then older than every background refetch, so on a cache
|
|
250
|
+
// with more live queries than the limit the query the user is actually
|
|
251
|
+
// LOOKING AT was the first thing dropped. Nothing downstream can undo
|
|
252
|
+
// that — a caller re-sorting the response can only reorder survivors.
|
|
253
|
+
const sorted = [...filtered].sort((a, b) => {
|
|
254
|
+
const mounted = Number(b.getObserversCount() > 0) - Number(a.getObserversCount() > 0);
|
|
255
|
+
return mounted !== 0 ? mounted : (b.state.dataUpdatedAt ?? 0) - (a.state.dataUpdatedAt ?? 0);
|
|
256
|
+
});
|
|
87
257
|
const capped = typeof limit === "number" && limit > 0 ? sorted.slice(0, limit) : sorted;
|
|
88
258
|
const queries = capped.map(q => {
|
|
89
259
|
const err = q.state.error;
|
|
@@ -98,6 +268,12 @@ export function createReactQuerySyncAdapter(queryClient) {
|
|
|
98
268
|
error: err ? err.message ?? String(err) : undefined,
|
|
99
269
|
...(includeData ? {
|
|
100
270
|
data: q.state.data
|
|
271
|
+
} : {}),
|
|
272
|
+
// The compact view exists so a caller can survey the cache without
|
|
273
|
+
// pulling payloads; the shape is the part of a payload it actually
|
|
274
|
+
// needs to write a correct edit, and it costs one line.
|
|
275
|
+
...(shapeSummary(q.state.data) ? {
|
|
276
|
+
shape: shapeSummary(q.state.data)
|
|
101
277
|
} : {})
|
|
102
278
|
};
|
|
103
279
|
});
|
|
@@ -115,17 +291,226 @@ export function createReactQuerySyncAdapter(queryClient) {
|
|
|
115
291
|
},
|
|
116
292
|
invalidate: params => queryClient.invalidateQueries(getQuery(params)),
|
|
117
293
|
reset: params => queryClient.resetQueries(getQuery(params)),
|
|
294
|
+
/**
|
|
295
|
+
* Dropping a query does NOT re-drive the screen watching it.
|
|
296
|
+
*
|
|
297
|
+
* React Query removes the cache entry; a mounted observer keeps its last
|
|
298
|
+
* result in local state until something makes it fetch again. So the app
|
|
299
|
+
* carries on rendering a value that now exists nowhere — including one
|
|
300
|
+
* the agent itself wrote a moment earlier. Measured on device: `remove`
|
|
301
|
+
* on the live menu query left the storefront showing a single fabricated
|
|
302
|
+
* item, with no skeleton, no refetch and no error, while the cache held
|
|
303
|
+
* no menu at all. Returning a bare null let that be reported as "done".
|
|
304
|
+
*
|
|
305
|
+
* `reset` is the action that actually re-drives an observed query, so
|
|
306
|
+
* the note names it rather than leaving the caller to find out from a
|
|
307
|
+
* screen that looks fine.
|
|
308
|
+
*/
|
|
118
309
|
remove: params => {
|
|
119
|
-
|
|
310
|
+
const target = getQuery(params);
|
|
311
|
+
const watched = queryClient.getQueryCache().findAll(target).some(q => q.getObserversCount() > 0);
|
|
312
|
+
queryClient.removeQueries(target);
|
|
313
|
+
return {
|
|
314
|
+
ok: true,
|
|
315
|
+
...(watched ? {
|
|
316
|
+
note: "Removed — but a screen is still watching this query, and React Query leaves that screen rendering its LAST value (including anything written into the cache before this) until something refetches. Nothing on screen will change on its own. Use `reset` instead when you want the screen to go back to loading and fetch again."
|
|
317
|
+
} : {})
|
|
318
|
+
};
|
|
120
319
|
},
|
|
121
320
|
setQueryData: params => {
|
|
122
321
|
const {
|
|
123
|
-
|
|
124
|
-
|
|
322
|
+
data,
|
|
323
|
+
merge,
|
|
324
|
+
force,
|
|
325
|
+
path,
|
|
326
|
+
value
|
|
125
327
|
} = params;
|
|
328
|
+
/**
|
|
329
|
+
* Address the query the way EVERY OTHER action here does.
|
|
330
|
+
*
|
|
331
|
+
* This action took `queryKey` and silently ignored `queryHash` — while
|
|
332
|
+
* `listQueries` hands out `queryHash` and getQueryData / invalidate /
|
|
333
|
+
* refetch / reset / remove / triggerError / triggerLoading all take it.
|
|
334
|
+
* A caller that used the hash got `getQueryData(undefined)` →
|
|
335
|
+
* `undefined`, and so a refusal about "(root): undefined → object"
|
|
336
|
+
* that named the wrong problem. With `force:true` — which that refusal
|
|
337
|
+
* literally suggests — it wrote under an undefined key and still
|
|
338
|
+
* returned `{ok:true, changedKeys:[…]}`: a success report for a write
|
|
339
|
+
* that touched nothing, which is the one outcome this codebase refuses
|
|
340
|
+
* to ship. Ask Buoy itself was covered because the ENGINE aliases hash
|
|
341
|
+
* → key before dispatch; the MCP server, the desktop dashboard and
|
|
342
|
+
* Scenarios all call straight in here and were not.
|
|
343
|
+
*/
|
|
344
|
+
const {
|
|
345
|
+
queryKey: rawKey,
|
|
346
|
+
queryHash
|
|
347
|
+
} = params;
|
|
348
|
+
let queryKey = rawKey;
|
|
349
|
+
if (queryKey === undefined && typeof queryHash === "string") {
|
|
350
|
+
const found = queryClient.getQueryCache().get(queryHash);
|
|
351
|
+
if (!found) {
|
|
352
|
+
return {
|
|
353
|
+
ok: false,
|
|
354
|
+
error: `Refused: no query with hash ${queryHash} is in the cache. Read listQueries for the current hashes — a query that has been removed or whose key includes changing params (a search term, a cart payload) will not match an older hash.`
|
|
355
|
+
};
|
|
356
|
+
}
|
|
357
|
+
queryKey = found.queryKey;
|
|
358
|
+
}
|
|
359
|
+
if (queryKey === undefined) {
|
|
360
|
+
return {
|
|
361
|
+
ok: false,
|
|
362
|
+
error: `Refused: no query was named. Send \`queryKey\` (the array from listQueries) or \`queryHash\` (its string form).`
|
|
363
|
+
};
|
|
364
|
+
}
|
|
365
|
+
const current = queryClient.getQueryData(queryKey);
|
|
366
|
+
/**
|
|
367
|
+
* There is nothing to merge INTO.
|
|
368
|
+
*
|
|
369
|
+
* The typed-edit guard answers this with "`(root)`: undefined →
|
|
370
|
+
* object", which describes the symptom and not the cause — and it is
|
|
371
|
+
* the same sentence a caller used to get when the query simply could
|
|
372
|
+
* not be found, so it taught nothing. A query with no data is usually
|
|
373
|
+
* one that FAILED or was never enabled, and that is what the caller
|
|
374
|
+
* needs to hear.
|
|
375
|
+
*/
|
|
376
|
+
const existing = queryClient.getQueryCache().find({
|
|
377
|
+
queryKey,
|
|
378
|
+
exact: true
|
|
379
|
+
});
|
|
380
|
+
// Only when the query is THERE and holds nothing. A key that is not in
|
|
381
|
+
// the cache at all is a deliberate seed — `setQueryData(["new"], …)`
|
|
382
|
+
// creates the entry, and that has always worked.
|
|
383
|
+
if (current === undefined && existing && (data !== undefined || path !== undefined)) {
|
|
384
|
+
const q = existing;
|
|
385
|
+
const status = q?.state.status;
|
|
386
|
+
const why = status === "error" ? `it FAILED${q?.state.error ? ` (${q.state.error instanceof Error ? q.state.error.message : String(q.state.error)})` : ""}` : status === "pending" ? "it has never resolved (it may be disabled, or still loading)" : "it holds no data";
|
|
387
|
+
return {
|
|
388
|
+
ok: false,
|
|
389
|
+
error: `Refused: this query has no data to edit — ${why}. There is nothing to merge into. Fix the cause first (restoreError, or let it fetch), or send force:true with the COMPLETE value if you mean to seed the cache from nothing.`,
|
|
390
|
+
status
|
|
391
|
+
};
|
|
392
|
+
}
|
|
393
|
+
// `path` is sugar over a merge, resolved against the data as it is now.
|
|
394
|
+
// It only ever produces a patch; everything below — the keyed expansion,
|
|
395
|
+
// the typed-edit guard, the receipt — then runs unchanged, so the guard
|
|
396
|
+
// stays the single place that decides what a write may do.
|
|
397
|
+
// `data` stopped being schema-required when `path` became an
|
|
398
|
+
// alternative, so the handler has to reject NEITHER — the same hole that
|
|
399
|
+
// let a bare `setState` fall through to a whole-store replace. Here it
|
|
400
|
+
// would write `undefined` over a cached value.
|
|
401
|
+
if (data === undefined && path === undefined) {
|
|
402
|
+
return {
|
|
403
|
+
ok: false,
|
|
404
|
+
error: `Refused: no change was described. Send \`data\` (with merge:true for a partial edit), or \`path\` and \`value\` to set one value.`
|
|
405
|
+
};
|
|
406
|
+
}
|
|
407
|
+
let patch = data;
|
|
408
|
+
if (path !== undefined) {
|
|
409
|
+
if (value === undefined) {
|
|
410
|
+
return {
|
|
411
|
+
ok: false,
|
|
412
|
+
error: `Refused: sent \`path\` without \`value\`. Say what to set "${path}" to; if you mean to clear it, send value:null and the field must already be nullable.`
|
|
413
|
+
};
|
|
414
|
+
}
|
|
415
|
+
const built = patchFromPath(current, path, value);
|
|
416
|
+
if (!built.ok) return {
|
|
417
|
+
ok: false,
|
|
418
|
+
error: `Refused: ${built.error}`
|
|
419
|
+
};
|
|
420
|
+
patch = built.patch;
|
|
421
|
+
}
|
|
422
|
+
if (merge || path !== undefined) {
|
|
423
|
+
// Address list items by their own id instead of resending the list:
|
|
424
|
+
// {"results": {"pikachu": {"name": "test123"}}}
|
|
425
|
+
// Expanded BEFORE the guard below, so the typed-edit rule still holds
|
|
426
|
+
// over the result. This is the only form that works when the read was
|
|
427
|
+
// truncated — and a capped read is the normal case here, not the edge
|
|
428
|
+
// one. See packages/shared/src/sync/keyedArrays.ts for the measurement.
|
|
429
|
+
const expandedKeyed = force ? {
|
|
430
|
+
patch,
|
|
431
|
+
notes: [],
|
|
432
|
+
error: undefined
|
|
433
|
+
} : expandKeyedArrays(current, patch);
|
|
434
|
+
// An id that contradicts its own address is refused HERE, where we
|
|
435
|
+
// still know both halves; the typed-edit guard downstream would only
|
|
436
|
+
// see the item we picked and could not name the ambiguity.
|
|
437
|
+
if (expandedKeyed.error) {
|
|
438
|
+
return {
|
|
439
|
+
ok: false,
|
|
440
|
+
error: `Refused: ${expandedKeyed.error}`
|
|
441
|
+
};
|
|
442
|
+
}
|
|
443
|
+
const effective = expandedKeyed.patch;
|
|
444
|
+
// Edit like the devtools value editor: only same-type changes to
|
|
445
|
+
// fields that already exist. One rule replaces every ad-hoc guard —
|
|
446
|
+
// no new keys, no type changes, no null-ing a rendered array/object,
|
|
447
|
+
// no resizing a list. This is what makes a crash structurally
|
|
448
|
+
// impossible. `force` drops to a raw write for the rare real case.
|
|
449
|
+
if (!force) {
|
|
450
|
+
const violations = typedEditViolations(current, effective, {
|
|
451
|
+
noun: "data"
|
|
452
|
+
});
|
|
453
|
+
if (violations.length > 0) {
|
|
454
|
+
const suggestion = suggestedPatch(effective, violations);
|
|
455
|
+
return {
|
|
456
|
+
ok: false,
|
|
457
|
+
error: refusalText(violations, "data", suggestion),
|
|
458
|
+
violations,
|
|
459
|
+
...(suggestion ? {
|
|
460
|
+
suggestedData: suggestion
|
|
461
|
+
} : {})
|
|
462
|
+
};
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
const next = deepMerge(current, effective);
|
|
466
|
+
// A list that was EMPTY had no item shape for anything to check the
|
|
467
|
+
// new item against — not the guard above, not the reader, not the
|
|
468
|
+
// app. The zustand tool has always said so; this one stayed silent
|
|
469
|
+
// while the system prompt told the model the note always arrives, so
|
|
470
|
+
// a write of an invented shape into an empty list came back a clean
|
|
471
|
+
// success and got reported as one.
|
|
472
|
+
const unchecked = unshapedLists(current).filter(p => touchesList(effective, p));
|
|
473
|
+
queryClient.setQueryData(queryKey, next, {
|
|
474
|
+
updatedAt: Date.now()
|
|
475
|
+
});
|
|
476
|
+
return {
|
|
477
|
+
ok: true,
|
|
478
|
+
mode: "merge",
|
|
479
|
+
...(path !== undefined ? {
|
|
480
|
+
path
|
|
481
|
+
} : {}),
|
|
482
|
+
changedKeys: isPlainObject(effective) ? Object.keys(effective) : undefined,
|
|
483
|
+
...(expandedKeyed.notes.length ? {
|
|
484
|
+
keyed: expandedKeyed.notes
|
|
485
|
+
} : {}),
|
|
486
|
+
...(unchecked.length ? {
|
|
487
|
+
unchecked: unchecked.map(p => `\`${p}\` was empty, so nothing in the app knows what its items look like and the shape of what you added was NOT checked. If the screen renders a field you left out, it will render undefined — read the query back and compare against how the screen uses it.`)
|
|
488
|
+
} : {})
|
|
489
|
+
};
|
|
490
|
+
}
|
|
491
|
+
// THE GUARD. A remote caller — an LLM most of all — reads the cached
|
|
492
|
+
// value through a wire that truncates big payloads, edits one field,
|
|
493
|
+
// and writes the torn object back. The screen then renders
|
|
494
|
+
// `data.stats.map` over a field that no longer exists and the app
|
|
495
|
+
// dies. If the replacement silently drops top-level keys the cache
|
|
496
|
+
// has, that is almost never intended, so refuse and name the fix.
|
|
497
|
+
if (!force && isPlainObject(current) && isPlainObject(data)) {
|
|
498
|
+
const missing = Object.keys(current).filter(k => !(k in data));
|
|
499
|
+
if (missing.length > 0) {
|
|
500
|
+
return {
|
|
501
|
+
ok: false,
|
|
502
|
+
error: `Refused: the new value drops ${missing.length} top-level field(s) the cached value has (${missing.slice(0, 8).join(", ")}${missing.length > 8 ? ", …" : ""}). A screen rendering those fields would crash. To change some fields, send merge:true with ONLY the fields that change. To really replace the whole value, send force:true.`,
|
|
503
|
+
missingKeys: missing
|
|
504
|
+
};
|
|
505
|
+
}
|
|
506
|
+
}
|
|
126
507
|
queryClient.setQueryData(queryKey, data, {
|
|
127
508
|
updatedAt: Date.now()
|
|
128
509
|
});
|
|
510
|
+
return {
|
|
511
|
+
ok: true,
|
|
512
|
+
mode: "replace"
|
|
513
|
+
};
|
|
129
514
|
},
|
|
130
515
|
triggerError: params => {
|
|
131
516
|
const query = getQuery(params);
|
|
@@ -179,7 +564,16 @@ export function createReactQuerySyncAdapter(queryClient) {
|
|
|
179
564
|
}
|
|
180
565
|
},
|
|
181
566
|
clearQueryCache: () => {
|
|
567
|
+
// Same trap as `remove`, across every screen at once. See its note.
|
|
568
|
+
const watched = queryClient.getQueryCache().getAll().filter(q => q.getObserversCount() > 0).length;
|
|
182
569
|
queryClient.getQueryCache().clear();
|
|
570
|
+
return {
|
|
571
|
+
ok: true,
|
|
572
|
+
cleared: true,
|
|
573
|
+
...(watched ? {
|
|
574
|
+
note: `Cleared — but ${watched} quer${watched === 1 ? "y is" : "ies are"} still being watched by a screen, and React Query leaves those screens rendering their LAST value until something refetches. The cache is empty; the app on screen is not. Use \`reset\` on a query when you want its screen to reload.`
|
|
575
|
+
} : {})
|
|
576
|
+
};
|
|
183
577
|
},
|
|
184
578
|
clearMutationCache: () => {
|
|
185
579
|
queryClient.getMutationCache().clear();
|
|
@@ -1,16 +1,3 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Sync adapter for the React Query tool, consumed by @buoy-gg/external-sync's
|
|
3
|
-
* `useExternalSync` (structurally matches its ToolSyncAdapter interface so
|
|
4
|
-
* this package doesn't need a dependency on it).
|
|
5
|
-
*
|
|
6
|
-
* Action handlers are ported from buoy-desktop's react-query-external-sync
|
|
7
|
-
* (useSyncQueries.ts) so the dashboard keeps identical behavior, including
|
|
8
|
-
* the trigger/restore loading and error simulations used by the TanStack
|
|
9
|
-
* devtools UI.
|
|
10
|
-
*
|
|
11
|
-
* v2: oversized / unserializable query (and mutation) data is replaced with
|
|
12
|
-
* on-device markers. Dashboards fetch real data via `getQueryData`.
|
|
13
|
-
*/
|
|
14
1
|
import { type QueryClient } from "@tanstack/react-query";
|
|
15
2
|
export declare function createReactQuerySyncAdapter(queryClient: QueryClient): {
|
|
16
3
|
version: number;
|
|
@@ -29,12 +16,41 @@ export declare function createReactQuerySyncAdapter(queryClient: QueryClient): {
|
|
|
29
16
|
found: false;
|
|
30
17
|
reason: string;
|
|
31
18
|
queryHash?: undefined;
|
|
19
|
+
path?: undefined;
|
|
20
|
+
shape?: undefined;
|
|
32
21
|
data?: undefined;
|
|
33
22
|
} | {
|
|
34
23
|
found: true;
|
|
35
24
|
queryHash: string;
|
|
25
|
+
path: string;
|
|
26
|
+
shape: {
|
|
27
|
+
shape: string;
|
|
28
|
+
lists?: import("@buoy-gg/shared-ui").ListShape[];
|
|
29
|
+
} | undefined;
|
|
36
30
|
data: unknown;
|
|
37
31
|
reason?: undefined;
|
|
32
|
+
} | {
|
|
33
|
+
reason: string;
|
|
34
|
+
error?: string | undefined;
|
|
35
|
+
found: true;
|
|
36
|
+
queryHash: string;
|
|
37
|
+
hasData: false;
|
|
38
|
+
status: import("@tanstack/query-core").QueryStatus;
|
|
39
|
+
fetchStatus: import("@tanstack/query-core").FetchStatus;
|
|
40
|
+
path?: undefined;
|
|
41
|
+
shape?: undefined;
|
|
42
|
+
data?: undefined;
|
|
43
|
+
} | {
|
|
44
|
+
data: unknown;
|
|
45
|
+
shape?: {
|
|
46
|
+
shape: string;
|
|
47
|
+
lists?: import("@buoy-gg/shared-ui").ListShape[];
|
|
48
|
+
} | undefined;
|
|
49
|
+
found: true;
|
|
50
|
+
queryHash: string;
|
|
51
|
+
hasData: true;
|
|
52
|
+
reason?: undefined;
|
|
53
|
+
path?: undefined;
|
|
38
54
|
};
|
|
39
55
|
/**
|
|
40
56
|
* Compact, token-bounded cache reader for a remote driver (MCP/LLM):
|
|
@@ -45,6 +61,10 @@ export declare function createReactQuerySyncAdapter(queryClient: QueryClient): {
|
|
|
45
61
|
*/
|
|
46
62
|
listQueries: (params?: unknown) => {
|
|
47
63
|
queries: {
|
|
64
|
+
shape?: {
|
|
65
|
+
shape: string;
|
|
66
|
+
lists?: import("@buoy-gg/shared-ui").ListShape[];
|
|
67
|
+
} | undefined;
|
|
48
68
|
data?: unknown;
|
|
49
69
|
queryHash: string;
|
|
50
70
|
queryKey: readonly unknown[];
|
|
@@ -62,13 +82,77 @@ export declare function createReactQuerySyncAdapter(queryClient: QueryClient): {
|
|
|
62
82
|
refetch: (params: unknown) => Promise<unknown>;
|
|
63
83
|
invalidate: (params: unknown) => Promise<void>;
|
|
64
84
|
reset: (params: unknown) => Promise<void>;
|
|
65
|
-
|
|
66
|
-
|
|
85
|
+
/**
|
|
86
|
+
* Dropping a query does NOT re-drive the screen watching it.
|
|
87
|
+
*
|
|
88
|
+
* React Query removes the cache entry; a mounted observer keeps its last
|
|
89
|
+
* result in local state until something makes it fetch again. So the app
|
|
90
|
+
* carries on rendering a value that now exists nowhere — including one
|
|
91
|
+
* the agent itself wrote a moment earlier. Measured on device: `remove`
|
|
92
|
+
* on the live menu query left the storefront showing a single fabricated
|
|
93
|
+
* item, with no skeleton, no refetch and no error, while the cache held
|
|
94
|
+
* no menu at all. Returning a bare null let that be reported as "done".
|
|
95
|
+
*
|
|
96
|
+
* `reset` is the action that actually re-drives an observed query, so
|
|
97
|
+
* the note names it rather than leaving the caller to find out from a
|
|
98
|
+
* screen that looks fine.
|
|
99
|
+
*/
|
|
100
|
+
remove: (params: unknown) => {
|
|
101
|
+
note?: string | undefined;
|
|
102
|
+
ok: true;
|
|
103
|
+
};
|
|
104
|
+
setQueryData: (params: unknown) => {
|
|
105
|
+
ok: boolean;
|
|
106
|
+
error: string;
|
|
107
|
+
status?: undefined;
|
|
108
|
+
missingKeys?: undefined;
|
|
109
|
+
mode?: undefined;
|
|
110
|
+
} | {
|
|
111
|
+
ok: boolean;
|
|
112
|
+
error: string;
|
|
113
|
+
status: import("@tanstack/query-core").QueryStatus;
|
|
114
|
+
missingKeys?: undefined;
|
|
115
|
+
mode?: undefined;
|
|
116
|
+
} | {
|
|
117
|
+
suggestedData?: Record<string, unknown> | undefined;
|
|
118
|
+
ok: boolean;
|
|
119
|
+
error: string;
|
|
120
|
+
violations: import("@buoy-gg/shared-ui").EditViolation[];
|
|
121
|
+
status?: undefined;
|
|
122
|
+
missingKeys?: undefined;
|
|
123
|
+
mode?: undefined;
|
|
124
|
+
} | {
|
|
125
|
+
unchecked?: string[] | undefined;
|
|
126
|
+
keyed?: string[] | undefined;
|
|
127
|
+
changedKeys: string[] | undefined;
|
|
128
|
+
path?: string | undefined;
|
|
129
|
+
ok: boolean;
|
|
130
|
+
mode: string;
|
|
131
|
+
error?: undefined;
|
|
132
|
+
status?: undefined;
|
|
133
|
+
missingKeys?: undefined;
|
|
134
|
+
} | {
|
|
135
|
+
ok: boolean;
|
|
136
|
+
error: string;
|
|
137
|
+
missingKeys: string[];
|
|
138
|
+
status?: undefined;
|
|
139
|
+
mode?: undefined;
|
|
140
|
+
} | {
|
|
141
|
+
ok: boolean;
|
|
142
|
+
mode: string;
|
|
143
|
+
error?: undefined;
|
|
144
|
+
status?: undefined;
|
|
145
|
+
missingKeys?: undefined;
|
|
146
|
+
};
|
|
67
147
|
triggerError: (params: unknown) => void;
|
|
68
148
|
restoreError: (params: unknown) => Promise<void>;
|
|
69
149
|
triggerLoading: (params: unknown) => void;
|
|
70
150
|
restoreLoading: (params: unknown) => void;
|
|
71
|
-
clearQueryCache: () =>
|
|
151
|
+
clearQueryCache: () => {
|
|
152
|
+
note?: string | undefined;
|
|
153
|
+
ok: true;
|
|
154
|
+
cleared: true;
|
|
155
|
+
};
|
|
72
156
|
clearMutationCache: () => void;
|
|
73
157
|
setOnline: (params: unknown) => void;
|
|
74
158
|
};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"reactQuerySyncAdapter.d.ts","sourceRoot":"","sources":["../../../../src/react-query/sync/reactQuerySyncAdapter.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"reactQuerySyncAdapter.d.ts","sourceRoot":"","sources":["../../../../src/react-query/sync/reactQuerySyncAdapter.ts"],"names":[],"mappings":"AAcA,OAAO,EAAiB,KAAK,WAAW,EAAiB,MAAM,uBAAuB,CAAC;AAsKvF,wBAAgB,2BAA2B,CAAC,WAAW,EAAE,WAAW;;;;;;0BA8B1C,MAAM,IAAI;;QAa9B;;;;WAIG;+BACoB,OAAO;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;QAkD9B;;;;;;WAMG;+BACoB,OAAO;;;;;;;;;;;;;;;;;;;;0BAiDZ,OAAO;6BAKJ,OAAO;wBAEZ,OAAO;QACvB;;;;;;;;;;;;;;WAcG;yBACc,OAAO;;;;+BAgBD,OAAO;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;+BAsKP,OAAO;+BAaP,OAAO;iCAEL,OAAO;iCAwBP,OAAO;;;;;;;4BA0CZ,OAAO;;EAMhC"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@buoy-gg/react-query",
|
|
3
|
-
"version": "7.0.
|
|
3
|
+
"version": "7.0.38",
|
|
4
4
|
"description": "TanStack Query devtools inside your React Native app — cache inspection, refetch & invalidate, offline toggle. Part of Buoy devtools.",
|
|
5
5
|
"main": "lib/commonjs/index.js",
|
|
6
6
|
"module": "lib/module/index.js",
|
|
@@ -29,8 +29,8 @@
|
|
|
29
29
|
"**/snapshotProvider.ts"
|
|
30
30
|
],
|
|
31
31
|
"dependencies": {
|
|
32
|
-
"@buoy-gg/
|
|
33
|
-
"@buoy-gg/
|
|
32
|
+
"@buoy-gg/floating-tools-core": "^7.0.38",
|
|
33
|
+
"@buoy-gg/shared-ui": "^7.0.38"
|
|
34
34
|
},
|
|
35
35
|
"peerDependencies": {
|
|
36
36
|
"@tanstack/react-query": ">=5.0.0",
|