@buoy-gg/network 7.0.40 → 7.0.41
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 +6 -0
- package/lib/commonjs/index.js +7 -0
- package/lib/commonjs/network/components/NetworkListScreen.js +2 -3
- package/lib/commonjs/network/components/NetworkListScreen.web.js +2 -3
- package/lib/commonjs/network/components/NetworkModal.js +1 -1
- package/lib/commonjs/network/conditions/xhr.js +26 -1
- package/lib/commonjs/network/overrides/applyToXhr.js +99 -42
- package/lib/commonjs/network/overrides/matchRule.js +7 -1
- package/lib/commonjs/network/overrides/overriddenRows.js +37 -0
- package/lib/commonjs/network/utils/networkEventStore.js +24 -6
- package/lib/commonjs/network/utils/networkListener.js +104 -11
- package/lib/commonjs/network/utils/networkSavedStore.js +4 -4
- package/lib/commonjs/network/utils/requestId.js +16 -0
- package/lib/commonjs/network/utils/resolveRequestUrl.js +15 -0
- package/lib/commonjs/network/utils/startNetworkCapture.js +24 -0
- package/lib/commonjs/preset.js +3 -12
- package/lib/module/index.js +1 -0
- package/lib/module/network/components/NetworkListScreen.js +2 -3
- package/lib/module/network/components/NetworkListScreen.web.js +2 -3
- package/lib/module/network/components/NetworkModal.js +1 -1
- package/lib/module/network/conditions/xhr.js +27 -2
- package/lib/module/network/overrides/applyToXhr.js +98 -42
- package/lib/module/network/overrides/matchRule.js +7 -1
- package/lib/module/network/overrides/overriddenRows.js +33 -0
- package/lib/module/network/utils/networkEventStore.js +25 -7
- package/lib/module/network/utils/networkListener.js +104 -12
- package/lib/module/network/utils/networkSavedStore.js +4 -4
- package/lib/module/network/utils/requestId.js +12 -0
- package/lib/module/network/utils/resolveRequestUrl.js +11 -0
- package/lib/module/network/utils/startNetworkCapture.js +21 -0
- package/lib/module/preset.js +3 -12
- package/lib/typescript/index.d.ts +1 -0
- package/lib/typescript/index.d.ts.map +1 -1
- package/lib/typescript/network/components/NetworkListScreen.d.ts.map +1 -1
- package/lib/typescript/network/components/NetworkListScreen.web.d.ts.map +1 -1
- package/lib/typescript/network/conditions/xhr.d.ts.map +1 -1
- package/lib/typescript/network/overrides/applyToXhr.d.ts +1 -0
- package/lib/typescript/network/overrides/applyToXhr.d.ts.map +1 -1
- package/lib/typescript/network/overrides/matchRule.d.ts.map +1 -1
- package/lib/typescript/network/overrides/overriddenRows.d.ts +16 -0
- package/lib/typescript/network/overrides/overriddenRows.d.ts.map +1 -0
- package/lib/typescript/network/utils/networkEventStore.d.ts.map +1 -1
- package/lib/typescript/network/utils/networkListener.d.ts +3 -4
- package/lib/typescript/network/utils/networkListener.d.ts.map +1 -1
- package/lib/typescript/network/utils/networkSavedStore.d.ts +4 -4
- package/lib/typescript/network/utils/requestId.d.ts +2 -0
- package/lib/typescript/network/utils/requestId.d.ts.map +1 -0
- package/lib/typescript/network/utils/resolveRequestUrl.d.ts +3 -0
- package/lib/typescript/network/utils/resolveRequestUrl.d.ts.map +1 -0
- package/lib/typescript/network/utils/startNetworkCapture.d.ts +6 -0
- package/lib/typescript/network/utils/startNetworkCapture.d.ts.map +1 -0
- package/lib/typescript/preset.d.ts.map +1 -1
- package/lib/web/index.mjs +146 -0
- package/package.json +32 -10
package/README.md
CHANGED
|
@@ -93,3 +93,9 @@ Each call keeps the profile selected when it starts. Offline takes precedence ov
|
|
|
93
93
|
Conditions remain active when the panel is closed or its list is paused. They reset on a full JS reload or account access loss and are never persisted. Captured requests retain their condition stamp for saved snapshots, detail views and export. Desktop and MCP can control a connected device that advertises the conditions actions.
|
|
94
94
|
|
|
95
95
|
Network conditions are a development preview. The native compatibility matrix across transports, runtimes and devices is still being worked through, so confirm the behaviour you rely on in your own build before you depend on it.
|
|
96
|
+
|
|
97
|
+
## Web support (unreleased)
|
|
98
|
+
|
|
99
|
+
Browser fetch and XHR use the shared capture, rules, conditions, and request panels. CORS still controls which response data the page can read. The browser build is available in this checkout and has not been published yet. See the [web setup guide](https://buoy.gg/buoy/latest/docs/web-preview) for registration, dependencies, and browser boundaries.
|
|
100
|
+
|
|
101
|
+
The capture control reads “Pause network capture” while capture is enabled and “Resume network capture” while it is paused. Existing requests remain visible while capture is paused.
|
package/lib/commonjs/index.js
CHANGED
|
@@ -243,6 +243,12 @@ Object.defineProperty(exports, "splitPinned", {
|
|
|
243
243
|
return _withPinnedFirst.splitPinned;
|
|
244
244
|
}
|
|
245
245
|
});
|
|
246
|
+
Object.defineProperty(exports, "startNetworkCapture", {
|
|
247
|
+
enumerable: true,
|
|
248
|
+
get: function () {
|
|
249
|
+
return _startNetworkCapture.startNetworkCapture;
|
|
250
|
+
}
|
|
251
|
+
});
|
|
246
252
|
Object.defineProperty(exports, "subscribeNetworkConditions", {
|
|
247
253
|
enumerable: true,
|
|
248
254
|
get: function () {
|
|
@@ -298,6 +304,7 @@ Object.defineProperty(exports, "useTickEveryMinute", {
|
|
|
298
304
|
}
|
|
299
305
|
});
|
|
300
306
|
var _preset = require("./preset");
|
|
307
|
+
var _startNetworkCapture = require("./network/utils/startNetworkCapture");
|
|
301
308
|
var _NetworkModal = require("./network/components/NetworkModal");
|
|
302
309
|
var _NetworkEventDetailView = require("./network/components/NetworkEventDetailView");
|
|
303
310
|
var _NetworkEventItemCompact = require("./network/components/NetworkEventItemCompact");
|
|
@@ -12,7 +12,7 @@ var _NetworkUpgradeBanner = require("./NetworkUpgradeBanner");
|
|
|
12
12
|
var _useNetworkEventList = require("../hooks/useNetworkEventList");
|
|
13
13
|
var _useNetworkSaved = require("../hooks/useNetworkSaved");
|
|
14
14
|
var _useOverrideRules = require("../hooks/useOverrideRules");
|
|
15
|
-
var
|
|
15
|
+
var _overriddenRows = require("../overrides/overriddenRows");
|
|
16
16
|
var _withPinnedFirst = require("../utils/withPinnedFirst");
|
|
17
17
|
var _jsxRuntime = require("react/jsx-runtime");
|
|
18
18
|
/**
|
|
@@ -196,8 +196,7 @@ function NetworkListScreen({
|
|
|
196
196
|
// Paused rules still populate the strip — that's how you find the thing you
|
|
197
197
|
// paused in order to resume it. The header says PAUSED instead.
|
|
198
198
|
const activeRules = rules.filter(rule => rule.enabled && !isRuleSpent(rule));
|
|
199
|
-
const
|
|
200
|
-
const overridden = activeRules.length ? [...pinned, ...rest].filter(isOverridden) : [];
|
|
199
|
+
const overridden = (0, _overriddenRows.overriddenEvents)([...pinned, ...rest], activeRules);
|
|
201
200
|
const overriddenIds = new Set(overridden.map(event => event.id));
|
|
202
201
|
const pinnedRows = pinned.filter(event => !overriddenIds.has(event.id));
|
|
203
202
|
const restRows = overriddenIds.size ? rest.filter(event => !overriddenIds.has(event.id)) : rest;
|
|
@@ -9,7 +9,7 @@ var _NetworkEventItemCompact = require("./NetworkEventItemCompact.web");
|
|
|
9
9
|
var _useNetworkEventList = require("../hooks/useNetworkEventList");
|
|
10
10
|
var _useNetworkSaved = require("../hooks/useNetworkSaved");
|
|
11
11
|
var _useOverrideRules = require("../hooks/useOverrideRules");
|
|
12
|
-
var
|
|
12
|
+
var _overriddenRows = require("../overrides/overriddenRows");
|
|
13
13
|
var _withPinnedFirst = require("../utils/withPinnedFirst");
|
|
14
14
|
var _jsxRuntime = require("react/jsx-runtime");
|
|
15
15
|
/**
|
|
@@ -387,8 +387,7 @@ function NetworkListScreen({
|
|
|
387
387
|
// Hoist anything an active rule covers. Done after `splitPinned` so a request
|
|
388
388
|
// that is both pinned and overridden appears once, in the louder section.
|
|
389
389
|
const activeRules = rules.filter(rule => rule.enabled && !isRuleSpent(rule));
|
|
390
|
-
const
|
|
391
|
-
const overridden = activeRules.length ? [...pinned, ...rest].filter(isOverridden) : [];
|
|
390
|
+
const overridden = (0, _overriddenRows.overriddenEvents)([...pinned, ...rest], activeRules);
|
|
392
391
|
const overriddenIds = new Set(overridden.map(event => event.id));
|
|
393
392
|
const pinnedRows = pinned.filter(event => !overriddenIds.has(event.id));
|
|
394
393
|
const restRows = overriddenIds.size ? rest.filter(event => !overriddenIds.has(event.id)) : rest;
|
|
@@ -501,7 +501,7 @@ function NetworkModalInner({
|
|
|
501
501
|
}) : null, /*#__PURE__*/(0, _jsxRuntime.jsx)(_sharedUi.PowerToggleButton, {
|
|
502
502
|
isEnabled: !paused,
|
|
503
503
|
onToggle: () => setPaused(prev => !prev),
|
|
504
|
-
accessibilityLabel: "Pause network
|
|
504
|
+
accessibilityLabel: paused ? "Resume network capture" : "Pause network capture",
|
|
505
505
|
testID: "buoy-network-power"
|
|
506
506
|
}), /*#__PURE__*/(0, _jsxRuntime.jsx)(_NetworkHeaderLive.NetworkClearButton, {
|
|
507
507
|
paused: paused
|
|
@@ -7,7 +7,32 @@ exports.runConditionXhr = runConditionXhr;
|
|
|
7
7
|
var _applyToXhr = require("../overrides/applyToXhr");
|
|
8
8
|
/** Active simulation uses a send-to-completion deadline, including the wait. */
|
|
9
9
|
function runConditionXhr(target, url, condition, outcome, dispatch) {
|
|
10
|
-
if (!(0, _applyToXhr.canSynthesize)(target))
|
|
10
|
+
if (!(0, _applyToXhr.canSynthesize)(target)) {
|
|
11
|
+
const rule = {
|
|
12
|
+
id: "condition",
|
|
13
|
+
enabled: true,
|
|
14
|
+
urlPattern: url,
|
|
15
|
+
kind: "delay",
|
|
16
|
+
hits: 0,
|
|
17
|
+
seen: 0,
|
|
18
|
+
createdAt: Date.now()
|
|
19
|
+
};
|
|
20
|
+
const simulation = condition.profile === "offline" ? {
|
|
21
|
+
kind: "fail",
|
|
22
|
+
rule,
|
|
23
|
+
failKind: "network",
|
|
24
|
+
delayMs: 0
|
|
25
|
+
} : outcome ? {
|
|
26
|
+
...outcome,
|
|
27
|
+
delayMs: condition.delayMs + outcome.delayMs
|
|
28
|
+
} : {
|
|
29
|
+
kind: "delay",
|
|
30
|
+
rule,
|
|
31
|
+
delayMs: condition.delayMs
|
|
32
|
+
};
|
|
33
|
+
if (!(0, _applyToXhr.runOverride)(target, url, simulation, dispatch)) throw new Error("Network conditions are unavailable for this XMLHttpRequest implementation.");
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
11
36
|
const xhr = target;
|
|
12
37
|
if (xhr.readyState !== 1 || xhr._sent) throw new Error("Request is not open or has already been sent.");
|
|
13
38
|
const started = Date.now();
|
|
@@ -6,6 +6,7 @@ Object.defineProperty(exports, "__esModule", {
|
|
|
6
6
|
exports.canSynthesize = canSynthesize;
|
|
7
7
|
exports.canSynthesizeStandard = canSynthesizeStandard;
|
|
8
8
|
exports.completeNativeOverride = completeNativeOverride;
|
|
9
|
+
exports.hasPendingStandardRequest = hasPendingStandardRequest;
|
|
9
10
|
exports.runOverride = runOverride;
|
|
10
11
|
/**
|
|
11
12
|
* Perform an override outcome against an XMLHttpRequest — React Native's OR a
|
|
@@ -286,6 +287,7 @@ function respondStandard(xhr, url, outcome, isAborted) {
|
|
|
286
287
|
setStandardReadyState(xhr, LOADING);
|
|
287
288
|
if (isAborted()) return;
|
|
288
289
|
setStandardReadyState(xhr, DONE);
|
|
290
|
+
if (isAborted()) return;
|
|
289
291
|
fireEvent(xhr, "load");
|
|
290
292
|
fireEvent(xhr, "loadend");
|
|
291
293
|
}
|
|
@@ -314,60 +316,115 @@ function failStandard(xhr, outcome) {
|
|
|
314
316
|
* `delay` pass-through hands the request to the real network, where the
|
|
315
317
|
* prototype `abort()` must be reachable again.
|
|
316
318
|
*/
|
|
319
|
+
const pendingStandardRequests = new WeakMap();
|
|
320
|
+
function hasPendingStandardRequest(xhr) {
|
|
321
|
+
return pendingStandardRequests.has(xhr);
|
|
322
|
+
}
|
|
317
323
|
function runStandardOverride(xhr, url, outcome, passThrough) {
|
|
318
|
-
|
|
319
|
-
|
|
324
|
+
const started = Date.now();
|
|
325
|
+
const generation = {};
|
|
326
|
+
let timer;
|
|
327
|
+
let deadline;
|
|
320
328
|
let settled = false;
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
329
|
+
let aborted = false;
|
|
330
|
+
let dispatched = false;
|
|
331
|
+
let timeout = xhr.timeout;
|
|
332
|
+
const originalAbort = xhr.abort;
|
|
333
|
+
const originalOpen = xhr.open;
|
|
334
|
+
const names = ['abort', 'open', 'timeout', 'readyState', 'status', 'statusText', 'response', 'responseText', 'responseURL', 'getAllResponseHeaders', 'getResponseHeader'];
|
|
335
|
+
const descriptors = new Map(names.map(name => [name, Object.getOwnPropertyDescriptor(xhr, name)]));
|
|
336
|
+
let prototype = Object.getPrototypeOf(xhr);
|
|
337
|
+
let nativeTimeout;
|
|
338
|
+
while (prototype && !nativeTimeout) {
|
|
339
|
+
nativeTimeout = Object.getOwnPropertyDescriptor(prototype, 'timeout');
|
|
340
|
+
prototype = Object.getPrototypeOf(prototype);
|
|
341
|
+
}
|
|
342
|
+
const finish = () => {
|
|
343
|
+
settled = true;
|
|
344
|
+
if (pendingStandardRequests.get(xhr) === generation) pendingStandardRequests.delete(xhr);
|
|
345
|
+
clearTimeout(timer);
|
|
346
|
+
clearTimeout(deadline);
|
|
347
|
+
xhr.removeEventListener('loadend', finish);
|
|
348
|
+
};
|
|
349
|
+
const restore = () => {
|
|
350
|
+
finish();
|
|
351
|
+
for (const [name, descriptor] of descriptors) {
|
|
352
|
+
if (descriptor) Object.defineProperty(xhr, name, descriptor);else delete xhr[name];
|
|
327
353
|
}
|
|
354
|
+
xhr.timeout = timeout;
|
|
328
355
|
};
|
|
329
|
-
|
|
356
|
+
const scheduleDeadline = () => {
|
|
357
|
+
clearTimeout(deadline);
|
|
358
|
+
if (dispatched) {
|
|
359
|
+
nativeTimeout?.set?.call(xhr, timeout > 0 ? Math.max(1, timeout - (Date.now() - started)) : 0);
|
|
360
|
+
} else if (timeout > 0) {
|
|
361
|
+
deadline = setTimeout(() => {
|
|
362
|
+
if (settled) return;
|
|
363
|
+
clearTimeout(timer);
|
|
364
|
+
failStandard(xhr, {
|
|
365
|
+
...outcome,
|
|
366
|
+
kind: 'fail',
|
|
367
|
+
failKind: 'timeout'
|
|
368
|
+
});
|
|
369
|
+
finish();
|
|
370
|
+
}, Math.max(0, timeout - (Date.now() - started)));
|
|
371
|
+
}
|
|
372
|
+
};
|
|
373
|
+
Object.defineProperty(xhr, 'timeout', {
|
|
374
|
+
configurable: true,
|
|
375
|
+
get: () => timeout,
|
|
376
|
+
set: value => {
|
|
377
|
+
timeout = Math.max(0, Number(value) || 0);
|
|
378
|
+
scheduleDeadline();
|
|
379
|
+
}
|
|
380
|
+
});
|
|
381
|
+
Object.defineProperty(xhr, 'open', {
|
|
382
|
+
configurable: true,
|
|
383
|
+
value: (...args) => {
|
|
384
|
+
restore();
|
|
385
|
+
return Reflect.apply(originalOpen, xhr, args);
|
|
386
|
+
}
|
|
387
|
+
});
|
|
388
|
+
Object.defineProperty(xhr, 'abort', {
|
|
330
389
|
configurable: true,
|
|
331
|
-
writable: true,
|
|
332
390
|
value: () => {
|
|
333
|
-
if (
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
clearTimeout(timer);
|
|
338
|
-
timer = null;
|
|
391
|
+
if (dispatched || settled) {
|
|
392
|
+
restore();
|
|
393
|
+
originalAbort.call(xhr);
|
|
394
|
+
return;
|
|
339
395
|
}
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
// whatwg's request-error steps, which is also what RN's setReadyState
|
|
343
|
-
// dispatches off `_aborted`.
|
|
396
|
+
aborted = true;
|
|
397
|
+
clearTimeout(timer);
|
|
344
398
|
setStandardReadyState(xhr, DONE);
|
|
345
|
-
fireEvent(xhr,
|
|
346
|
-
fireEvent(xhr,
|
|
399
|
+
fireEvent(xhr, 'abort');
|
|
400
|
+
fireEvent(xhr, 'loadend');
|
|
401
|
+
finish();
|
|
402
|
+
if (!pendingStandardRequests.has(xhr)) defineOwn(xhr, 'readyState', 0);
|
|
347
403
|
}
|
|
348
404
|
});
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
405
|
+
pendingStandardRequests.set(xhr, generation);
|
|
406
|
+
xhr.addEventListener('loadend', finish);
|
|
407
|
+
scheduleDeadline();
|
|
408
|
+
timer = setTimeout(() => {
|
|
409
|
+
if (settled || aborted) return;
|
|
410
|
+
if (outcome.kind === 'delay') {
|
|
411
|
+
dispatched = true;
|
|
412
|
+
scheduleDeadline();
|
|
413
|
+
try {
|
|
414
|
+
passThrough();
|
|
415
|
+
} catch {
|
|
416
|
+
failStandard(xhr, {
|
|
417
|
+
...outcome,
|
|
418
|
+
kind: 'fail',
|
|
419
|
+
failKind: 'network'
|
|
420
|
+
});
|
|
421
|
+
finish();
|
|
422
|
+
}
|
|
357
423
|
return;
|
|
358
424
|
}
|
|
359
|
-
if (outcome.kind ===
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
respondStandard(xhr, url, outcome, () => aborted);
|
|
363
|
-
}
|
|
364
|
-
settled = true;
|
|
365
|
-
restoreAbort();
|
|
366
|
-
};
|
|
367
|
-
|
|
368
|
-
// ALWAYS asynchronous, even at 0ms — same contract as the RN path: callers
|
|
369
|
-
// legitimately attach handlers after `send()` returns (axios does).
|
|
370
|
-
timer = setTimeout(fire, outcome.delayMs);
|
|
425
|
+
if (outcome.kind === 'fail') failStandard(xhr, outcome);else respondStandard(xhr, url, outcome, () => aborted || pendingStandardRequests.get(xhr) !== generation);
|
|
426
|
+
finish();
|
|
427
|
+
}, outcome.delayMs);
|
|
371
428
|
return true;
|
|
372
429
|
}
|
|
373
430
|
|
|
@@ -57,7 +57,13 @@ function compilePattern(pattern) {
|
|
|
57
57
|
// Anchored, like Chrome's `^${...}$` — a pattern describes the whole URL, so
|
|
58
58
|
// `pokeapi.co` matches nothing and `*pokeapi.co*` matches everything on that
|
|
59
59
|
// host. Predictable beats clever; the rule editor writes full URLs for you.
|
|
60
|
-
|
|
60
|
+
//
|
|
61
|
+
// One exception: a pattern that starts with `/` is a PATH. Anchored to the
|
|
62
|
+
// full URL it could never match anything (every URL starts with a scheme),
|
|
63
|
+
// so it means "this path on any origin" — `/api/rewards` matches
|
|
64
|
+
// `http://localhost:3002/api/rewards`, and `/api/rewards*` also its queries.
|
|
65
|
+
const source = pattern.startsWith("/") ? `[a-zA-Z][a-zA-Z0-9+.-]*://[^/]*${escapeRegex(pattern)}` : escapeRegex(pattern);
|
|
66
|
+
const regex = new RegExp(`^${source}$`);
|
|
61
67
|
// Unbounded growth isn't possible in practice (patterns come from a capped
|
|
62
68
|
// rule list), but a user editing a pattern character by character churns
|
|
63
69
|
// entries, so keep the cache from becoming a leak.
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
Object.defineProperty(exports, "__esModule", {
|
|
4
|
+
value: true
|
|
5
|
+
});
|
|
6
|
+
exports.overriddenEvents = overriddenEvents;
|
|
7
|
+
var _matchRule = require("./matchRule");
|
|
8
|
+
/**
|
|
9
|
+
* Which captured requests belong in the list's OVERRIDDEN strip.
|
|
10
|
+
*
|
|
11
|
+
* A row belongs there when an active rule actually changed it (it carries the
|
|
12
|
+
* rule's override mark) or matched it after the rule existed (while overrides
|
|
13
|
+
* are paused those pass through, and the strip is how you find the rule again).
|
|
14
|
+
* Before a rule existed nothing overrode anything, so older matching requests
|
|
15
|
+
* are real server responses and stay in the list, with one exception: the
|
|
16
|
+
* newest of them, which is the request the rule was built from. That one hoists
|
|
17
|
+
* right away, so creating a rule shows where it points without waiting for the
|
|
18
|
+
* endpoint to be called again.
|
|
19
|
+
*/
|
|
20
|
+
function overriddenEvents(events, activeRules) {
|
|
21
|
+
if (activeRules.length === 0) return [];
|
|
22
|
+
const activeIds = new Set(activeRules.map(rule => rule.id));
|
|
23
|
+
const picked = new Set();
|
|
24
|
+
for (const rule of activeRules) {
|
|
25
|
+
let source;
|
|
26
|
+
for (const event of events) {
|
|
27
|
+
if (event.override && activeIds.has(event.override.ruleId)) {
|
|
28
|
+
picked.add(event.id);
|
|
29
|
+
continue;
|
|
30
|
+
}
|
|
31
|
+
if (!(0, _matchRule.ruleMatches)(rule, event.url, String(event.method || "GET"))) continue;
|
|
32
|
+
if (event.timestamp >= rule.createdAt) picked.add(event.id);else if (!source || event.timestamp > source.timestamp) source = event;
|
|
33
|
+
}
|
|
34
|
+
if (source) picked.add(source.id);
|
|
35
|
+
}
|
|
36
|
+
return events.filter(event => picked.has(event.id));
|
|
37
|
+
}
|
|
@@ -140,9 +140,8 @@ class NetworkEventStore extends _sharedUi.BaseEventStore {
|
|
|
140
140
|
}
|
|
141
141
|
|
|
142
142
|
// If no other listeners exist, stop the network interceptor
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
listener.stopListening();
|
|
143
|
+
if ((0, _networkListener.getNetworkListenerCount)() === 0) {
|
|
144
|
+
(0, _networkListener.stopNetworkListener)();
|
|
146
145
|
}
|
|
147
146
|
}
|
|
148
147
|
|
|
@@ -155,9 +154,8 @@ class NetworkEventStore extends _sharedUi.BaseEventStore {
|
|
|
155
154
|
disableCapture() {
|
|
156
155
|
this.mirrorMode = true;
|
|
157
156
|
this.takeBootBuffer();
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
listener.stopListening();
|
|
157
|
+
if ((0, _networkListener.getNetworkListenerCount)() === 0) {
|
|
158
|
+
(0, _networkListener.stopNetworkListener)();
|
|
161
159
|
}
|
|
162
160
|
super.disableCapture();
|
|
163
161
|
}
|
|
@@ -204,6 +202,8 @@ class NetworkEventStore extends _sharedUi.BaseEventStore {
|
|
|
204
202
|
request
|
|
205
203
|
} = event;
|
|
206
204
|
if (event.type === "request") {
|
|
205
|
+
// Replayed request notifications must not reset a completed row or add a second key.
|
|
206
|
+
if (this.events.some(item => item.id === request.id)) return;
|
|
207
207
|
// Build full URL with query params if present
|
|
208
208
|
const queryString = request.params ? `?${new URLSearchParams(request.params).toString()}` : "";
|
|
209
209
|
const fullUrl = `${request.url}${queryString}`;
|
|
@@ -242,6 +242,24 @@ class NetworkEventStore extends _sharedUi.BaseEventStore {
|
|
|
242
242
|
this.events = [networkEvent, ...this.events].slice(0, this.maxEvents);
|
|
243
243
|
this.notify(networkEvent);
|
|
244
244
|
this.notifyArrayListeners();
|
|
245
|
+
} else if (event.type === "request-body") {
|
|
246
|
+
const current = this.events.find(item => item.id === request.id);
|
|
247
|
+
if (!current) return;
|
|
248
|
+
const updated = {
|
|
249
|
+
...current,
|
|
250
|
+
requestData: request.data,
|
|
251
|
+
requestSize: this.getDataSize(request.data)
|
|
252
|
+
};
|
|
253
|
+
if (request.client === "graphql") {
|
|
254
|
+
updated.operationName = (0, _extractOperationName.extractOperationName)(request.data) || undefined;
|
|
255
|
+
const variables = request.data?.variables;
|
|
256
|
+
if (variables && typeof variables === "object") updated.graphqlVariables = variables;
|
|
257
|
+
}
|
|
258
|
+
this.events = this.events.map(item => item.id === request.id ? updated : item);
|
|
259
|
+
if (this.pendingRequests.has(request.id)) this.pendingRequests.set(request.id, updated);
|
|
260
|
+
_networkSavedStore.networkSavedStore.syncFromLive(updated);
|
|
261
|
+
this.notify(updated);
|
|
262
|
+
this.notifyArrayListeners();
|
|
245
263
|
} else if (event.type === "response" || event.type === "error") {
|
|
246
264
|
const index = this.events.findIndex(e => e.id === request.id);
|
|
247
265
|
if (index !== -1) {
|
|
@@ -4,6 +4,8 @@ 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 _resolveRequestUrl = require("./resolveRequestUrl");
|
|
8
|
+
var _requestId = require("./requestId");
|
|
7
9
|
var _xhr = require("../conditions/xhr");
|
|
8
10
|
var _store = require("../conditions/store");
|
|
9
11
|
var _wait = require("../conditions/wait");
|
|
@@ -17,6 +19,59 @@ var _httpCompletion = require("./httpCompletion");
|
|
|
17
19
|
* Simple and reliable network interception for React Native
|
|
18
20
|
*/
|
|
19
21
|
|
|
22
|
+
let devOrigin;
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Headers another Buoy tool adds to a request inside our wrapper.
|
|
26
|
+
*
|
|
27
|
+
* Impersonate patches fetch/XHR at boot, so this listener usually wraps it
|
|
28
|
+
* and records the request before the impersonation header is added. The
|
|
29
|
+
* request on the wire carried the header and the Network row did not. Tools
|
|
30
|
+
* that add headers register a function here, keyed by URL, so the row shows
|
|
31
|
+
* what was actually sent.
|
|
32
|
+
*/
|
|
33
|
+
function injectedHeaders(url) {
|
|
34
|
+
const injectors = globalThis.__buoyHeaderInjectors;
|
|
35
|
+
if (!injectors || injectors.length === 0) return undefined;
|
|
36
|
+
let out;
|
|
37
|
+
for (const inject of injectors) {
|
|
38
|
+
try {
|
|
39
|
+
const h = inject(url);
|
|
40
|
+
if (h) out = {
|
|
41
|
+
...out,
|
|
42
|
+
...h
|
|
43
|
+
};
|
|
44
|
+
} catch {
|
|
45
|
+
/* a broken injector must not break capture */
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
return out;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Origin of the Metro dev server this JS bundle was loaded from, read once from
|
|
53
|
+
* the bundle URL React Native reports. `null` in release builds (the bundle is
|
|
54
|
+
* a file) and anywhere react-native isn't present.
|
|
55
|
+
*/
|
|
56
|
+
function devServerOrigin() {
|
|
57
|
+
if (devOrigin !== undefined) return devOrigin;
|
|
58
|
+
devOrigin = null;
|
|
59
|
+
try {
|
|
60
|
+
if (typeof __DEV__ !== "undefined" && !__DEV__) return devOrigin;
|
|
61
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
62
|
+
const rn = require("react-native");
|
|
63
|
+
const sc = rn?.NativeModules?.SourceCode;
|
|
64
|
+
const url = sc?.getConstants?.().scriptURL ?? sc?.scriptURL;
|
|
65
|
+
if (typeof url === "string" && /^https?:\/\//.test(url)) {
|
|
66
|
+
const u = new URL(url);
|
|
67
|
+
devOrigin = `${u.protocol}//${u.host}`;
|
|
68
|
+
}
|
|
69
|
+
} catch {
|
|
70
|
+
devOrigin = null;
|
|
71
|
+
}
|
|
72
|
+
return devOrigin;
|
|
73
|
+
}
|
|
74
|
+
|
|
20
75
|
/**
|
|
21
76
|
* True only while `originalFetch` is being CALLED (not awaited).
|
|
22
77
|
*
|
|
@@ -122,7 +177,6 @@ function isInternalBuoyHost(url) {
|
|
|
122
177
|
class NetworkListener {
|
|
123
178
|
listeners = [];
|
|
124
179
|
isListening = false;
|
|
125
|
-
requestCounter = 1000;
|
|
126
180
|
|
|
127
181
|
// Store original methods
|
|
128
182
|
|
|
@@ -170,6 +224,11 @@ class NetworkListener {
|
|
|
170
224
|
if ((parsed.hostname === "buoy.gg" || parsed.hostname === "www.buoy.gg") && parsed.pathname.startsWith("/api/license")) return true;
|
|
171
225
|
const local = parsed.hostname === "localhost" || parsed.hostname === "127.0.0.1" || parsed.hostname === "10.0.2.2";
|
|
172
226
|
if (local && ["8081", "8802"].includes(parsed.port)) return true;
|
|
227
|
+
// The dev server this bundle actually came from, whatever its host and
|
|
228
|
+
// port (a LAN address, --port 8082…): Metro's own traffic — symbolicate,
|
|
229
|
+
// logs, HMR, the bundle — is not the app's.
|
|
230
|
+
const dev = devServerOrigin();
|
|
231
|
+
if (dev && `${parsed.protocol}//${parsed.host}` === dev) return true;
|
|
173
232
|
} catch {
|
|
174
233
|
return false;
|
|
175
234
|
}
|
|
@@ -625,7 +684,7 @@ class NetworkListener {
|
|
|
625
684
|
this.originalFetch = globalThis.fetch;
|
|
626
685
|
const generation = ++this.fetchGeneration;
|
|
627
686
|
const wrappedFetch = async (input, init) => {
|
|
628
|
-
const url = typeof input === "string" ? input : input instanceof URL ? input.href : input.url;
|
|
687
|
+
const url = (0, _resolveRequestUrl.resolveRequestUrl)(typeof input === "string" ? input : input instanceof URL ? input.href : input.url);
|
|
629
688
|
|
|
630
689
|
// A newer install supersedes this wrapper. If it is still stranded in
|
|
631
690
|
// another tool's chain it must pass through silently, or one request
|
|
@@ -639,7 +698,7 @@ class NetworkListener {
|
|
|
639
698
|
return passthroughFetch(input, init);
|
|
640
699
|
}
|
|
641
700
|
const startTime = Date.now();
|
|
642
|
-
const requestId =
|
|
701
|
+
const requestId = (0, _requestId.nextRequestId)("fetch");
|
|
643
702
|
const inputRequest = typeof input === "object" && "method" in input ? input : undefined;
|
|
644
703
|
const method = init?.method ?? inputRequest?.method ?? "GET";
|
|
645
704
|
const signal = init?.signal !== undefined ? init.signal : inputRequest?.signal;
|
|
@@ -663,9 +722,12 @@ class NetworkListener {
|
|
|
663
722
|
requestHeaders[key] = value;
|
|
664
723
|
});
|
|
665
724
|
} else {
|
|
666
|
-
requestHeaders =
|
|
725
|
+
requestHeaders = {
|
|
726
|
+
...headers
|
|
727
|
+
};
|
|
667
728
|
}
|
|
668
729
|
}
|
|
730
|
+
Object.assign(requestHeaders, injectedHeaders(url));
|
|
669
731
|
|
|
670
732
|
// Parse request body
|
|
671
733
|
// Explicitly typed: the late emit for a streamed reply reads this from
|
|
@@ -714,6 +776,35 @@ class NetworkListener {
|
|
|
714
776
|
override,
|
|
715
777
|
condition
|
|
716
778
|
});
|
|
779
|
+
|
|
780
|
+
// Clone before dispatch; inspect the body without delaying or consuming the app's upload.
|
|
781
|
+
if (init?.body === undefined && inputRequest?.body && !inputRequest.bodyUsed) {
|
|
782
|
+
try {
|
|
783
|
+
const bodyCopy = inputRequest.clone();
|
|
784
|
+
void self.readStreamedBody(bodyCopy).then(({
|
|
785
|
+
body
|
|
786
|
+
}) => {
|
|
787
|
+
try {
|
|
788
|
+
requestData = JSON.parse(body);
|
|
789
|
+
} catch {
|
|
790
|
+
requestData = body;
|
|
791
|
+
}
|
|
792
|
+
self.emit({
|
|
793
|
+
type: "request-body",
|
|
794
|
+
timestamp: new Date(),
|
|
795
|
+
request: {
|
|
796
|
+
id: requestId || "unknown",
|
|
797
|
+
url: cleanUrl,
|
|
798
|
+
method,
|
|
799
|
+
data: requestData,
|
|
800
|
+
client: clientType
|
|
801
|
+
}
|
|
802
|
+
});
|
|
803
|
+
}).catch(() => {});
|
|
804
|
+
} catch {
|
|
805
|
+
// An unreadable body must not prevent the app's request.
|
|
806
|
+
}
|
|
807
|
+
}
|
|
717
808
|
try {
|
|
718
809
|
if (condition || outcome) await (0, _wait.waitBeforeDispatch)((condition?.delayMs ?? 0) + (outcome?.delayMs ?? 0), signal);
|
|
719
810
|
(0, _wait.throwIfAborted)(signal);
|
|
@@ -912,9 +1003,9 @@ class NetworkListener {
|
|
|
912
1003
|
if (generation === self.xhrGeneration) {
|
|
913
1004
|
pendingCapture.get(this)?.();
|
|
914
1005
|
const xhr = this;
|
|
915
|
-
xhr._buoyRequestId =
|
|
1006
|
+
xhr._buoyRequestId = (0, _requestId.nextRequestId)("xhr");
|
|
916
1007
|
xhr._method = method;
|
|
917
|
-
xhr._url = url;
|
|
1008
|
+
xhr._url = (0, _resolveRequestUrl.resolveRequestUrl)(url);
|
|
918
1009
|
xhr._buoyStartTime = Date.now();
|
|
919
1010
|
xhr._requestHeaders = {};
|
|
920
1011
|
}
|
|
@@ -936,6 +1027,7 @@ class NetworkListener {
|
|
|
936
1027
|
if (generation !== self.xhrGeneration || insideFetchHandoff) {
|
|
937
1028
|
return passthroughSend.call(this, data);
|
|
938
1029
|
}
|
|
1030
|
+
if ((0, _applyToXhr.hasPendingStandardRequest)(this)) throw new DOMException("The request has already been sent.", "InvalidStateError");
|
|
939
1031
|
if (this.readyState !== 1 || this._sent) {
|
|
940
1032
|
return passthroughSend.call(this, data);
|
|
941
1033
|
}
|
|
@@ -947,6 +1039,7 @@ class NetworkListener {
|
|
|
947
1039
|
xhr._buoyStartTime = startTime;
|
|
948
1040
|
xhr._buoyTtfb = undefined;
|
|
949
1041
|
const requestHeaders = xhr._requestHeaders || {};
|
|
1042
|
+
Object.assign(requestHeaders, injectedHeaders(url));
|
|
950
1043
|
|
|
951
1044
|
// Skip ignored URLs
|
|
952
1045
|
if (self.shouldIgnoreUrl(url)) {
|
|
@@ -1209,7 +1302,7 @@ const startNetworkListener = () => getNetworkListener().startListening();
|
|
|
1209
1302
|
* Stop network traffic monitoring
|
|
1210
1303
|
*/
|
|
1211
1304
|
exports.startNetworkListener = startNetworkListener;
|
|
1212
|
-
const stopNetworkListener = () =>
|
|
1305
|
+
const stopNetworkListener = () => _networkListener?.stopListening();
|
|
1213
1306
|
|
|
1214
1307
|
/**
|
|
1215
1308
|
* Add a listener for network events
|
|
@@ -1234,7 +1327,7 @@ const addNetworkListener = listener => getNetworkListener().addListener(listener
|
|
|
1234
1327
|
* Remove all registered network event listeners
|
|
1235
1328
|
*/
|
|
1236
1329
|
exports.addNetworkListener = addNetworkListener;
|
|
1237
|
-
const removeAllNetworkListeners = () =>
|
|
1330
|
+
const removeAllNetworkListeners = () => _networkListener?.removeAllListeners();
|
|
1238
1331
|
|
|
1239
1332
|
/**
|
|
1240
1333
|
* Check if network monitoring is currently active
|
|
@@ -1242,7 +1335,7 @@ const removeAllNetworkListeners = () => getNetworkListener().removeAllListeners(
|
|
|
1242
1335
|
* @returns True if currently intercepting network traffic
|
|
1243
1336
|
*/
|
|
1244
1337
|
exports.removeAllNetworkListeners = removeAllNetworkListeners;
|
|
1245
|
-
const isNetworkListening = () =>
|
|
1338
|
+
const isNetworkListening = () => _networkListener?.isActive ?? false;
|
|
1246
1339
|
|
|
1247
1340
|
/**
|
|
1248
1341
|
* Get the number of registered network event listeners
|
|
@@ -1250,7 +1343,7 @@ const isNetworkListening = () => getNetworkListener().isActive;
|
|
|
1250
1343
|
* @returns Number of active listeners
|
|
1251
1344
|
*/
|
|
1252
1345
|
exports.isNetworkListening = isNetworkListening;
|
|
1253
|
-
const getNetworkListenerCount = () =>
|
|
1346
|
+
const getNetworkListenerCount = () => _networkListener?.listenerCount ?? 0;
|
|
1254
1347
|
|
|
1255
1348
|
/**
|
|
1256
1349
|
* Whether both interceptors are actually reachable through the live global
|
|
@@ -1258,5 +1351,5 @@ const getNetworkListenerCount = () => getNetworkListener().listenerCount;
|
|
|
1258
1351
|
* even after another tool re-assigns `globalThis.fetch` over the wrapper).
|
|
1259
1352
|
*/
|
|
1260
1353
|
exports.getNetworkListenerCount = getNetworkListenerCount;
|
|
1261
|
-
const isNetworkInterceptionLive = () =>
|
|
1354
|
+
const isNetworkInterceptionLive = () => _networkListener?.isInterceptionLive() ?? false;
|
|
1262
1355
|
exports.isNetworkInterceptionLive = isNetworkInterceptionLive;
|
|
@@ -22,10 +22,10 @@ var _sharedUi = require("@buoy-gg/shared-ui");
|
|
|
22
22
|
* A record exists while either flag is true and is dropped when both go false,
|
|
23
23
|
* so all four states fall out without special cases.
|
|
24
24
|
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
25
|
+
* Request IDs include a runtime session and a counter. Saved records also
|
|
26
|
+
* track their owning session because live rows disappear on app reload.
|
|
27
|
+
* Records use a generated `key`, carry the `sessionId` that captured them,
|
|
28
|
+
* and only trust `liveId` while that
|
|
29
29
|
* session is the current one. Snapshots restored from an earlier session get
|
|
30
30
|
* their event `id` rewritten to `saved:<key>` so a restored pin can never
|
|
31
31
|
* resolve to (or dedupe against) an unrelated live request.
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
Object.defineProperty(exports, "__esModule", {
|
|
4
|
+
value: true
|
|
5
|
+
});
|
|
6
|
+
exports.nextRequestId = nextRequestId;
|
|
7
|
+
const runtime = globalThis;
|
|
8
|
+
|
|
9
|
+
// Keep identity across Fast Refresh; distinguish requests from earlier app runs.
|
|
10
|
+
function nextRequestId(client) {
|
|
11
|
+
const sequence = runtime.__BUOY_NETWORK_REQUEST_SEQUENCE__ ??= {
|
|
12
|
+
session: `${Date.now().toString(36)}-${Math.random().toString(36).slice(2)}`,
|
|
13
|
+
next: 0
|
|
14
|
+
};
|
|
15
|
+
return `${client}_${sequence.session}_${++sequence.next}`;
|
|
16
|
+
}
|