@avasapp/agent-bridge 0.4.2 → 0.6.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 +36 -14
- package/dist/adapters/ably.cjs +79 -3
- package/dist/adapters/ably.d.cts +2 -1
- package/dist/adapters/async-storage.cjs +158 -0
- package/dist/adapters/async-storage.d.cts +14 -0
- package/dist/adapters/device.cjs +1110 -0
- package/dist/adapters/device.d.cts +49 -0
- package/dist/adapters/expo-router.cjs +112 -6
- package/dist/adapters/expo-router.d.cts +1 -1
- package/dist/adapters/jotai.cjs +266 -0
- package/dist/adapters/jotai.d.cts +19 -0
- package/dist/adapters/react-native-mmkv.cjs +80 -0
- package/dist/adapters/react-native-mmkv.d.cts +3 -1
- package/dist/adapters/react-native-webview.d.cts +1 -1
- package/dist/adapters/react-navigation.cjs +293 -0
- package/dist/adapters/react-navigation.d.cts +55 -0
- package/dist/adapters/redux.cjs +322 -0
- package/dist/adapters/redux.d.cts +28 -0
- package/dist/adapters/secure-store.cjs +169 -0
- package/dist/adapters/secure-store.d.cts +28 -0
- package/dist/adapters/socket-io.cjs +77 -3
- package/dist/adapters/socket-io.d.cts +2 -1
- package/dist/adapters/tanstack-query.cjs +3 -0
- package/dist/adapters/tanstack-query.d.cts +13 -4
- package/dist/adapters/zustand.cjs +193 -22
- package/dist/adapters/zustand.d.cts +11 -15
- package/dist/{chunk-TA2OLPPT.js → chunk-Y6QFU5ON.js} +477 -165
- package/dist/cli.js +2370 -103
- package/dist/client/index.cjs +441 -136
- package/dist/client/index.d.ts +67 -4
- package/dist/client/index.js +5 -3
- package/dist/expo/index.d.cts +1 -1
- package/dist/network/index.cjs +320 -73
- package/dist/network/index.d.cts +18 -3
- package/dist/noop/async-storage.cjs +26 -0
- package/dist/noop/device.cjs +26 -0
- package/dist/noop/jotai.cjs +26 -0
- package/dist/noop/react-navigation.cjs +26 -0
- package/dist/noop/realtime.cjs +6 -1
- package/dist/noop/redux.cjs +28 -0
- package/dist/noop/secure-store.cjs +26 -0
- package/dist/realtime/index.cjs +71 -4
- package/dist/realtime/index.d.cts +21 -2
- package/dist/runtime/index.cjs +838 -128
- package/dist/runtime/index.d.cts +4 -2
- package/dist/store-common-j0cHHvBr.d.cts +23 -0
- package/dist/{types-BmpykP1P.d.cts → types-qlwdcGjt.d.cts} +22 -0
- package/entries/async-storage.cjs +9 -0
- package/entries/device.cjs +9 -0
- package/entries/jotai.cjs +9 -0
- package/entries/react-navigation.cjs +9 -0
- package/entries/redux.cjs +9 -0
- package/entries/secure-store.cjs +9 -0
- package/package.json +34 -2
- package/skills/agent-bridge/SKILL.md +70 -22
- package/skills/agent-bridge/references/device.md +21 -0
- package/skills/agent-bridge/references/flows.md +19 -8
- package/skills/agent-bridge/references/large-data.md +6 -6
- package/skills/agent-bridge/references/network.md +25 -7
- package/skills/agent-bridge/references/realtime.md +5 -5
- package/skills/agent-bridge/references/restore.md +25 -7
- package/skills/agent-bridge/references/screen.md +25 -11
- package/skills/agent-bridge/references/state.md +31 -11
- package/skills/agent-bridge/references/webview.md +9 -9
- package/skills/agent-bridge/references/writing-scenarios.md +3 -3
|
@@ -0,0 +1,1110 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
|
|
20
|
+
// src/adapters/device.ts
|
|
21
|
+
var device_exports = {};
|
|
22
|
+
__export(device_exports, {
|
|
23
|
+
deviceTools: () => deviceTools
|
|
24
|
+
});
|
|
25
|
+
module.exports = __toCommonJS(device_exports);
|
|
26
|
+
|
|
27
|
+
// src/adapters/device/types.ts
|
|
28
|
+
var KEY = /* @__PURE__ */ Symbol.for("@avasapp/agent-bridge/device");
|
|
29
|
+
function deviceState() {
|
|
30
|
+
const g = globalThis;
|
|
31
|
+
return g[KEY] ?? (g[KEY] = {});
|
|
32
|
+
}
|
|
33
|
+
function loadReactNative() {
|
|
34
|
+
return require("react-native");
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// src/adapters/device/app-state.ts
|
|
38
|
+
var STATES = ["active", "background", "inactive"];
|
|
39
|
+
function emitState(rn, state) {
|
|
40
|
+
const st = deviceState();
|
|
41
|
+
st.emitting = true;
|
|
42
|
+
try {
|
|
43
|
+
rn.DeviceEventEmitter.emit("appStateDidChange", { app_state: state });
|
|
44
|
+
if (rn.Platform?.OS === "android") rn.DeviceEventEmitter.emit("appStateFocusChange", state === "active");
|
|
45
|
+
} finally {
|
|
46
|
+
st.emitting = false;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
function appStateArea(rn) {
|
|
50
|
+
const restore = () => {
|
|
51
|
+
const st = deviceState();
|
|
52
|
+
const faked = st.appState;
|
|
53
|
+
if (!faked) return { restored: false };
|
|
54
|
+
const { AppState } = rn();
|
|
55
|
+
delete st.appState;
|
|
56
|
+
delete AppState.currentState;
|
|
57
|
+
AppState.currentState = faked.real;
|
|
58
|
+
if (faked.real !== faked.fake) emitState(rn(), faked.real);
|
|
59
|
+
return { restored: true, state: faked.real };
|
|
60
|
+
};
|
|
61
|
+
return {
|
|
62
|
+
pending: () => {
|
|
63
|
+
const faked = deviceState().appState;
|
|
64
|
+
return faked ? { state: faked.fake, real: faked.real } : null;
|
|
65
|
+
},
|
|
66
|
+
restore,
|
|
67
|
+
tools: {
|
|
68
|
+
"device.appState": {
|
|
69
|
+
description: 'Move the app to "active", "background" or "inactive": every AppState change listener fires and AppState.currentState reports it until device.restore. Real native changes still reach listeners meanwhile. Returns { state, previous }.',
|
|
70
|
+
maxArgs: 1,
|
|
71
|
+
run: (state) => {
|
|
72
|
+
if (typeof state !== "string" || !STATES.includes(state))
|
|
73
|
+
throw new Error(`Pass one of: ${STATES.join(", ")}`);
|
|
74
|
+
const native = rn();
|
|
75
|
+
const { AppState } = native;
|
|
76
|
+
const st = deviceState();
|
|
77
|
+
const previous = st.appState?.fake ?? AppState.currentState;
|
|
78
|
+
if (!st.appState) {
|
|
79
|
+
const faked = { fake: state, real: AppState.currentState };
|
|
80
|
+
st.appState = faked;
|
|
81
|
+
Object.defineProperty(AppState, "currentState", {
|
|
82
|
+
configurable: true,
|
|
83
|
+
enumerable: true,
|
|
84
|
+
get: () => deviceState().appState?.fake ?? faked.real,
|
|
85
|
+
set: (value) => {
|
|
86
|
+
if (!deviceState().emitting && deviceState().appState) deviceState().appState.real = value;
|
|
87
|
+
}
|
|
88
|
+
});
|
|
89
|
+
} else st.appState.fake = state;
|
|
90
|
+
emitState(native, state);
|
|
91
|
+
return { state, previous };
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// src/adapters/device/clock.ts
|
|
99
|
+
function clockArea() {
|
|
100
|
+
const restore = () => {
|
|
101
|
+
const st = deviceState();
|
|
102
|
+
if (!st.RealDate) return { restored: false };
|
|
103
|
+
globalThis.Date = st.RealDate;
|
|
104
|
+
st.RealDate.prototype.constructor = st.RealDate;
|
|
105
|
+
delete st.RealDate;
|
|
106
|
+
delete st.offsetMs;
|
|
107
|
+
return { restored: true };
|
|
108
|
+
};
|
|
109
|
+
const install2 = () => {
|
|
110
|
+
const st = deviceState();
|
|
111
|
+
const Real = st.RealDate ?? (st.RealDate = globalThis.Date);
|
|
112
|
+
const now2 = () => Real.now() + (deviceState().offsetMs ?? 0);
|
|
113
|
+
const proxy = new Proxy(Real, {
|
|
114
|
+
construct: (target, args, newTarget) => Reflect.construct(target, args.length ? args : [now2()], newTarget),
|
|
115
|
+
apply: () => new Real(now2()).toString(),
|
|
116
|
+
get: (target, key) => key === "now" ? now2 : Reflect.get(target, key)
|
|
117
|
+
});
|
|
118
|
+
Real.prototype.constructor = proxy;
|
|
119
|
+
globalThis.Date = proxy;
|
|
120
|
+
};
|
|
121
|
+
return {
|
|
122
|
+
pending: () => {
|
|
123
|
+
const { RealDate, offsetMs } = deviceState();
|
|
124
|
+
return RealDate ? { offsetMs, now: (/* @__PURE__ */ new Date()).toISOString() } : null;
|
|
125
|
+
},
|
|
126
|
+
restore,
|
|
127
|
+
tools: {
|
|
128
|
+
"device.setTime": {
|
|
129
|
+
description: 'Fake the clock: ["2026-12-25T09:00:00Z"] or [{ offsetMs: 86400000 }] shifts Date.now() and new Date() app-wide until device.restore. Timers, performance.now and values read earlier are not shifted, and react-query only rechecks staleness on its next trigger (follow with device.appState "active"). Returns { offsetMs, now }.',
|
|
130
|
+
maxArgs: 1,
|
|
131
|
+
run: (to) => {
|
|
132
|
+
const st = deviceState();
|
|
133
|
+
const real = (st.RealDate ?? Date).now();
|
|
134
|
+
let offsetMs;
|
|
135
|
+
if (typeof to === "string") {
|
|
136
|
+
const at = (st.RealDate ?? Date).parse(to);
|
|
137
|
+
if (Number.isNaN(at)) throw new Error('Pass an ISO date, e.g. "2026-12-25T09:00:00Z"');
|
|
138
|
+
offsetMs = at - real;
|
|
139
|
+
} else if (to && typeof to === "object" && Number.isFinite(to.offsetMs))
|
|
140
|
+
offsetMs = to.offsetMs;
|
|
141
|
+
else throw new Error("Pass an ISO date or { offsetMs }");
|
|
142
|
+
st.offsetMs = offsetMs;
|
|
143
|
+
if (!st.RealDate || globalThis.Date === st.RealDate) install2();
|
|
144
|
+
return { offsetMs, now: (/* @__PURE__ */ new Date()).toISOString() };
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// src/runtime/screen/settle.ts
|
|
152
|
+
var STATE = /* @__PURE__ */ Symbol.for("@avasapp/agent-bridge/commits");
|
|
153
|
+
var UPDATE_LANES = (1 << 22) - 1;
|
|
154
|
+
var hook = () => globalThis.__REACT_DEVTOOLS_GLOBAL_HOOK__;
|
|
155
|
+
var now = () => performance.now();
|
|
156
|
+
function install() {
|
|
157
|
+
const h = hook();
|
|
158
|
+
if (!h) return null;
|
|
159
|
+
const current = h.onCommitFiberRoot;
|
|
160
|
+
const existing = current?.[STATE];
|
|
161
|
+
if (existing) return existing;
|
|
162
|
+
const state = { commits: 0, listeners: /* @__PURE__ */ new Set() };
|
|
163
|
+
const wrapped = function(...args) {
|
|
164
|
+
state.commits += 1;
|
|
165
|
+
for (const listener of state.listeners) {
|
|
166
|
+
try {
|
|
167
|
+
listener();
|
|
168
|
+
} catch {
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
return current?.apply(this, args);
|
|
172
|
+
};
|
|
173
|
+
Object.defineProperty(wrapped, STATE, { value: state });
|
|
174
|
+
h.onCommitFiberRoot = wrapped;
|
|
175
|
+
return state;
|
|
176
|
+
}
|
|
177
|
+
function busy() {
|
|
178
|
+
const h = hook();
|
|
179
|
+
if (!h?.renderers || !h.getFiberRoots) return false;
|
|
180
|
+
for (const id of h.renderers.keys()) {
|
|
181
|
+
for (const root of h.getFiberRoots(id)) {
|
|
182
|
+
const pending = root.pendingLanes;
|
|
183
|
+
if (typeof pending !== "number") continue;
|
|
184
|
+
if (pending & ~(root.suspendedLanes ?? 0) & UPDATE_LANES) return true;
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
return false;
|
|
188
|
+
}
|
|
189
|
+
var frame = () => new Promise((resolve) => {
|
|
190
|
+
const raf = globalThis.requestAnimationFrame;
|
|
191
|
+
if (raf) raf(() => resolve());
|
|
192
|
+
else setTimeout(resolve, 16);
|
|
193
|
+
});
|
|
194
|
+
async function settle(options = {}) {
|
|
195
|
+
const maxMs = options.maxMs ?? 500;
|
|
196
|
+
const t0 = now();
|
|
197
|
+
const state = install();
|
|
198
|
+
const start = state?.commits ?? 0;
|
|
199
|
+
const result = () => ({
|
|
200
|
+
commits: (state?.commits ?? 0) - start,
|
|
201
|
+
ms: Math.round((now() - t0) * 100) / 100
|
|
202
|
+
});
|
|
203
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
204
|
+
if (!state) return result();
|
|
205
|
+
let seen = state.commits;
|
|
206
|
+
if (seen === start && !busy()) return result();
|
|
207
|
+
while (now() - t0 < maxMs) {
|
|
208
|
+
await frame();
|
|
209
|
+
if (state.commits === seen && !busy()) break;
|
|
210
|
+
seen = state.commits;
|
|
211
|
+
}
|
|
212
|
+
return result();
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// src/adapters/expo-router-route.ts
|
|
216
|
+
var ROOT = "__root";
|
|
217
|
+
var NOT_FOUND = "+not-found";
|
|
218
|
+
var SETTLE_MS = 1e3;
|
|
219
|
+
var decode = (value) => {
|
|
220
|
+
try {
|
|
221
|
+
return decodeURIComponent(value);
|
|
222
|
+
} catch {
|
|
223
|
+
return value;
|
|
224
|
+
}
|
|
225
|
+
};
|
|
226
|
+
var isGroup = (segment) => segment.startsWith("(") && segment.endsWith(")");
|
|
227
|
+
var focused = (state) => state.routes[state.index ?? 0];
|
|
228
|
+
function routeFromState(state) {
|
|
229
|
+
let route = state && focused(state);
|
|
230
|
+
if (!route) return { pathname: "/", href: "/", params: {}, segments: [], name: "" };
|
|
231
|
+
if (route.name !== ROOT) {
|
|
232
|
+
const path = route.path ?? (route.name === NOT_FOUND ? "/" : `/${route.name}`);
|
|
233
|
+
return { pathname: path, href: path, params: {}, segments: [route.name], name: route.name };
|
|
234
|
+
}
|
|
235
|
+
const segments = [];
|
|
236
|
+
const raw = {};
|
|
237
|
+
let name = "";
|
|
238
|
+
let next = route.state;
|
|
239
|
+
while (next && (route = focused(next))) {
|
|
240
|
+
Object.assign(raw, route.params);
|
|
241
|
+
name = route.name;
|
|
242
|
+
segments.push(...route.name.replace(/^\//, "").split("/"));
|
|
243
|
+
next = route.state;
|
|
244
|
+
}
|
|
245
|
+
let pending = route?.params;
|
|
246
|
+
while (pending && typeof pending.screen === "string") {
|
|
247
|
+
segments.push(...pending.screen.replace(/^\//, "").split("/"));
|
|
248
|
+
name = pending.screen;
|
|
249
|
+
pending = pending.params && typeof pending.params === "object" ? pending.params : void 0;
|
|
250
|
+
}
|
|
251
|
+
if (segments.at(-1) === "index") segments.pop();
|
|
252
|
+
delete raw.screen;
|
|
253
|
+
delete raw.params;
|
|
254
|
+
const params = {};
|
|
255
|
+
for (const [key, value] of Object.entries(raw)) {
|
|
256
|
+
if (typeof value === "string") params[key] = decode(value);
|
|
257
|
+
else if (Array.isArray(value)) params[key] = value.map((v) => decode(String(v)));
|
|
258
|
+
else if (value != null) params[key] = String(value);
|
|
259
|
+
}
|
|
260
|
+
const inPath = /* @__PURE__ */ new Set();
|
|
261
|
+
const fill = (key) => {
|
|
262
|
+
inPath.add(key);
|
|
263
|
+
const value = params[key];
|
|
264
|
+
return value === void 0 ? [] : Array.isArray(value) ? value : [value];
|
|
265
|
+
};
|
|
266
|
+
const pathname = "/" + segments.filter((s) => !isGroup(s)).flatMap((s) => {
|
|
267
|
+
if (s === NOT_FOUND) return fill("not-found");
|
|
268
|
+
if (s.startsWith("[...") && s.endsWith("]")) return fill(s.slice(4, -1).replace(/\?$/, ""));
|
|
269
|
+
if (s.startsWith("[") && s.endsWith("]")) return fill(s.slice(1, -1));
|
|
270
|
+
return [s];
|
|
271
|
+
}).join("/");
|
|
272
|
+
const search = new URLSearchParams();
|
|
273
|
+
for (const [key, value] of Object.entries(params))
|
|
274
|
+
if (!inPath.has(key)) for (const v of [value].flat()) search.append(key, v);
|
|
275
|
+
const query = search.toString();
|
|
276
|
+
return { pathname, href: query ? `${pathname}?${query}` : pathname, params, segments, name };
|
|
277
|
+
}
|
|
278
|
+
function settle2(navigation, go) {
|
|
279
|
+
return new Promise((resolve, reject) => {
|
|
280
|
+
let off;
|
|
281
|
+
const done = () => {
|
|
282
|
+
off?.();
|
|
283
|
+
clearTimeout(timer);
|
|
284
|
+
resolve();
|
|
285
|
+
};
|
|
286
|
+
const timer = setTimeout(done, navigation.addListener ? SETTLE_MS : 0);
|
|
287
|
+
off = navigation.addListener?.("__unsafe_action__", done);
|
|
288
|
+
try {
|
|
289
|
+
go();
|
|
290
|
+
} catch (error) {
|
|
291
|
+
off?.();
|
|
292
|
+
clearTimeout(timer);
|
|
293
|
+
reject(error);
|
|
294
|
+
}
|
|
295
|
+
});
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
// src/adapters/device/link.ts
|
|
299
|
+
var expoLinking = () => globalThis.expo?.modules?.ExpoLinking;
|
|
300
|
+
function linkArea(rn, navigation) {
|
|
301
|
+
const restore = () => {
|
|
302
|
+
const st = deviceState();
|
|
303
|
+
const had = st.origGetInitialURL !== void 0;
|
|
304
|
+
if (had) rn().Linking.getInitialURL = st.origGetInitialURL;
|
|
305
|
+
const url = st.initialUrl;
|
|
306
|
+
delete st.origGetInitialURL;
|
|
307
|
+
delete st.initialUrl;
|
|
308
|
+
return { restored: had, ...url && { url } };
|
|
309
|
+
};
|
|
310
|
+
return {
|
|
311
|
+
pending: () => {
|
|
312
|
+
const { initialUrl } = deviceState();
|
|
313
|
+
return initialUrl ? { initialUrl } : null;
|
|
314
|
+
},
|
|
315
|
+
restore,
|
|
316
|
+
tools: {
|
|
317
|
+
"device.openUrl": {
|
|
318
|
+
description: 'Deliver an incoming deep link, e.g. ["myapp://orders/5"], to every Linking "url" listener and, when the native expo-linking module exists, its useLinkingURL() hook without leaving the app, and make Linking.getInitialURL() return it until device.restore. getLinkingURL() still reads the native value. A cold start already read its URL, so this tests links that arrive while the app runs. Returns { url, listeners } and, with the navigation container, the route the app ended on.',
|
|
319
|
+
maxArgs: 1,
|
|
320
|
+
run: async (url) => {
|
|
321
|
+
if (typeof url !== "string" || !url) throw new Error('Pass the URL, e.g. "myapp://orders/5"');
|
|
322
|
+
const { Linking, DeviceEventEmitter } = rn();
|
|
323
|
+
const st = deviceState();
|
|
324
|
+
if (st.origGetInitialURL === void 0) {
|
|
325
|
+
st.origGetInitialURL = Linking.getInitialURL;
|
|
326
|
+
Linking.getInitialURL = () => Promise.resolve(deviceState().initialUrl ?? null);
|
|
327
|
+
}
|
|
328
|
+
st.initialUrl = url;
|
|
329
|
+
const native = expoLinking();
|
|
330
|
+
const emit = () => {
|
|
331
|
+
DeviceEventEmitter.emit("url", { url });
|
|
332
|
+
if (typeof native?.emit === "function") native.emit("onURLReceived", { url });
|
|
333
|
+
};
|
|
334
|
+
const listeners2 = DeviceEventEmitter.listenerCount?.("url");
|
|
335
|
+
if (!navigation || navigation.isReady && !navigation.isReady()) {
|
|
336
|
+
emit();
|
|
337
|
+
return { url, listeners: listeners2 };
|
|
338
|
+
}
|
|
339
|
+
await settle2(navigation, emit);
|
|
340
|
+
await settle();
|
|
341
|
+
const route = routeFromState(navigation.getRootState());
|
|
342
|
+
return { url, listeners: listeners2, route };
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
};
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
// src/adapters/device/notification.ts
|
|
350
|
+
var count = 0;
|
|
351
|
+
function emitter(rn) {
|
|
352
|
+
const native = globalThis.expo?.modules?.ExpoNotificationsEmitter;
|
|
353
|
+
return typeof native?.emit === "function" ? { target: native, via: "ExpoNotificationsEmitter" } : { target: rn.DeviceEventEmitter, via: "DeviceEventEmitter" };
|
|
354
|
+
}
|
|
355
|
+
function notificationArea(rn, Notifications) {
|
|
356
|
+
return {
|
|
357
|
+
pending: () => null,
|
|
358
|
+
restore: () => ({}),
|
|
359
|
+
tools: {
|
|
360
|
+
"device.notification": {
|
|
361
|
+
description: "Deliver a push notification to expo-notifications listeners: [{ title?, body?, data? }, { tapped? }]. By default addNotificationReceivedListener fires (app open); { tapped: true } fires addNotificationResponseReceivedListener with the default action, as if the user tapped it. Returns { id, event, via, listeners }: which emitter it used and how many listeners it reached (0 means nothing heard it).",
|
|
362
|
+
maxArgs: 2,
|
|
363
|
+
run: (content, options = {}) => {
|
|
364
|
+
if (!content || typeof content !== "object" || Array.isArray(content))
|
|
365
|
+
throw new Error("Pass { title, body, data }");
|
|
366
|
+
const { tapped } = options ?? {};
|
|
367
|
+
const { title = null, body = null, data = {}, ...rest } = content;
|
|
368
|
+
const id = `agent-bridge-${++count}`;
|
|
369
|
+
const notification = {
|
|
370
|
+
date: Date.now() / 1e3,
|
|
371
|
+
request: {
|
|
372
|
+
identifier: id,
|
|
373
|
+
content: { title, body, subtitle: null, sound: null, badge: null, data, ...rest },
|
|
374
|
+
trigger: { type: "push" }
|
|
375
|
+
}
|
|
376
|
+
};
|
|
377
|
+
const { target, via } = emitter(rn());
|
|
378
|
+
const send = (event, payload) => {
|
|
379
|
+
target.emit(event, payload);
|
|
380
|
+
return target.listenerCount?.(event);
|
|
381
|
+
};
|
|
382
|
+
if (tapped) {
|
|
383
|
+
const listeners3 = send("onDidReceiveNotificationResponse", {
|
|
384
|
+
notification,
|
|
385
|
+
actionIdentifier: Notifications.DEFAULT_ACTION_IDENTIFIER ?? "expo.modules.notifications.actions.DEFAULT"
|
|
386
|
+
});
|
|
387
|
+
return { id, event: "response", via, listeners: listeners3 };
|
|
388
|
+
}
|
|
389
|
+
const listeners2 = send("onDidReceiveNotification", notification);
|
|
390
|
+
return { id, event: "received", via, listeners: listeners2 };
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
};
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
// src/shared/mock-signal.ts
|
|
398
|
+
var KEY2 = /* @__PURE__ */ Symbol.for("@avasapp/agent-bridge/mock-signal");
|
|
399
|
+
var listeners = () => {
|
|
400
|
+
const g = globalThis;
|
|
401
|
+
return g[KEY2] ?? (g[KEY2] = /* @__PURE__ */ new Set());
|
|
402
|
+
};
|
|
403
|
+
var signalRequestStarted = (requestId) => {
|
|
404
|
+
for (const l of [...listeners()]) l.started?.(requestId);
|
|
405
|
+
};
|
|
406
|
+
var signalMockAnswered = (mockId, requestId) => {
|
|
407
|
+
for (const l of [...listeners()]) l.answered?.(mockId, requestId);
|
|
408
|
+
};
|
|
409
|
+
|
|
410
|
+
// src/network/api.ts
|
|
411
|
+
function queryOf(url) {
|
|
412
|
+
const search = /\?([^#]*)/.exec(url)?.[1];
|
|
413
|
+
const out = {};
|
|
414
|
+
if (!search) return out;
|
|
415
|
+
for (const pair of search.split("&")) {
|
|
416
|
+
if (!pair) continue;
|
|
417
|
+
const [key, value = ""] = pair.split("=");
|
|
418
|
+
const decode2 = (s) => {
|
|
419
|
+
try {
|
|
420
|
+
return decodeURIComponent(s.replace(/\+/g, " "));
|
|
421
|
+
} catch {
|
|
422
|
+
return s;
|
|
423
|
+
}
|
|
424
|
+
};
|
|
425
|
+
out[decode2(key)] = decode2(value);
|
|
426
|
+
}
|
|
427
|
+
return out;
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
// src/network/body.ts
|
|
431
|
+
var MAX_BODY = 2048;
|
|
432
|
+
var MAX_FULL_BODY = 1e6;
|
|
433
|
+
var FULL_BUDGET = 4e6;
|
|
434
|
+
function compactBody(text) {
|
|
435
|
+
if (text === void 0 || text === "") return void 0;
|
|
436
|
+
try {
|
|
437
|
+
return JSON.stringify(JSON.parse(text));
|
|
438
|
+
} catch {
|
|
439
|
+
return text;
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
function truncateBody(text) {
|
|
443
|
+
return text.length > MAX_BODY ? `${text.slice(0, MAX_BODY)}\u2026 (+${text.length - MAX_BODY} chars)` : text;
|
|
444
|
+
}
|
|
445
|
+
function bodyText(body) {
|
|
446
|
+
if (body === void 0 || body === null) return void 0;
|
|
447
|
+
if (typeof body === "string") return body;
|
|
448
|
+
if (typeof URLSearchParams !== "undefined" && body instanceof URLSearchParams)
|
|
449
|
+
return body.toString();
|
|
450
|
+
if (typeof FormData !== "undefined" && body instanceof FormData)
|
|
451
|
+
return "[FormData]";
|
|
452
|
+
if (typeof Blob !== "undefined" && body instanceof Blob)
|
|
453
|
+
return `[Blob ${body.size} bytes]`;
|
|
454
|
+
if (body instanceof ArrayBuffer || ArrayBuffer.isView(body))
|
|
455
|
+
return `[binary ${body.byteLength} bytes]`;
|
|
456
|
+
return String(body);
|
|
457
|
+
}
|
|
458
|
+
function isTextual(contentType) {
|
|
459
|
+
if (!contentType) return true;
|
|
460
|
+
if (/event-stream/i.test(contentType)) return false;
|
|
461
|
+
return /json|text|xml|javascript|x-www-form-urlencoded/i.test(contentType);
|
|
462
|
+
}
|
|
463
|
+
function errorMessage(error) {
|
|
464
|
+
return error instanceof Error ? error.message : String(error);
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
// src/network/headers.ts
|
|
468
|
+
var lower = (headers) => Object.fromEntries(Object.entries(headers).map(([k, v]) => [k.toLowerCase(), v]));
|
|
469
|
+
function keepHeaders(headers) {
|
|
470
|
+
return headers && Object.keys(headers).length ? lower(headers) : void 0;
|
|
471
|
+
}
|
|
472
|
+
function parseHeaderBlock(block) {
|
|
473
|
+
const out = {};
|
|
474
|
+
for (const line of (block ?? "").split(/\r?\n/)) {
|
|
475
|
+
const at = line.indexOf(":");
|
|
476
|
+
if (at <= 0) continue;
|
|
477
|
+
const name = line.slice(0, at).trim().toLowerCase();
|
|
478
|
+
const value = line.slice(at + 1).trim();
|
|
479
|
+
out[name] = name in out ? `${out[name]}, ${value}` : value;
|
|
480
|
+
}
|
|
481
|
+
return out;
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
// src/network/match.ts
|
|
485
|
+
function operationName(probe) {
|
|
486
|
+
const json = probe.json();
|
|
487
|
+
if (json && typeof json === "object" && !Array.isArray(json)) {
|
|
488
|
+
const name = json.operationName;
|
|
489
|
+
if (typeof name === "string") return name;
|
|
490
|
+
}
|
|
491
|
+
return queryOf(probe.url).operationName;
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
// src/network/state.ts
|
|
495
|
+
var LOG_SIZE = 100;
|
|
496
|
+
var KEY3 = /* @__PURE__ */ Symbol.for("@avasapp/agent-bridge/network");
|
|
497
|
+
function networkState() {
|
|
498
|
+
const g = globalThis;
|
|
499
|
+
g[KEY3] ?? (g[KEY3] = {
|
|
500
|
+
installed: false,
|
|
501
|
+
log: [],
|
|
502
|
+
nextLogId: 1,
|
|
503
|
+
logSize: LOG_SIZE,
|
|
504
|
+
mocks: [],
|
|
505
|
+
nextSeq: 1,
|
|
506
|
+
insideFetch: 0,
|
|
507
|
+
devHost: null,
|
|
508
|
+
skip: [],
|
|
509
|
+
uninstall: [],
|
|
510
|
+
strictApp: [],
|
|
511
|
+
strictAgent: null,
|
|
512
|
+
blocked: /* @__PURE__ */ new Map()
|
|
513
|
+
});
|
|
514
|
+
return g[KEY3];
|
|
515
|
+
}
|
|
516
|
+
var URL_PARTS = /^(?:[a-z][a-z0-9+.-]*:)?(?:\/\/([^/?#]*))?([^?#]*)/i;
|
|
517
|
+
var hostOf = (url) => URL_PARTS.exec(url)?.[1] ?? "";
|
|
518
|
+
var pathOf = (url) => URL_PARTS.exec(url)?.[2] ?? "";
|
|
519
|
+
var DEV_PATH = /^\/(symbolicate|logs|status|hot|message|inspector|expo-dev-plugins|_expo)(\/.*)?$|\.(bundle|map)$/;
|
|
520
|
+
function isSkipped(state, url) {
|
|
521
|
+
const host = hostOf(url);
|
|
522
|
+
if (state.devHost && host === state.devHost) return true;
|
|
523
|
+
if (DEV_PATH.test(pathOf(url))) return true;
|
|
524
|
+
return state.skip.some(
|
|
525
|
+
(rule) => typeof rule === "string" ? url.includes(rule) : rule.test(url)
|
|
526
|
+
);
|
|
527
|
+
}
|
|
528
|
+
function deriveDevHost() {
|
|
529
|
+
try {
|
|
530
|
+
const rn = require("react-native");
|
|
531
|
+
const module2 = rn.TurboModuleRegistry?.get?.("SourceCode") ?? rn.NativeModules?.SourceCode;
|
|
532
|
+
const scriptURL = module2?.getConstants?.().scriptURL ?? module2?.scriptURL;
|
|
533
|
+
return typeof scriptURL === "string" && /^https?:/i.test(scriptURL) ? hostOf(scriptURL) || null : null;
|
|
534
|
+
} catch {
|
|
535
|
+
return null;
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
var FULL = /* @__PURE__ */ new WeakMap();
|
|
539
|
+
var fullOf = (entry) => {
|
|
540
|
+
let full = FULL.get(entry);
|
|
541
|
+
if (!full) FULL.set(entry, full = {});
|
|
542
|
+
return full;
|
|
543
|
+
};
|
|
544
|
+
function enforceBudget() {
|
|
545
|
+
const { log } = networkState();
|
|
546
|
+
const size = (f) => (f?.requestBody?.length ?? 0) + (f?.responseBody?.length ?? 0);
|
|
547
|
+
let total = log.reduce((sum, e) => sum + size(FULL.get(e)), 0);
|
|
548
|
+
for (const entry of log) {
|
|
549
|
+
if (total <= FULL_BUDGET) return;
|
|
550
|
+
const full = FULL.get(entry);
|
|
551
|
+
if (!full || !size(full)) continue;
|
|
552
|
+
total -= size(full);
|
|
553
|
+
delete full.requestBody;
|
|
554
|
+
if (full.responseBody !== void 0) {
|
|
555
|
+
delete full.responseBody;
|
|
556
|
+
full.response = "evicted";
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
function keepRequestBody(entry, text) {
|
|
561
|
+
const body = compactBody(text);
|
|
562
|
+
if (body === void 0) return;
|
|
563
|
+
entry.requestBody = truncateBody(body);
|
|
564
|
+
if (text.length <= MAX_FULL_BODY) {
|
|
565
|
+
fullOf(entry).requestBody = text;
|
|
566
|
+
enforceBudget();
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
function setResponseBody(entry, text, contentType) {
|
|
570
|
+
const full = fullOf(entry);
|
|
571
|
+
if (contentType !== void 0) full.contentType = contentType;
|
|
572
|
+
if (text === void 0) {
|
|
573
|
+
full.response = "none";
|
|
574
|
+
return;
|
|
575
|
+
}
|
|
576
|
+
const body = compactBody(text);
|
|
577
|
+
if (body !== void 0) entry.responseBody = truncateBody(body);
|
|
578
|
+
if (text.length > MAX_FULL_BODY) {
|
|
579
|
+
full.response = "cut";
|
|
580
|
+
return;
|
|
581
|
+
}
|
|
582
|
+
full.responseBody = text;
|
|
583
|
+
full.response = "kept";
|
|
584
|
+
enforceBudget();
|
|
585
|
+
}
|
|
586
|
+
function markResponse(entry, response) {
|
|
587
|
+
fullOf(entry).response = response;
|
|
588
|
+
}
|
|
589
|
+
function startEntry(state, fields) {
|
|
590
|
+
const entry = {
|
|
591
|
+
id: state.nextLogId++,
|
|
592
|
+
method: fields.method,
|
|
593
|
+
url: fields.url,
|
|
594
|
+
startedAt: Date.now(),
|
|
595
|
+
ms: 0,
|
|
596
|
+
pending: true
|
|
597
|
+
};
|
|
598
|
+
keepRequestBody(entry, fields.requestBody);
|
|
599
|
+
const requestHeaders = keepHeaders(fields.requestHeaders);
|
|
600
|
+
if (requestHeaders) fullOf(entry).requestHeaders = requestHeaders;
|
|
601
|
+
state.log.push(entry);
|
|
602
|
+
signalRequestStarted(entry.id);
|
|
603
|
+
if (state.log.length > state.logSize)
|
|
604
|
+
state.log.splice(0, state.log.length - state.logSize);
|
|
605
|
+
return entry;
|
|
606
|
+
}
|
|
607
|
+
function finishEntry(entry, fields) {
|
|
608
|
+
entry.ms = Date.now() - entry.startedAt;
|
|
609
|
+
delete entry.pending;
|
|
610
|
+
const { responseBody, contentType, responseHeaders, ...rest } = fields;
|
|
611
|
+
const kept = keepHeaders(responseHeaders);
|
|
612
|
+
if (kept) fullOf(entry).responseHeaders = kept;
|
|
613
|
+
for (const [key, value] of Object.entries(rest))
|
|
614
|
+
if (value !== void 0) entry[key] = value;
|
|
615
|
+
if (responseBody !== void 0) setResponseBody(entry, responseBody, contentType);
|
|
616
|
+
}
|
|
617
|
+
function setRequestBody(entry, text) {
|
|
618
|
+
if (entry.requestBody === void 0) keepRequestBody(entry, text);
|
|
619
|
+
}
|
|
620
|
+
function removeMocks(state, which) {
|
|
621
|
+
const before = state.mocks.length;
|
|
622
|
+
state.mocks = state.mocks.filter((m) => !which(m));
|
|
623
|
+
return before - state.mocks.length;
|
|
624
|
+
}
|
|
625
|
+
var mockOrder = (a, b) => Number(b.source === "agent") - Number(a.source === "agent") || b.priority - a.priority || b.seq - a.seq;
|
|
626
|
+
function setDeviceOffline(offline) {
|
|
627
|
+
networkState().deviceOffline = offline;
|
|
628
|
+
}
|
|
629
|
+
function matchingMocks(state, method, url, body) {
|
|
630
|
+
if (state.deviceOffline) return [];
|
|
631
|
+
const found = state.mocks.filter((m) => m.test(method, url));
|
|
632
|
+
const probe = body && found.some((m) => m.needsBody) ? mockRequest(method, url, {}, body.text) : null;
|
|
633
|
+
return found.filter((m) => !m.needsBody || !probe || m.testBody(probe)).sort(mockOrder);
|
|
634
|
+
}
|
|
635
|
+
async function answer(state, candidates, request, bodyKnown = true) {
|
|
636
|
+
for (const mock of candidates) {
|
|
637
|
+
if (mock.needsBody && !bodyKnown && !mock.testBody(await request())) continue;
|
|
638
|
+
if (!state.mocks.includes(mock)) continue;
|
|
639
|
+
const response = typeof mock.response === "function" ? await mock.response(await request()) : mock.response;
|
|
640
|
+
if (!response || !state.mocks.includes(mock)) continue;
|
|
641
|
+
mock.hits += 1;
|
|
642
|
+
if (mock.times !== void 0 && mock.hits >= mock.times)
|
|
643
|
+
removeMocks(state, (m) => m === mock);
|
|
644
|
+
return { mock, response };
|
|
645
|
+
}
|
|
646
|
+
return null;
|
|
647
|
+
}
|
|
648
|
+
function mockRequest(method, url, headers, body) {
|
|
649
|
+
let parsed;
|
|
650
|
+
return {
|
|
651
|
+
method,
|
|
652
|
+
url,
|
|
653
|
+
headers,
|
|
654
|
+
body,
|
|
655
|
+
json: () => {
|
|
656
|
+
if (!parsed) {
|
|
657
|
+
try {
|
|
658
|
+
parsed = { value: body === void 0 ? void 0 : JSON.parse(body) };
|
|
659
|
+
} catch {
|
|
660
|
+
parsed = { value: void 0 };
|
|
661
|
+
}
|
|
662
|
+
}
|
|
663
|
+
return parsed.value;
|
|
664
|
+
}
|
|
665
|
+
};
|
|
666
|
+
}
|
|
667
|
+
var contentTypeOf = (headers) => Object.entries(headers).find(([k]) => k.toLowerCase() === "content-type")?.[1];
|
|
668
|
+
function responseParts(response) {
|
|
669
|
+
const r = response;
|
|
670
|
+
const status = r.status ?? 200;
|
|
671
|
+
if (r.json !== void 0)
|
|
672
|
+
return {
|
|
673
|
+
status,
|
|
674
|
+
text: JSON.stringify(r.json),
|
|
675
|
+
headers: { "content-type": "application/json", ...r.headers }
|
|
676
|
+
};
|
|
677
|
+
return { status, text: r.body ?? "", headers: { ...r.headers } };
|
|
678
|
+
}
|
|
679
|
+
function strictRules(state) {
|
|
680
|
+
if (state.strictAgent === false) return [];
|
|
681
|
+
return state.strictAgent ? [state.strictAgent, ...state.strictApp] : state.strictApp;
|
|
682
|
+
}
|
|
683
|
+
var BLOCKED_HEADER = "x-agent-bridge";
|
|
684
|
+
function blockedResponse(state, method, url, body) {
|
|
685
|
+
if (state.deviceOffline) return { offline: true };
|
|
686
|
+
const rules = strictRules(state);
|
|
687
|
+
if (!rules.length || rules.some((r) => r.allow.some((test) => test(url))))
|
|
688
|
+
return null;
|
|
689
|
+
const operation = operationName(mockRequest(method, url, {}, body));
|
|
690
|
+
const key = operation ? `${method} ${url} (${operation})` : `${method} ${url}`;
|
|
691
|
+
const probe = mockRequest(method, url, {}, body);
|
|
692
|
+
const near = state.mocks.filter((m) => m.needsBody && m.test(method, url) && !m.testBody(probe)).map((m) => m.id);
|
|
693
|
+
const seen = state.blocked.get(key);
|
|
694
|
+
if (seen) seen.count += 1;
|
|
695
|
+
else state.blocked.set(key, { method, url, count: 1, ...operation && { operation } });
|
|
696
|
+
const error = `agent-bridge strict network: no mock for ${key}${near.length ? ` (${near.join(", ")} matched the URL but not the body)` : ""}`;
|
|
697
|
+
console.error(`${error}. Mock it, or allow it in strict mode.`);
|
|
698
|
+
if (rules[0].offline) return { offline: true };
|
|
699
|
+
return {
|
|
700
|
+
status: rules[0].status,
|
|
701
|
+
json: { error },
|
|
702
|
+
headers: { [BLOCKED_HEADER]: "blocked" }
|
|
703
|
+
};
|
|
704
|
+
}
|
|
705
|
+
var isOffline = (response) => response.offline === true;
|
|
706
|
+
var OFFLINE_MESSAGE = "Network request failed";
|
|
707
|
+
var sleep = (ms) => ms > 0 ? new Promise((resolve) => setTimeout(resolve, ms)) : Promise.resolve();
|
|
708
|
+
|
|
709
|
+
// src/network/fetch.ts
|
|
710
|
+
function headersRecord(headers) {
|
|
711
|
+
const out = {};
|
|
712
|
+
if (!headers) return out;
|
|
713
|
+
const h = headers;
|
|
714
|
+
if (Array.isArray(headers)) {
|
|
715
|
+
for (const [key, value] of headers) out[String(key).toLowerCase()] = String(value);
|
|
716
|
+
} else if (typeof h.forEach === "function") {
|
|
717
|
+
h.forEach((value, key) => {
|
|
718
|
+
out[key.toLowerCase()] = value;
|
|
719
|
+
});
|
|
720
|
+
} else {
|
|
721
|
+
for (const [key, value] of Object.entries(headers))
|
|
722
|
+
out[key.toLowerCase()] = String(value);
|
|
723
|
+
}
|
|
724
|
+
return out;
|
|
725
|
+
}
|
|
726
|
+
var nullBody = (status) => [101, 204, 205, 304].includes(status);
|
|
727
|
+
function mockedResponse(response) {
|
|
728
|
+
const { status, text, headers } = responseParts(response);
|
|
729
|
+
const body = nullBody(status) ? null : text;
|
|
730
|
+
if (typeof Response !== "undefined")
|
|
731
|
+
return new Response(body, { status, headers });
|
|
732
|
+
return {
|
|
733
|
+
ok: status >= 200 && status < 300,
|
|
734
|
+
status,
|
|
735
|
+
statusText: "",
|
|
736
|
+
url: "",
|
|
737
|
+
headers: { get: (name) => headers[name.toLowerCase()] ?? null },
|
|
738
|
+
text: async () => text,
|
|
739
|
+
json: async () => JSON.parse(text),
|
|
740
|
+
clone() {
|
|
741
|
+
return mockedResponse(response);
|
|
742
|
+
}
|
|
743
|
+
};
|
|
744
|
+
}
|
|
745
|
+
function abortError() {
|
|
746
|
+
const error = new Error("Aborted");
|
|
747
|
+
error.name = "AbortError";
|
|
748
|
+
return error;
|
|
749
|
+
}
|
|
750
|
+
function patchFetch(state) {
|
|
751
|
+
const g = globalThis;
|
|
752
|
+
const original = g.fetch;
|
|
753
|
+
if (typeof original !== "function") return null;
|
|
754
|
+
const callOriginal = (input, init) => {
|
|
755
|
+
state.insideFetch += 1;
|
|
756
|
+
try {
|
|
757
|
+
return original.call(globalThis, input, init);
|
|
758
|
+
} finally {
|
|
759
|
+
state.insideFetch -= 1;
|
|
760
|
+
}
|
|
761
|
+
};
|
|
762
|
+
const wrapped = function fetch(input, init) {
|
|
763
|
+
const req = typeof input === "object" && input ? input : {};
|
|
764
|
+
const url = typeof input === "string" ? input : req.url ?? String(input);
|
|
765
|
+
if (isSkipped(state, url)) return callOriginal(input, init);
|
|
766
|
+
const method = String(init?.method ?? req.method ?? "GET").toUpperCase();
|
|
767
|
+
let requestBody = bodyText(init?.body);
|
|
768
|
+
const entry = startEntry(state, {
|
|
769
|
+
method,
|
|
770
|
+
url,
|
|
771
|
+
requestBody,
|
|
772
|
+
requestHeaders: headersRecord(init?.headers ?? req.headers)
|
|
773
|
+
});
|
|
774
|
+
const real = () => {
|
|
775
|
+
let promise;
|
|
776
|
+
try {
|
|
777
|
+
promise = callOriginal(input, init);
|
|
778
|
+
} catch (error) {
|
|
779
|
+
finishEntry(entry, { error: errorMessage(error) });
|
|
780
|
+
throw error;
|
|
781
|
+
}
|
|
782
|
+
return promise.then(
|
|
783
|
+
(res) => {
|
|
784
|
+
finishEntry(entry, { status: res.status, responseHeaders: headersRecord(res.headers) });
|
|
785
|
+
const type = res.headers?.get?.("content-type") ?? void 0;
|
|
786
|
+
if (isTextual(type) && typeof res.clone === "function") {
|
|
787
|
+
markResponse(entry, "pending");
|
|
788
|
+
res.clone().text().then((text) => setResponseBody(entry, text, type)).catch(() => markResponse(entry, "none"));
|
|
789
|
+
} else markResponse(entry, "none");
|
|
790
|
+
return res;
|
|
791
|
+
},
|
|
792
|
+
(error) => {
|
|
793
|
+
finishEntry(entry, { error: errorMessage(error) });
|
|
794
|
+
throw error;
|
|
795
|
+
}
|
|
796
|
+
);
|
|
797
|
+
};
|
|
798
|
+
const passOn = () => {
|
|
799
|
+
const blocked = blockedResponse(state, method, url, requestBody);
|
|
800
|
+
if (!blocked) return real();
|
|
801
|
+
if (!state.deviceOffline) entry.blocked = true;
|
|
802
|
+
if (isOffline(blocked)) {
|
|
803
|
+
finishEntry(entry, { error: `${OFFLINE_MESSAGE} (${state.deviceOffline ? "device offline" : "strict network"})` });
|
|
804
|
+
return Promise.reject(new TypeError(OFFLINE_MESSAGE));
|
|
805
|
+
}
|
|
806
|
+
const { status, text, headers } = responseParts(blocked);
|
|
807
|
+
finishEntry(entry, { status, responseBody: text, contentType: contentTypeOf(headers), responseHeaders: headers });
|
|
808
|
+
return Promise.resolve(mockedResponse(blocked));
|
|
809
|
+
};
|
|
810
|
+
const bodyKnown = init?.body !== void 0 || !req.clone || method === "GET" || method === "HEAD" || !isTextual(headersRecord(init?.headers ?? req.headers)["content-type"]);
|
|
811
|
+
const candidates = matchingMocks(state, method, url, bodyKnown ? { text: requestBody } : void 0);
|
|
812
|
+
if (!candidates.length) return passOn();
|
|
813
|
+
let built;
|
|
814
|
+
const request = () => built ?? (built = (async () => {
|
|
815
|
+
let body = requestBody;
|
|
816
|
+
if (body === void 0 && !bodyKnown) {
|
|
817
|
+
body = await Promise.resolve().then(() => req.clone().text()).catch(() => void 0);
|
|
818
|
+
setRequestBody(entry, body);
|
|
819
|
+
requestBody = body;
|
|
820
|
+
}
|
|
821
|
+
return mockRequest(
|
|
822
|
+
method,
|
|
823
|
+
url,
|
|
824
|
+
headersRecord(init?.headers ?? req.headers),
|
|
825
|
+
body || void 0
|
|
826
|
+
);
|
|
827
|
+
})());
|
|
828
|
+
const answered = answer(state, candidates, request, bodyKnown).catch((error) => {
|
|
829
|
+
entry.mocked = true;
|
|
830
|
+
finishEntry(entry, { error: errorMessage(error) });
|
|
831
|
+
throw error;
|
|
832
|
+
});
|
|
833
|
+
return answered.then(async (hit) => {
|
|
834
|
+
if (!hit) return passOn();
|
|
835
|
+
entry.mocked = true;
|
|
836
|
+
if (hit.mock.source === "agent") signalMockAnswered(hit.mock.id, entry.id);
|
|
837
|
+
await sleep(hit.mock.delayMs);
|
|
838
|
+
if (init?.signal?.aborted) {
|
|
839
|
+
finishEntry(entry, { error: "aborted" });
|
|
840
|
+
throw abortError();
|
|
841
|
+
}
|
|
842
|
+
if (isOffline(hit.response)) {
|
|
843
|
+
finishEntry(entry, { error: `${OFFLINE_MESSAGE} (mocked offline)` });
|
|
844
|
+
throw new TypeError(OFFLINE_MESSAGE);
|
|
845
|
+
}
|
|
846
|
+
const { status, text, headers } = responseParts(hit.response);
|
|
847
|
+
finishEntry(entry, { status, responseBody: text, contentType: contentTypeOf(headers), responseHeaders: headers });
|
|
848
|
+
return mockedResponse(hit.response);
|
|
849
|
+
});
|
|
850
|
+
};
|
|
851
|
+
g.fetch = wrapped;
|
|
852
|
+
return () => {
|
|
853
|
+
if (g.fetch === wrapped) g.fetch = original;
|
|
854
|
+
};
|
|
855
|
+
}
|
|
856
|
+
|
|
857
|
+
// src/network/xhr.ts
|
|
858
|
+
var META = /* @__PURE__ */ Symbol("agent-bridge.xhr");
|
|
859
|
+
var isRN = (xhr) => typeof xhr.__didReceiveResponse === "function" && typeof xhr.__didCompleteResponse === "function";
|
|
860
|
+
function responseTextOf(xhr) {
|
|
861
|
+
try {
|
|
862
|
+
if (xhr.responseType === "" || xhr.responseType === "text") return xhr.responseText;
|
|
863
|
+
if (xhr.responseType === "json") return JSON.stringify(xhr.response);
|
|
864
|
+
} catch {
|
|
865
|
+
}
|
|
866
|
+
return void 0;
|
|
867
|
+
}
|
|
868
|
+
function track(xhr, entry) {
|
|
869
|
+
let error;
|
|
870
|
+
xhr.addEventListener("error", () => error = OFFLINE_MESSAGE);
|
|
871
|
+
xhr.addEventListener("timeout", () => error = "timed out");
|
|
872
|
+
xhr.addEventListener("abort", () => error = "aborted");
|
|
873
|
+
xhr.addEventListener("loadend", () => {
|
|
874
|
+
const text = error ? void 0 : responseTextOf(xhr);
|
|
875
|
+
finishEntry(entry, {
|
|
876
|
+
status: error ? void 0 : xhr.status,
|
|
877
|
+
error,
|
|
878
|
+
responseBody: text,
|
|
879
|
+
contentType: xhr.getResponseHeader?.("content-type") ?? void 0,
|
|
880
|
+
responseHeaders: error ? void 0 : parseHeaderBlock(xhr.getAllResponseHeaders?.())
|
|
881
|
+
});
|
|
882
|
+
if (!error && text === void 0) markResponse(entry, "none");
|
|
883
|
+
});
|
|
884
|
+
}
|
|
885
|
+
var fakeRequestId = 0;
|
|
886
|
+
function fire(xhr, type) {
|
|
887
|
+
const Ctor = (type === "readystatechange" ? void 0 : globalThis.ProgressEvent) ?? globalThis.Event;
|
|
888
|
+
xhr.dispatchEvent(new Ctor(type));
|
|
889
|
+
}
|
|
890
|
+
function respond(xhr, mocked, url) {
|
|
891
|
+
const offline = isOffline(mocked);
|
|
892
|
+
const { status, text, headers } = responseParts(mocked);
|
|
893
|
+
if (isRN(xhr)) {
|
|
894
|
+
const id = -++fakeRequestId;
|
|
895
|
+
xhr.__didCreateRequest(id);
|
|
896
|
+
if (offline) return xhr.__didCompleteResponse(id, OFFLINE_MESSAGE, false);
|
|
897
|
+
xhr.__didReceiveResponse(id, status, headers, url);
|
|
898
|
+
if (xhr.responseType === "arraybuffer" && typeof btoa === "function") {
|
|
899
|
+
xhr.__didReceiveData(id, btoa(unescape(encodeURIComponent(text))));
|
|
900
|
+
} else if (xhr.responseType !== "blob") {
|
|
901
|
+
xhr.__didReceiveData(id, text);
|
|
902
|
+
}
|
|
903
|
+
return xhr.__didCompleteResponse(id, "", false);
|
|
904
|
+
}
|
|
905
|
+
const define = (values) => {
|
|
906
|
+
for (const [key, value] of Object.entries(values))
|
|
907
|
+
Object.defineProperty(xhr, key, { value, configurable: true });
|
|
908
|
+
};
|
|
909
|
+
const lower2 = Object.fromEntries(
|
|
910
|
+
Object.entries(headers).map(([k, v]) => [k.toLowerCase(), v])
|
|
911
|
+
);
|
|
912
|
+
let response = text;
|
|
913
|
+
if (xhr.responseType === "json") {
|
|
914
|
+
try {
|
|
915
|
+
response = JSON.parse(text);
|
|
916
|
+
} catch {
|
|
917
|
+
response = null;
|
|
918
|
+
}
|
|
919
|
+
}
|
|
920
|
+
define({
|
|
921
|
+
readyState: 4,
|
|
922
|
+
status: offline ? 0 : status,
|
|
923
|
+
statusText: "",
|
|
924
|
+
responseURL: url,
|
|
925
|
+
responseText: offline ? "" : text,
|
|
926
|
+
response: offline ? "" : response,
|
|
927
|
+
getAllResponseHeaders: () => offline ? "" : Object.entries(lower2).map(([k, v]) => `${k}: ${v}\r
|
|
928
|
+
`).join(""),
|
|
929
|
+
getResponseHeader: (name) => lower2[name.toLowerCase()] ?? null
|
|
930
|
+
});
|
|
931
|
+
fire(xhr, "readystatechange");
|
|
932
|
+
fire(xhr, offline ? "error" : "load");
|
|
933
|
+
fire(xhr, "loadend");
|
|
934
|
+
}
|
|
935
|
+
function patchXhr(state) {
|
|
936
|
+
const XHR = globalThis.XMLHttpRequest;
|
|
937
|
+
if (typeof XHR !== "function") return null;
|
|
938
|
+
const proto = XHR.prototype;
|
|
939
|
+
const { open, send, setRequestHeader, abort } = proto;
|
|
940
|
+
proto.open = function(method, url, ...rest) {
|
|
941
|
+
this[META] = {
|
|
942
|
+
method: String(method).toUpperCase(),
|
|
943
|
+
url: String(url),
|
|
944
|
+
// Opened by the patched fetch's own original: fetch logs it already.
|
|
945
|
+
internal: state.insideFetch > 0,
|
|
946
|
+
headers: {}
|
|
947
|
+
};
|
|
948
|
+
return open.call(this, method, url, ...rest);
|
|
949
|
+
};
|
|
950
|
+
proto.setRequestHeader = function(name, value) {
|
|
951
|
+
const meta = this[META];
|
|
952
|
+
if (meta) meta.headers[name.toLowerCase()] = value;
|
|
953
|
+
return setRequestHeader.call(this, name, value);
|
|
954
|
+
};
|
|
955
|
+
proto.abort = function() {
|
|
956
|
+
const meta = this[META];
|
|
957
|
+
if (meta) meta.aborted = true;
|
|
958
|
+
return abort.call(this);
|
|
959
|
+
};
|
|
960
|
+
proto.send = function(body) {
|
|
961
|
+
const meta = this[META];
|
|
962
|
+
if (!meta || meta.internal || isSkipped(state, meta.url))
|
|
963
|
+
return send.call(this, body);
|
|
964
|
+
meta.aborted = false;
|
|
965
|
+
const { method, url } = meta;
|
|
966
|
+
const text = bodyText(body);
|
|
967
|
+
const entry = startEntry(state, {
|
|
968
|
+
method,
|
|
969
|
+
url,
|
|
970
|
+
requestBody: text,
|
|
971
|
+
requestHeaders: meta.headers
|
|
972
|
+
});
|
|
973
|
+
const passOn = () => {
|
|
974
|
+
if (meta.aborted) {
|
|
975
|
+
finishEntry(entry, { error: "aborted" });
|
|
976
|
+
return;
|
|
977
|
+
}
|
|
978
|
+
const blocked = blockedResponse(state, method, url, text);
|
|
979
|
+
if (!blocked) {
|
|
980
|
+
track(this, entry);
|
|
981
|
+
return send.call(this, body);
|
|
982
|
+
}
|
|
983
|
+
const { status, text: out, headers } = responseParts(blocked);
|
|
984
|
+
if (!state.deviceOffline) entry.blocked = true;
|
|
985
|
+
finishEntry(
|
|
986
|
+
entry,
|
|
987
|
+
isOffline(blocked) ? { error: `${OFFLINE_MESSAGE} (${state.deviceOffline ? "device offline" : "strict network"})` } : { status, responseBody: out, contentType: contentTypeOf(headers), responseHeaders: headers }
|
|
988
|
+
);
|
|
989
|
+
void Promise.resolve().then(() => {
|
|
990
|
+
if (!meta.aborted) respond(this, blocked, url);
|
|
991
|
+
});
|
|
992
|
+
};
|
|
993
|
+
const candidates = matchingMocks(state, method, url, { text });
|
|
994
|
+
if (!candidates.length) return passOn();
|
|
995
|
+
const request = async () => mockRequest(method, url, meta.headers, text);
|
|
996
|
+
answer(state, candidates, request).then(
|
|
997
|
+
async (hit) => {
|
|
998
|
+
if (!hit) return passOn();
|
|
999
|
+
entry.mocked = true;
|
|
1000
|
+
if (hit.mock.source === "agent") signalMockAnswered(hit.mock.id, entry.id);
|
|
1001
|
+
await sleep(hit.mock.delayMs);
|
|
1002
|
+
if (meta.aborted) {
|
|
1003
|
+
finishEntry(entry, { error: "aborted" });
|
|
1004
|
+
return;
|
|
1005
|
+
}
|
|
1006
|
+
const offline = isOffline(hit.response);
|
|
1007
|
+
const { status, text: out, headers } = responseParts(hit.response);
|
|
1008
|
+
finishEntry(
|
|
1009
|
+
entry,
|
|
1010
|
+
offline ? { error: `${OFFLINE_MESSAGE} (mocked offline)` } : { status, responseBody: out, contentType: contentTypeOf(headers), responseHeaders: headers }
|
|
1011
|
+
);
|
|
1012
|
+
respond(this, hit.response, url);
|
|
1013
|
+
},
|
|
1014
|
+
(error) => {
|
|
1015
|
+
entry.mocked = true;
|
|
1016
|
+
finishEntry(entry, { error: errorMessage(error) });
|
|
1017
|
+
respond(this, { offline: true }, url);
|
|
1018
|
+
}
|
|
1019
|
+
);
|
|
1020
|
+
};
|
|
1021
|
+
return () => {
|
|
1022
|
+
Object.assign(proto, { open, send, setRequestHeader, abort });
|
|
1023
|
+
};
|
|
1024
|
+
}
|
|
1025
|
+
|
|
1026
|
+
// src/network/index.ts
|
|
1027
|
+
function installNetwork() {
|
|
1028
|
+
const state = networkState();
|
|
1029
|
+
if (state.installed) return;
|
|
1030
|
+
state.installed = true;
|
|
1031
|
+
state.devHost = deriveDevHost();
|
|
1032
|
+
for (const undo of [patchXhr(state), patchFetch(state)])
|
|
1033
|
+
if (undo) state.uninstall.push(undo);
|
|
1034
|
+
}
|
|
1035
|
+
|
|
1036
|
+
// src/adapters/device/offline.ts
|
|
1037
|
+
var NETINFO_EVENT = "netInfo.networkStatusDidChange";
|
|
1038
|
+
var NONE = { type: "none", isConnected: false, isInternetReachable: false, details: null };
|
|
1039
|
+
function offlineArea(rn, NetInfo) {
|
|
1040
|
+
const restore = () => {
|
|
1041
|
+
const st = deviceState();
|
|
1042
|
+
if (!st.offline) return { restored: false };
|
|
1043
|
+
st.offline = false;
|
|
1044
|
+
setDeviceOffline(false);
|
|
1045
|
+
const faked = st.netInfo;
|
|
1046
|
+
if (faked && st.origRefresh) {
|
|
1047
|
+
faked.refresh = st.origRefresh;
|
|
1048
|
+
Promise.resolve(faked.refresh()).catch(() => {
|
|
1049
|
+
});
|
|
1050
|
+
}
|
|
1051
|
+
delete st.netInfo;
|
|
1052
|
+
delete st.origRefresh;
|
|
1053
|
+
return { restored: true };
|
|
1054
|
+
};
|
|
1055
|
+
return {
|
|
1056
|
+
pending: () => deviceState().offline ? { offline: true, netInfo: Boolean(NetInfo) } : null,
|
|
1057
|
+
restore,
|
|
1058
|
+
tools: {
|
|
1059
|
+
"device.offline": {
|
|
1060
|
+
description: "device.offline true makes every fetch and XHR fail like no network (mocks included; Metro stays reachable) and, when NetInfo was passed in, tells its listeners, NetInfo.fetch and useNetInfo the device is offline. false goes back. device.restore also does. Returns { offline }.",
|
|
1061
|
+
maxArgs: 1,
|
|
1062
|
+
run: (offline) => {
|
|
1063
|
+
if (typeof offline !== "boolean") throw new Error("Pass true or false");
|
|
1064
|
+
if (!offline) {
|
|
1065
|
+
restore();
|
|
1066
|
+
return { offline: false };
|
|
1067
|
+
}
|
|
1068
|
+
const st = deviceState();
|
|
1069
|
+
st.offline = true;
|
|
1070
|
+
installNetwork();
|
|
1071
|
+
setDeviceOffline(true);
|
|
1072
|
+
if (NetInfo) {
|
|
1073
|
+
if (!st.origRefresh && typeof NetInfo.refresh === "function") {
|
|
1074
|
+
st.netInfo = NetInfo;
|
|
1075
|
+
st.origRefresh = NetInfo.refresh;
|
|
1076
|
+
NetInfo.refresh = () => Promise.resolve(NONE);
|
|
1077
|
+
}
|
|
1078
|
+
rn().DeviceEventEmitter.emit(NETINFO_EVENT, NONE);
|
|
1079
|
+
}
|
|
1080
|
+
return { offline: true };
|
|
1081
|
+
}
|
|
1082
|
+
}
|
|
1083
|
+
}
|
|
1084
|
+
};
|
|
1085
|
+
}
|
|
1086
|
+
|
|
1087
|
+
// src/adapters/device.ts
|
|
1088
|
+
function deviceTools(options = {}) {
|
|
1089
|
+
const rn = () => options.reactNative ?? loadReactNative();
|
|
1090
|
+
const areas = [
|
|
1091
|
+
linkArea(rn, options.navigation),
|
|
1092
|
+
appStateArea(rn),
|
|
1093
|
+
offlineArea(rn, options.NetInfo)
|
|
1094
|
+
];
|
|
1095
|
+
if (options.Notifications) areas.push(notificationArea(rn, options.Notifications));
|
|
1096
|
+
if (options.time) areas.push(clockArea());
|
|
1097
|
+
const tools = Object.assign({}, ...areas.map((a) => a.tools));
|
|
1098
|
+
tools["device.restore"] = {
|
|
1099
|
+
description: "Undo every device event: deep link, app state, offline and clock.",
|
|
1100
|
+
pending: () => {
|
|
1101
|
+
const pending = Object.assign({}, ...areas.map((a) => a.pending()));
|
|
1102
|
+
return Object.keys(pending).length ? pending : false;
|
|
1103
|
+
},
|
|
1104
|
+
run: () => {
|
|
1105
|
+
const results = areas.map((a) => a.restore());
|
|
1106
|
+
return { ...Object.assign({}, ...results), restored: results.some((r) => Boolean(r.restored)) };
|
|
1107
|
+
}
|
|
1108
|
+
};
|
|
1109
|
+
return tools;
|
|
1110
|
+
}
|