@buoy-gg/react-query 7.0.16 → 7.0.18
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/lib/commonjs/react-query/sync/dehydrate.js +154 -9
- package/lib/commonjs/react-query/sync/reactQuerySyncAdapter.js +28 -1
- package/lib/commonjs/react-query/sync/snapshotProvider.js +246 -0
- package/lib/commonjs/react-query/sync/useReactQuerySyncAdapter.js +8 -0
- package/lib/module/react-query/sync/dehydrate.js +146 -9
- package/lib/module/react-query/sync/reactQuerySyncAdapter.js +29 -2
- package/lib/module/react-query/sync/snapshotProvider.js +241 -0
- package/lib/module/react-query/sync/useReactQuerySyncAdapter.js +9 -1
- package/lib/typescript/react-query/sync/dehydrate.d.ts +34 -0
- package/lib/typescript/react-query/sync/dehydrate.d.ts.map +1 -1
- package/lib/typescript/react-query/sync/reactQuerySyncAdapter.d.ts +19 -0
- package/lib/typescript/react-query/sync/reactQuerySyncAdapter.d.ts.map +1 -1
- package/lib/typescript/react-query/sync/snapshotProvider.d.ts +22 -0
- package/lib/typescript/react-query/sync/snapshotProvider.d.ts.map +1 -0
- package/lib/typescript/react-query/sync/useReactQuerySyncAdapter.d.ts.map +1 -1
- package/package.json +7 -4
|
@@ -3,13 +3,131 @@
|
|
|
3
3
|
Object.defineProperty(exports, "__esModule", {
|
|
4
4
|
value: true
|
|
5
5
|
});
|
|
6
|
+
exports.UNSERIALIZABLE_DATA = exports.SNAPSHOT_DATA_INLINE_LIMIT = exports.MAX_QUERY_DETAIL_BYTES = exports.DATA_ON_DEVICE = void 0;
|
|
7
|
+
exports.capQueryDetail = capQueryDetail;
|
|
6
8
|
exports.dehydrateQueryClient = dehydrateQueryClient;
|
|
9
|
+
exports.isDataOnDeviceMarker = isDataOnDeviceMarker;
|
|
10
|
+
exports.isUnserializableMarker = isUnserializableMarker;
|
|
11
|
+
exports.isWireDataMarker = isWireDataMarker;
|
|
12
|
+
exports.jsonLongerThan = jsonLongerThan;
|
|
13
|
+
exports.toWireData = toWireData;
|
|
14
|
+
exports.toWireError = toWireError;
|
|
7
15
|
/**
|
|
8
16
|
* Dehydration for external sync — ported from buoy-desktop's
|
|
9
17
|
* react-query-external-sync/hydration.ts so the desktop dashboard can
|
|
10
18
|
* hydrate an identical mirror of the device's QueryClient.
|
|
11
19
|
*/
|
|
12
20
|
|
|
21
|
+
/**
|
|
22
|
+
* Sentinel used in WIRE-FORM snapshots in place of oversized query/mutation
|
|
23
|
+
* data. Same hazard as redux/zustand: a single cached payload can exceed the
|
|
24
|
+
* 2MB emit budget (dashboard goes stale) or freeze JSON.stringify. Dashboards
|
|
25
|
+
* fetch the real value via `getQueryData`.
|
|
26
|
+
*/
|
|
27
|
+
const DATA_ON_DEVICE = exports.DATA_ON_DEVICE = Object.freeze({
|
|
28
|
+
__buoyDataOnDevice: true,
|
|
29
|
+
note: "Query data stays on the device — fetched on demand via getQueryData."
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
/** Sentinel when JSON.stringify throws (circular refs, bigint, …). */
|
|
33
|
+
const UNSERIALIZABLE_DATA = exports.UNSERIALIZABLE_DATA = Object.freeze({
|
|
34
|
+
__buoyUnserializable: true,
|
|
35
|
+
note: "Query data is not JSON-serializable (circular or bigint) and stayed on the device."
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
/** Keep small payloads inline; strip anything over this (same as network bodies). */
|
|
39
|
+
const SNAPSHOT_DATA_INLINE_LIMIT = exports.SNAPSHOT_DATA_INLINE_LIMIT = 16 * 1024;
|
|
40
|
+
const MAX_QUERY_DETAIL_BYTES = exports.MAX_QUERY_DETAIL_BYTES = 8 * 1024 * 1024;
|
|
41
|
+
function jsonLongerThan(value, limit) {
|
|
42
|
+
let bytes = 0;
|
|
43
|
+
const stack = [value];
|
|
44
|
+
const seen = new Set();
|
|
45
|
+
while (stack.length > 0) {
|
|
46
|
+
if (bytes > limit) return true;
|
|
47
|
+
const v = stack.pop();
|
|
48
|
+
if (v === null || v === undefined) bytes += 4;else if (typeof v === "string") bytes += v.length + 2;else if (typeof v === "number") bytes += 8;else if (typeof v === "boolean") bytes += 5;else if (typeof v === "object") {
|
|
49
|
+
if (seen.has(v)) continue;
|
|
50
|
+
seen.add(v);
|
|
51
|
+
if (Array.isArray(v)) {
|
|
52
|
+
bytes += 2 + v.length;
|
|
53
|
+
for (let i = 0; i < v.length; i++) stack.push(v[i]);
|
|
54
|
+
} else {
|
|
55
|
+
bytes += 2;
|
|
56
|
+
for (const k in v) {
|
|
57
|
+
bytes += k.length + 4;
|
|
58
|
+
stack.push(v[k]);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
} else bytes += 4;
|
|
62
|
+
}
|
|
63
|
+
return bytes > limit;
|
|
64
|
+
}
|
|
65
|
+
function isDataOnDeviceMarker(value) {
|
|
66
|
+
return !!value && typeof value === "object" && value.__buoyDataOnDevice === true;
|
|
67
|
+
}
|
|
68
|
+
function isUnserializableMarker(value) {
|
|
69
|
+
return !!value && typeof value === "object" && value.__buoyUnserializable === true;
|
|
70
|
+
}
|
|
71
|
+
function isWireDataMarker(value) {
|
|
72
|
+
return isDataOnDeviceMarker(value) || isUnserializableMarker(value);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** Replace unserializable / oversized values with a wire marker. */
|
|
76
|
+
function toWireData(value) {
|
|
77
|
+
if (value === undefined) return undefined;
|
|
78
|
+
try {
|
|
79
|
+
JSON.stringify(value);
|
|
80
|
+
} catch {
|
|
81
|
+
return UNSERIALIZABLE_DATA;
|
|
82
|
+
}
|
|
83
|
+
if (jsonLongerThan(value, SNAPSHOT_DATA_INLINE_LIMIT)) {
|
|
84
|
+
return DATA_ON_DEVICE;
|
|
85
|
+
}
|
|
86
|
+
return value;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Query/mutation `error` (and `fetchFailureReason` / `failureReason`) often
|
|
91
|
+
* hold the live Axios/fetch error: `request` points at XHR (cyclic),
|
|
92
|
+
* `response.data` is the same fat body we already stripped from `state.data`.
|
|
93
|
+
* Spreading `query.state` left that copy on the wire after capping data.
|
|
94
|
+
* The dashboard only reads `error.message`.
|
|
95
|
+
*/
|
|
96
|
+
function toWireError(value) {
|
|
97
|
+
if (value == null) return value;
|
|
98
|
+
if (typeof value === "object") {
|
|
99
|
+
const err = value;
|
|
100
|
+
const name = typeof err.name === "string" ? err.name : undefined;
|
|
101
|
+
const message = typeof err.message === "string" ? err.message : undefined;
|
|
102
|
+
if (name !== undefined || message !== undefined) {
|
|
103
|
+
return toWireData({
|
|
104
|
+
...(name !== undefined ? {
|
|
105
|
+
name
|
|
106
|
+
} : {}),
|
|
107
|
+
...(message !== undefined ? {
|
|
108
|
+
message
|
|
109
|
+
} : {})
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
return toWireData(value);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/** Cap an on-demand detail payload (action result, not the per-tick snapshot). */
|
|
117
|
+
function capQueryDetail(value) {
|
|
118
|
+
try {
|
|
119
|
+
JSON.stringify(value);
|
|
120
|
+
} catch {
|
|
121
|
+
return UNSERIALIZABLE_DATA;
|
|
122
|
+
}
|
|
123
|
+
if (jsonLongerThan(value, MAX_QUERY_DETAIL_BYTES)) {
|
|
124
|
+
return {
|
|
125
|
+
__buoyTruncated: true,
|
|
126
|
+
note: `Exceeds the ${MAX_QUERY_DETAIL_BYTES / (1024 * 1024)}MB detail limit — inspect it on-device.`
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
return value;
|
|
130
|
+
}
|
|
13
131
|
function dehydrateQueryClient(client) {
|
|
14
132
|
const mutations = client.getMutationCache().getAll().map(mutation => dehydrateMutation(mutation));
|
|
15
133
|
const queries = client.getQueryCache().getAll().map(query => dehydrateQuery(query));
|
|
@@ -19,37 +137,64 @@ function dehydrateQueryClient(client) {
|
|
|
19
137
|
};
|
|
20
138
|
}
|
|
21
139
|
function dehydrateMutation(mutation) {
|
|
140
|
+
const state = mutation.state;
|
|
22
141
|
return {
|
|
23
142
|
mutationId: mutation.mutationId,
|
|
24
143
|
mutationKey: mutation.options.mutationKey,
|
|
25
|
-
state:
|
|
144
|
+
state: {
|
|
145
|
+
...state,
|
|
146
|
+
data: toWireData(state.data),
|
|
147
|
+
variables: toWireData(state.variables),
|
|
148
|
+
error: toWireError(state.error),
|
|
149
|
+
failureReason: toWireError(state.failureReason)
|
|
150
|
+
},
|
|
26
151
|
...(mutation.options.scope && {
|
|
27
152
|
scope: mutation.options.scope
|
|
28
153
|
}),
|
|
29
154
|
...(mutation.meta && {
|
|
30
|
-
meta: mutation.meta
|
|
155
|
+
meta: toWireData(mutation.meta)
|
|
31
156
|
})
|
|
32
157
|
};
|
|
33
158
|
}
|
|
159
|
+
function toWireObserverOptions(options) {
|
|
160
|
+
// queryFn / select / persister are functions. initialData and
|
|
161
|
+
// placeholderData are often the SAME fat tree as state.data (apps seed
|
|
162
|
+
// the cache with it). Capping state.data still left those copies on the
|
|
163
|
+
// wire and dropped the Query panel.
|
|
164
|
+
const {
|
|
165
|
+
queryFn: _queryFn,
|
|
166
|
+
select: _select,
|
|
167
|
+
persister: _persister,
|
|
168
|
+
initialData: _initialData,
|
|
169
|
+
placeholderData: _placeholderData,
|
|
170
|
+
...light
|
|
171
|
+
} = options;
|
|
172
|
+
return {
|
|
173
|
+
...light,
|
|
174
|
+
queryFn: undefined,
|
|
175
|
+
...(options.meta ? {
|
|
176
|
+
meta: toWireData(options.meta)
|
|
177
|
+
} : {})
|
|
178
|
+
};
|
|
179
|
+
}
|
|
34
180
|
function dehydrateQuery(query) {
|
|
35
181
|
const observerStates = query.observers.map(observer => ({
|
|
36
182
|
queryHash: query.queryHash,
|
|
37
|
-
options: observer.options
|
|
38
|
-
// Remove queryFn from observer options so the dashboard can't accidentally
|
|
39
|
-
// run device fetch functions (they aren't serializable anyway)
|
|
40
|
-
queryFn: undefined
|
|
183
|
+
options: toWireObserverOptions(observer.options)
|
|
41
184
|
}));
|
|
42
185
|
return {
|
|
43
186
|
state: {
|
|
44
187
|
...query.state,
|
|
45
188
|
...(query.state.data !== undefined && {
|
|
46
|
-
data: query.state.data
|
|
47
|
-
})
|
|
189
|
+
data: toWireData(query.state.data)
|
|
190
|
+
}),
|
|
191
|
+
error: toWireError(query.state.error),
|
|
192
|
+
fetchFailureReason: toWireError(query.state.fetchFailureReason)
|
|
48
193
|
},
|
|
49
194
|
queryKey: query.queryKey,
|
|
50
195
|
queryHash: query.queryHash,
|
|
51
196
|
...(query.meta && {
|
|
52
|
-
meta: query.meta
|
|
197
|
+
meta: toWireData(query.meta)
|
|
53
198
|
}),
|
|
54
199
|
observers: observerStates
|
|
55
200
|
};
|
|
@@ -16,6 +16,9 @@ var _getQueryStatusLabel = require("../utils/getQueryStatusLabel");
|
|
|
16
16
|
* (useSyncQueries.ts) so the dashboard keeps identical behavior, including
|
|
17
17
|
* the trigger/restore loading and error simulations used by the TanStack
|
|
18
18
|
* devtools UI.
|
|
19
|
+
*
|
|
20
|
+
* v2: oversized / unserializable query (and mutation) data is replaced with
|
|
21
|
+
* on-device markers. Dashboards fetch real data via `getQueryData`.
|
|
19
22
|
*/
|
|
20
23
|
|
|
21
24
|
function createReactQuerySyncAdapter(queryClient) {
|
|
@@ -30,7 +33,7 @@ function createReactQuerySyncAdapter(queryClient) {
|
|
|
30
33
|
return query;
|
|
31
34
|
};
|
|
32
35
|
return {
|
|
33
|
-
version:
|
|
36
|
+
version: 2,
|
|
34
37
|
getSnapshot: () => ({
|
|
35
38
|
dehydratedState: (0, _dehydrate.dehydrateQueryClient)(queryClient),
|
|
36
39
|
isOnlineManagerOnline: _reactQuery.onlineManager.isOnline()
|
|
@@ -46,6 +49,30 @@ function createReactQuerySyncAdapter(queryClient) {
|
|
|
46
49
|
};
|
|
47
50
|
},
|
|
48
51
|
actions: {
|
|
52
|
+
/**
|
|
53
|
+
* On-demand detail: one query's real cached `data`. Wire-form snapshots
|
|
54
|
+
* replace oversized / unserializable data with markers; this is the
|
|
55
|
+
* explicit, size-guarded channel for the detail pane.
|
|
56
|
+
*/
|
|
57
|
+
getQueryData: params => {
|
|
58
|
+
const {
|
|
59
|
+
queryHash
|
|
60
|
+
} = params ?? {};
|
|
61
|
+
if (!queryHash) return {
|
|
62
|
+
found: false,
|
|
63
|
+
reason: "missing queryHash"
|
|
64
|
+
};
|
|
65
|
+
const query = queryClient.getQueryCache().get(queryHash);
|
|
66
|
+
if (!query) return {
|
|
67
|
+
found: false,
|
|
68
|
+
reason: "unknown queryHash"
|
|
69
|
+
};
|
|
70
|
+
return {
|
|
71
|
+
found: true,
|
|
72
|
+
queryHash,
|
|
73
|
+
data: (0, _dehydrate.capQueryDetail)(query.state.data)
|
|
74
|
+
};
|
|
75
|
+
},
|
|
49
76
|
/**
|
|
50
77
|
* Compact, token-bounded cache reader for a remote driver (MCP/LLM):
|
|
51
78
|
* projects each live query to light fields (hash, key, status label,
|
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
Object.defineProperty(exports, "__esModule", {
|
|
4
|
+
value: true
|
|
5
|
+
});
|
|
6
|
+
exports.createQuerySnapshotProvider = createQuerySnapshotProvider;
|
|
7
|
+
exports.registerQuerySnapshotProvider = registerQuerySnapshotProvider;
|
|
8
|
+
var _sharedUi = require("@buoy-gg/shared-ui");
|
|
9
|
+
var _dataViewer = require("@buoy-gg/shared-ui/dataViewer");
|
|
10
|
+
var _dehydrate = require("./dehydrate");
|
|
11
|
+
/**
|
|
12
|
+
* Time Machine snapshot provider for React Query.
|
|
13
|
+
*
|
|
14
|
+
* Unlike the other providers this one can't register at module init — the
|
|
15
|
+
* QueryClient only exists in React context — so useReactQuerySyncAdapter
|
|
16
|
+
* (mounted app-lifetime by FloatingDevTools' auto external sync) registers
|
|
17
|
+
* it per-client via an effect.
|
|
18
|
+
*
|
|
19
|
+
* Restore is a device-safe DIFF against the live cache, NOT the desktop
|
|
20
|
+
* Hydrate: that one detaches live observers and mocks queryFns, which is
|
|
21
|
+
* fine on a mirror client and destructive in a running app. Here existing
|
|
22
|
+
* queries get `setState` (observers stay attached and are notified, keeping
|
|
23
|
+
* their real queryFns), missing ones are built into the cache, and extras
|
|
24
|
+
* are removed. Mutations aren't restorable (mutationFns can't be captured);
|
|
25
|
+
* the mutation cache is cleared instead.
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
/** A restored query must never claim an in-flight fetch it doesn't have. */
|
|
29
|
+
function normalizeState(state) {
|
|
30
|
+
return {
|
|
31
|
+
...state,
|
|
32
|
+
fetchStatus: "idle",
|
|
33
|
+
fetchMeta: null
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* The comparable projection of a query for previews: status/error/data ONLY.
|
|
39
|
+
* Timestamps (dataUpdatedAt etc.) are excluded — including them makes every
|
|
40
|
+
* query read as "changed" the moment anything refetches.
|
|
41
|
+
*/
|
|
42
|
+
function comparableQuery(q) {
|
|
43
|
+
return {
|
|
44
|
+
status: q.state.status,
|
|
45
|
+
error: q.state.error ?? null,
|
|
46
|
+
data: q.state.data
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
function queryLabel(queryKey, queryHash) {
|
|
50
|
+
try {
|
|
51
|
+
const s = JSON.stringify(queryKey);
|
|
52
|
+
return s.length > 60 ? `${s.slice(0, 57)}…` : s;
|
|
53
|
+
} catch {
|
|
54
|
+
return queryHash;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
const ITEM_RENDER_BYTE_LIMIT = 256 * 1024;
|
|
58
|
+
function createQuerySnapshotProvider(queryClient) {
|
|
59
|
+
return {
|
|
60
|
+
id: "query",
|
|
61
|
+
label: "React Query",
|
|
62
|
+
status: () => ({
|
|
63
|
+
canCapture: true,
|
|
64
|
+
canRestore: true
|
|
65
|
+
}),
|
|
66
|
+
async capture() {
|
|
67
|
+
const {
|
|
68
|
+
queries,
|
|
69
|
+
mutations
|
|
70
|
+
} = (0, _dehydrate.dehydrateQueryClient)(queryClient);
|
|
71
|
+
const warnings = [];
|
|
72
|
+
if (mutations.length > 0) {
|
|
73
|
+
warnings.push(`${mutations.length} mutation(s) not captured (mutation functions cannot be restored)`);
|
|
74
|
+
}
|
|
75
|
+
return {
|
|
76
|
+
data: {
|
|
77
|
+
queries: queries.map(({
|
|
78
|
+
observers: _observers,
|
|
79
|
+
...query
|
|
80
|
+
}) => ({
|
|
81
|
+
...query,
|
|
82
|
+
state: normalizeState(query.state)
|
|
83
|
+
}))
|
|
84
|
+
},
|
|
85
|
+
warnings
|
|
86
|
+
};
|
|
87
|
+
},
|
|
88
|
+
async restore(data, options) {
|
|
89
|
+
const snapshot = data;
|
|
90
|
+
const excludedSet = new Set(options?.excludeKeys ?? []);
|
|
91
|
+
const includedSet = options?.includeKeys ? new Set(options.includeKeys) : null;
|
|
92
|
+
const touch = key => !excludedSet.has(key) && (!includedSet || includedSet.has(key));
|
|
93
|
+
let excludedHits = 0;
|
|
94
|
+
const result = {
|
|
95
|
+
ok: true,
|
|
96
|
+
applied: 0,
|
|
97
|
+
skipped: [],
|
|
98
|
+
warnings: []
|
|
99
|
+
};
|
|
100
|
+
if (!snapshot?.queries) {
|
|
101
|
+
return {
|
|
102
|
+
ok: false,
|
|
103
|
+
applied: 0,
|
|
104
|
+
skipped: [],
|
|
105
|
+
warnings: ["empty query snapshot"]
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
const cache = queryClient.getQueryCache();
|
|
109
|
+
const snapHashes = new Set(snapshot.queries.map(q => q.queryHash));
|
|
110
|
+
for (const snap of snapshot.queries) {
|
|
111
|
+
if (!touch(snap.queryHash)) {
|
|
112
|
+
excludedHits += 1;
|
|
113
|
+
continue;
|
|
114
|
+
}
|
|
115
|
+
try {
|
|
116
|
+
const state = normalizeState(snap.state);
|
|
117
|
+
const existing = cache.get(snap.queryHash);
|
|
118
|
+
if (existing) {
|
|
119
|
+
// Observers stay attached (with their real queryFns) and get
|
|
120
|
+
// notified — the safe on-device path, same as triggerError's.
|
|
121
|
+
existing.setState(state);
|
|
122
|
+
} else {
|
|
123
|
+
cache.build(queryClient, {
|
|
124
|
+
queryKey: snap.queryKey,
|
|
125
|
+
queryHash: snap.queryHash,
|
|
126
|
+
...(snap.meta && {
|
|
127
|
+
meta: snap.meta
|
|
128
|
+
})
|
|
129
|
+
}, state);
|
|
130
|
+
}
|
|
131
|
+
result.applied += 1;
|
|
132
|
+
} catch (e) {
|
|
133
|
+
result.skipped.push({
|
|
134
|
+
id: snap.queryHash,
|
|
135
|
+
reason: String(e)
|
|
136
|
+
});
|
|
137
|
+
result.ok = false;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// Cache entries the snapshot doesn't know about get removed; a mounted
|
|
142
|
+
// observer will rebuild + refetch its query on next interaction.
|
|
143
|
+
for (const query of cache.getAll().slice()) {
|
|
144
|
+
if (!touch(query.queryHash)) {
|
|
145
|
+
excludedHits += 1;
|
|
146
|
+
continue;
|
|
147
|
+
}
|
|
148
|
+
if (!snapHashes.has(query.queryHash)) {
|
|
149
|
+
try {
|
|
150
|
+
cache.remove(query);
|
|
151
|
+
result.applied += 1;
|
|
152
|
+
} catch (e) {
|
|
153
|
+
result.skipped.push({
|
|
154
|
+
id: query.queryHash,
|
|
155
|
+
reason: String(e)
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
const mutationCount = queryClient.getMutationCache().getAll().length;
|
|
161
|
+
if (mutationCount > 0) {
|
|
162
|
+
queryClient.getMutationCache().clear();
|
|
163
|
+
result.warnings.push(`mutation cache cleared (${mutationCount} mutation(s))`);
|
|
164
|
+
}
|
|
165
|
+
if (excludedHits > 0) {
|
|
166
|
+
result.warnings.push(`${excludedHits} quer(y/ies) excluded by you`);
|
|
167
|
+
}
|
|
168
|
+
return result;
|
|
169
|
+
},
|
|
170
|
+
/** Per-query restore preview. `base` omitted = compare against live cache. */
|
|
171
|
+
async previewItems(target, base) {
|
|
172
|
+
const targetQueries = target?.queries ?? [];
|
|
173
|
+
const liveMode = base === undefined;
|
|
174
|
+
const baseQueries = liveMode ? (0, _dehydrate.dehydrateQueryClient)(queryClient).queries.map(({
|
|
175
|
+
observers: _o,
|
|
176
|
+
...q
|
|
177
|
+
}) => q) : base?.queries ?? [];
|
|
178
|
+
const baseByHash = new Map(baseQueries.map(q => [q.queryHash, q]));
|
|
179
|
+
const targetByHash = new Map(targetQueries.map(q => [q.queryHash, q]));
|
|
180
|
+
const items = [];
|
|
181
|
+
const notes = [];
|
|
182
|
+
for (const q of targetQueries) {
|
|
183
|
+
const label = queryLabel(q.queryKey, q.queryHash);
|
|
184
|
+
const baseQ = baseByHash.get(q.queryHash);
|
|
185
|
+
if (!baseQ) {
|
|
186
|
+
items.push({
|
|
187
|
+
key: q.queryHash,
|
|
188
|
+
label,
|
|
189
|
+
verdict: "added",
|
|
190
|
+
newValue: comparableQuery(q)
|
|
191
|
+
});
|
|
192
|
+
continue;
|
|
193
|
+
}
|
|
194
|
+
const oldVal = comparableQuery(baseQ);
|
|
195
|
+
const newVal = comparableQuery(q);
|
|
196
|
+
if ((0, _dataViewer.boundedDeepEqual)(oldVal, newVal)) {
|
|
197
|
+
items.push({
|
|
198
|
+
key: q.queryHash,
|
|
199
|
+
label,
|
|
200
|
+
verdict: "unchanged"
|
|
201
|
+
});
|
|
202
|
+
} else if ((0, _dataViewer.jsonLikelyLargerThan)(oldVal, ITEM_RENDER_BYTE_LIMIT) || (0, _dataViewer.jsonLikelyLargerThan)(newVal, ITEM_RENDER_BYTE_LIMIT)) {
|
|
203
|
+
items.push({
|
|
204
|
+
key: q.queryHash,
|
|
205
|
+
label,
|
|
206
|
+
verdict: "too-large",
|
|
207
|
+
reason: "query data too large to render a diff — restore still applies it"
|
|
208
|
+
});
|
|
209
|
+
} else {
|
|
210
|
+
items.push({
|
|
211
|
+
key: q.queryHash,
|
|
212
|
+
label,
|
|
213
|
+
verdict: "changed",
|
|
214
|
+
oldValue: oldVal,
|
|
215
|
+
newValue: newVal
|
|
216
|
+
});
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
for (const q of baseQueries) {
|
|
220
|
+
if (!targetByHash.has(q.queryHash)) {
|
|
221
|
+
items.push({
|
|
222
|
+
key: q.queryHash,
|
|
223
|
+
label: queryLabel(q.queryKey, q.queryHash),
|
|
224
|
+
verdict: "removed",
|
|
225
|
+
oldValue: comparableQuery(q)
|
|
226
|
+
});
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
if (liveMode) {
|
|
230
|
+
const mutationCount = queryClient.getMutationCache().getAll().length;
|
|
231
|
+
if (mutationCount > 0) {
|
|
232
|
+
notes.push(`${mutationCount} in-memory mutation record(s) will be cleared`);
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
return {
|
|
236
|
+
items,
|
|
237
|
+
notes
|
|
238
|
+
};
|
|
239
|
+
}
|
|
240
|
+
};
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
/** Register for one client; returns the unregister fn. Last client wins. */
|
|
244
|
+
function registerQuerySnapshotProvider(queryClient) {
|
|
245
|
+
return (0, _sharedUi.registerStateSnapshotProvider)(createQuerySnapshotProvider(queryClient));
|
|
246
|
+
}
|
|
@@ -7,6 +7,7 @@ exports.useReactQuerySyncAdapter = useReactQuerySyncAdapter;
|
|
|
7
7
|
var _react = require("react");
|
|
8
8
|
var _reactQuery = require("@tanstack/react-query");
|
|
9
9
|
var _reactQuerySyncAdapter = require("./reactQuerySyncAdapter");
|
|
10
|
+
var _snapshotProvider = require("./snapshotProvider");
|
|
10
11
|
/**
|
|
11
12
|
* Context-based variant of createReactQuerySyncAdapter for zero-config
|
|
12
13
|
* wiring (FloatingDevTools' auto external sync): reads the QueryClient from
|
|
@@ -22,5 +23,12 @@ function useReactQuerySyncAdapter() {
|
|
|
22
23
|
} catch {
|
|
23
24
|
queryClient = null;
|
|
24
25
|
}
|
|
26
|
+
|
|
27
|
+
// Register the Time Machine snapshot provider for this client — this hook
|
|
28
|
+
// is the one app-lifetime spot that actually holds the QueryClient.
|
|
29
|
+
(0, _react.useEffect)(() => {
|
|
30
|
+
if (!queryClient) return;
|
|
31
|
+
return (0, _snapshotProvider.registerQuerySnapshotProvider)(queryClient);
|
|
32
|
+
}, [queryClient]);
|
|
25
33
|
return (0, _react.useMemo)(() => queryClient ? (0, _reactQuerySyncAdapter.createReactQuerySyncAdapter)(queryClient) : null, [queryClient]);
|
|
26
34
|
}
|
|
@@ -6,6 +6,116 @@
|
|
|
6
6
|
* hydrate an identical mirror of the device's QueryClient.
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
|
+
/**
|
|
10
|
+
* Sentinel used in WIRE-FORM snapshots in place of oversized query/mutation
|
|
11
|
+
* data. Same hazard as redux/zustand: a single cached payload can exceed the
|
|
12
|
+
* 2MB emit budget (dashboard goes stale) or freeze JSON.stringify. Dashboards
|
|
13
|
+
* fetch the real value via `getQueryData`.
|
|
14
|
+
*/
|
|
15
|
+
export const DATA_ON_DEVICE = Object.freeze({
|
|
16
|
+
__buoyDataOnDevice: true,
|
|
17
|
+
note: "Query data stays on the device — fetched on demand via getQueryData."
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
/** Sentinel when JSON.stringify throws (circular refs, bigint, …). */
|
|
21
|
+
export const UNSERIALIZABLE_DATA = Object.freeze({
|
|
22
|
+
__buoyUnserializable: true,
|
|
23
|
+
note: "Query data is not JSON-serializable (circular or bigint) and stayed on the device."
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
/** Keep small payloads inline; strip anything over this (same as network bodies). */
|
|
27
|
+
export const SNAPSHOT_DATA_INLINE_LIMIT = 16 * 1024;
|
|
28
|
+
export const MAX_QUERY_DETAIL_BYTES = 8 * 1024 * 1024;
|
|
29
|
+
export function jsonLongerThan(value, limit) {
|
|
30
|
+
let bytes = 0;
|
|
31
|
+
const stack = [value];
|
|
32
|
+
const seen = new Set();
|
|
33
|
+
while (stack.length > 0) {
|
|
34
|
+
if (bytes > limit) return true;
|
|
35
|
+
const v = stack.pop();
|
|
36
|
+
if (v === null || v === undefined) bytes += 4;else if (typeof v === "string") bytes += v.length + 2;else if (typeof v === "number") bytes += 8;else if (typeof v === "boolean") bytes += 5;else if (typeof v === "object") {
|
|
37
|
+
if (seen.has(v)) continue;
|
|
38
|
+
seen.add(v);
|
|
39
|
+
if (Array.isArray(v)) {
|
|
40
|
+
bytes += 2 + v.length;
|
|
41
|
+
for (let i = 0; i < v.length; i++) stack.push(v[i]);
|
|
42
|
+
} else {
|
|
43
|
+
bytes += 2;
|
|
44
|
+
for (const k in v) {
|
|
45
|
+
bytes += k.length + 4;
|
|
46
|
+
stack.push(v[k]);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
} else bytes += 4;
|
|
50
|
+
}
|
|
51
|
+
return bytes > limit;
|
|
52
|
+
}
|
|
53
|
+
export function isDataOnDeviceMarker(value) {
|
|
54
|
+
return !!value && typeof value === "object" && value.__buoyDataOnDevice === true;
|
|
55
|
+
}
|
|
56
|
+
export function isUnserializableMarker(value) {
|
|
57
|
+
return !!value && typeof value === "object" && value.__buoyUnserializable === true;
|
|
58
|
+
}
|
|
59
|
+
export function isWireDataMarker(value) {
|
|
60
|
+
return isDataOnDeviceMarker(value) || isUnserializableMarker(value);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** Replace unserializable / oversized values with a wire marker. */
|
|
64
|
+
export function toWireData(value) {
|
|
65
|
+
if (value === undefined) return undefined;
|
|
66
|
+
try {
|
|
67
|
+
JSON.stringify(value);
|
|
68
|
+
} catch {
|
|
69
|
+
return UNSERIALIZABLE_DATA;
|
|
70
|
+
}
|
|
71
|
+
if (jsonLongerThan(value, SNAPSHOT_DATA_INLINE_LIMIT)) {
|
|
72
|
+
return DATA_ON_DEVICE;
|
|
73
|
+
}
|
|
74
|
+
return value;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Query/mutation `error` (and `fetchFailureReason` / `failureReason`) often
|
|
79
|
+
* hold the live Axios/fetch error: `request` points at XHR (cyclic),
|
|
80
|
+
* `response.data` is the same fat body we already stripped from `state.data`.
|
|
81
|
+
* Spreading `query.state` left that copy on the wire after capping data.
|
|
82
|
+
* The dashboard only reads `error.message`.
|
|
83
|
+
*/
|
|
84
|
+
export function toWireError(value) {
|
|
85
|
+
if (value == null) return value;
|
|
86
|
+
if (typeof value === "object") {
|
|
87
|
+
const err = value;
|
|
88
|
+
const name = typeof err.name === "string" ? err.name : undefined;
|
|
89
|
+
const message = typeof err.message === "string" ? err.message : undefined;
|
|
90
|
+
if (name !== undefined || message !== undefined) {
|
|
91
|
+
return toWireData({
|
|
92
|
+
...(name !== undefined ? {
|
|
93
|
+
name
|
|
94
|
+
} : {}),
|
|
95
|
+
...(message !== undefined ? {
|
|
96
|
+
message
|
|
97
|
+
} : {})
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
return toWireData(value);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/** Cap an on-demand detail payload (action result, not the per-tick snapshot). */
|
|
105
|
+
export function capQueryDetail(value) {
|
|
106
|
+
try {
|
|
107
|
+
JSON.stringify(value);
|
|
108
|
+
} catch {
|
|
109
|
+
return UNSERIALIZABLE_DATA;
|
|
110
|
+
}
|
|
111
|
+
if (jsonLongerThan(value, MAX_QUERY_DETAIL_BYTES)) {
|
|
112
|
+
return {
|
|
113
|
+
__buoyTruncated: true,
|
|
114
|
+
note: `Exceeds the ${MAX_QUERY_DETAIL_BYTES / (1024 * 1024)}MB detail limit — inspect it on-device.`
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
return value;
|
|
118
|
+
}
|
|
9
119
|
export function dehydrateQueryClient(client) {
|
|
10
120
|
const mutations = client.getMutationCache().getAll().map(mutation => dehydrateMutation(mutation));
|
|
11
121
|
const queries = client.getQueryCache().getAll().map(query => dehydrateQuery(query));
|
|
@@ -15,37 +125,64 @@ export function dehydrateQueryClient(client) {
|
|
|
15
125
|
};
|
|
16
126
|
}
|
|
17
127
|
function dehydrateMutation(mutation) {
|
|
128
|
+
const state = mutation.state;
|
|
18
129
|
return {
|
|
19
130
|
mutationId: mutation.mutationId,
|
|
20
131
|
mutationKey: mutation.options.mutationKey,
|
|
21
|
-
state:
|
|
132
|
+
state: {
|
|
133
|
+
...state,
|
|
134
|
+
data: toWireData(state.data),
|
|
135
|
+
variables: toWireData(state.variables),
|
|
136
|
+
error: toWireError(state.error),
|
|
137
|
+
failureReason: toWireError(state.failureReason)
|
|
138
|
+
},
|
|
22
139
|
...(mutation.options.scope && {
|
|
23
140
|
scope: mutation.options.scope
|
|
24
141
|
}),
|
|
25
142
|
...(mutation.meta && {
|
|
26
|
-
meta: mutation.meta
|
|
143
|
+
meta: toWireData(mutation.meta)
|
|
27
144
|
})
|
|
28
145
|
};
|
|
29
146
|
}
|
|
147
|
+
function toWireObserverOptions(options) {
|
|
148
|
+
// queryFn / select / persister are functions. initialData and
|
|
149
|
+
// placeholderData are often the SAME fat tree as state.data (apps seed
|
|
150
|
+
// the cache with it). Capping state.data still left those copies on the
|
|
151
|
+
// wire and dropped the Query panel.
|
|
152
|
+
const {
|
|
153
|
+
queryFn: _queryFn,
|
|
154
|
+
select: _select,
|
|
155
|
+
persister: _persister,
|
|
156
|
+
initialData: _initialData,
|
|
157
|
+
placeholderData: _placeholderData,
|
|
158
|
+
...light
|
|
159
|
+
} = options;
|
|
160
|
+
return {
|
|
161
|
+
...light,
|
|
162
|
+
queryFn: undefined,
|
|
163
|
+
...(options.meta ? {
|
|
164
|
+
meta: toWireData(options.meta)
|
|
165
|
+
} : {})
|
|
166
|
+
};
|
|
167
|
+
}
|
|
30
168
|
function dehydrateQuery(query) {
|
|
31
169
|
const observerStates = query.observers.map(observer => ({
|
|
32
170
|
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
|
|
171
|
+
options: toWireObserverOptions(observer.options)
|
|
37
172
|
}));
|
|
38
173
|
return {
|
|
39
174
|
state: {
|
|
40
175
|
...query.state,
|
|
41
176
|
...(query.state.data !== undefined && {
|
|
42
|
-
data: query.state.data
|
|
43
|
-
})
|
|
177
|
+
data: toWireData(query.state.data)
|
|
178
|
+
}),
|
|
179
|
+
error: toWireError(query.state.error),
|
|
180
|
+
fetchFailureReason: toWireError(query.state.fetchFailureReason)
|
|
44
181
|
},
|
|
45
182
|
queryKey: query.queryKey,
|
|
46
183
|
queryHash: query.queryHash,
|
|
47
184
|
...(query.meta && {
|
|
48
|
-
meta: query.meta
|
|
185
|
+
meta: toWireData(query.meta)
|
|
49
186
|
}),
|
|
50
187
|
observers: observerStates
|
|
51
188
|
};
|
|
@@ -9,9 +9,12 @@
|
|
|
9
9
|
* (useSyncQueries.ts) so the dashboard keeps identical behavior, including
|
|
10
10
|
* the trigger/restore loading and error simulations used by the TanStack
|
|
11
11
|
* devtools UI.
|
|
12
|
+
*
|
|
13
|
+
* v2: oversized / unserializable query (and mutation) data is replaced with
|
|
14
|
+
* on-device markers. Dashboards fetch real data via `getQueryData`.
|
|
12
15
|
*/
|
|
13
16
|
import { onlineManager } from "@tanstack/react-query";
|
|
14
|
-
import { dehydrateQueryClient } from "./dehydrate";
|
|
17
|
+
import { capQueryDetail, dehydrateQueryClient } from "./dehydrate";
|
|
15
18
|
import { getQueryStatusLabel } from "../utils/getQueryStatusLabel";
|
|
16
19
|
export function createReactQuerySyncAdapter(queryClient) {
|
|
17
20
|
const getQuery = params => {
|
|
@@ -25,7 +28,7 @@ export function createReactQuerySyncAdapter(queryClient) {
|
|
|
25
28
|
return query;
|
|
26
29
|
};
|
|
27
30
|
return {
|
|
28
|
-
version:
|
|
31
|
+
version: 2,
|
|
29
32
|
getSnapshot: () => ({
|
|
30
33
|
dehydratedState: dehydrateQueryClient(queryClient),
|
|
31
34
|
isOnlineManagerOnline: onlineManager.isOnline()
|
|
@@ -41,6 +44,30 @@ export function createReactQuerySyncAdapter(queryClient) {
|
|
|
41
44
|
};
|
|
42
45
|
},
|
|
43
46
|
actions: {
|
|
47
|
+
/**
|
|
48
|
+
* On-demand detail: one query's real cached `data`. Wire-form snapshots
|
|
49
|
+
* replace oversized / unserializable data with markers; this is the
|
|
50
|
+
* explicit, size-guarded channel for the detail pane.
|
|
51
|
+
*/
|
|
52
|
+
getQueryData: params => {
|
|
53
|
+
const {
|
|
54
|
+
queryHash
|
|
55
|
+
} = params ?? {};
|
|
56
|
+
if (!queryHash) return {
|
|
57
|
+
found: false,
|
|
58
|
+
reason: "missing queryHash"
|
|
59
|
+
};
|
|
60
|
+
const query = queryClient.getQueryCache().get(queryHash);
|
|
61
|
+
if (!query) return {
|
|
62
|
+
found: false,
|
|
63
|
+
reason: "unknown queryHash"
|
|
64
|
+
};
|
|
65
|
+
return {
|
|
66
|
+
found: true,
|
|
67
|
+
queryHash,
|
|
68
|
+
data: capQueryDetail(query.state.data)
|
|
69
|
+
};
|
|
70
|
+
},
|
|
44
71
|
/**
|
|
45
72
|
* Compact, token-bounded cache reader for a remote driver (MCP/LLM):
|
|
46
73
|
* projects each live query to light fields (hash, key, status label,
|
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Time Machine snapshot provider for React Query.
|
|
5
|
+
*
|
|
6
|
+
* Unlike the other providers this one can't register at module init — the
|
|
7
|
+
* QueryClient only exists in React context — so useReactQuerySyncAdapter
|
|
8
|
+
* (mounted app-lifetime by FloatingDevTools' auto external sync) registers
|
|
9
|
+
* it per-client via an effect.
|
|
10
|
+
*
|
|
11
|
+
* Restore is a device-safe DIFF against the live cache, NOT the desktop
|
|
12
|
+
* Hydrate: that one detaches live observers and mocks queryFns, which is
|
|
13
|
+
* fine on a mirror client and destructive in a running app. Here existing
|
|
14
|
+
* queries get `setState` (observers stay attached and are notified, keeping
|
|
15
|
+
* their real queryFns), missing ones are built into the cache, and extras
|
|
16
|
+
* are removed. Mutations aren't restorable (mutationFns can't be captured);
|
|
17
|
+
* the mutation cache is cleared instead.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
import { registerStateSnapshotProvider } from "@buoy-gg/shared-ui";
|
|
21
|
+
import { boundedDeepEqual, jsonLikelyLargerThan } from "@buoy-gg/shared-ui/dataViewer";
|
|
22
|
+
import { dehydrateQueryClient } from "./dehydrate";
|
|
23
|
+
/** A restored query must never claim an in-flight fetch it doesn't have. */
|
|
24
|
+
function normalizeState(state) {
|
|
25
|
+
return {
|
|
26
|
+
...state,
|
|
27
|
+
fetchStatus: "idle",
|
|
28
|
+
fetchMeta: null
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* The comparable projection of a query for previews: status/error/data ONLY.
|
|
34
|
+
* Timestamps (dataUpdatedAt etc.) are excluded — including them makes every
|
|
35
|
+
* query read as "changed" the moment anything refetches.
|
|
36
|
+
*/
|
|
37
|
+
function comparableQuery(q) {
|
|
38
|
+
return {
|
|
39
|
+
status: q.state.status,
|
|
40
|
+
error: q.state.error ?? null,
|
|
41
|
+
data: q.state.data
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
function queryLabel(queryKey, queryHash) {
|
|
45
|
+
try {
|
|
46
|
+
const s = JSON.stringify(queryKey);
|
|
47
|
+
return s.length > 60 ? `${s.slice(0, 57)}…` : s;
|
|
48
|
+
} catch {
|
|
49
|
+
return queryHash;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
const ITEM_RENDER_BYTE_LIMIT = 256 * 1024;
|
|
53
|
+
export function createQuerySnapshotProvider(queryClient) {
|
|
54
|
+
return {
|
|
55
|
+
id: "query",
|
|
56
|
+
label: "React Query",
|
|
57
|
+
status: () => ({
|
|
58
|
+
canCapture: true,
|
|
59
|
+
canRestore: true
|
|
60
|
+
}),
|
|
61
|
+
async capture() {
|
|
62
|
+
const {
|
|
63
|
+
queries,
|
|
64
|
+
mutations
|
|
65
|
+
} = dehydrateQueryClient(queryClient);
|
|
66
|
+
const warnings = [];
|
|
67
|
+
if (mutations.length > 0) {
|
|
68
|
+
warnings.push(`${mutations.length} mutation(s) not captured (mutation functions cannot be restored)`);
|
|
69
|
+
}
|
|
70
|
+
return {
|
|
71
|
+
data: {
|
|
72
|
+
queries: queries.map(({
|
|
73
|
+
observers: _observers,
|
|
74
|
+
...query
|
|
75
|
+
}) => ({
|
|
76
|
+
...query,
|
|
77
|
+
state: normalizeState(query.state)
|
|
78
|
+
}))
|
|
79
|
+
},
|
|
80
|
+
warnings
|
|
81
|
+
};
|
|
82
|
+
},
|
|
83
|
+
async restore(data, options) {
|
|
84
|
+
const snapshot = data;
|
|
85
|
+
const excludedSet = new Set(options?.excludeKeys ?? []);
|
|
86
|
+
const includedSet = options?.includeKeys ? new Set(options.includeKeys) : null;
|
|
87
|
+
const touch = key => !excludedSet.has(key) && (!includedSet || includedSet.has(key));
|
|
88
|
+
let excludedHits = 0;
|
|
89
|
+
const result = {
|
|
90
|
+
ok: true,
|
|
91
|
+
applied: 0,
|
|
92
|
+
skipped: [],
|
|
93
|
+
warnings: []
|
|
94
|
+
};
|
|
95
|
+
if (!snapshot?.queries) {
|
|
96
|
+
return {
|
|
97
|
+
ok: false,
|
|
98
|
+
applied: 0,
|
|
99
|
+
skipped: [],
|
|
100
|
+
warnings: ["empty query snapshot"]
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
const cache = queryClient.getQueryCache();
|
|
104
|
+
const snapHashes = new Set(snapshot.queries.map(q => q.queryHash));
|
|
105
|
+
for (const snap of snapshot.queries) {
|
|
106
|
+
if (!touch(snap.queryHash)) {
|
|
107
|
+
excludedHits += 1;
|
|
108
|
+
continue;
|
|
109
|
+
}
|
|
110
|
+
try {
|
|
111
|
+
const state = normalizeState(snap.state);
|
|
112
|
+
const existing = cache.get(snap.queryHash);
|
|
113
|
+
if (existing) {
|
|
114
|
+
// Observers stay attached (with their real queryFns) and get
|
|
115
|
+
// notified — the safe on-device path, same as triggerError's.
|
|
116
|
+
existing.setState(state);
|
|
117
|
+
} else {
|
|
118
|
+
cache.build(queryClient, {
|
|
119
|
+
queryKey: snap.queryKey,
|
|
120
|
+
queryHash: snap.queryHash,
|
|
121
|
+
...(snap.meta && {
|
|
122
|
+
meta: snap.meta
|
|
123
|
+
})
|
|
124
|
+
}, state);
|
|
125
|
+
}
|
|
126
|
+
result.applied += 1;
|
|
127
|
+
} catch (e) {
|
|
128
|
+
result.skipped.push({
|
|
129
|
+
id: snap.queryHash,
|
|
130
|
+
reason: String(e)
|
|
131
|
+
});
|
|
132
|
+
result.ok = false;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// Cache entries the snapshot doesn't know about get removed; a mounted
|
|
137
|
+
// observer will rebuild + refetch its query on next interaction.
|
|
138
|
+
for (const query of cache.getAll().slice()) {
|
|
139
|
+
if (!touch(query.queryHash)) {
|
|
140
|
+
excludedHits += 1;
|
|
141
|
+
continue;
|
|
142
|
+
}
|
|
143
|
+
if (!snapHashes.has(query.queryHash)) {
|
|
144
|
+
try {
|
|
145
|
+
cache.remove(query);
|
|
146
|
+
result.applied += 1;
|
|
147
|
+
} catch (e) {
|
|
148
|
+
result.skipped.push({
|
|
149
|
+
id: query.queryHash,
|
|
150
|
+
reason: String(e)
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
const mutationCount = queryClient.getMutationCache().getAll().length;
|
|
156
|
+
if (mutationCount > 0) {
|
|
157
|
+
queryClient.getMutationCache().clear();
|
|
158
|
+
result.warnings.push(`mutation cache cleared (${mutationCount} mutation(s))`);
|
|
159
|
+
}
|
|
160
|
+
if (excludedHits > 0) {
|
|
161
|
+
result.warnings.push(`${excludedHits} quer(y/ies) excluded by you`);
|
|
162
|
+
}
|
|
163
|
+
return result;
|
|
164
|
+
},
|
|
165
|
+
/** Per-query restore preview. `base` omitted = compare against live cache. */
|
|
166
|
+
async previewItems(target, base) {
|
|
167
|
+
const targetQueries = target?.queries ?? [];
|
|
168
|
+
const liveMode = base === undefined;
|
|
169
|
+
const baseQueries = liveMode ? dehydrateQueryClient(queryClient).queries.map(({
|
|
170
|
+
observers: _o,
|
|
171
|
+
...q
|
|
172
|
+
}) => q) : base?.queries ?? [];
|
|
173
|
+
const baseByHash = new Map(baseQueries.map(q => [q.queryHash, q]));
|
|
174
|
+
const targetByHash = new Map(targetQueries.map(q => [q.queryHash, q]));
|
|
175
|
+
const items = [];
|
|
176
|
+
const notes = [];
|
|
177
|
+
for (const q of targetQueries) {
|
|
178
|
+
const label = queryLabel(q.queryKey, q.queryHash);
|
|
179
|
+
const baseQ = baseByHash.get(q.queryHash);
|
|
180
|
+
if (!baseQ) {
|
|
181
|
+
items.push({
|
|
182
|
+
key: q.queryHash,
|
|
183
|
+
label,
|
|
184
|
+
verdict: "added",
|
|
185
|
+
newValue: comparableQuery(q)
|
|
186
|
+
});
|
|
187
|
+
continue;
|
|
188
|
+
}
|
|
189
|
+
const oldVal = comparableQuery(baseQ);
|
|
190
|
+
const newVal = comparableQuery(q);
|
|
191
|
+
if (boundedDeepEqual(oldVal, newVal)) {
|
|
192
|
+
items.push({
|
|
193
|
+
key: q.queryHash,
|
|
194
|
+
label,
|
|
195
|
+
verdict: "unchanged"
|
|
196
|
+
});
|
|
197
|
+
} else if (jsonLikelyLargerThan(oldVal, ITEM_RENDER_BYTE_LIMIT) || jsonLikelyLargerThan(newVal, ITEM_RENDER_BYTE_LIMIT)) {
|
|
198
|
+
items.push({
|
|
199
|
+
key: q.queryHash,
|
|
200
|
+
label,
|
|
201
|
+
verdict: "too-large",
|
|
202
|
+
reason: "query data too large to render a diff — restore still applies it"
|
|
203
|
+
});
|
|
204
|
+
} else {
|
|
205
|
+
items.push({
|
|
206
|
+
key: q.queryHash,
|
|
207
|
+
label,
|
|
208
|
+
verdict: "changed",
|
|
209
|
+
oldValue: oldVal,
|
|
210
|
+
newValue: newVal
|
|
211
|
+
});
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
for (const q of baseQueries) {
|
|
215
|
+
if (!targetByHash.has(q.queryHash)) {
|
|
216
|
+
items.push({
|
|
217
|
+
key: q.queryHash,
|
|
218
|
+
label: queryLabel(q.queryKey, q.queryHash),
|
|
219
|
+
verdict: "removed",
|
|
220
|
+
oldValue: comparableQuery(q)
|
|
221
|
+
});
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
if (liveMode) {
|
|
225
|
+
const mutationCount = queryClient.getMutationCache().getAll().length;
|
|
226
|
+
if (mutationCount > 0) {
|
|
227
|
+
notes.push(`${mutationCount} in-memory mutation record(s) will be cleared`);
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
return {
|
|
231
|
+
items,
|
|
232
|
+
notes
|
|
233
|
+
};
|
|
234
|
+
}
|
|
235
|
+
};
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/** Register for one client; returns the unregister fn. Last client wins. */
|
|
239
|
+
export function registerQuerySnapshotProvider(queryClient) {
|
|
240
|
+
return registerStateSnapshotProvider(createQuerySnapshotProvider(queryClient));
|
|
241
|
+
}
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
|
|
3
|
-
import { useMemo } from "react";
|
|
3
|
+
import { useEffect, useMemo } from "react";
|
|
4
4
|
import { useQueryClient } from "@tanstack/react-query";
|
|
5
5
|
import { createReactQuerySyncAdapter } from "./reactQuerySyncAdapter";
|
|
6
|
+
import { registerQuerySnapshotProvider } from "./snapshotProvider";
|
|
6
7
|
|
|
7
8
|
/**
|
|
8
9
|
* Context-based variant of createReactQuerySyncAdapter for zero-config
|
|
@@ -19,5 +20,12 @@ export function useReactQuerySyncAdapter() {
|
|
|
19
20
|
} catch {
|
|
20
21
|
queryClient = null;
|
|
21
22
|
}
|
|
23
|
+
|
|
24
|
+
// Register the Time Machine snapshot provider for this client — this hook
|
|
25
|
+
// is the one app-lifetime spot that actually holds the QueryClient.
|
|
26
|
+
useEffect(() => {
|
|
27
|
+
if (!queryClient) return;
|
|
28
|
+
return registerQuerySnapshotProvider(queryClient);
|
|
29
|
+
}, [queryClient]);
|
|
22
30
|
return useMemo(() => queryClient ? createReactQuerySyncAdapter(queryClient) : null, [queryClient]);
|
|
23
31
|
}
|
|
@@ -4,6 +4,40 @@
|
|
|
4
4
|
* hydrate an identical mirror of the device's QueryClient.
|
|
5
5
|
*/
|
|
6
6
|
import type { DefaultError, MutationKey, MutationMeta, MutationScope, MutationState, QueryClient, QueryKey, QueryMeta, QueryObserverOptions, QueryState } from "@tanstack/react-query";
|
|
7
|
+
/**
|
|
8
|
+
* Sentinel used in WIRE-FORM snapshots in place of oversized query/mutation
|
|
9
|
+
* data. Same hazard as redux/zustand: a single cached payload can exceed the
|
|
10
|
+
* 2MB emit budget (dashboard goes stale) or freeze JSON.stringify. Dashboards
|
|
11
|
+
* fetch the real value via `getQueryData`.
|
|
12
|
+
*/
|
|
13
|
+
export declare const DATA_ON_DEVICE: Readonly<{
|
|
14
|
+
__buoyDataOnDevice: true;
|
|
15
|
+
note: "Query data stays on the device — fetched on demand via getQueryData.";
|
|
16
|
+
}>;
|
|
17
|
+
/** Sentinel when JSON.stringify throws (circular refs, bigint, …). */
|
|
18
|
+
export declare const UNSERIALIZABLE_DATA: Readonly<{
|
|
19
|
+
__buoyUnserializable: true;
|
|
20
|
+
note: "Query data is not JSON-serializable (circular or bigint) and stayed on the device.";
|
|
21
|
+
}>;
|
|
22
|
+
/** Keep small payloads inline; strip anything over this (same as network bodies). */
|
|
23
|
+
export declare const SNAPSHOT_DATA_INLINE_LIMIT: number;
|
|
24
|
+
export declare const MAX_QUERY_DETAIL_BYTES: number;
|
|
25
|
+
export declare function jsonLongerThan(value: unknown, limit: number): boolean;
|
|
26
|
+
export declare function isDataOnDeviceMarker(value: unknown): boolean;
|
|
27
|
+
export declare function isUnserializableMarker(value: unknown): boolean;
|
|
28
|
+
export declare function isWireDataMarker(value: unknown): boolean;
|
|
29
|
+
/** Replace unserializable / oversized values with a wire marker. */
|
|
30
|
+
export declare function toWireData(value: unknown): unknown;
|
|
31
|
+
/**
|
|
32
|
+
* Query/mutation `error` (and `fetchFailureReason` / `failureReason`) often
|
|
33
|
+
* hold the live Axios/fetch error: `request` points at XHR (cyclic),
|
|
34
|
+
* `response.data` is the same fat body we already stripped from `state.data`.
|
|
35
|
+
* Spreading `query.state` left that copy on the wire after capping data.
|
|
36
|
+
* The dashboard only reads `error.message`.
|
|
37
|
+
*/
|
|
38
|
+
export declare function toWireError(value: unknown): unknown;
|
|
39
|
+
/** Cap an on-demand detail payload (action result, not the per-tick snapshot). */
|
|
40
|
+
export declare function capQueryDetail(value: unknown): unknown;
|
|
7
41
|
export interface DehydratedState {
|
|
8
42
|
mutations: DehydratedMutation[];
|
|
9
43
|
queries: DehydratedQuery[];
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"dehydrate.d.ts","sourceRoot":"","sources":["../../../../src/react-query/sync/dehydrate.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AACH,OAAO,KAAK,EACV,YAAY,EAEZ,WAAW,EACX,YAAY,EACZ,aAAa,EACb,aAAa,EAEb,WAAW,EAEX,QAAQ,EACR,SAAS,EACT,oBAAoB,EACpB,UAAU,EACX,MAAM,uBAAuB,CAAC;AAE/B,MAAM,WAAW,eAAe;IAC9B,SAAS,EAAE,kBAAkB,EAAE,CAAC;IAChC,OAAO,EAAE,eAAe,EAAE,CAAC;CAC5B;AAED,MAAM,WAAW,kBAAkB;IACjC,UAAU,EAAE,MAAM,CAAC;IACnB,WAAW,CAAC,EAAE,WAAW,CAAC;IAC1B,KAAK,EAAE,aAAa,CAAC;IACrB,IAAI,CAAC,EAAE,YAAY,CAAC;IACpB,KAAK,CAAC,EAAE,aAAa,CAAC;CACvB;AAED,MAAM,WAAW,eAAe;IAC9B,SAAS,EAAE,MAAM,CAAC;IAClB,QAAQ,EAAE,QAAQ,CAAC;IACnB,KAAK,EAAE,UAAU,CAAC;IAClB,IAAI,CAAC,EAAE,SAAS,CAAC;IACjB,SAAS,EAAE,aAAa,EAAE,CAAC;CAC5B;AAED,MAAM,WAAW,aAAa,CAC5B,YAAY,GAAG,OAAO,EACtB,MAAM,GAAG,YAAY,EACrB,KAAK,GAAG,YAAY,EACpB,UAAU,GAAG,YAAY,EACzB,SAAS,SAAS,QAAQ,GAAG,QAAQ;IAErC,SAAS,EAAE,MAAM,CAAC;IAClB,OAAO,EAAE,oBAAoB,CAC3B,YAAY,EACZ,MAAM,EACN,KAAK,EACL,UAAU,EACV,SAAS,CACV,CAAC;CACH;AAED,wBAAgB,oBAAoB,CAAC,MAAM,EAAE,WAAW,GAAG,eAAe,CAYzE"}
|
|
1
|
+
{"version":3,"file":"dehydrate.d.ts","sourceRoot":"","sources":["../../../../src/react-query/sync/dehydrate.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AACH,OAAO,KAAK,EACV,YAAY,EAEZ,WAAW,EACX,YAAY,EACZ,aAAa,EACb,aAAa,EAEb,WAAW,EAEX,QAAQ,EACR,SAAS,EACT,oBAAoB,EACpB,UAAU,EACX,MAAM,uBAAuB,CAAC;AAE/B;;;;;GAKG;AACH,eAAO,MAAM,cAAc;;;EAGzB,CAAC;AAEH,sEAAsE;AACtE,eAAO,MAAM,mBAAmB;;;EAG9B,CAAC;AAEH,qFAAqF;AACrF,eAAO,MAAM,0BAA0B,QAAY,CAAC;AAEpD,eAAO,MAAM,sBAAsB,QAAkB,CAAC;AAEtD,wBAAgB,cAAc,CAAC,KAAK,EAAE,OAAO,EAAE,KAAK,EAAE,MAAM,GAAG,OAAO,CA2BrE;AAED,wBAAgB,oBAAoB,CAAC,KAAK,EAAE,OAAO,GAAG,OAAO,CAM5D;AAED,wBAAgB,sBAAsB,CAAC,KAAK,EAAE,OAAO,GAAG,OAAO,CAM9D;AAED,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,OAAO,GAAG,OAAO,CAExD;AAED,oEAAoE;AACpE,wBAAgB,UAAU,CAAC,KAAK,EAAE,OAAO,GAAG,OAAO,CAWlD;AAED;;;;;;GAMG;AACH,wBAAgB,WAAW,CAAC,KAAK,EAAE,OAAO,GAAG,OAAO,CAcnD;AAED,kFAAkF;AAClF,wBAAgB,cAAc,CAAC,KAAK,EAAE,OAAO,GAAG,OAAO,CAatD;AAED,MAAM,WAAW,eAAe;IAC9B,SAAS,EAAE,kBAAkB,EAAE,CAAC;IAChC,OAAO,EAAE,eAAe,EAAE,CAAC;CAC5B;AAED,MAAM,WAAW,kBAAkB;IACjC,UAAU,EAAE,MAAM,CAAC;IACnB,WAAW,CAAC,EAAE,WAAW,CAAC;IAC1B,KAAK,EAAE,aAAa,CAAC;IACrB,IAAI,CAAC,EAAE,YAAY,CAAC;IACpB,KAAK,CAAC,EAAE,aAAa,CAAC;CACvB;AAED,MAAM,WAAW,eAAe;IAC9B,SAAS,EAAE,MAAM,CAAC;IAClB,QAAQ,EAAE,QAAQ,CAAC;IACnB,KAAK,EAAE,UAAU,CAAC;IAClB,IAAI,CAAC,EAAE,SAAS,CAAC;IACjB,SAAS,EAAE,aAAa,EAAE,CAAC;CAC5B;AAED,MAAM,WAAW,aAAa,CAC5B,YAAY,GAAG,OAAO,EACtB,MAAM,GAAG,YAAY,EACrB,KAAK,GAAG,YAAY,EACpB,UAAU,GAAG,YAAY,EACzB,SAAS,SAAS,QAAQ,GAAG,QAAQ;IAErC,SAAS,EAAE,MAAM,CAAC;IAClB,OAAO,EAAE,oBAAoB,CAC3B,YAAY,EACZ,MAAM,EACN,KAAK,EACL,UAAU,EACV,SAAS,CACV,CAAC;CACH;AAED,wBAAgB,oBAAoB,CAAC,MAAM,EAAE,WAAW,GAAG,eAAe,CAYzE"}
|
|
@@ -7,6 +7,9 @@
|
|
|
7
7
|
* (useSyncQueries.ts) so the dashboard keeps identical behavior, including
|
|
8
8
|
* the trigger/restore loading and error simulations used by the TanStack
|
|
9
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`.
|
|
10
13
|
*/
|
|
11
14
|
import { type QueryClient } from "@tanstack/react-query";
|
|
12
15
|
export declare function createReactQuerySyncAdapter(queryClient: QueryClient): {
|
|
@@ -17,6 +20,22 @@ export declare function createReactQuerySyncAdapter(queryClient: QueryClient): {
|
|
|
17
20
|
};
|
|
18
21
|
subscribe: (onChange: () => void) => () => void;
|
|
19
22
|
actions: {
|
|
23
|
+
/**
|
|
24
|
+
* On-demand detail: one query's real cached `data`. Wire-form snapshots
|
|
25
|
+
* replace oversized / unserializable data with markers; this is the
|
|
26
|
+
* explicit, size-guarded channel for the detail pane.
|
|
27
|
+
*/
|
|
28
|
+
getQueryData: (params: unknown) => {
|
|
29
|
+
found: false;
|
|
30
|
+
reason: string;
|
|
31
|
+
queryHash?: undefined;
|
|
32
|
+
data?: undefined;
|
|
33
|
+
} | {
|
|
34
|
+
found: true;
|
|
35
|
+
queryHash: string;
|
|
36
|
+
data: unknown;
|
|
37
|
+
reason?: undefined;
|
|
38
|
+
};
|
|
20
39
|
/**
|
|
21
40
|
* Compact, token-bounded cache reader for a remote driver (MCP/LLM):
|
|
22
41
|
* projects each live query to light fields (hash, key, status label,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"reactQuerySyncAdapter.d.ts","sourceRoot":"","sources":["../../../../src/react-query/sync/reactQuerySyncAdapter.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"reactQuerySyncAdapter.d.ts","sourceRoot":"","sources":["../../../../src/react-query/sync/reactQuerySyncAdapter.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AACH,OAAO,EAAiB,KAAK,WAAW,EAAiB,MAAM,uBAAuB,CAAC;AA0BvF,wBAAgB,2BAA2B,CAAC,WAAW,EAAE,WAAW;;;;;;0BAgB1C,MAAM,IAAI;;QAa9B;;;;WAIG;+BACoB,OAAO;;;;;;;;;;;QAY9B;;;;;;WAMG;8BACmB,OAAO;;;;;;;;;;;;;;;;0BAkCX,OAAO;6BAKJ,OAAO;wBAEZ,OAAO;yBACN,OAAO;+BAGD,OAAO;+BAIP,OAAO;+BAaP,OAAO;iCAEL,OAAO;iCAwBP,OAAO;;;4BA4BZ,OAAO;;EAMhC"}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Time Machine snapshot provider for React Query.
|
|
3
|
+
*
|
|
4
|
+
* Unlike the other providers this one can't register at module init — the
|
|
5
|
+
* QueryClient only exists in React context — so useReactQuerySyncAdapter
|
|
6
|
+
* (mounted app-lifetime by FloatingDevTools' auto external sync) registers
|
|
7
|
+
* it per-client via an effect.
|
|
8
|
+
*
|
|
9
|
+
* Restore is a device-safe DIFF against the live cache, NOT the desktop
|
|
10
|
+
* Hydrate: that one detaches live observers and mocks queryFns, which is
|
|
11
|
+
* fine on a mirror client and destructive in a running app. Here existing
|
|
12
|
+
* queries get `setState` (observers stay attached and are notified, keeping
|
|
13
|
+
* their real queryFns), missing ones are built into the cache, and extras
|
|
14
|
+
* are removed. Mutations aren't restorable (mutationFns can't be captured);
|
|
15
|
+
* the mutation cache is cleared instead.
|
|
16
|
+
*/
|
|
17
|
+
import type { QueryClient } from "@tanstack/react-query";
|
|
18
|
+
import { type StateSnapshotProvider } from "@buoy-gg/shared-ui";
|
|
19
|
+
export declare function createQuerySnapshotProvider(queryClient: QueryClient): StateSnapshotProvider;
|
|
20
|
+
/** Register for one client; returns the unregister fn. Last client wins. */
|
|
21
|
+
export declare function registerQuerySnapshotProvider(queryClient: QueryClient): () => void;
|
|
22
|
+
//# sourceMappingURL=snapshotProvider.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"snapshotProvider.d.ts","sourceRoot":"","sources":["../../../../src/react-query/sync/snapshotProvider.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AACH,OAAO,KAAK,EAAE,WAAW,EAAc,MAAM,uBAAuB,CAAC;AACrE,OAAO,EAEL,KAAK,qBAAqB,EAK3B,MAAM,oBAAoB,CAAC;AAuC5B,wBAAgB,2BAA2B,CACzC,WAAW,EAAE,WAAW,GACvB,qBAAqB,CAuLvB;AAED,4EAA4E;AAC5E,wBAAgB,6BAA6B,CAC3C,WAAW,EAAE,WAAW,GACvB,MAAM,IAAI,CAEZ"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"useReactQuerySyncAdapter.d.ts","sourceRoot":"","sources":["../../../../src/react-query/sync/useReactQuerySyncAdapter.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,2BAA2B,EAAE,MAAM,yBAAyB,CAAC;
|
|
1
|
+
{"version":3,"file":"useReactQuerySyncAdapter.d.ts","sourceRoot":"","sources":["../../../../src/react-query/sync/useReactQuerySyncAdapter.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,2BAA2B,EAAE,MAAM,yBAAyB,CAAC;AAGtE;;;;;GAKG;AACH,wBAAgB,wBAAwB,IAAI,UAAU,CACpD,OAAO,2BAA2B,CACnC,GAAG,IAAI,CAqBP"}
|
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.18",
|
|
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",
|
|
@@ -24,10 +24,13 @@
|
|
|
24
24
|
"files": [
|
|
25
25
|
"lib"
|
|
26
26
|
],
|
|
27
|
-
"sideEffects":
|
|
27
|
+
"sideEffects": [
|
|
28
|
+
"**/snapshotProvider.js",
|
|
29
|
+
"**/snapshotProvider.ts"
|
|
30
|
+
],
|
|
28
31
|
"dependencies": {
|
|
29
|
-
"@buoy-gg/
|
|
30
|
-
"@buoy-gg/
|
|
32
|
+
"@buoy-gg/floating-tools-core": "^7.0.18",
|
|
33
|
+
"@buoy-gg/shared-ui": "^7.0.18"
|
|
31
34
|
},
|
|
32
35
|
"peerDependencies": {
|
|
33
36
|
"@tanstack/react-query": ">=5.0.0",
|