@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.
Files changed (54) hide show
  1. package/README.md +6 -0
  2. package/lib/commonjs/index.js +7 -0
  3. package/lib/commonjs/network/components/NetworkListScreen.js +2 -3
  4. package/lib/commonjs/network/components/NetworkListScreen.web.js +2 -3
  5. package/lib/commonjs/network/components/NetworkModal.js +1 -1
  6. package/lib/commonjs/network/conditions/xhr.js +26 -1
  7. package/lib/commonjs/network/overrides/applyToXhr.js +99 -42
  8. package/lib/commonjs/network/overrides/matchRule.js +7 -1
  9. package/lib/commonjs/network/overrides/overriddenRows.js +37 -0
  10. package/lib/commonjs/network/utils/networkEventStore.js +24 -6
  11. package/lib/commonjs/network/utils/networkListener.js +104 -11
  12. package/lib/commonjs/network/utils/networkSavedStore.js +4 -4
  13. package/lib/commonjs/network/utils/requestId.js +16 -0
  14. package/lib/commonjs/network/utils/resolveRequestUrl.js +15 -0
  15. package/lib/commonjs/network/utils/startNetworkCapture.js +24 -0
  16. package/lib/commonjs/preset.js +3 -12
  17. package/lib/module/index.js +1 -0
  18. package/lib/module/network/components/NetworkListScreen.js +2 -3
  19. package/lib/module/network/components/NetworkListScreen.web.js +2 -3
  20. package/lib/module/network/components/NetworkModal.js +1 -1
  21. package/lib/module/network/conditions/xhr.js +27 -2
  22. package/lib/module/network/overrides/applyToXhr.js +98 -42
  23. package/lib/module/network/overrides/matchRule.js +7 -1
  24. package/lib/module/network/overrides/overriddenRows.js +33 -0
  25. package/lib/module/network/utils/networkEventStore.js +25 -7
  26. package/lib/module/network/utils/networkListener.js +104 -12
  27. package/lib/module/network/utils/networkSavedStore.js +4 -4
  28. package/lib/module/network/utils/requestId.js +12 -0
  29. package/lib/module/network/utils/resolveRequestUrl.js +11 -0
  30. package/lib/module/network/utils/startNetworkCapture.js +21 -0
  31. package/lib/module/preset.js +3 -12
  32. package/lib/typescript/index.d.ts +1 -0
  33. package/lib/typescript/index.d.ts.map +1 -1
  34. package/lib/typescript/network/components/NetworkListScreen.d.ts.map +1 -1
  35. package/lib/typescript/network/components/NetworkListScreen.web.d.ts.map +1 -1
  36. package/lib/typescript/network/conditions/xhr.d.ts.map +1 -1
  37. package/lib/typescript/network/overrides/applyToXhr.d.ts +1 -0
  38. package/lib/typescript/network/overrides/applyToXhr.d.ts.map +1 -1
  39. package/lib/typescript/network/overrides/matchRule.d.ts.map +1 -1
  40. package/lib/typescript/network/overrides/overriddenRows.d.ts +16 -0
  41. package/lib/typescript/network/overrides/overriddenRows.d.ts.map +1 -0
  42. package/lib/typescript/network/utils/networkEventStore.d.ts.map +1 -1
  43. package/lib/typescript/network/utils/networkListener.d.ts +3 -4
  44. package/lib/typescript/network/utils/networkListener.d.ts.map +1 -1
  45. package/lib/typescript/network/utils/networkSavedStore.d.ts +4 -4
  46. package/lib/typescript/network/utils/requestId.d.ts +2 -0
  47. package/lib/typescript/network/utils/requestId.d.ts.map +1 -0
  48. package/lib/typescript/network/utils/resolveRequestUrl.d.ts +3 -0
  49. package/lib/typescript/network/utils/resolveRequestUrl.d.ts.map +1 -0
  50. package/lib/typescript/network/utils/startNetworkCapture.d.ts +6 -0
  51. package/lib/typescript/network/utils/startNetworkCapture.d.ts.map +1 -0
  52. package/lib/typescript/preset.d.ts.map +1 -1
  53. package/lib/web/index.mjs +146 -0
  54. 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.
@@ -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 _matchRule = require("../overrides/matchRule");
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 isOverridden = event => activeRules.some(rule => (0, _matchRule.ruleMatches)(rule, event.url, String(event.method || "GET")));
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 _matchRule = require("../overrides/matchRule");
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 isOverridden = event => activeRules.some(rule => (0, _matchRule.ruleMatches)(rule, event.url, String(event.method || "GET")));
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 list",
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)) throw new Error("Network conditions require React Native XMLHttpRequest.");
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
- let timer = null;
319
- let aborted = false;
324
+ const started = Date.now();
325
+ const generation = {};
326
+ let timer;
327
+ let deadline;
320
328
  let settled = false;
321
- const restoreAbort = () => {
322
- try {
323
- delete xhr.abort;
324
- } catch {
325
- // Non-configurable shadow can't happen (we define it configurable), but
326
- // never let cleanup throw into app code.
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
- Object.defineProperty(xhr, "abort", {
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 (settled || aborted) return;
334
- aborted = true;
335
- settled = true;
336
- if (timer !== null) {
337
- clearTimeout(timer);
338
- timer = null;
391
+ if (dispatched || settled) {
392
+ restore();
393
+ originalAbort.call(xhr);
394
+ return;
339
395
  }
340
- restoreAbort();
341
- // The in-flight abort sequence: DONE, then 'abort', then 'loadend' —
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, "abort");
346
- fireEvent(xhr, "loadend");
399
+ fireEvent(xhr, 'abort');
400
+ fireEvent(xhr, 'loadend');
401
+ finish();
402
+ if (!pendingStandardRequests.has(xhr)) defineOwn(xhr, 'readyState', 0);
347
403
  }
348
404
  });
349
- const fire = () => {
350
- timer = null;
351
- if (aborted) return;
352
- if (outcome.kind === "delay") {
353
- // The real request is about to be genuinely in flight — put the
354
- // prototype `abort()` back FIRST so cancelling reaches the network.
355
- restoreAbort();
356
- passThrough();
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 === "fail") {
360
- failStandard(xhr, outcome);
361
- } else {
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
- const regex = new RegExp(`^${escapeRegex(pattern)}$`);
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
- const listener = (0, _networkListener.networkListener)();
144
- if (listener.listenerCount === 0) {
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
- const listener = (0, _networkListener.networkListener)();
159
- if (listener.listenerCount === 0 && listener.isActive) {
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 = `fetch_${++self.requestCounter}`;
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 = headers;
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 = `xhr_${++self.requestCounter}`;
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 = () => getNetworkListener().stopListening();
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 = () => getNetworkListener().removeAllListeners();
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 = () => getNetworkListener().isActive;
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 = () => getNetworkListener().listenerCount;
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 = () => getNetworkListener().isInterceptionLive();
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
- * IDENTITY WARNING: `networkListener` mints request IDs from a per-runtime
26
- * counter (`fetch_1`, `xhr_2`), so after an app reload `fetch_1` is a
27
- * DIFFERENT request. Records are therefore keyed on a generated `key`, carry
28
- * the `sessionId` that captured them, and only trust `liveId` while that
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
+ }