@buoy-gg/network 7.0.35 → 7.0.38
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +45 -12
- package/lib/commonjs/index.js +26 -1
- package/lib/commonjs/index.web.js +26 -1
- package/lib/commonjs/network/components/NetworkEventDetailView.js +6 -1
- package/lib/commonjs/network/components/NetworkHeaderMenu.js +4 -0
- package/lib/commonjs/network/components/NetworkModal.js +24 -8
- package/lib/commonjs/network/conditions/NetworkConditionsControl.js +235 -0
- package/lib/commonjs/network/conditions/NetworkConditionsControl.test.js +136 -0
- package/lib/commonjs/network/conditions/NetworkConditionsIcon.js +33 -0
- package/lib/commonjs/network/conditions/NetworkFloatingTool.js +310 -0
- package/lib/commonjs/network/conditions/NetworkFloatingTool.test.js +219 -0
- package/lib/commonjs/network/conditions/store.js +75 -0
- package/lib/commonjs/network/conditions/wait.js +43 -0
- package/lib/commonjs/network/conditions/xhr.js +110 -0
- package/lib/commonjs/network/hooks/useNetworkEvents.js +1 -1
- package/lib/commonjs/network/overrides/applyToXhr.js +10 -8
- package/lib/commonjs/network/overrides/engine.js +2 -1
- package/lib/commonjs/network/sync/networkSyncAdapter.js +224 -3
- package/lib/commonjs/network/utils/generateNetworkCopyText.js +2 -0
- package/lib/commonjs/network/utils/networkEventStore.js +5 -77
- package/lib/commonjs/network/utils/networkListener.js +245 -46
- package/lib/commonjs/network/utils/networkSavedStore.js +2 -2
- package/lib/commonjs/network/utils/selectSavedEvents.js +2 -2
- package/lib/commonjs/preset.js +2 -1
- package/lib/module/index.js +2 -1
- package/lib/module/index.web.js +2 -1
- package/lib/module/network/components/NetworkEventDetailView.js +6 -1
- package/lib/module/network/components/NetworkHeaderMenu.js +4 -0
- package/lib/module/network/components/NetworkModal.js +25 -9
- package/lib/module/network/conditions/NetworkConditionsControl.js +231 -0
- package/lib/module/network/conditions/NetworkConditionsControl.test.js +135 -0
- package/lib/module/network/conditions/NetworkConditionsIcon.js +29 -0
- package/lib/module/network/conditions/NetworkFloatingTool.js +304 -0
- package/lib/module/network/conditions/NetworkFloatingTool.test.js +218 -0
- package/lib/module/network/conditions/store.js +65 -0
- package/lib/module/network/conditions/wait.js +38 -0
- package/lib/module/network/conditions/xhr.js +106 -0
- package/lib/module/network/hooks/useNetworkEvents.js +1 -1
- package/lib/module/network/overrides/applyToXhr.js +9 -8
- package/lib/module/network/overrides/engine.js +2 -1
- package/lib/module/network/sync/networkSyncAdapter.js +225 -3
- package/lib/module/network/utils/generateNetworkCopyText.js +2 -0
- package/lib/module/network/utils/networkEventStore.js +6 -77
- package/lib/module/network/utils/networkListener.js +246 -47
- package/lib/module/network/utils/networkSavedStore.js +2 -2
- package/lib/module/network/utils/selectSavedEvents.js +2 -2
- package/lib/module/preset.js +3 -2
- package/lib/typescript/index.d.ts +2 -0
- package/lib/typescript/index.d.ts.map +1 -1
- package/lib/typescript/index.web.d.ts +2 -0
- package/lib/typescript/index.web.d.ts.map +1 -1
- package/lib/typescript/network/components/NetworkEventDetailView.d.ts.map +1 -1
- package/lib/typescript/network/components/NetworkHeaderMenu.d.ts +2 -1
- package/lib/typescript/network/components/NetworkHeaderMenu.d.ts.map +1 -1
- package/lib/typescript/network/components/NetworkModal.d.ts +3 -0
- package/lib/typescript/network/components/NetworkModal.d.ts.map +1 -1
- package/lib/typescript/network/conditions/NetworkConditionsControl.d.ts +12 -0
- package/lib/typescript/network/conditions/NetworkConditionsControl.d.ts.map +1 -0
- package/lib/typescript/network/conditions/NetworkConditionsControl.test.d.ts +2 -0
- package/lib/typescript/network/conditions/NetworkConditionsControl.test.d.ts.map +1 -0
- package/lib/typescript/network/conditions/NetworkConditionsIcon.d.ts +4 -0
- package/lib/typescript/network/conditions/NetworkConditionsIcon.d.ts.map +1 -0
- package/lib/typescript/network/conditions/NetworkFloatingTool.d.ts +15 -0
- package/lib/typescript/network/conditions/NetworkFloatingTool.d.ts.map +1 -0
- package/lib/typescript/network/conditions/NetworkFloatingTool.test.d.ts +2 -0
- package/lib/typescript/network/conditions/NetworkFloatingTool.test.d.ts.map +1 -0
- package/lib/typescript/network/conditions/store.d.ts +30 -0
- package/lib/typescript/network/conditions/store.d.ts.map +1 -0
- package/lib/typescript/network/conditions/wait.d.ts +4 -0
- package/lib/typescript/network/conditions/wait.d.ts.map +1 -0
- package/lib/typescript/network/conditions/xhr.d.ts +5 -0
- package/lib/typescript/network/conditions/xhr.d.ts.map +1 -0
- package/lib/typescript/network/hooks/useNetworkEvents.d.ts +1 -1
- package/lib/typescript/network/overrides/applyToXhr.d.ts +2 -0
- package/lib/typescript/network/overrides/applyToXhr.d.ts.map +1 -1
- package/lib/typescript/network/overrides/engine.d.ts +0 -10
- package/lib/typescript/network/overrides/engine.d.ts.map +1 -1
- package/lib/typescript/network/sync/networkSyncAdapter.d.ts +29 -0
- package/lib/typescript/network/sync/networkSyncAdapter.d.ts.map +1 -1
- package/lib/typescript/network/types/index.d.ts +1 -0
- package/lib/typescript/network/types/index.d.ts.map +1 -1
- package/lib/typescript/network/utils/generateNetworkCopyText.d.ts.map +1 -1
- package/lib/typescript/network/utils/networkEventStore.d.ts +0 -15
- package/lib/typescript/network/utils/networkEventStore.d.ts.map +1 -1
- package/lib/typescript/network/utils/networkListener.d.ts +13 -28
- package/lib/typescript/network/utils/networkListener.d.ts.map +1 -1
- package/lib/typescript/network/utils/networkSavedStore.d.ts +2 -2
- package/lib/typescript/network/utils/selectSavedEvents.d.ts +2 -2
- package/lib/typescript/preset.d.ts +0 -16
- package/lib/typescript/preset.d.ts.map +1 -1
- package/package.json +2 -2
|
@@ -46,14 +46,6 @@ var _networkSavedStore = require("./networkSavedStore");
|
|
|
46
46
|
const BOOT_BUFFER_MAX = 200;
|
|
47
47
|
class NetworkEventStore extends _sharedUi.BaseEventStore {
|
|
48
48
|
pendingRequests = new Map();
|
|
49
|
-
/**
|
|
50
|
-
* Recently seen `METHOD:url` keys → when, and which event id we kept.
|
|
51
|
-
*
|
|
52
|
-
* The timestamp powers the dual-interception dedup below. The id is what lets
|
|
53
|
-
* a DROPPED duplicate still contribute something the kept event doesn't know
|
|
54
|
-
* — see `applyOverrideMark`.
|
|
55
|
-
*/
|
|
56
|
-
recentRequests = new Map();
|
|
57
49
|
rawListenerUnsubscribe = null;
|
|
58
50
|
/** Raw events captured before the first consumer subscribed — see startBootCapture. */
|
|
59
51
|
bootBuffer = [];
|
|
@@ -85,6 +77,7 @@ class NetworkEventStore extends _sharedUi.BaseEventStore {
|
|
|
85
77
|
* state therefore never gains events it didn't ask for).
|
|
86
78
|
*/
|
|
87
79
|
startBootCapture() {
|
|
80
|
+
if ((0, _sharedUi.getAccountAccess)().status === "locked") return;
|
|
88
81
|
if (this.mirrorMode || this.bootBufferUnsubscribe || this.isCapturing()) {
|
|
89
82
|
return;
|
|
90
83
|
}
|
|
@@ -139,6 +132,7 @@ class NetworkEventStore extends _sharedUi.BaseEventStore {
|
|
|
139
132
|
* Stop capturing network events
|
|
140
133
|
*/
|
|
141
134
|
stopCapturing() {
|
|
135
|
+
this.takeBootBuffer();
|
|
142
136
|
// Remove our raw listener
|
|
143
137
|
if (this.rawListenerUnsubscribe) {
|
|
144
138
|
this.rawListenerUnsubscribe();
|
|
@@ -205,57 +199,11 @@ class NetworkEventStore extends _sharedUi.BaseEventStore {
|
|
|
205
199
|
* Process a network listener event
|
|
206
200
|
*/
|
|
207
201
|
processNetworkEvent(event) {
|
|
202
|
+
if ((0, _sharedUi.getAccountAccess)().status === "locked") return;
|
|
208
203
|
const {
|
|
209
204
|
request
|
|
210
205
|
} = event;
|
|
211
206
|
if (event.type === "request") {
|
|
212
|
-
// Check for duplicate request based on URL, method, and timing.
|
|
213
|
-
// The clock is the event's CAPTURE time, not Date.now(): boot-buffered
|
|
214
|
-
// events are flushed in one synchronous burst, and wall-clock "now"
|
|
215
|
-
// would put two genuinely separate boot requests to the same endpoint
|
|
216
|
-
// inside the 50ms dual-interception window, silently dropping one. For
|
|
217
|
-
// live events the two clocks are the same (the raw event is emitted
|
|
218
|
-
// synchronously at capture).
|
|
219
|
-
const requestKey = `${request.method}:${request.url}`;
|
|
220
|
-
const now = event.timestamp instanceof Date ? event.timestamp.getTime() : Date.now();
|
|
221
|
-
const recent = this.recentRequests.get(requestKey);
|
|
222
|
-
|
|
223
|
-
// If same request within 50ms, likely a duplicate from XHR/fetch dual
|
|
224
|
-
// interception. The age must be non-negative: Date.now() is not
|
|
225
|
-
// monotonic, and after a backwards clock adjustment (iOS time sync) a
|
|
226
|
-
// future-dated entry would satisfy `< 50` for EVERY request to this
|
|
227
|
-
// endpoint until the clock caught up — capture going silent for minutes
|
|
228
|
-
// to hours, "fixed" only by Clear wiping this map (field-reported). A
|
|
229
|
-
// negative age falls through and re-stamps the entry with the new clock.
|
|
230
|
-
if (recent && now - recent.at >= 0 && now - recent.at < 50) {
|
|
231
|
-
// …but the duplicate is not always redundant. On React Native a
|
|
232
|
-
// `fetch()` is seen twice: once by the fetch wrapper, which is what
|
|
233
|
-
// creates this event, and again by the XHR wrapper underneath it —
|
|
234
|
-
// and ONLY the XHR layer resolves overrides, because that's where the
|
|
235
|
-
// request can actually be intercepted. So the mark arrives on the copy
|
|
236
|
-
// we're about to throw away. Move it across before dropping.
|
|
237
|
-
if (event.override) {
|
|
238
|
-
this.applyOverrideMark(recent.id, event.override);
|
|
239
|
-
}
|
|
240
|
-
return;
|
|
241
|
-
}
|
|
242
|
-
this.recentRequests.set(requestKey, {
|
|
243
|
-
at: now,
|
|
244
|
-
id: request.id
|
|
245
|
-
});
|
|
246
|
-
|
|
247
|
-
// Clean up old entries to prevent memory leak. Future-dated entries are
|
|
248
|
-
// stale by definition (clock moved backwards) — drop those too, or they
|
|
249
|
-
// survive every sweep until the clock overtakes them.
|
|
250
|
-
if (this.recentRequests.size > 100) {
|
|
251
|
-
const cutoff = now - 5000;
|
|
252
|
-
for (const [key, entry] of this.recentRequests.entries()) {
|
|
253
|
-
if (entry.at < cutoff || entry.at > now) {
|
|
254
|
-
this.recentRequests.delete(key);
|
|
255
|
-
}
|
|
256
|
-
}
|
|
257
|
-
}
|
|
258
|
-
|
|
259
207
|
// Build full URL with query params if present
|
|
260
208
|
const queryString = request.params ? `?${new URLSearchParams(request.params).toString()}` : "";
|
|
261
209
|
const fullUrl = `${request.url}${queryString}`;
|
|
@@ -287,7 +235,8 @@ class NetworkEventStore extends _sharedUi.BaseEventStore {
|
|
|
287
235
|
graphqlVariables,
|
|
288
236
|
// Present when this request came straight through the XHR wrapper
|
|
289
237
|
// (axios, raw XHR). For `fetch` it arrives later — see the dedup branch.
|
|
290
|
-
override: event.override
|
|
238
|
+
override: event.override,
|
|
239
|
+
condition: event.condition
|
|
291
240
|
};
|
|
292
241
|
this.pendingRequests.set(request.id, networkEvent);
|
|
293
242
|
this.events = [networkEvent, ...this.events].slice(0, this.maxEvents);
|
|
@@ -331,26 +280,6 @@ class NetworkEventStore extends _sharedUi.BaseEventStore {
|
|
|
331
280
|
}
|
|
332
281
|
}
|
|
333
282
|
}
|
|
334
|
-
|
|
335
|
-
/**
|
|
336
|
-
* Stamp an override mark onto an already-recorded event.
|
|
337
|
-
*
|
|
338
|
-
* Only ever moves a mark from a dropped duplicate onto the event that was
|
|
339
|
-
* kept, so it never overwrites one that is already there.
|
|
340
|
-
*/
|
|
341
|
-
applyOverrideMark(id, override) {
|
|
342
|
-
const index = this.events.findIndex(e => e.id === id);
|
|
343
|
-
if (index === -1 || this.events[index].override) return;
|
|
344
|
-
const updatedEvent = {
|
|
345
|
-
...this.events[index],
|
|
346
|
-
override
|
|
347
|
-
};
|
|
348
|
-
this.events = this.events.map((e, i) => i === index ? updatedEvent : e);
|
|
349
|
-
const pending = this.pendingRequests.get(id);
|
|
350
|
-
if (pending) this.pendingRequests.set(id, updatedEvent);
|
|
351
|
-
this.notify(updatedEvent);
|
|
352
|
-
this.notifyArrayListeners();
|
|
353
|
-
}
|
|
354
283
|
extractHost(url) {
|
|
355
284
|
try {
|
|
356
285
|
const urlObj = new URL(url);
|
|
@@ -388,7 +317,6 @@ class NetworkEventStore extends _sharedUi.BaseEventStore {
|
|
|
388
317
|
clearEvents() {
|
|
389
318
|
super.clearEvents();
|
|
390
319
|
this.pendingRequests.clear();
|
|
391
|
-
this.recentRequests.clear();
|
|
392
320
|
}
|
|
393
321
|
|
|
394
322
|
/**
|
|
@@ -4,6 +4,9 @@ Object.defineProperty(exports, "__esModule", {
|
|
|
4
4
|
value: true
|
|
5
5
|
});
|
|
6
6
|
exports.stopNetworkListener = exports.startNetworkListener = exports.removeAllNetworkListeners = exports.networkListener = exports.isNetworkListening = exports.isNetworkInterceptionLive = exports.getNetworkListenerCount = exports.addNetworkListener = exports.BUOY_XHR_INTERCEPTOR_FLAG = exports.BUOY_FETCH_INTERCEPTOR_FLAG = void 0;
|
|
7
|
+
var _xhr = require("../conditions/xhr");
|
|
8
|
+
var _store = require("../conditions/store");
|
|
9
|
+
var _wait = require("../conditions/wait");
|
|
7
10
|
var _engine = require("../overrides/engine");
|
|
8
11
|
var _applyToXhr = require("../overrides/applyToXhr");
|
|
9
12
|
var _applyToFetch = require("../overrides/applyToFetch");
|
|
@@ -26,7 +29,7 @@ var _httpCompletion = require("./httpCompletion");
|
|
|
26
29
|
* executor, so a flag raised across the call expression alone is enough to
|
|
27
30
|
* cover the inner `send()` without leaking across the await.
|
|
28
31
|
*/
|
|
29
|
-
let
|
|
32
|
+
let insideFetchHandoff = false;
|
|
30
33
|
|
|
31
34
|
// Extended XMLHttpRequest interface for monkey-patching
|
|
32
35
|
|
|
@@ -71,17 +74,56 @@ const BUOY_XHR_INTERCEPTOR_FLAG = exports.BUOY_XHR_INTERCEPTOR_FLAG = "__buoyNet
|
|
|
71
74
|
* @performance Uses lazy singleton pattern to minimize memory footprint
|
|
72
75
|
* @performance Includes URL filtering to ignore development traffic
|
|
73
76
|
*/
|
|
77
|
+
/**
|
|
78
|
+
* Hosts another Buoy package has claimed as its own traffic.
|
|
79
|
+
*
|
|
80
|
+
* A GLOBAL, not an import: `@buoy-gg/network` cannot depend on `agent-core`
|
|
81
|
+
* (Ask Buoy is optional and this package is older), and `agent-core` ships
|
|
82
|
+
* with no dependencies at all. `registerSelfEndpoint` in agent-core writes the
|
|
83
|
+
* set; this reads it. Absent, undefined or the wrong shape all mean "nothing
|
|
84
|
+
* to exclude", so nothing breaks in an app without Ask Buoy.
|
|
85
|
+
*
|
|
86
|
+
* WHY IT IS NOT ENOUGH TO FILTER LATER. The interceptor reads the response
|
|
87
|
+
* body — `await processResponseBody(response.clone())` — BEFORE handing the
|
|
88
|
+
* response back. On a streamed reply that resolves only at EOF, so an
|
|
89
|
+
* intercepted model endpoint delivers its whole answer at once and Ask Buoy
|
|
90
|
+
* cannot stream a word. Ignoring the URL is what keeps the socket the
|
|
91
|
+
* caller's. It also keeps override rules off model traffic, which a broad
|
|
92
|
+
* user-authored rule would otherwise rewrite.
|
|
93
|
+
*/
|
|
94
|
+
/**
|
|
95
|
+
* Content types whose body arrives over time rather than all at once.
|
|
96
|
+
*
|
|
97
|
+
* These are the ones where reading the clone before returning the response
|
|
98
|
+
* changes the CALLER's behaviour instead of merely observing it.
|
|
99
|
+
*/
|
|
100
|
+
const STREAMED_CONTENT_TYPES = ["text/event-stream", "application/x-ndjson", "application/stream+json"];
|
|
101
|
+
const STREAMING_PLACEHOLDER = "[streaming\u2026]";
|
|
102
|
+
function isStreamedContentType(contentType) {
|
|
103
|
+
if (!contentType) return false;
|
|
104
|
+
const lower = contentType.toLowerCase();
|
|
105
|
+
return STREAMED_CONTENT_TYPES.some(t => lower.includes(t));
|
|
106
|
+
}
|
|
107
|
+
const INTERNAL_HOSTS_KEY = "__BUOY_INTERNAL_HOSTS__";
|
|
108
|
+
function isInternalBuoyHost(url) {
|
|
109
|
+
const hosts = globalThis[INTERNAL_HOSTS_KEY];
|
|
110
|
+
if (!(hosts instanceof Set) || hosts.size === 0) return false;
|
|
111
|
+
let authority;
|
|
112
|
+
try {
|
|
113
|
+
authority = new URL(url).host.toLowerCase();
|
|
114
|
+
} catch {
|
|
115
|
+
return false;
|
|
116
|
+
}
|
|
117
|
+
for (const host of hosts) {
|
|
118
|
+
if (typeof host === "string" && host && authority === host.toLowerCase()) return true;
|
|
119
|
+
}
|
|
120
|
+
return false;
|
|
121
|
+
}
|
|
74
122
|
class NetworkListener {
|
|
75
123
|
listeners = [];
|
|
76
124
|
isListening = false;
|
|
77
125
|
requestCounter = 1000;
|
|
78
126
|
|
|
79
|
-
// URLs to ignore (Metro bundler, symbolicate, etc.)
|
|
80
|
-
ignoredUrls = [/\/symbolicate$/, /\/logs$/, /\/debugger-proxy/, /\/reload$/, /\/launch-js-devtools/, /localhost:8081/, /100\.64\.\d+\.\d+:8081/,
|
|
81
|
-
// iOS simulator
|
|
82
|
-
/10\.0\.\d+\.\d+:8081/ // Android emulator
|
|
83
|
-
];
|
|
84
|
-
|
|
85
127
|
// Store original methods
|
|
86
128
|
|
|
87
129
|
/**
|
|
@@ -105,7 +147,7 @@ class NetworkListener {
|
|
|
105
147
|
xhrGeneration = 0;
|
|
106
148
|
constructor() {
|
|
107
149
|
// Store original methods
|
|
108
|
-
this.originalFetch = globalThis.fetch
|
|
150
|
+
this.originalFetch = globalThis.fetch;
|
|
109
151
|
this.originalXHROpen = XMLHttpRequest.prototype.open;
|
|
110
152
|
this.originalXHRSend = XMLHttpRequest.prototype.send;
|
|
111
153
|
this.originalXHRSetRequestHeader = XMLHttpRequest.prototype.setRequestHeader;
|
|
@@ -121,7 +163,15 @@ class NetworkListener {
|
|
|
121
163
|
* @returns True if the URL should be ignored
|
|
122
164
|
*/
|
|
123
165
|
shouldIgnoreUrl(url) {
|
|
124
|
-
|
|
166
|
+
try {
|
|
167
|
+
const parsed = new URL(url);
|
|
168
|
+
if (parsed.hostname === "keygen.sh" || parsed.hostname.endsWith(".keygen.sh")) return true;
|
|
169
|
+
const local = parsed.hostname === "localhost" || parsed.hostname === "127.0.0.1" || parsed.hostname === "10.0.2.2";
|
|
170
|
+
if (local && ["8081", "8802"].includes(parsed.port)) return true;
|
|
171
|
+
} catch {
|
|
172
|
+
return false;
|
|
173
|
+
}
|
|
174
|
+
return isInternalBuoyHost(url);
|
|
125
175
|
}
|
|
126
176
|
|
|
127
177
|
// Emit event to all listeners
|
|
@@ -167,6 +217,70 @@ class NetworkListener {
|
|
|
167
217
|
* @param maxSize - Maximum body size in bytes (default: 1MB)
|
|
168
218
|
* @returns Object containing body, size, and truncation status
|
|
169
219
|
*/
|
|
220
|
+
/**
|
|
221
|
+
* Read a streamed body in the background, with a hard ceiling.
|
|
222
|
+
*
|
|
223
|
+
* Deliberately not `processResponseBody`: that one calls `.text()`, which
|
|
224
|
+
* accumulates the WHOLE body before it checks the size, so a long-lived SSE
|
|
225
|
+
* stream would grow unbounded in memory with nothing to stop it. This stops
|
|
226
|
+
* at the cap and cancels the reader, and a runtime with no `body` (React
|
|
227
|
+
* Native's own fetch) falls back to `.text()` — that response was never
|
|
228
|
+
* streaming there in the first place.
|
|
229
|
+
*/
|
|
230
|
+
async readStreamedBody(response, maxSize = 256 * 1024) {
|
|
231
|
+
const body = response.body;
|
|
232
|
+
if (!body || typeof body.getReader !== "function") {
|
|
233
|
+
const text = await response.text();
|
|
234
|
+
return text.length > maxSize ? {
|
|
235
|
+
body: `${text.slice(0, maxSize)}\n\n... [truncated]`,
|
|
236
|
+
size: text.length
|
|
237
|
+
} : {
|
|
238
|
+
body: text,
|
|
239
|
+
size: text.length
|
|
240
|
+
};
|
|
241
|
+
}
|
|
242
|
+
const reader = body.getReader();
|
|
243
|
+
const decoder = new TextDecoder();
|
|
244
|
+
let out = "";
|
|
245
|
+
let size = 0;
|
|
246
|
+
let capped = false;
|
|
247
|
+
try {
|
|
248
|
+
for (;;) {
|
|
249
|
+
const {
|
|
250
|
+
done,
|
|
251
|
+
value
|
|
252
|
+
} = await reader.read();
|
|
253
|
+
if (done) break;
|
|
254
|
+
size += value?.byteLength ?? 0;
|
|
255
|
+
if (!capped) {
|
|
256
|
+
out += decoder.decode(value, {
|
|
257
|
+
stream: true
|
|
258
|
+
});
|
|
259
|
+
if (size >= maxSize) {
|
|
260
|
+
out = `${out.slice(0, maxSize)}\n\n... [truncated]`;
|
|
261
|
+
capped = true;
|
|
262
|
+
break;
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
if (!capped) out += decoder.decode();
|
|
267
|
+
} finally {
|
|
268
|
+
try {
|
|
269
|
+
void Promise.resolve(reader.cancel()).catch(() => {});
|
|
270
|
+
} catch {
|
|
271
|
+
/* already gone */
|
|
272
|
+
}
|
|
273
|
+
try {
|
|
274
|
+
reader.releaseLock();
|
|
275
|
+
} catch {
|
|
276
|
+
/* already released */
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
return {
|
|
280
|
+
body: out,
|
|
281
|
+
size
|
|
282
|
+
};
|
|
283
|
+
}
|
|
170
284
|
async processResponseBody(response, maxSize = 1024 * 1024 // 1MB default
|
|
171
285
|
) {
|
|
172
286
|
try {
|
|
@@ -239,14 +353,14 @@ class NetworkListener {
|
|
|
239
353
|
* Handle XMLHttpRequest response processing
|
|
240
354
|
* This method processes the response and emits appropriate events
|
|
241
355
|
*/
|
|
242
|
-
handleXHRResponse(xhr, requestId, cleanUrl, method, requestHeaders, requestData, params, startTime, isError = false, clientType = "xhr", override) {
|
|
356
|
+
handleXHRResponse(xhr, requestId, cleanUrl, method, requestHeaders, requestData, params, startTime, isError = false, clientType = "xhr", override, condition) {
|
|
243
357
|
const duration = startTime ? Date.now() - startTime : 0;
|
|
244
358
|
|
|
245
359
|
// XHR's duration was ALREADY total (measured at readyState 4 / load), so
|
|
246
|
-
// only the split is new. `
|
|
360
|
+
// only the split is new. `_buoyTtfb` is stamped at HEADERS_RECEIVED; it is
|
|
247
361
|
// absent for a request that failed before any headers arrived, and the
|
|
248
362
|
// breakdown is then correctly omitted rather than faked as 0.
|
|
249
|
-
const ttfb = xhr.
|
|
363
|
+
const ttfb = xhr._buoyTtfb;
|
|
250
364
|
const timings = ttfb === undefined ? undefined : {
|
|
251
365
|
ttfb,
|
|
252
366
|
download: Math.max(0, duration - ttfb)
|
|
@@ -269,7 +383,8 @@ class NetworkListener {
|
|
|
269
383
|
error: {
|
|
270
384
|
message: isError ? "Request failed" : "Network error or request aborted"
|
|
271
385
|
},
|
|
272
|
-
override
|
|
386
|
+
override,
|
|
387
|
+
condition
|
|
273
388
|
});
|
|
274
389
|
return;
|
|
275
390
|
}
|
|
@@ -411,7 +526,8 @@ class NetworkListener {
|
|
|
411
526
|
body,
|
|
412
527
|
size: responseSize
|
|
413
528
|
},
|
|
414
|
-
override
|
|
529
|
+
override,
|
|
530
|
+
condition
|
|
415
531
|
});
|
|
416
532
|
} else {
|
|
417
533
|
this.emit({
|
|
@@ -437,7 +553,8 @@ class NetworkListener {
|
|
|
437
553
|
error: {
|
|
438
554
|
message: "Network Error"
|
|
439
555
|
},
|
|
440
|
-
override
|
|
556
|
+
override,
|
|
557
|
+
condition
|
|
441
558
|
});
|
|
442
559
|
}
|
|
443
560
|
}
|
|
@@ -503,7 +620,7 @@ class NetworkListener {
|
|
|
503
620
|
// re-installing after a clobber must call through the replacement, and a
|
|
504
621
|
// stale generation must replay the chain exactly as it stood beneath it.
|
|
505
622
|
const passthroughFetch = globalThis.fetch.bind(globalThis);
|
|
506
|
-
this.originalFetch =
|
|
623
|
+
this.originalFetch = globalThis.fetch;
|
|
507
624
|
const generation = ++this.fetchGeneration;
|
|
508
625
|
const wrappedFetch = async (input, init) => {
|
|
509
626
|
const url = typeof input === "string" ? input : input instanceof URL ? input.href : input.url;
|
|
@@ -521,7 +638,11 @@ class NetworkListener {
|
|
|
521
638
|
}
|
|
522
639
|
const startTime = Date.now();
|
|
523
640
|
const requestId = `fetch_${++self.requestCounter}`;
|
|
524
|
-
const
|
|
641
|
+
const inputRequest = typeof input === "object" && "method" in input ? input : undefined;
|
|
642
|
+
const method = init?.method ?? inputRequest?.method ?? "GET";
|
|
643
|
+
const signal = init?.signal !== undefined ? init.signal : inputRequest?.signal;
|
|
644
|
+
(0, _wait.throwIfAborted)(signal);
|
|
645
|
+
const condition = (0, _store.snapshotNetworkCondition)(url);
|
|
525
646
|
const {
|
|
526
647
|
url: cleanUrl,
|
|
527
648
|
params
|
|
@@ -529,21 +650,24 @@ class NetworkListener {
|
|
|
529
650
|
|
|
530
651
|
// Parse request headers
|
|
531
652
|
let requestHeaders = {};
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
653
|
+
const headers = init?.headers ?? inputRequest?.headers;
|
|
654
|
+
if (headers) {
|
|
655
|
+
if (headers instanceof Headers) {
|
|
656
|
+
headers.forEach((value, key) => {
|
|
535
657
|
requestHeaders[key] = value;
|
|
536
658
|
});
|
|
537
|
-
} else if (Array.isArray(
|
|
538
|
-
|
|
659
|
+
} else if (Array.isArray(headers)) {
|
|
660
|
+
headers.forEach(([key, value]) => {
|
|
539
661
|
requestHeaders[key] = value;
|
|
540
662
|
});
|
|
541
663
|
} else {
|
|
542
|
-
requestHeaders =
|
|
664
|
+
requestHeaders = headers;
|
|
543
665
|
}
|
|
544
666
|
}
|
|
545
667
|
|
|
546
668
|
// Parse request body
|
|
669
|
+
// Explicitly typed: the late emit for a streamed reply reads this from
|
|
670
|
+
// inside a callback, which defeats the inference it used to rely on.
|
|
547
671
|
let requestData;
|
|
548
672
|
if (init?.body) {
|
|
549
673
|
if (typeof init.body === "string") {
|
|
@@ -569,7 +693,7 @@ class NetworkListener {
|
|
|
569
693
|
* Skipped when `canSynthesizeResponse()` is false so a runtime without a
|
|
570
694
|
* `Response` global degrades to a real request rather than a broken one.
|
|
571
695
|
*/
|
|
572
|
-
const outcome = (0, _applyToFetch.canSynthesizeResponse)() ? (0, _engine.overrideForRequest)(url, method) : null;
|
|
696
|
+
const outcome = condition?.profile !== "offline" && (0, _applyToFetch.canSynthesizeResponse)() ? (0, _engine.overrideForRequest)(url, method) : null;
|
|
573
697
|
const override = outcome ? (0, _types.markForOutcome)(outcome) : undefined;
|
|
574
698
|
|
|
575
699
|
// Emit request event
|
|
@@ -585,24 +709,28 @@ class NetworkListener {
|
|
|
585
709
|
params: params || undefined,
|
|
586
710
|
client: clientType
|
|
587
711
|
},
|
|
588
|
-
override
|
|
712
|
+
override,
|
|
713
|
+
condition
|
|
589
714
|
});
|
|
590
715
|
try {
|
|
591
|
-
if (outcome) await (0,
|
|
716
|
+
if (condition || outcome) await (0, _wait.waitBeforeDispatch)((condition?.delayMs ?? 0) + (outcome?.delayMs ?? 0), signal);
|
|
717
|
+
(0, _wait.throwIfAborted)(signal);
|
|
718
|
+
if (condition?.profile === "offline") throw new TypeError("Network request failed (Buoy Offline)");
|
|
592
719
|
if (outcome?.kind === "fail") throw (0, _applyToFetch.synthesizeFetchError)(outcome);
|
|
593
720
|
let response;
|
|
594
721
|
if (outcome?.kind === "respond") {
|
|
595
722
|
response = (0, _applyToFetch.synthesizeResponse)(url, outcome);
|
|
596
723
|
} else {
|
|
597
|
-
// The flag spans the CALL only — see `
|
|
724
|
+
// The flag spans the CALL only — see `insideFetchHandoff`. It has
|
|
598
725
|
// to be cleared before the await, or it would still be raised for
|
|
599
726
|
// every unrelated request that starts while this one is in flight.
|
|
600
|
-
|
|
727
|
+
const previousFetchHandoff = insideFetchHandoff;
|
|
728
|
+
insideFetchHandoff = true;
|
|
601
729
|
let pending;
|
|
602
730
|
try {
|
|
603
731
|
pending = passthroughFetch(input, init);
|
|
604
732
|
} finally {
|
|
605
|
-
|
|
733
|
+
insideFetchHandoff = previousFetchHandoff;
|
|
606
734
|
}
|
|
607
735
|
response = await pending;
|
|
608
736
|
}
|
|
@@ -612,12 +740,29 @@ class NetworkListener {
|
|
|
612
740
|
// look faster through fetch than through XHR.
|
|
613
741
|
const ttfb = Date.now() - startTime;
|
|
614
742
|
|
|
743
|
+
/**
|
|
744
|
+
* A STREAMED reply is never awaited here.
|
|
745
|
+
*
|
|
746
|
+
* `processResponseBody` reads the clone to the end, and for
|
|
747
|
+
* `text/event-stream` the end is when the server finishes — seconds
|
|
748
|
+
* later, for an LLM. Awaiting it made the interceptor hold the
|
|
749
|
+
* response back until then, so every consumer of a streaming endpoint
|
|
750
|
+
* inside an app with capture on received the whole thing at once and
|
|
751
|
+
* could not stream a word of it. The row is emitted now with what is
|
|
752
|
+
* known, and the body is filled in when the stream closes: the store
|
|
753
|
+
* merges a second `response` event onto the same request id.
|
|
754
|
+
*/
|
|
755
|
+
const streamed = typeof response.body?.getReader === "function" || isStreamedContentType(response.headers?.get?.("content-type"));
|
|
756
|
+
|
|
615
757
|
// Clone response to read body with size limits
|
|
616
758
|
const responseClone = response.clone();
|
|
617
759
|
const {
|
|
618
760
|
body,
|
|
619
761
|
size: responseSize
|
|
620
|
-
} =
|
|
762
|
+
} = streamed ? {
|
|
763
|
+
body: STREAMING_PLACEHOLDER,
|
|
764
|
+
size: 0
|
|
765
|
+
} : await self.processResponseBody(responseClone);
|
|
621
766
|
|
|
622
767
|
// Now the body is in — this is the comparable, total number.
|
|
623
768
|
const duration = Date.now() - startTime;
|
|
@@ -674,8 +819,46 @@ class NetworkListener {
|
|
|
674
819
|
body,
|
|
675
820
|
size: responseSize
|
|
676
821
|
},
|
|
677
|
-
override
|
|
822
|
+
override,
|
|
823
|
+
condition
|
|
678
824
|
});
|
|
825
|
+
if (streamed) {
|
|
826
|
+
// Bounded, cancelled at the cap, and never awaited by the caller.
|
|
827
|
+
void self.readStreamedBody(responseClone).then(late => {
|
|
828
|
+
let capturedBody = late.body;
|
|
829
|
+
if (response.headers?.get?.("content-type")?.includes("json")) {
|
|
830
|
+
try {
|
|
831
|
+
capturedBody = JSON.parse(late.body);
|
|
832
|
+
} catch {/* Partial or malformed JSON stays text. */}
|
|
833
|
+
}
|
|
834
|
+
self.emit({
|
|
835
|
+
type: "response",
|
|
836
|
+
timestamp: new Date(),
|
|
837
|
+
duration: Date.now() - startTime,
|
|
838
|
+
timings,
|
|
839
|
+
request: {
|
|
840
|
+
id: requestId || "unknown",
|
|
841
|
+
url: cleanUrl,
|
|
842
|
+
method,
|
|
843
|
+
headers: requestHeaders,
|
|
844
|
+
data: requestData,
|
|
845
|
+
params: params || undefined,
|
|
846
|
+
client: clientType
|
|
847
|
+
},
|
|
848
|
+
response: {
|
|
849
|
+
status: response.status,
|
|
850
|
+
statusText: response.statusText,
|
|
851
|
+
headers: responseHeaders,
|
|
852
|
+
body: capturedBody,
|
|
853
|
+
size: late.size
|
|
854
|
+
},
|
|
855
|
+
override,
|
|
856
|
+
condition
|
|
857
|
+
});
|
|
858
|
+
}).catch(() => {
|
|
859
|
+
/* the caller's response is unaffected either way */
|
|
860
|
+
});
|
|
861
|
+
}
|
|
679
862
|
return response;
|
|
680
863
|
} catch (error) {
|
|
681
864
|
const duration = Date.now() - startTime;
|
|
@@ -698,7 +881,8 @@ class NetworkListener {
|
|
|
698
881
|
message: error instanceof Error ? error.message : "Network error",
|
|
699
882
|
stack: error instanceof Error ? error.stack : undefined
|
|
700
883
|
},
|
|
701
|
-
override
|
|
884
|
+
override,
|
|
885
|
+
condition
|
|
702
886
|
});
|
|
703
887
|
throw error;
|
|
704
888
|
}
|
|
@@ -719,18 +903,20 @@ class NetworkListener {
|
|
|
719
903
|
this.originalXHRSend = passthroughSend;
|
|
720
904
|
this.originalXHRSetRequestHeader = passthroughSetRequestHeader;
|
|
721
905
|
const generation = ++this.xhrGeneration;
|
|
906
|
+
const pendingCapture = new WeakMap();
|
|
722
907
|
const patchedOpen = function (method, url, async, user, password) {
|
|
723
|
-
//
|
|
724
|
-
|
|
908
|
+
// Native validation runs before replacing metadata or capture listeners.
|
|
909
|
+
const result = passthroughOpen.call(this, method, url, async ?? true, user, password);
|
|
725
910
|
if (generation === self.xhrGeneration) {
|
|
911
|
+
pendingCapture.get(this)?.();
|
|
726
912
|
const xhr = this;
|
|
727
|
-
xhr.
|
|
913
|
+
xhr._buoyRequestId = `xhr_${++self.requestCounter}`;
|
|
728
914
|
xhr._method = method;
|
|
729
915
|
xhr._url = url;
|
|
730
|
-
xhr.
|
|
916
|
+
xhr._buoyStartTime = Date.now();
|
|
731
917
|
xhr._requestHeaders = {};
|
|
732
918
|
}
|
|
733
|
-
return
|
|
919
|
+
return result;
|
|
734
920
|
};
|
|
735
921
|
|
|
736
922
|
// Track request headers
|
|
@@ -745,14 +931,19 @@ class NetworkListener {
|
|
|
745
931
|
// @ts-ignore - this does exist on native
|
|
746
932
|
data) {
|
|
747
933
|
// Superseded generation: silent pass-through (see the fetch wrapper).
|
|
748
|
-
if (generation !== self.xhrGeneration) {
|
|
934
|
+
if (generation !== self.xhrGeneration || insideFetchHandoff) {
|
|
935
|
+
return passthroughSend.call(this, data);
|
|
936
|
+
}
|
|
937
|
+
if (this.readyState !== 1 || this._sent) {
|
|
749
938
|
return passthroughSend.call(this, data);
|
|
750
939
|
}
|
|
751
940
|
const xhr = this;
|
|
752
|
-
const requestId = xhr.
|
|
941
|
+
const requestId = xhr._buoyRequestId;
|
|
753
942
|
const method = xhr._method || "GET";
|
|
754
943
|
const url = xhr._url || "";
|
|
755
|
-
const startTime =
|
|
944
|
+
const startTime = Date.now();
|
|
945
|
+
xhr._buoyStartTime = startTime;
|
|
946
|
+
xhr._buoyTtfb = undefined;
|
|
756
947
|
const requestHeaders = xhr._requestHeaders || {};
|
|
757
948
|
|
|
758
949
|
// Skip ignored URLs
|
|
@@ -792,14 +983,15 @@ class NetworkListener {
|
|
|
792
983
|
* This is the layer that covers `axios` and raw XMLHttpRequest. It also
|
|
793
984
|
* covers `fetch` on BARE React Native, where the whatwg-fetch polyfill
|
|
794
985
|
* runs through XHR — which is why the fetch wrapper raises
|
|
795
|
-
* `
|
|
986
|
+
* `insideFetchHandoff` around its pass-through, so one request can't
|
|
796
987
|
* be overridden twice.
|
|
797
988
|
*
|
|
798
989
|
* Matching uses the raw `url` (query string included), which is what the
|
|
799
990
|
* developer sees in the list and writes into a rule. No-ops in production
|
|
800
991
|
* builds; see `overrideForRequest`.
|
|
801
992
|
*/
|
|
802
|
-
const
|
|
993
|
+
const condition = (0, _store.snapshotNetworkCondition)(url);
|
|
994
|
+
const outcome = condition?.profile === "offline" ? null : (0, _engine.overrideForRequest)(url, method);
|
|
803
995
|
const override = outcome ? (0, _types.markForOutcome)(outcome) : undefined;
|
|
804
996
|
|
|
805
997
|
// Emit request event
|
|
@@ -815,7 +1007,8 @@ class NetworkListener {
|
|
|
815
1007
|
params: params || undefined,
|
|
816
1008
|
client: clientType
|
|
817
1009
|
},
|
|
818
|
-
override
|
|
1010
|
+
override,
|
|
1011
|
+
condition
|
|
819
1012
|
});
|
|
820
1013
|
|
|
821
1014
|
// Track if we've already processed this request to avoid duplicate events
|
|
@@ -823,18 +1016,20 @@ class NetworkListener {
|
|
|
823
1016
|
const processResponse = (isError = false) => {
|
|
824
1017
|
if (processed) return;
|
|
825
1018
|
processed = true;
|
|
826
|
-
self.handleXHRResponse(this, requestId || "unknown", cleanUrl, method, requestHeaders, requestData, params, startTime || 0, isError, clientType, override);
|
|
1019
|
+
self.handleXHRResponse(this, requestId || "unknown", cleanUrl, method, requestHeaders, requestData, params, startTime || 0, isError, clientType, override, condition);
|
|
827
1020
|
// Clean up event listeners after processing to prevent memory leaks
|
|
828
1021
|
cleanup();
|
|
829
1022
|
};
|
|
830
1023
|
|
|
831
1024
|
// Cleanup function to remove event listeners
|
|
832
1025
|
const cleanup = () => {
|
|
1026
|
+
pendingCapture.delete(this);
|
|
833
1027
|
this.removeEventListener("load", loadListener);
|
|
834
1028
|
this.removeEventListener("error", errorListener);
|
|
835
1029
|
this.removeEventListener("abort", abortListener);
|
|
836
1030
|
this.removeEventListener("readystatechange", readyStateListener);
|
|
837
1031
|
};
|
|
1032
|
+
pendingCapture.set(this, cleanup);
|
|
838
1033
|
|
|
839
1034
|
// Use addEventListener to listen to events WITHOUT replacing user handlers
|
|
840
1035
|
// This is critical because React Native's XMLHttpRequest uses EventTarget
|
|
@@ -856,8 +1051,8 @@ class NetworkListener {
|
|
|
856
1051
|
// TTFB here costs one comparison and one clock read.
|
|
857
1052
|
if (this.readyState === 2) {
|
|
858
1053
|
const xhr = this;
|
|
859
|
-
if (xhr.
|
|
860
|
-
xhr.
|
|
1054
|
+
if (xhr._buoyTtfb === undefined && xhr._buoyStartTime) {
|
|
1055
|
+
xhr._buoyTtfb = Date.now() - xhr._buoyStartTime;
|
|
861
1056
|
}
|
|
862
1057
|
}
|
|
863
1058
|
if (this.readyState === 4 && !processed) {
|
|
@@ -870,6 +1065,10 @@ class NetworkListener {
|
|
|
870
1065
|
this.addEventListener("error", errorListener);
|
|
871
1066
|
this.addEventListener("abort", abortListener);
|
|
872
1067
|
this.addEventListener("readystatechange", readyStateListener);
|
|
1068
|
+
if (condition) {
|
|
1069
|
+
(0, _xhr.runConditionXhr)(this, url, condition, outcome, () => passthroughSend.call(this, data));
|
|
1070
|
+
return;
|
|
1071
|
+
}
|
|
873
1072
|
if (outcome) {
|
|
874
1073
|
// Listeners are attached above FIRST: `runOverride` drives the same
|
|
875
1074
|
// readyState transitions a real response would, and the interceptor has
|
|
@@ -44,8 +44,8 @@ const MAX_PINNED = exports.MAX_PINNED = 25;
|
|
|
44
44
|
const MAX_SAVED = exports.MAX_SAVED = 50;
|
|
45
45
|
/**
|
|
46
46
|
* @deprecated Read the live limit with `useLimit("network.saved")` — this only
|
|
47
|
-
* reports the FREE tier's value and
|
|
48
|
-
*
|
|
47
|
+
* reports the FREE tier's value and ignores the Weekend Pass. Kept because it
|
|
48
|
+
* is part of the package's public exports.
|
|
49
49
|
*/
|
|
50
50
|
const FREE_TIER_SAVED_LIMIT = exports.FREE_TIER_SAVED_LIMIT = _sharedUi.TIER_LIMITS["network.saved"].free;
|
|
51
51
|
/** Per-body cap on a snapshot. */
|
|
@@ -19,8 +19,8 @@ var _networkSavedStore = require("./networkSavedStore");
|
|
|
19
19
|
function selectSavedEvents(savedRecords,
|
|
20
20
|
/**
|
|
21
21
|
* How many saves this tier may see — `useLimit("network.saved")`. Takes a
|
|
22
|
-
* number rather than `isPro` because
|
|
23
|
-
* boolean cannot express
|
|
22
|
+
* number rather than `isPro` because Pro's cap is 50, not unlimited, and a
|
|
23
|
+
* boolean cannot express that.
|
|
24
24
|
*/
|
|
25
25
|
limit, search) {
|
|
26
26
|
/**
|