@buoy-gg/network 7.0.39 → 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 (56) 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/overrides/resolveOverride.js +1 -1
  11. package/lib/commonjs/network/utils/networkEventStore.js +24 -6
  12. package/lib/commonjs/network/utils/networkListener.js +106 -11
  13. package/lib/commonjs/network/utils/networkSavedStore.js +4 -4
  14. package/lib/commonjs/network/utils/requestId.js +16 -0
  15. package/lib/commonjs/network/utils/resolveRequestUrl.js +15 -0
  16. package/lib/commonjs/network/utils/startNetworkCapture.js +24 -0
  17. package/lib/commonjs/preset.js +3 -12
  18. package/lib/module/index.js +1 -0
  19. package/lib/module/network/components/NetworkListScreen.js +2 -3
  20. package/lib/module/network/components/NetworkListScreen.web.js +2 -3
  21. package/lib/module/network/components/NetworkModal.js +1 -1
  22. package/lib/module/network/conditions/xhr.js +27 -2
  23. package/lib/module/network/overrides/applyToXhr.js +98 -42
  24. package/lib/module/network/overrides/matchRule.js +7 -1
  25. package/lib/module/network/overrides/overriddenRows.js +33 -0
  26. package/lib/module/network/overrides/resolveOverride.js +1 -1
  27. package/lib/module/network/utils/networkEventStore.js +25 -7
  28. package/lib/module/network/utils/networkListener.js +106 -12
  29. package/lib/module/network/utils/networkSavedStore.js +4 -4
  30. package/lib/module/network/utils/requestId.js +12 -0
  31. package/lib/module/network/utils/resolveRequestUrl.js +11 -0
  32. package/lib/module/network/utils/startNetworkCapture.js +21 -0
  33. package/lib/module/preset.js +3 -12
  34. package/lib/typescript/index.d.ts +1 -0
  35. package/lib/typescript/index.d.ts.map +1 -1
  36. package/lib/typescript/network/components/NetworkListScreen.d.ts.map +1 -1
  37. package/lib/typescript/network/components/NetworkListScreen.web.d.ts.map +1 -1
  38. package/lib/typescript/network/conditions/xhr.d.ts.map +1 -1
  39. package/lib/typescript/network/overrides/applyToXhr.d.ts +1 -0
  40. package/lib/typescript/network/overrides/applyToXhr.d.ts.map +1 -1
  41. package/lib/typescript/network/overrides/matchRule.d.ts.map +1 -1
  42. package/lib/typescript/network/overrides/overriddenRows.d.ts +16 -0
  43. package/lib/typescript/network/overrides/overriddenRows.d.ts.map +1 -0
  44. package/lib/typescript/network/utils/networkEventStore.d.ts.map +1 -1
  45. package/lib/typescript/network/utils/networkListener.d.ts +3 -4
  46. package/lib/typescript/network/utils/networkListener.d.ts.map +1 -1
  47. package/lib/typescript/network/utils/networkSavedStore.d.ts +4 -4
  48. package/lib/typescript/network/utils/requestId.d.ts +2 -0
  49. package/lib/typescript/network/utils/requestId.d.ts.map +1 -0
  50. package/lib/typescript/network/utils/resolveRequestUrl.d.ts +3 -0
  51. package/lib/typescript/network/utils/resolveRequestUrl.d.ts.map +1 -0
  52. package/lib/typescript/network/utils/startNetworkCapture.d.ts +6 -0
  53. package/lib/typescript/network/utils/startNetworkCapture.d.ts.map +1 -0
  54. package/lib/typescript/preset.d.ts.map +1 -1
  55. package/lib/web/index.mjs +146 -0
  56. package/package.json +32 -10
@@ -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
+ }
@@ -0,0 +1,15 @@
1
+ "use strict";
2
+
3
+ Object.defineProperty(exports, "__esModule", {
4
+ value: true
5
+ });
6
+ exports.resolveRequestUrl = resolveRequestUrl;
7
+ /** Resolve browser-relative URLs for capture without changing the request arguments. */
8
+ function resolveRequestUrl(url) {
9
+ if (typeof document === "undefined") return url;
10
+ try {
11
+ return new URL(url, document.baseURI).href;
12
+ } catch {
13
+ return url;
14
+ }
15
+ }
@@ -0,0 +1,24 @@
1
+ "use strict";
2
+
3
+ Object.defineProperty(exports, "__esModule", {
4
+ value: true
5
+ });
6
+ exports.startNetworkCapture = startNetworkCapture;
7
+ var _networkEventStore = require("./networkEventStore");
8
+ var _overrideRulesStore = require("../overrides/overrideRulesStore");
9
+ /**
10
+ * Keep local capture active while the Network panel is closed.
11
+ * Call in the app being inspected and dispose during unmount or HMR cleanup.
12
+ */
13
+ function startNetworkCapture() {
14
+ const native = typeof navigator !== "undefined" && navigator.product === "ReactNative";
15
+ if (!native && (typeof window === "undefined" || typeof document === "undefined")) return () => {};
16
+ void _overrideRulesStore.overrideRulesStore.ensureLoaded();
17
+ const unsubscribe = _networkEventStore.networkEventStore.subscribeToEvents(() => {});
18
+ let stopped = false;
19
+ return () => {
20
+ if (stopped) return;
21
+ stopped = true;
22
+ unsubscribe();
23
+ };
24
+ }
@@ -27,16 +27,6 @@ var _jsxRuntime = require("react/jsx-runtime");
27
27
  * ```
28
28
  */
29
29
 
30
- /**
31
- * Load persisted response-override rules as soon as the tool is registered.
32
- *
33
- * Deliberately at import time rather than when the Network modal mounts: an
34
- * override has to fire from app boot, with the tool closed and nothing
35
- * capturing — that is the whole point of persisting rules ("force this endpoint
36
- * to 500, then restart and watch the boot path"). Hydrating also re-installs
37
- * the interceptor if any rule is still active. One storage read, dev-only.
38
- */void _overrideRulesStore.overrideRulesStore.ensureLoaded();
39
-
40
30
  /**
41
31
  * Install interception at IMPORT time and buffer boot traffic.
42
32
  *
@@ -52,8 +42,9 @@ var _jsxRuntime = require("react/jsx-runtime");
52
42
  * desktop dashboard and web mirrors also evaluate this module and must
53
43
  * never intercept their OWN process's traffic.
54
44
  * - Dev-only, like the rest of the automatic capture behavior.
55
- */
56
- const isReactNativeRuntime = typeof navigator !== "undefined" && navigator.product === "ReactNative";
45
+ */const isReactNativeRuntime = typeof navigator !== "undefined" && navigator.product === "ReactNative";
46
+ // Browser capture initializes persisted rules explicitly through startNetworkCapture.
47
+ if (isReactNativeRuntime) void _overrideRulesStore.overrideRulesStore.ensureLoaded();
57
48
  if (isReactNativeRuntime && typeof __DEV__ !== "undefined" && __DEV__) {
58
49
  _networkEventStore.networkEventStore.startBootCapture();
59
50
  }
@@ -14,6 +14,7 @@
14
14
  // PRESET (Primary entry point for users)
15
15
  // =============================================================================
16
16
  export { networkToolPreset, createNetworkTool } from "./preset";
17
+ export { startNetworkCapture } from "./network/utils/startNetworkCapture";
17
18
 
18
19
  // =============================================================================
19
20
  // COMPONENTS (For custom UI implementations)
@@ -20,7 +20,7 @@ import { NetworkUpgradeBanner } from "./NetworkUpgradeBanner";
20
20
  import { useNetworkEventList } from "../hooks/useNetworkEventList";
21
21
  import { useNetworkSaved } from "../hooks/useNetworkSaved";
22
22
  import { useOverrideRules } from "../hooks/useOverrideRules";
23
- import { ruleMatches } from "../overrides/matchRule";
23
+ import { overriddenEvents } from "../overrides/overriddenRows";
24
24
  import { splitPinned } from "../utils/withPinnedFirst";
25
25
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
26
26
  const keyExtractor = item => item.id;
@@ -192,8 +192,7 @@ export function NetworkListScreen({
192
192
  // Paused rules still populate the strip — that's how you find the thing you
193
193
  // paused in order to resume it. The header says PAUSED instead.
194
194
  const activeRules = rules.filter(rule => rule.enabled && !isRuleSpent(rule));
195
- const isOverridden = event => activeRules.some(rule => ruleMatches(rule, event.url, String(event.method || "GET")));
196
- const overridden = activeRules.length ? [...pinned, ...rest].filter(isOverridden) : [];
195
+ const overridden = overriddenEvents([...pinned, ...rest], activeRules);
197
196
  const overriddenIds = new Set(overridden.map(event => event.id));
198
197
  const pinnedRows = pinned.filter(event => !overriddenIds.has(event.id));
199
198
  const restRows = overriddenIds.size ? rest.filter(event => !overriddenIds.has(event.id)) : rest;
@@ -53,7 +53,7 @@ import { NetworkEventItemCompact } from "./NetworkEventItemCompact.web";
53
53
  import { useNetworkEventList } from "../hooks/useNetworkEventList";
54
54
  import { useNetworkSaved } from "../hooks/useNetworkSaved";
55
55
  import { useOverrideRules } from "../hooks/useOverrideRules";
56
- import { ruleMatches } from "../overrides/matchRule";
56
+ import { overriddenEvents } from "../overrides/overriddenRows";
57
57
  import { splitPinned } from "../utils/withPinnedFirst";
58
58
 
59
59
  // =============================
@@ -385,8 +385,7 @@ export function NetworkListScreen({
385
385
  // Hoist anything an active rule covers. Done after `splitPinned` so a request
386
386
  // that is both pinned and overridden appears once, in the louder section.
387
387
  const activeRules = rules.filter(rule => rule.enabled && !isRuleSpent(rule));
388
- const isOverridden = event => activeRules.some(rule => ruleMatches(rule, event.url, String(event.method || "GET")));
389
- const overridden = activeRules.length ? [...pinned, ...rest].filter(isOverridden) : [];
388
+ const overridden = overriddenEvents([...pinned, ...rest], activeRules);
390
389
  const overriddenIds = new Set(overridden.map(event => event.id));
391
390
  const pinnedRows = pinned.filter(event => !overriddenIds.has(event.id));
392
391
  const restRows = overriddenIds.size ? rest.filter(event => !overriddenIds.has(event.id)) : rest;
@@ -496,7 +496,7 @@ function NetworkModalInner({
496
496
  }) : null, /*#__PURE__*/_jsx(PowerToggleButton, {
497
497
  isEnabled: !paused,
498
498
  onToggle: () => setPaused(prev => !prev),
499
- accessibilityLabel: "Pause network list",
499
+ accessibilityLabel: paused ? "Resume network capture" : "Pause network capture",
500
500
  testID: "buoy-network-power"
501
501
  }), /*#__PURE__*/_jsx(NetworkClearButton, {
502
502
  paused: paused
@@ -1,9 +1,34 @@
1
1
  "use strict";
2
2
 
3
- import { canSynthesize, completeNativeOverride } from "../overrides/applyToXhr";
3
+ import { canSynthesize, completeNativeOverride, runOverride } from "../overrides/applyToXhr";
4
4
  /** Active simulation uses a send-to-completion deadline, including the wait. */
5
5
  export function runConditionXhr(target, url, condition, outcome, dispatch) {
6
- if (!canSynthesize(target)) throw new Error("Network conditions require React Native XMLHttpRequest.");
6
+ if (!canSynthesize(target)) {
7
+ const rule = {
8
+ id: "condition",
9
+ enabled: true,
10
+ urlPattern: url,
11
+ kind: "delay",
12
+ hits: 0,
13
+ seen: 0,
14
+ createdAt: Date.now()
15
+ };
16
+ const simulation = condition.profile === "offline" ? {
17
+ kind: "fail",
18
+ rule,
19
+ failKind: "network",
20
+ delayMs: 0
21
+ } : outcome ? {
22
+ ...outcome,
23
+ delayMs: condition.delayMs + outcome.delayMs
24
+ } : {
25
+ kind: "delay",
26
+ rule,
27
+ delayMs: condition.delayMs
28
+ };
29
+ if (!runOverride(target, url, simulation, dispatch)) throw new Error("Network conditions are unavailable for this XMLHttpRequest implementation.");
30
+ return;
31
+ }
7
32
  const xhr = target;
8
33
  if (xhr.readyState !== 1 || xhr._sent) throw new Error("Request is not open or has already been sent.");
9
34
  const started = Date.now();
@@ -279,6 +279,7 @@ function respondStandard(xhr, url, outcome, isAborted) {
279
279
  setStandardReadyState(xhr, LOADING);
280
280
  if (isAborted()) return;
281
281
  setStandardReadyState(xhr, DONE);
282
+ if (isAborted()) return;
282
283
  fireEvent(xhr, "load");
283
284
  fireEvent(xhr, "loadend");
284
285
  }
@@ -307,60 +308,115 @@ function failStandard(xhr, outcome) {
307
308
  * `delay` pass-through hands the request to the real network, where the
308
309
  * prototype `abort()` must be reachable again.
309
310
  */
311
+ const pendingStandardRequests = new WeakMap();
312
+ export function hasPendingStandardRequest(xhr) {
313
+ return pendingStandardRequests.has(xhr);
314
+ }
310
315
  function runStandardOverride(xhr, url, outcome, passThrough) {
311
- let timer = null;
312
- let aborted = false;
316
+ const started = Date.now();
317
+ const generation = {};
318
+ let timer;
319
+ let deadline;
313
320
  let settled = false;
314
- const restoreAbort = () => {
315
- try {
316
- delete xhr.abort;
317
- } catch {
318
- // Non-configurable shadow can't happen (we define it configurable), but
319
- // never let cleanup throw into app code.
321
+ let aborted = false;
322
+ let dispatched = false;
323
+ let timeout = xhr.timeout;
324
+ const originalAbort = xhr.abort;
325
+ const originalOpen = xhr.open;
326
+ const names = ['abort', 'open', 'timeout', 'readyState', 'status', 'statusText', 'response', 'responseText', 'responseURL', 'getAllResponseHeaders', 'getResponseHeader'];
327
+ const descriptors = new Map(names.map(name => [name, Object.getOwnPropertyDescriptor(xhr, name)]));
328
+ let prototype = Object.getPrototypeOf(xhr);
329
+ let nativeTimeout;
330
+ while (prototype && !nativeTimeout) {
331
+ nativeTimeout = Object.getOwnPropertyDescriptor(prototype, 'timeout');
332
+ prototype = Object.getPrototypeOf(prototype);
333
+ }
334
+ const finish = () => {
335
+ settled = true;
336
+ if (pendingStandardRequests.get(xhr) === generation) pendingStandardRequests.delete(xhr);
337
+ clearTimeout(timer);
338
+ clearTimeout(deadline);
339
+ xhr.removeEventListener('loadend', finish);
340
+ };
341
+ const restore = () => {
342
+ finish();
343
+ for (const [name, descriptor] of descriptors) {
344
+ if (descriptor) Object.defineProperty(xhr, name, descriptor);else delete xhr[name];
320
345
  }
346
+ xhr.timeout = timeout;
321
347
  };
322
- Object.defineProperty(xhr, "abort", {
348
+ const scheduleDeadline = () => {
349
+ clearTimeout(deadline);
350
+ if (dispatched) {
351
+ nativeTimeout?.set?.call(xhr, timeout > 0 ? Math.max(1, timeout - (Date.now() - started)) : 0);
352
+ } else if (timeout > 0) {
353
+ deadline = setTimeout(() => {
354
+ if (settled) return;
355
+ clearTimeout(timer);
356
+ failStandard(xhr, {
357
+ ...outcome,
358
+ kind: 'fail',
359
+ failKind: 'timeout'
360
+ });
361
+ finish();
362
+ }, Math.max(0, timeout - (Date.now() - started)));
363
+ }
364
+ };
365
+ Object.defineProperty(xhr, 'timeout', {
366
+ configurable: true,
367
+ get: () => timeout,
368
+ set: value => {
369
+ timeout = Math.max(0, Number(value) || 0);
370
+ scheduleDeadline();
371
+ }
372
+ });
373
+ Object.defineProperty(xhr, 'open', {
374
+ configurable: true,
375
+ value: (...args) => {
376
+ restore();
377
+ return Reflect.apply(originalOpen, xhr, args);
378
+ }
379
+ });
380
+ Object.defineProperty(xhr, 'abort', {
323
381
  configurable: true,
324
- writable: true,
325
382
  value: () => {
326
- if (settled || aborted) return;
327
- aborted = true;
328
- settled = true;
329
- if (timer !== null) {
330
- clearTimeout(timer);
331
- timer = null;
383
+ if (dispatched || settled) {
384
+ restore();
385
+ originalAbort.call(xhr);
386
+ return;
332
387
  }
333
- restoreAbort();
334
- // The in-flight abort sequence: DONE, then 'abort', then 'loadend' —
335
- // whatwg's request-error steps, which is also what RN's setReadyState
336
- // dispatches off `_aborted`.
388
+ aborted = true;
389
+ clearTimeout(timer);
337
390
  setStandardReadyState(xhr, DONE);
338
- fireEvent(xhr, "abort");
339
- fireEvent(xhr, "loadend");
391
+ fireEvent(xhr, 'abort');
392
+ fireEvent(xhr, 'loadend');
393
+ finish();
394
+ if (!pendingStandardRequests.has(xhr)) defineOwn(xhr, 'readyState', 0);
340
395
  }
341
396
  });
342
- const fire = () => {
343
- timer = null;
344
- if (aborted) return;
345
- if (outcome.kind === "delay") {
346
- // The real request is about to be genuinely in flight — put the
347
- // prototype `abort()` back FIRST so cancelling reaches the network.
348
- restoreAbort();
349
- passThrough();
397
+ pendingStandardRequests.set(xhr, generation);
398
+ xhr.addEventListener('loadend', finish);
399
+ scheduleDeadline();
400
+ timer = setTimeout(() => {
401
+ if (settled || aborted) return;
402
+ if (outcome.kind === 'delay') {
403
+ dispatched = true;
404
+ scheduleDeadline();
405
+ try {
406
+ passThrough();
407
+ } catch {
408
+ failStandard(xhr, {
409
+ ...outcome,
410
+ kind: 'fail',
411
+ failKind: 'network'
412
+ });
413
+ finish();
414
+ }
350
415
  return;
351
416
  }
352
- if (outcome.kind === "fail") {
353
- failStandard(xhr, outcome);
354
- } else {
355
- respondStandard(xhr, url, outcome, () => aborted);
356
- }
357
- settled = true;
358
- restoreAbort();
359
- };
360
-
361
- // ALWAYS asynchronous, even at 0ms — same contract as the RN path: callers
362
- // legitimately attach handlers after `send()` returns (axios does).
363
- timer = setTimeout(fire, outcome.delayMs);
417
+ if (outcome.kind === 'fail') failStandard(xhr, outcome);else respondStandard(xhr, url, outcome, () => aborted || pendingStandardRequests.get(xhr) !== generation);
418
+ finish();
419
+ }, outcome.delayMs);
364
420
  return true;
365
421
  }
366
422
 
@@ -51,7 +51,13 @@ export function compilePattern(pattern) {
51
51
  // Anchored, like Chrome's `^${...}$` — a pattern describes the whole URL, so
52
52
  // `pokeapi.co` matches nothing and `*pokeapi.co*` matches everything on that
53
53
  // host. Predictable beats clever; the rule editor writes full URLs for you.
54
- const regex = new RegExp(`^${escapeRegex(pattern)}$`);
54
+ //
55
+ // One exception: a pattern that starts with `/` is a PATH. Anchored to the
56
+ // full URL it could never match anything (every URL starts with a scheme),
57
+ // so it means "this path on any origin" — `/api/rewards` matches
58
+ // `http://localhost:3002/api/rewards`, and `/api/rewards*` also its queries.
59
+ const source = pattern.startsWith("/") ? `[a-zA-Z][a-zA-Z0-9+.-]*://[^/]*${escapeRegex(pattern)}` : escapeRegex(pattern);
60
+ const regex = new RegExp(`^${source}$`);
55
61
  // Unbounded growth isn't possible in practice (patterns come from a capped
56
62
  // rule list), but a user editing a pattern character by character churns
57
63
  // entries, so keep the cache from becoming a leak.
@@ -0,0 +1,33 @@
1
+ "use strict";
2
+
3
+ import { ruleMatches } from "./matchRule";
4
+ /**
5
+ * Which captured requests belong in the list's OVERRIDDEN strip.
6
+ *
7
+ * A row belongs there when an active rule actually changed it (it carries the
8
+ * rule's override mark) or matched it after the rule existed (while overrides
9
+ * are paused those pass through, and the strip is how you find the rule again).
10
+ * Before a rule existed nothing overrode anything, so older matching requests
11
+ * are real server responses and stay in the list, with one exception: the
12
+ * newest of them, which is the request the rule was built from. That one hoists
13
+ * right away, so creating a rule shows where it points without waiting for the
14
+ * endpoint to be called again.
15
+ */
16
+ export function overriddenEvents(events, activeRules) {
17
+ if (activeRules.length === 0) return [];
18
+ const activeIds = new Set(activeRules.map(rule => rule.id));
19
+ const picked = new Set();
20
+ for (const rule of activeRules) {
21
+ let source;
22
+ for (const event of events) {
23
+ if (event.override && activeIds.has(event.override.ruleId)) {
24
+ picked.add(event.id);
25
+ continue;
26
+ }
27
+ if (!ruleMatches(rule, event.url, String(event.method || "GET"))) continue;
28
+ if (event.timestamp >= rule.createdAt) picked.add(event.id);else if (!source || event.timestamp > source.timestamp) source = event;
29
+ }
30
+ if (source) picked.add(source.id);
31
+ }
32
+ return events.filter(event => picked.has(event.id));
33
+ }
@@ -23,7 +23,7 @@ import { STATUS_MAX, STATUS_MIN } from "./types";
23
23
  * "the dev tools stopped working and I don't know why". Metro/debugger traffic
24
24
  * is already excluded upstream by `networkListener.shouldIgnoreUrl`.
25
25
  */
26
- const NEVER_OVERRIDE = [/(^|\.)keygen\.sh/i];
26
+ const NEVER_OVERRIDE = [/(^|\.)keygen\.sh/i, /^https?:\/\/(www\.)?buoy\.gg\/api\/license(\/|$)/i];
27
27
  function isProtectedUrl(url) {
28
28
  return NEVER_OVERRIDE.some(pattern => pattern.test(url));
29
29
  }
@@ -24,7 +24,7 @@ import { getAccountAccess } from "@buoy-gg/shared-ui";
24
24
 
25
25
  import { BaseEventStore } from "@buoy-gg/shared-ui";
26
26
  import { extractOperationName } from "./extractOperationName";
27
- import { networkListener, addNetworkListener } from "./networkListener";
27
+ import { networkListener, addNetworkListener, getNetworkListenerCount, stopNetworkListener } from "./networkListener";
28
28
  import { networkSavedStore } from "./networkSavedStore";
29
29
 
30
30
  /**
@@ -138,9 +138,8 @@ class NetworkEventStore extends BaseEventStore {
138
138
  }
139
139
 
140
140
  // If no other listeners exist, stop the network interceptor
141
- const listener = networkListener();
142
- if (listener.listenerCount === 0) {
143
- listener.stopListening();
141
+ if (getNetworkListenerCount() === 0) {
142
+ stopNetworkListener();
144
143
  }
145
144
  }
146
145
 
@@ -153,9 +152,8 @@ class NetworkEventStore extends BaseEventStore {
153
152
  disableCapture() {
154
153
  this.mirrorMode = true;
155
154
  this.takeBootBuffer();
156
- const listener = networkListener();
157
- if (listener.listenerCount === 0 && listener.isActive) {
158
- listener.stopListening();
155
+ if (getNetworkListenerCount() === 0) {
156
+ stopNetworkListener();
159
157
  }
160
158
  super.disableCapture();
161
159
  }
@@ -202,6 +200,8 @@ class NetworkEventStore extends BaseEventStore {
202
200
  request
203
201
  } = event;
204
202
  if (event.type === "request") {
203
+ // Replayed request notifications must not reset a completed row or add a second key.
204
+ if (this.events.some(item => item.id === request.id)) return;
205
205
  // Build full URL with query params if present
206
206
  const queryString = request.params ? `?${new URLSearchParams(request.params).toString()}` : "";
207
207
  const fullUrl = `${request.url}${queryString}`;
@@ -240,6 +240,24 @@ class NetworkEventStore extends BaseEventStore {
240
240
  this.events = [networkEvent, ...this.events].slice(0, this.maxEvents);
241
241
  this.notify(networkEvent);
242
242
  this.notifyArrayListeners();
243
+ } else if (event.type === "request-body") {
244
+ const current = this.events.find(item => item.id === request.id);
245
+ if (!current) return;
246
+ const updated = {
247
+ ...current,
248
+ requestData: request.data,
249
+ requestSize: this.getDataSize(request.data)
250
+ };
251
+ if (request.client === "graphql") {
252
+ updated.operationName = extractOperationName(request.data) || undefined;
253
+ const variables = request.data?.variables;
254
+ if (variables && typeof variables === "object") updated.graphqlVariables = variables;
255
+ }
256
+ this.events = this.events.map(item => item.id === request.id ? updated : item);
257
+ if (this.pendingRequests.has(request.id)) this.pendingRequests.set(request.id, updated);
258
+ networkSavedStore.syncFromLive(updated);
259
+ this.notify(updated);
260
+ this.notifyArrayListeners();
243
261
  } else if (event.type === "response" || event.type === "error") {
244
262
  const index = this.events.findIndex(e => e.id === request.id);
245
263
  if (index !== -1) {