@avasapp/agent-bridge 0.3.0 → 0.4.0
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 +149 -11
- package/dist/adapters/ably.cjs +22 -4
- package/dist/adapters/ably.d.cts +8 -1
- package/dist/adapters/expo-router.cjs +22 -1
- package/dist/adapters/expo-router.d.cts +5 -1
- package/dist/adapters/react-native-mmkv.cjs +1 -0
- package/dist/adapters/react-native-mmkv.d.cts +1 -1
- package/dist/adapters/react-native-webview.cjs +672 -0
- package/dist/adapters/react-native-webview.d.cts +79 -0
- package/dist/adapters/socket-io.cjs +3 -1
- package/dist/adapters/socket-io.d.cts +1 -1
- package/dist/adapters/tanstack-query.cjs +256 -17
- package/dist/adapters/tanstack-query.d.cts +1 -1
- package/dist/adapters/zustand.cjs +152 -12
- package/dist/adapters/zustand.d.cts +19 -3
- package/dist/{chunk-S2FYN2R5.js → chunk-AJD3XTNT.js} +223 -57
- package/dist/cli.js +817 -56
- package/dist/client/index.cjs +218 -56
- package/dist/client/index.d.ts +20 -2
- package/dist/client/index.js +1 -1
- package/dist/expo/index.d.cts +1 -1
- package/dist/network/index.cjs +254 -51
- package/dist/network/index.d.cts +9 -2
- package/dist/noop/index.cjs +3 -0
- package/dist/noop/react-native-webview.cjs +31 -0
- package/dist/realtime/index.cjs +2 -1
- package/dist/realtime/index.d.cts +3 -1
- package/dist/runtime/index.cjs +860 -100
- package/dist/runtime/index.d.cts +31 -5
- package/dist/{types-6kKQsLC1.d.cts → types-BmpykP1P.d.cts} +23 -1
- package/entries/react-native-webview.cjs +9 -0
- package/package.json +17 -6
- package/skills/agent-bridge/SKILL.md +196 -5
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { T as Tools } from '../types-BmpykP1P.cjs';
|
|
2
|
+
|
|
3
|
+
/** What the adapter needs from a react-native-webview instance. */
|
|
4
|
+
type WebViewLike = {
|
|
5
|
+
injectJavaScript: (script: string) => void;
|
|
6
|
+
postMessage: (data: string) => void;
|
|
7
|
+
reload: () => void;
|
|
8
|
+
};
|
|
9
|
+
/**
|
|
10
|
+
* Keeps secrets out of the message log. Either dotted paths into JSON message
|
|
11
|
+
* bodies (`['token', 'user.phone']`), or a hook called for the whole body
|
|
12
|
+
* (`path` is `''`) and every nested value of a JSON body with its dotted path;
|
|
13
|
+
* what it returns is logged instead. Redaction is by key: a secret inside a
|
|
14
|
+
* string value is not found by a path list; scrub values in the hook. A body that isn't JSON (a script from
|
|
15
|
+
* `injectJavaScript`, plain text) can only be redacted by the hook.
|
|
16
|
+
*/
|
|
17
|
+
type WebViewRedact = string[] | ((webview: string, path: string, value: unknown) => unknown);
|
|
18
|
+
type WebViewOptions = {
|
|
19
|
+
/** What agents call this WebView: `webview.press`'s `webview` option, `webview.snapshot <name>`. */
|
|
20
|
+
name: string;
|
|
21
|
+
/** Origins besides the page the WebView first loads that may be read and driven, e.g. `https://pay.example.com`. */
|
|
22
|
+
allowedOrigins?: string[];
|
|
23
|
+
redact?: WebViewRedact;
|
|
24
|
+
/** Your own `injectedJavaScriptBeforeContentLoaded`; it runs after the adapter's. */
|
|
25
|
+
injectedJavaScriptBeforeContentLoaded?: string;
|
|
26
|
+
/** The adapter listens to these; pass yours here so spreading `props` doesn't replace them. */
|
|
27
|
+
onLoadStart?: (event: NativeEvent) => void;
|
|
28
|
+
onLoadEnd?: (event: NativeEvent) => void;
|
|
29
|
+
onNavigationStateChange?: (event: NativeNavState) => void;
|
|
30
|
+
/** How long a call waits for the page to load and its script to check in (default 10 000 ms). */
|
|
31
|
+
firstLoadTimeoutMs?: number;
|
|
32
|
+
};
|
|
33
|
+
/** What react-native-webview tells the app, from native code, not from the page. */
|
|
34
|
+
type NativeEvent = {
|
|
35
|
+
nativeEvent?: {
|
|
36
|
+
url?: string;
|
|
37
|
+
isTopFrame?: boolean;
|
|
38
|
+
};
|
|
39
|
+
};
|
|
40
|
+
type NativeNavState = {
|
|
41
|
+
url?: string;
|
|
42
|
+
loading?: boolean;
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Registers a react-native-webview so agents can read and drive it. Spread
|
|
47
|
+
* `props` on the WebView and give it `wrap(onMessage)` as its `onMessage`:
|
|
48
|
+
*
|
|
49
|
+
* ```tsx
|
|
50
|
+
* const ref = useRef<WebView>(null)
|
|
51
|
+
* const webview = useWebViewTools(ref, { name: 'checkout' })
|
|
52
|
+
* <WebView ref={ref} {...webview.props} onMessage={webview.wrap(onMessage)} />
|
|
53
|
+
* ```
|
|
54
|
+
*
|
|
55
|
+
* The bridge's own replies never reach `onMessage`. Add `webviewTools()` to the
|
|
56
|
+
* bridge's tools.
|
|
57
|
+
*/
|
|
58
|
+
declare function useWebViewTools(ref: {
|
|
59
|
+
current: WebViewLike | null;
|
|
60
|
+
}, options: WebViewOptions): {
|
|
61
|
+
props: {
|
|
62
|
+
injectedJavaScriptBeforeContentLoaded: string;
|
|
63
|
+
injectedJavaScriptBeforeContentLoadedForMainFrameOnly: true;
|
|
64
|
+
onLoadStart: (event: NativeEvent) => void;
|
|
65
|
+
onLoadEnd: (event: NativeEvent) => void;
|
|
66
|
+
onNavigationStateChange: (state: NativeNavState) => void;
|
|
67
|
+
};
|
|
68
|
+
wrap: (onMessage?: (event: never) => unknown) => (event: {
|
|
69
|
+
nativeEvent?: {
|
|
70
|
+
data?: unknown;
|
|
71
|
+
url?: string;
|
|
72
|
+
isTopFrame?: boolean;
|
|
73
|
+
};
|
|
74
|
+
}) => unknown;
|
|
75
|
+
};
|
|
76
|
+
/** Read and drive the WebViews registered with `useWebViewTools`. */
|
|
77
|
+
declare function webviewTools(): Tools;
|
|
78
|
+
|
|
79
|
+
export { type WebViewLike, type WebViewOptions, type WebViewRedact, useWebViewTools, webviewTools };
|
|
@@ -218,7 +218,7 @@ function createRealtimeTap(options = {}) {
|
|
|
218
218
|
}
|
|
219
219
|
},
|
|
220
220
|
[`${ns}.emit`]: {
|
|
221
|
-
description: "Deliver a fake message to every listener on a channel: [channel, message]. Runs the app's own handlers. Returns { id, delivered }.",
|
|
221
|
+
description: "Deliver a fake message to every listener on a channel: [channel, message]. Runs the app's own handlers. Returns { id, delivered }." + (options.emitUsage ? ` ${options.emitUsage}` : ""),
|
|
222
222
|
run: (channel, ...values) => {
|
|
223
223
|
const entries = channels.get(channel);
|
|
224
224
|
if (!entries?.size)
|
|
@@ -254,6 +254,7 @@ function createRealtimeTap(options = {}) {
|
|
|
254
254
|
run: (channel) => setMute(channel, null)
|
|
255
255
|
},
|
|
256
256
|
[`${ns}.restore`]: {
|
|
257
|
+
pending: () => mutes.size > 0 || faked !== null,
|
|
257
258
|
description: "Undo the agent: unmute every channel and go back to the real connection state.",
|
|
258
259
|
run: () => {
|
|
259
260
|
const unmuted = [...mutes.keys()];
|
|
@@ -294,6 +295,7 @@ var socketIoTools = realtimeAdapter(
|
|
|
294
295
|
return { data: values.length === 1 ? values[0] : values };
|
|
295
296
|
},
|
|
296
297
|
toMessage: (values) => values,
|
|
298
|
+
emitUsage: `socket.io: the channel is the event name and the rest are the listener's arguments, so ["chat", "a", "b"] calls listener("a", "b"). Do not repeat the event name in the arguments.`,
|
|
297
299
|
// socket.io-client announces lifecycle events with emitReserved.
|
|
298
300
|
connection: fakeableConnection(socket, {
|
|
299
301
|
property: "connected",
|
|
@@ -23,6 +23,151 @@ __export(tanstack_query_exports, {
|
|
|
23
23
|
queryTools: () => queryTools
|
|
24
24
|
});
|
|
25
25
|
module.exports = __toCommonJS(tanstack_query_exports);
|
|
26
|
+
var import_query_core = require("@tanstack/query-core");
|
|
27
|
+
|
|
28
|
+
// src/shared/mock-signal.ts
|
|
29
|
+
var KEY = /* @__PURE__ */ Symbol.for("@avasapp/agent-bridge/mock-signal");
|
|
30
|
+
var listeners = () => {
|
|
31
|
+
const g = globalThis;
|
|
32
|
+
return g[KEY] ?? (g[KEY] = /* @__PURE__ */ new Set());
|
|
33
|
+
};
|
|
34
|
+
function onMockSignal(listener) {
|
|
35
|
+
const all = listeners();
|
|
36
|
+
all.add(listener);
|
|
37
|
+
return () => {
|
|
38
|
+
all.delete(listener);
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// src/adapters/query-mocked.ts
|
|
43
|
+
var KEY2 = /* @__PURE__ */ Symbol.for("@avasapp/agent-bridge/query-mocked");
|
|
44
|
+
var trackers = globalThis[KEY2] ?? (globalThis[KEY2] = /* @__PURE__ */ new WeakMap());
|
|
45
|
+
var isInfinite = (query) => query.queryType === "infinite" || !!query.options.behavior;
|
|
46
|
+
function trackMockedQueries(queryClient, isPinned) {
|
|
47
|
+
const existing = trackers.get(queryClient);
|
|
48
|
+
if (existing) {
|
|
49
|
+
existing.isPinned = isPinned;
|
|
50
|
+
return existing;
|
|
51
|
+
}
|
|
52
|
+
const cache = queryClient.getQueryCache();
|
|
53
|
+
const mocked = /* @__PURE__ */ new Map();
|
|
54
|
+
const inflight = /* @__PURE__ */ new Map();
|
|
55
|
+
const instrumented = /* @__PURE__ */ new WeakSet();
|
|
56
|
+
const holder = { current: null };
|
|
57
|
+
let cleared = 0;
|
|
58
|
+
const newFetch = () => ({
|
|
59
|
+
own: /* @__PURE__ */ new Set(),
|
|
60
|
+
ids: /* @__PURE__ */ new Set(),
|
|
61
|
+
loose: /* @__PURE__ */ new Set(),
|
|
62
|
+
fetchMore: false,
|
|
63
|
+
gone: false
|
|
64
|
+
});
|
|
65
|
+
const reset = (hash) => {
|
|
66
|
+
const query = cache.get(hash);
|
|
67
|
+
if (!query || tracker.isPinned(hash)) return;
|
|
68
|
+
cleared += 1;
|
|
69
|
+
queryClient.resetQueries({ queryKey: query.queryKey, exact: true }).catch(() => {
|
|
70
|
+
});
|
|
71
|
+
};
|
|
72
|
+
const instrument = (query) => {
|
|
73
|
+
if (instrumented.has(query)) return;
|
|
74
|
+
instrumented.add(query);
|
|
75
|
+
const original = query.fetch.bind(query);
|
|
76
|
+
query.fetch = ((...args) => {
|
|
77
|
+
let f = inflight.get(query.queryHash);
|
|
78
|
+
if (!f) inflight.set(query.queryHash, f = newFetch());
|
|
79
|
+
const outer = holder.current;
|
|
80
|
+
holder.current = f;
|
|
81
|
+
try {
|
|
82
|
+
return original(...args);
|
|
83
|
+
} finally {
|
|
84
|
+
holder.current = outer;
|
|
85
|
+
}
|
|
86
|
+
});
|
|
87
|
+
};
|
|
88
|
+
for (const query of cache.getAll()) instrument(query);
|
|
89
|
+
const build = cache.build.bind(cache);
|
|
90
|
+
cache.build = ((...args) => {
|
|
91
|
+
const query = build(...args);
|
|
92
|
+
instrument(query);
|
|
93
|
+
return query;
|
|
94
|
+
});
|
|
95
|
+
cache.subscribe((event) => {
|
|
96
|
+
if (event.type !== "updated") return;
|
|
97
|
+
const { query, action } = event;
|
|
98
|
+
const hash = query.queryHash;
|
|
99
|
+
if (action.type === "fetch") {
|
|
100
|
+
let f = inflight.get(hash);
|
|
101
|
+
if (!f) inflight.set(hash, f = newFetch());
|
|
102
|
+
if (action.meta?.fetchMore) f.fetchMore = true;
|
|
103
|
+
} else if (action.type === "success") {
|
|
104
|
+
if (action.manual) {
|
|
105
|
+
if (!inflight.has(hash)) mocked.delete(hash);
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
const f = inflight.get(hash);
|
|
109
|
+
inflight.delete(hash);
|
|
110
|
+
if (!f) return;
|
|
111
|
+
const used = answeredBy(f);
|
|
112
|
+
const before = mocked.get(hash);
|
|
113
|
+
if (used.size) {
|
|
114
|
+
if (f.gone) {
|
|
115
|
+
mocked.delete(hash);
|
|
116
|
+
reset(hash);
|
|
117
|
+
} else mocked.set(hash, /* @__PURE__ */ new Set([...before ?? [], ...used]));
|
|
118
|
+
} else if (before && !f.fetchMore && !isInfinite(query)) {
|
|
119
|
+
mocked.delete(hash);
|
|
120
|
+
}
|
|
121
|
+
} else if (query.state.fetchStatus === "idle") {
|
|
122
|
+
inflight.delete(hash);
|
|
123
|
+
}
|
|
124
|
+
});
|
|
125
|
+
const tracker = {
|
|
126
|
+
isPinned,
|
|
127
|
+
pending: () => mocked.size > 0,
|
|
128
|
+
takeCleared: () => {
|
|
129
|
+
const n = cleared;
|
|
130
|
+
cleared = 0;
|
|
131
|
+
return n;
|
|
132
|
+
},
|
|
133
|
+
clear: (ids) => {
|
|
134
|
+
const before = cleared;
|
|
135
|
+
for (const [hash, from] of [...mocked]) {
|
|
136
|
+
if (ids && !ids.some((id) => from.has(id))) continue;
|
|
137
|
+
mocked.delete(hash);
|
|
138
|
+
reset(hash);
|
|
139
|
+
}
|
|
140
|
+
return cleared - before;
|
|
141
|
+
}
|
|
142
|
+
};
|
|
143
|
+
trackers.set(queryClient, tracker);
|
|
144
|
+
listen(new WeakRef(tracker), inflight, holder);
|
|
145
|
+
return tracker;
|
|
146
|
+
}
|
|
147
|
+
var answeredBy = (f) => /* @__PURE__ */ new Set([...f.ids, ...f.loose]);
|
|
148
|
+
function listen(ref, inflight, holder) {
|
|
149
|
+
const stop = onMockSignal({
|
|
150
|
+
started: (requestId) => {
|
|
151
|
+
if (!ref.deref()) return stop();
|
|
152
|
+
holder.current?.own.add(requestId);
|
|
153
|
+
},
|
|
154
|
+
answered: (mockId, requestId) => {
|
|
155
|
+
if (!ref.deref()) return stop();
|
|
156
|
+
const owner = [...inflight.values()].find((f) => f.own.has(requestId));
|
|
157
|
+
if (owner) owner.ids.add(mockId);
|
|
158
|
+
else for (const f of inflight.values()) f.loose.add(mockId);
|
|
159
|
+
},
|
|
160
|
+
removed: (ids) => {
|
|
161
|
+
const tracker = ref.deref();
|
|
162
|
+
if (!tracker) return stop();
|
|
163
|
+
for (const f of inflight.values())
|
|
164
|
+
if (!ids || [...answeredBy(f)].some((id) => ids.includes(id))) f.gone = true;
|
|
165
|
+
tracker.clear(ids);
|
|
166
|
+
}
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// src/adapters/tanstack-query.ts
|
|
26
171
|
var pinStates = /* @__PURE__ */ new WeakMap();
|
|
27
172
|
function pinStateFor(queryClient) {
|
|
28
173
|
let state = pinStates.get(queryClient);
|
|
@@ -39,11 +184,56 @@ function pinStateFor(queryClient) {
|
|
|
39
184
|
return state;
|
|
40
185
|
}
|
|
41
186
|
var rendered = () => new Promise((resolve) => setTimeout(resolve, 0));
|
|
187
|
+
var keyOf = (args) => args.length === 1 && Array.isArray(args[0]) ? args[0] : args;
|
|
188
|
+
var isGetOptions = (v) => !!v && typeof v === "object" && !Array.isArray(v) && Object.keys(v).length > 0 && Object.keys(v).every((k) => k === "pages" || k === "path");
|
|
189
|
+
function slicePages(data, range) {
|
|
190
|
+
const infinite = data;
|
|
191
|
+
if (!infinite || !Array.isArray(infinite.pages))
|
|
192
|
+
throw new Error("pages only applies to an infinite query (data with a pages array)");
|
|
193
|
+
const [from, to] = Array.isArray(range) ? range : [range];
|
|
194
|
+
if (typeof from !== "number" || to !== void 0 && typeof to !== "number")
|
|
195
|
+
throw new Error(
|
|
196
|
+
`pages is [from, to] (to exclusive, like Array.slice) or a start index, got ${JSON.stringify(range)}`
|
|
197
|
+
);
|
|
198
|
+
return {
|
|
199
|
+
...infinite,
|
|
200
|
+
pages: infinite.pages.slice(from, to),
|
|
201
|
+
...Array.isArray(infinite.pageParams) && {
|
|
202
|
+
pageParams: infinite.pageParams.slice(from, to)
|
|
203
|
+
},
|
|
204
|
+
totalPages: infinite.pages.length
|
|
205
|
+
};
|
|
206
|
+
}
|
|
207
|
+
function atPath(value, path) {
|
|
208
|
+
const segments = typeof path === "string" ? path.split(".").filter(Boolean) : Array.isArray(path) && path.every((p) => typeof p === "string" || typeof p === "number") ? path : null;
|
|
209
|
+
if (!segments)
|
|
210
|
+
throw new Error(
|
|
211
|
+
`path is a dotted string or an array of segments, got ${JSON.stringify(path)}`
|
|
212
|
+
);
|
|
213
|
+
let current = value;
|
|
214
|
+
segments.forEach((segment, i) => {
|
|
215
|
+
if (current === null || typeof current !== "object" || !(String(segment) in current)) {
|
|
216
|
+
const where = segments.slice(0, i).join(".") || "the data";
|
|
217
|
+
const has = current && typeof current === "object" ? Array.isArray(current) ? `an array of ${current.length}` : `keys ${Object.keys(current).slice(0, 10).join(", ") || "(none)"}` : JSON.stringify(current) ?? "undefined";
|
|
218
|
+
throw new Error(`No "${segment}" in ${where}: it has ${has}`);
|
|
219
|
+
}
|
|
220
|
+
current = current[segment];
|
|
221
|
+
});
|
|
222
|
+
return current;
|
|
223
|
+
}
|
|
42
224
|
function queryTools(queryClient) {
|
|
43
225
|
const cache = queryClient.getQueryCache();
|
|
44
226
|
const state = pinStateFor(queryClient);
|
|
45
227
|
const { pins, seeded } = state;
|
|
46
228
|
const hashOf = (key) => cache.build(queryClient, { queryKey: key }).queryHash;
|
|
229
|
+
const existingHash = (key) => cache.find({ queryKey: key, exact: true })?.queryHash ?? (0, import_query_core.hashKey)(key);
|
|
230
|
+
const arrayKey = (tool, key) => {
|
|
231
|
+
if (!Array.isArray(key))
|
|
232
|
+
throw new Error(
|
|
233
|
+
`${tool} takes the key as an array, got ${JSON.stringify(key)}. Wrap it: [[${JSON.stringify(key)}], <data>]`
|
|
234
|
+
);
|
|
235
|
+
return key;
|
|
236
|
+
};
|
|
47
237
|
const apply = (key, data) => {
|
|
48
238
|
state.applying = true;
|
|
49
239
|
try {
|
|
@@ -63,8 +253,8 @@ function queryTools(queryClient) {
|
|
|
63
253
|
apply(pin.key, pin.data);
|
|
64
254
|
}));
|
|
65
255
|
};
|
|
66
|
-
const
|
|
67
|
-
const removed = pins.delete(
|
|
256
|
+
const unpinKey = (key, hash = existingHash(key)) => {
|
|
257
|
+
const removed = pins.delete(hash);
|
|
68
258
|
if (!pins.size) {
|
|
69
259
|
state.unsubscribe?.();
|
|
70
260
|
state.unsubscribe = null;
|
|
@@ -73,6 +263,7 @@ function queryTools(queryClient) {
|
|
|
73
263
|
void queryClient.invalidateQueries({ queryKey: key, exact: true });
|
|
74
264
|
return removed;
|
|
75
265
|
};
|
|
266
|
+
const unpin = (...args) => unpinKey(keyOf(args));
|
|
76
267
|
const refetchReal = (key) => {
|
|
77
268
|
const query = cache.find({ queryKey: key, exact: true });
|
|
78
269
|
if (!query) return false;
|
|
@@ -88,23 +279,46 @@ function queryTools(queryClient) {
|
|
|
88
279
|
});
|
|
89
280
|
return true;
|
|
90
281
|
};
|
|
282
|
+
const mocked = trackMockedQueries(queryClient, (hash) => pins.has(hash));
|
|
91
283
|
return {
|
|
92
284
|
"query.list": {
|
|
93
|
-
description:
|
|
94
|
-
run: () => cache.
|
|
285
|
+
description: 'Cached queries: key, status, fetchStatus, observer count, pinned, isStale, isInvalidated, dataUpdatedAt, errorUpdatedAt. Optional key prefix filter, e.g. ("todos") or (["todos", 1]).',
|
|
286
|
+
run: (...args) => cache.findAll(args.length ? { queryKey: keyOf(args) } : {}).map((q) => ({
|
|
95
287
|
key: q.queryKey,
|
|
96
288
|
status: q.state.status,
|
|
289
|
+
fetchStatus: q.state.fetchStatus,
|
|
97
290
|
observers: q.getObserversCount(),
|
|
98
|
-
pinned: pins.has(q.queryHash)
|
|
291
|
+
pinned: pins.has(q.queryHash),
|
|
292
|
+
isStale: q.isStale(),
|
|
293
|
+
isInvalidated: q.state.isInvalidated,
|
|
294
|
+
dataUpdatedAt: q.state.dataUpdatedAt,
|
|
295
|
+
errorUpdatedAt: q.state.errorUpdatedAt
|
|
99
296
|
}))
|
|
100
297
|
},
|
|
101
298
|
"query.get": {
|
|
102
|
-
description:
|
|
103
|
-
run: (
|
|
299
|
+
description: 'Cached data for a query key, e.g. ("todos", 1) or (["todos", 1]). Errors when no such query is cached, listing keys that start the same. For a big one, a last { pages: [from, to] } returns only those pages of an infinite query (to exclusive, like Array.slice; adds totalPages), and { path: "pages.0.items" } returns only the value there (after pages, if both).',
|
|
300
|
+
run: (...args) => {
|
|
301
|
+
const last = args[args.length - 1];
|
|
302
|
+
const options = args.length > 1 && isGetOptions(last) && !cache.find({ queryKey: keyOf(args), exact: true }) ? last : void 0;
|
|
303
|
+
const key = keyOf(options ? args.slice(0, -1) : args);
|
|
304
|
+
if (!key.length) throw new Error("query.get needs a query key");
|
|
305
|
+
if (!cache.find({ queryKey: key, exact: true })) {
|
|
306
|
+
const near = cache.findAll({ queryKey: key.slice(0, 1) }).slice(0, 5).map((q) => JSON.stringify(q.queryKey));
|
|
307
|
+
throw new Error(
|
|
308
|
+
`No cached query with key ${JSON.stringify(key)}.${near.length ? ` Similar: ${near.join(", ")}` : " See query.list."}`
|
|
309
|
+
);
|
|
310
|
+
}
|
|
311
|
+
let data = queryClient.getQueryData(key);
|
|
312
|
+
if (options?.pages !== void 0) data = slicePages(data, options.pages);
|
|
313
|
+
if (options?.path !== void 0) data = atPath(data, options.path);
|
|
314
|
+
return data;
|
|
315
|
+
}
|
|
104
316
|
},
|
|
105
317
|
"query.set": {
|
|
318
|
+
maxArgs: 2,
|
|
106
319
|
description: "Replace cached data once. A refetch will overwrite it; use query.pin to keep it.",
|
|
107
|
-
run: async (
|
|
320
|
+
run: async (given, data) => {
|
|
321
|
+
const key = arrayKey("query.set", given);
|
|
108
322
|
const hash = hashOf(key);
|
|
109
323
|
if (!state.changed.has(hash)) state.changed.set(hash, key);
|
|
110
324
|
queryClient.setQueryData(key, data);
|
|
@@ -113,8 +327,10 @@ function queryTools(queryClient) {
|
|
|
113
327
|
}
|
|
114
328
|
},
|
|
115
329
|
"query.pin": {
|
|
330
|
+
maxArgs: 2,
|
|
116
331
|
description: "Seed data for a query key and keep it through refetches until unpinned.",
|
|
117
|
-
run: async (
|
|
332
|
+
run: async (given, data) => {
|
|
333
|
+
const key = arrayKey("query.pin", given);
|
|
118
334
|
pins.set(hashOf(key), { key, data });
|
|
119
335
|
watch();
|
|
120
336
|
apply(key, data);
|
|
@@ -127,19 +343,40 @@ function queryTools(queryClient) {
|
|
|
127
343
|
run: unpin
|
|
128
344
|
},
|
|
129
345
|
"query.unpinAll": {
|
|
346
|
+
maxArgs: 0,
|
|
130
347
|
description: "Stop every pin and refetch real data.",
|
|
131
|
-
run: () => [...pins
|
|
348
|
+
run: () => [...pins].filter(([hash, { key }]) => unpinKey(key, hash)).length
|
|
132
349
|
},
|
|
133
350
|
"query.refetch": {
|
|
134
|
-
description:
|
|
135
|
-
run: async (
|
|
351
|
+
description: 'Refetch the queries matching a key prefix, e.g. ("todos", 1) or (["todos", 1]). Returns { matched, refetched, data } (data of the exact key); errors when none match or none can be refetched (disabled, or no queryFn).',
|
|
352
|
+
run: async (...args) => {
|
|
353
|
+
const key = keyOf(args);
|
|
354
|
+
const found = cache.findAll({ queryKey: key });
|
|
355
|
+
if (!found.length)
|
|
356
|
+
throw new Error(
|
|
357
|
+
`No cached query matches key ${JSON.stringify(key)}. See query.list.`
|
|
358
|
+
);
|
|
359
|
+
const defaultFn = queryClient.getDefaultOptions().queries?.queryFn;
|
|
360
|
+
const refetched = found.filter(
|
|
361
|
+
(q) => !q.isDisabled() && (q.options.queryFn ?? defaultFn)
|
|
362
|
+
).length;
|
|
363
|
+
if (!refetched)
|
|
364
|
+
throw new Error(
|
|
365
|
+
`${found.length} cached ${found.length === 1 ? "query matches" : "queries match"} key ${JSON.stringify(key)}, but none can be refetched (disabled, or no queryFn).`
|
|
366
|
+
);
|
|
136
367
|
await queryClient.refetchQueries({ queryKey: key });
|
|
137
368
|
await rendered();
|
|
138
|
-
return
|
|
369
|
+
return {
|
|
370
|
+
matched: found.length,
|
|
371
|
+
refetched,
|
|
372
|
+
data: queryClient.getQueryData(key)
|
|
373
|
+
};
|
|
139
374
|
}
|
|
140
375
|
},
|
|
141
376
|
"query.restore": {
|
|
142
|
-
|
|
377
|
+
maxArgs: 0,
|
|
378
|
+
pending: () => pins.size > 0 || state.changed.size > 0 || mocked.pending(),
|
|
379
|
+
description: "Undo the agent: unpin everything, refetch real data for keys changed with query.set, and reset queries that fetched while a net.mock answered (net.unmock and net.restore do this too). Returns { unpinned, refetched, mockedCleared }.",
|
|
143
380
|
run: async () => {
|
|
144
381
|
const unpinned = [...pins.values()].map((p) => p.key);
|
|
145
382
|
const changed = [...state.changed].filter(([hash]) => !pins.has(hash)).map(([, key]) => key);
|
|
@@ -149,13 +386,15 @@ function queryTools(queryClient) {
|
|
|
149
386
|
state.unsubscribe = null;
|
|
150
387
|
for (const key of unpinned) refetchReal(key);
|
|
151
388
|
const refetched = changed.filter((key) => refetchReal(key)).length;
|
|
389
|
+
mocked.clear();
|
|
390
|
+
const mockedCleared = mocked.takeCleared();
|
|
152
391
|
await rendered();
|
|
153
|
-
return { unpinned: unpinned.length, refetched };
|
|
392
|
+
return { unpinned: unpinned.length, refetched, mockedCleared };
|
|
154
393
|
}
|
|
155
394
|
},
|
|
156
395
|
"query.invalidate": {
|
|
157
|
-
description:
|
|
158
|
-
run: (
|
|
396
|
+
description: 'Invalidate queries matching a key prefix, e.g. ("todos") or (["todos"]).',
|
|
397
|
+
run: (...args) => queryClient.invalidateQueries({ queryKey: keyOf(args) })
|
|
159
398
|
}
|
|
160
399
|
};
|
|
161
400
|
}
|