@buoy-gg/network 7.0.35 → 7.0.38

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