@buoy-gg/network 7.0.4 → 7.0.5

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 (69) hide show
  1. package/README.md +2 -1
  2. package/lib/commonjs/index.js +85 -0
  3. package/lib/commonjs/network/components/NetworkEventDetailView.js +201 -40
  4. package/lib/commonjs/network/components/NetworkEventItemCompact.js +52 -5
  5. package/lib/commonjs/network/components/NetworkHeaderLive.js +132 -0
  6. package/lib/commonjs/network/components/NetworkListScreen.js +134 -56
  7. package/lib/commonjs/network/components/NetworkModal.js +154 -36
  8. package/lib/commonjs/network/components/NetworkSavedScreen.js +245 -0
  9. package/lib/commonjs/network/components/NetworkUpgradeBanner.js +76 -0
  10. package/lib/commonjs/network/hooks/useLiveNetworkEvent.js +62 -0
  11. package/lib/commonjs/network/hooks/useNetworkSaved.js +38 -0
  12. package/lib/commonjs/network/index.js +84 -0
  13. package/lib/commonjs/network/sync/networkSyncAdapter.js +85 -7
  14. package/lib/commonjs/network/utils/generateCurl.js +71 -0
  15. package/lib/commonjs/network/utils/networkEventStore.js +10 -1
  16. package/lib/commonjs/network/utils/networkListener.js +54 -1
  17. package/lib/commonjs/network/utils/networkSavedStore.js +575 -0
  18. package/lib/commonjs/network/utils/withPinnedFirst.js +32 -0
  19. package/lib/module/index.js +17 -1
  20. package/lib/module/network/components/NetworkEventDetailView.js +204 -41
  21. package/lib/module/network/components/NetworkEventItemCompact.js +53 -6
  22. package/lib/module/network/components/NetworkHeaderLive.js +132 -2
  23. package/lib/module/network/components/NetworkListScreen.js +136 -58
  24. package/lib/module/network/components/NetworkModal.js +156 -39
  25. package/lib/module/network/components/NetworkSavedScreen.js +241 -0
  26. package/lib/module/network/components/NetworkUpgradeBanner.js +72 -0
  27. package/lib/module/network/hooks/useLiveNetworkEvent.js +58 -0
  28. package/lib/module/network/hooks/useNetworkSaved.js +34 -0
  29. package/lib/module/network/index.js +7 -1
  30. package/lib/module/network/sync/networkSyncAdapter.js +85 -7
  31. package/lib/module/network/utils/generateCurl.js +67 -0
  32. package/lib/module/network/utils/networkEventStore.js +10 -1
  33. package/lib/module/network/utils/networkListener.js +54 -1
  34. package/lib/module/network/utils/networkSavedStore.js +569 -0
  35. package/lib/module/network/utils/withPinnedFirst.js +28 -0
  36. package/lib/typescript/index.d.ts +13 -1
  37. package/lib/typescript/index.d.ts.map +1 -1
  38. package/lib/typescript/network/components/NetworkEventDetailView.d.ts.map +1 -1
  39. package/lib/typescript/network/components/NetworkEventItemCompact.d.ts +12 -0
  40. package/lib/typescript/network/components/NetworkEventItemCompact.d.ts.map +1 -1
  41. package/lib/typescript/network/components/NetworkHeaderLive.d.ts +28 -0
  42. package/lib/typescript/network/components/NetworkHeaderLive.d.ts.map +1 -1
  43. package/lib/typescript/network/components/NetworkListScreen.d.ts.map +1 -1
  44. package/lib/typescript/network/components/NetworkModal.d.ts +34 -0
  45. package/lib/typescript/network/components/NetworkModal.d.ts.map +1 -1
  46. package/lib/typescript/network/components/NetworkSavedScreen.d.ts +29 -0
  47. package/lib/typescript/network/components/NetworkSavedScreen.d.ts.map +1 -0
  48. package/lib/typescript/network/components/NetworkUpgradeBanner.d.ts +17 -0
  49. package/lib/typescript/network/components/NetworkUpgradeBanner.d.ts.map +1 -0
  50. package/lib/typescript/network/hooks/useLiveNetworkEvent.d.ts +19 -0
  51. package/lib/typescript/network/hooks/useLiveNetworkEvent.d.ts.map +1 -0
  52. package/lib/typescript/network/hooks/useNetworkSaved.d.ts +21 -0
  53. package/lib/typescript/network/hooks/useNetworkSaved.d.ts.map +1 -0
  54. package/lib/typescript/network/index.d.ts +8 -1
  55. package/lib/typescript/network/index.d.ts.map +1 -1
  56. package/lib/typescript/network/sync/networkSyncAdapter.d.ts +52 -3
  57. package/lib/typescript/network/sync/networkSyncAdapter.d.ts.map +1 -1
  58. package/lib/typescript/network/types/index.d.ts +43 -0
  59. package/lib/typescript/network/types/index.d.ts.map +1 -1
  60. package/lib/typescript/network/utils/generateCurl.d.ts +3 -0
  61. package/lib/typescript/network/utils/generateCurl.d.ts.map +1 -0
  62. package/lib/typescript/network/utils/networkEventStore.d.ts.map +1 -1
  63. package/lib/typescript/network/utils/networkListener.d.ts +4 -0
  64. package/lib/typescript/network/utils/networkListener.d.ts.map +1 -1
  65. package/lib/typescript/network/utils/networkSavedStore.d.ts +186 -0
  66. package/lib/typescript/network/utils/networkSavedStore.d.ts.map +1 -0
  67. package/lib/typescript/network/utils/withPinnedFirst.d.ts +24 -0
  68. package/lib/typescript/network/utils/withPinnedFirst.d.ts.map +1 -0
  69. package/package.json +2 -2
@@ -0,0 +1,62 @@
1
+ "use strict";
2
+
3
+ Object.defineProperty(exports, "__esModule", {
4
+ value: true
5
+ });
6
+ exports.useLiveNetworkEvent = useLiveNetworkEvent;
7
+ var _react = require("react");
8
+ var _networkEventStore = require("../utils/networkEventStore");
9
+ var _networkSavedStore = require("../utils/networkSavedStore");
10
+ /**
11
+ * Resolves a selected event ID to the LIVE event object in the store.
12
+ *
13
+ * The detail view used to be handed the `NetworkEvent` captured at press
14
+ * time. The store updates immutably — a response/error replaces the pending
15
+ * event with a NEW object — so a request opened while pending stayed
16
+ * "Pending" for as long as the detail view was open, and only showed its
17
+ * real status after going back and re-opening the row (which re-read the
18
+ * store). This hook closes that gap: callers keep the ID as UI state and the
19
+ * leaf that renders the detail subscribes for its current value.
20
+ *
21
+ * Subscribing here (not at the modal root) keeps the event stream out of the
22
+ * shell's render path — same rule the list/header leaves follow.
23
+ */
24
+
25
+ /**
26
+ * Live store first, then the pinned/saved snapshots. The fallback is what
27
+ * makes a pinned request openable after a Clear, after it aged past the
28
+ * 500-event cap, or in a later app launch — cases where the live store has
29
+ * nothing and the persisted snapshot is the only copy left.
30
+ */
31
+ function readEvent(eventId) {
32
+ return _networkEventStore.networkEventStore.getEvents().find(e => e.id === eventId) ?? _networkSavedStore.networkSavedStore.getEventById(eventId);
33
+ }
34
+ function useLiveNetworkEvent(eventId, options = {}) {
35
+ const {
36
+ paused = false
37
+ } = options;
38
+ const [entry, setEntry] = (0, _react.useState)(null);
39
+ (0, _react.useEffect)(() => {
40
+ // `paused` is the power toggle's freeze-stream semantic: a frozen tool
41
+ // keeps showing the version it froze on.
42
+ if (!eventId || paused) return undefined;
43
+ // subscribeToEvents replays the current array immediately, so the open
44
+ // detail catches up on subscribe as well as on every later update.
45
+ return _networkEventStore.networkEventStore.subscribeToEvents(events => {
46
+ const next = events.find(e => e.id === eventId);
47
+ // Gone from the store (cleared, or aged past maxEvents) — keep the last
48
+ // known version rather than blanking an open detail view.
49
+ if (!next) return;
50
+ setEntry(prev => prev && prev.id === eventId && prev.event === next ? prev : {
51
+ id: eventId,
52
+ event: next
53
+ });
54
+ });
55
+ }, [eventId, paused]);
56
+ if (!eventId) return null;
57
+ // A store read during render covers the frame where `eventId` just changed:
58
+ // the effect's first emission lands a commit later, and until then `entry`
59
+ // still describes the previously selected request.
60
+ if (entry && entry.id === eventId) return entry.event;
61
+ return readEvent(eventId);
62
+ }
@@ -0,0 +1,38 @@
1
+ "use strict";
2
+
3
+ Object.defineProperty(exports, "__esModule", {
4
+ value: true
5
+ });
6
+ exports.useNetworkSaved = useNetworkSaved;
7
+ var _react = require("react");
8
+ var _networkSavedStore = require("../utils/networkSavedStore");
9
+ /**
10
+ * Narrow subscription to the pinned/saved store.
11
+ *
12
+ * The store rebuilds its derived view once per mutation, so the object this
13
+ * returns keeps a stable identity between pins — memoized rows don't re-render
14
+ * because a network event arrived, only because the user pinned something.
15
+ *
16
+ * Mutations happen through module-scope callbacks (stable identities), so
17
+ * passing them into memoized rows costs nothing.
18
+ */
19
+
20
+ const togglePin = event => _networkSavedStore.networkSavedStore.togglePin(event);
21
+ const toggleSave = event => _networkSavedStore.networkSavedStore.toggleSave(event);
22
+ const remove = key => _networkSavedStore.networkSavedStore.remove(key);
23
+ const clearSaved = () => _networkSavedStore.networkSavedStore.clearSaved();
24
+ const clearPinned = () => _networkSavedStore.networkSavedStore.clearPinned();
25
+ function useNetworkSaved() {
26
+ const [state, setState] = (0, _react.useState)(() => _networkSavedStore.networkSavedStore.getState());
27
+ (0, _react.useEffect)(() => _networkSavedStore.networkSavedStore.subscribe(() => {
28
+ setState(_networkSavedStore.networkSavedStore.getState());
29
+ }), []);
30
+ return {
31
+ ...state,
32
+ togglePin,
33
+ toggleSave,
34
+ remove,
35
+ clearSaved,
36
+ clearPinned
37
+ };
38
+ }
@@ -3,6 +3,36 @@
3
3
  Object.defineProperty(exports, "__esModule", {
4
4
  value: true
5
5
  });
6
+ Object.defineProperty(exports, "FREE_TIER_SAVED_LIMIT", {
7
+ enumerable: true,
8
+ get: function () {
9
+ return _networkSavedStore.FREE_TIER_SAVED_LIMIT;
10
+ }
11
+ });
12
+ Object.defineProperty(exports, "MAX_PINNED", {
13
+ enumerable: true,
14
+ get: function () {
15
+ return _networkSavedStore.MAX_PINNED;
16
+ }
17
+ });
18
+ Object.defineProperty(exports, "MAX_SAVED", {
19
+ enumerable: true,
20
+ get: function () {
21
+ return _networkSavedStore.MAX_SAVED;
22
+ }
23
+ });
24
+ Object.defineProperty(exports, "NETWORK_SESSION_ID", {
25
+ enumerable: true,
26
+ get: function () {
27
+ return _networkSavedStore.SESSION_ID;
28
+ }
29
+ });
30
+ Object.defineProperty(exports, "NetworkDetailPinActions", {
31
+ enumerable: true,
32
+ get: function () {
33
+ return _NetworkHeaderLive.NetworkDetailPinActions;
34
+ }
35
+ });
6
36
  Object.defineProperty(exports, "NetworkEventDetailView", {
7
37
  enumerable: true,
8
38
  get: function () {
@@ -21,6 +51,24 @@ Object.defineProperty(exports, "NetworkModal", {
21
51
  return _NetworkModal.NetworkModal;
22
52
  }
23
53
  });
54
+ Object.defineProperty(exports, "NetworkSavedButton", {
55
+ enumerable: true,
56
+ get: function () {
57
+ return _NetworkHeaderLive.NetworkSavedButton;
58
+ }
59
+ });
60
+ Object.defineProperty(exports, "NetworkSavedScreen", {
61
+ enumerable: true,
62
+ get: function () {
63
+ return _NetworkSavedScreen.NetworkSavedScreen;
64
+ }
65
+ });
66
+ Object.defineProperty(exports, "NetworkToolView", {
67
+ enumerable: true,
68
+ get: function () {
69
+ return _NetworkModal.NetworkToolView;
70
+ }
71
+ });
24
72
  Object.defineProperty(exports, "TickProvider", {
25
73
  enumerable: true,
26
74
  get: function () {
@@ -33,6 +81,12 @@ Object.defineProperty(exports, "addNetworkListener", {
33
81
  return _networkListener.addNetworkListener;
34
82
  }
35
83
  });
84
+ Object.defineProperty(exports, "flagsForEventId", {
85
+ enumerable: true,
86
+ get: function () {
87
+ return _networkSavedStore.flagsForEventId;
88
+ }
89
+ });
36
90
  Object.defineProperty(exports, "formatBytes", {
37
91
  enumerable: true,
38
92
  get: function () {
@@ -75,12 +129,24 @@ Object.defineProperty(exports, "networkListener", {
75
129
  return _networkListener.networkListener;
76
130
  }
77
131
  });
132
+ Object.defineProperty(exports, "networkSavedStore", {
133
+ enumerable: true,
134
+ get: function () {
135
+ return _networkSavedStore.networkSavedStore;
136
+ }
137
+ });
78
138
  Object.defineProperty(exports, "removeAllNetworkListeners", {
79
139
  enumerable: true,
80
140
  get: function () {
81
141
  return _networkListener.removeAllNetworkListeners;
82
142
  }
83
143
  });
144
+ Object.defineProperty(exports, "splitPinned", {
145
+ enumerable: true,
146
+ get: function () {
147
+ return _withPinnedFirst.splitPinned;
148
+ }
149
+ });
84
150
  Object.defineProperty(exports, "startNetworkListener", {
85
151
  enumerable: true,
86
152
  get: function () {
@@ -93,12 +159,24 @@ Object.defineProperty(exports, "stopNetworkListener", {
93
159
  return _networkListener.stopNetworkListener;
94
160
  }
95
161
  });
162
+ Object.defineProperty(exports, "useLiveNetworkEvent", {
163
+ enumerable: true,
164
+ get: function () {
165
+ return _useLiveNetworkEvent.useLiveNetworkEvent;
166
+ }
167
+ });
96
168
  Object.defineProperty(exports, "useNetworkEvents", {
97
169
  enumerable: true,
98
170
  get: function () {
99
171
  return _useNetworkEvents.useNetworkEvents;
100
172
  }
101
173
  });
174
+ Object.defineProperty(exports, "useNetworkSaved", {
175
+ enumerable: true,
176
+ get: function () {
177
+ return _useNetworkSaved.useNetworkSaved;
178
+ }
179
+ });
102
180
  Object.defineProperty(exports, "useTickEveryMinute", {
103
181
  enumerable: true,
104
182
  get: function () {
@@ -108,8 +186,14 @@ Object.defineProperty(exports, "useTickEveryMinute", {
108
186
  var _NetworkModal = require("./components/NetworkModal");
109
187
  var _NetworkEventDetailView = require("./components/NetworkEventDetailView");
110
188
  var _NetworkEventItemCompact = require("./components/NetworkEventItemCompact");
189
+ var _NetworkSavedScreen = require("./components/NetworkSavedScreen");
190
+ var _NetworkHeaderLive = require("./components/NetworkHeaderLive");
111
191
  var _useNetworkEvents = require("./hooks/useNetworkEvents");
192
+ var _useLiveNetworkEvent = require("./hooks/useLiveNetworkEvent");
193
+ var _useNetworkSaved = require("./hooks/useNetworkSaved");
112
194
  var _useTickEveryMinute = require("./hooks/useTickEveryMinute");
113
195
  var _networkListener = require("./utils/networkListener");
114
196
  var _networkEventStore = require("./utils/networkEventStore");
197
+ var _networkSavedStore = require("./utils/networkSavedStore");
198
+ var _withPinnedFirst = require("./utils/withPinnedFirst");
115
199
  var _formatting = require("./utils/formatting");
@@ -5,6 +5,7 @@ Object.defineProperty(exports, "__esModule", {
5
5
  });
6
6
  exports.networkSyncAdapter = exports.SNAPSHOT_BODY_INLINE_LIMIT = void 0;
7
7
  var _networkEventStore = require("../utils/networkEventStore");
8
+ var _networkSavedStore = require("../utils/networkSavedStore");
8
9
  /**
9
10
  * Request/response bodies larger than this (using the cached size proxy already
10
11
  * on the event — no per-snapshot stringify) are NOT streamed in the list
@@ -30,6 +31,9 @@ const stripLargeBodies = event => {
30
31
  if (stripResponse) stripped.responseData = undefined;
31
32
  return stripped;
32
33
  };
34
+ const resolveEvent = id => _networkEventStore.networkEventStore.getEventById(id) ?? _networkSavedStore.networkSavedStore.getEventById(id);
35
+ const readId = params => params && typeof params === "object" ? params.id : undefined;
36
+ const readBool = (params, key) => !!(params && typeof params === "object" && params[key]);
33
37
 
34
38
  /**
35
39
  * Sync adapter for the network tool, consumed by @buoy-gg/external-sync's
@@ -41,29 +45,103 @@ const stripLargeBodies = event => {
41
45
  *
42
46
  * v2: large bodies are stripped from the snapshot and fetched lazily via
43
47
  * `getEventBody` (see SNAPSHOT_BODY_INLINE_LIMIT).
48
+ *
49
+ * v3: the payload became an OBJECT — `{ events, saved }` — so pinned/saved
50
+ * requests reach the dashboard. They can't ride as flags on the events: a pin
51
+ * outlives the event that produced it (that is its whole purpose), so it has
52
+ * to be its own list. Dashboards that predate v3 read `payload` as an array;
53
+ * `NetworkPanel`/`NetworkInline` accept either shape.
44
54
  */
45
55
  const networkSyncAdapter = exports.networkSyncAdapter = {
46
- version: 2,
47
- getSnapshot: () => _networkEventStore.networkEventStore.getEvents().map(stripLargeBodies),
48
- subscribe: onChange => _networkEventStore.networkEventStore.subscribeToEvents(onChange),
56
+ version: 3,
57
+ getSnapshot: () => ({
58
+ events: _networkEventStore.networkEventStore.getEvents().map(stripLargeBodies),
59
+ // Saved snapshots go through the same body strip. They are few (capped in
60
+ // the store) but they ride on EVERY snapshot, so a fat one would land in
61
+ // the same 200ms loop that the body-stripping exists to protect.
62
+ saved: _networkSavedStore.networkSavedStore.getRecords().map(record => ({
63
+ ...record,
64
+ event: stripLargeBodies(record.event)
65
+ }))
66
+ }),
67
+ subscribe: onChange => {
68
+ // Pinning with no traffic in flight still has to reach the dashboard, so
69
+ // both stores drive the snapshot.
70
+ const unsubscribeEvents = _networkEventStore.networkEventStore.subscribeToEvents(onChange);
71
+ const unsubscribeSaved = _networkSavedStore.networkSavedStore.subscribe(onChange);
72
+ return () => {
73
+ unsubscribeEvents();
74
+ unsubscribeSaved();
75
+ };
76
+ },
49
77
  actions: {
50
78
  clearEvents: () => {
51
79
  _networkEventStore.networkEventStore.clearEvents();
52
80
  },
53
81
  /**
54
82
  * Return the full (un-stripped) request/response bodies for one event,
55
- * fetched on demand by the dashboard when its detail view opens. Returns
56
- * null if the event is no longer in the store.
83
+ * fetched on demand by the dashboard when its detail view opens. Falls
84
+ * back to the saved store, which may hold the only remaining copy of a
85
+ * request that has since been cleared. Returns null if neither has it.
57
86
  */
58
87
  getEventBody: params => {
59
- const id = params && typeof params === "object" ? params.id : undefined;
88
+ const id = readId(params);
60
89
  if (!id) return null;
61
- const event = _networkEventStore.networkEventStore.getEventById(id);
90
+ const event = resolveEvent(id);
62
91
  if (!event) return null;
63
92
  return {
64
93
  requestData: event.requestData ?? null,
65
94
  responseData: event.responseData ?? null
66
95
  };
96
+ },
97
+ /** Pin/unpin a request from the dashboard. */
98
+ setPinned: params => {
99
+ const id = readId(params);
100
+ if (!id) return null;
101
+ const event = resolveEvent(id);
102
+ if (!event) return null;
103
+ const wanted = readBool(params, "pinned");
104
+ // `flagsForEventId`, not `isPinned` — the id may be a `saved:<key>`
105
+ // snapshot, which the live-id Sets deliberately don't carry.
106
+ const current = (0, _networkSavedStore.flagsForEventId)(_networkSavedStore.networkSavedStore.getState(), id).pinned;
107
+ if (current === wanted) return {
108
+ ok: true
109
+ };
110
+ return _networkSavedStore.networkSavedStore.togglePin(event);
111
+ },
112
+ /** Save/unsave a request from the dashboard. */
113
+ setSaved: params => {
114
+ const id = readId(params);
115
+ if (!id) return null;
116
+ const event = resolveEvent(id);
117
+ if (!event) return null;
118
+ const wanted = readBool(params, "saved");
119
+ const current = (0, _networkSavedStore.flagsForEventId)(_networkSavedStore.networkSavedStore.getState(), id).saved;
120
+ if (current === wanted) return {
121
+ ok: true
122
+ };
123
+ return _networkSavedStore.networkSavedStore.toggleSave(event);
124
+ },
125
+ /** Drop one pinned/saved record by its stable key. */
126
+ removeSavedRecord: params => {
127
+ const key = params && typeof params === "object" ? params.key : undefined;
128
+ if (!key) return null;
129
+ _networkSavedStore.networkSavedStore.remove(key);
130
+ return {
131
+ ok: true
132
+ };
133
+ },
134
+ clearSavedRequests: () => {
135
+ _networkSavedStore.networkSavedStore.clearSaved();
136
+ return {
137
+ ok: true
138
+ };
139
+ },
140
+ clearPinnedRequests: () => {
141
+ _networkSavedStore.networkSavedStore.clearPinned();
142
+ return {
143
+ ok: true
144
+ };
67
145
  }
68
146
  }
69
147
  };
@@ -0,0 +1,71 @@
1
+ "use strict";
2
+
3
+ Object.defineProperty(exports, "__esModule", {
4
+ value: true
5
+ });
6
+ exports.generateCurl = generateCurl;
7
+ /**
8
+ * Render a captured request as a runnable `curl` command.
9
+ *
10
+ * The point is to get a request OUT of the app and into a terminal, Postman or
11
+ * a bug report without retyping it — so the output has to actually run, which
12
+ * makes quoting the whole job.
13
+ */
14
+
15
+ /** Headers that describe the captured hop rather than the request, and would
16
+ * either break the replay or lie about it. curl sets these itself. */
17
+ const SKIPPED_HEADERS = new Set(["content-length",
18
+ // curl recomputes it; a stale value truncates the body
19
+ "host",
20
+ // implied by the URL
21
+ "connection", "accept-encoding" // curl would then have to decode what it asked for
22
+ ]);
23
+
24
+ /**
25
+ * Single-quote for POSIX shells.
26
+ *
27
+ * Inside single quotes every byte is literal, so the ONLY character needing
28
+ * care is the single quote itself — closed, escaped, reopened. Newlines and
29
+ * `$`/backtick survive untouched, which is what makes this safe for JSON
30
+ * bodies and tokens.
31
+ */
32
+ function shellQuote(value) {
33
+ return `'${value.replace(/'/g, `'\\''`)}'`;
34
+ }
35
+ function serializeBody(data) {
36
+ if (data === undefined || data === null) return null;
37
+ if (typeof data === "string") return data;
38
+ try {
39
+ return JSON.stringify(data);
40
+ } catch {
41
+ // Circular or otherwise unserializable — better to emit no body than a
42
+ // command that silently posts "[object Object]".
43
+ return null;
44
+ }
45
+ }
46
+ function generateCurl(event) {
47
+ const method = (event.method || "GET").toUpperCase();
48
+ const body = serializeBody(event.requestData);
49
+
50
+ // Flags are collected as whole `-X PATCH` strings rather than as a flat token
51
+ // list, so the line-wrap below can't mis-pair them — an earlier version
52
+ // destructured `[cmd, url, ...rest]` and put the URL in the wrong slot the
53
+ // moment an -X was present.
54
+ const flags = [];
55
+
56
+ // curl defaults to GET, and to POST once -d is present, so -X only carries
57
+ // information when it disagrees with that.
58
+ if (method !== "GET" && !(method === "POST" && body !== null)) {
59
+ flags.push(`-X ${method}`);
60
+ }
61
+ for (const [key, value] of Object.entries(event.requestHeaders || {})) {
62
+ if (SKIPPED_HEADERS.has(key.toLowerCase())) continue;
63
+ flags.push(`-H ${shellQuote(`${key}: ${value}`)}`);
64
+ }
65
+ if (body !== null) flags.push(`--data-raw ${shellQuote(body)}`);
66
+ const head = `curl ${shellQuote(event.url)}`;
67
+ if (flags.length === 0) return head;
68
+ // One flag per line — these get long, and a wrapped blob is unreadable in a
69
+ // terminal or pasted into a ticket.
70
+ return `${head} \\\n ${flags.join(" \\\n ")}`;
71
+ }
@@ -7,6 +7,7 @@ exports.networkEventStore = void 0;
7
7
  var _sharedUi = require("@buoy-gg/shared-ui");
8
8
  var _extractOperationName = require("./extractOperationName");
9
9
  var _networkListener = require("./networkListener");
10
+ var _networkSavedStore = require("./networkSavedStore");
10
11
  /**
11
12
  * Network Event Store
12
13
  *
@@ -158,7 +159,11 @@ class NetworkEventStore extends _sharedUi.BaseEventStore {
158
159
  if (index !== -1) {
159
160
  const updatedEvent = {
160
161
  ...this.events[index],
161
- duration: event.duration
162
+ duration: event.duration,
163
+ // Left undefined when the interceptor couldn't measure it (e.g. a
164
+ // request that failed before headers). Consumers hide the breakdown
165
+ // in that case rather than rendering zeroes.
166
+ timings: event.timings
162
167
  };
163
168
  if (event.response) {
164
169
  updatedEvent.status = event.response.status;
@@ -174,6 +179,10 @@ class NetworkEventStore extends _sharedUi.BaseEventStore {
174
179
  }
175
180
  this.events = this.events.map((e, i) => i === index ? updatedEvent : e);
176
181
  this.pendingRequests.delete(request.id);
182
+ // Refresh any pinned/saved snapshot of this request — a request pinned
183
+ // while in flight would otherwise stay frozen as "Pending" forever.
184
+ // No-ops on a single Set check when nothing is pinned.
185
+ _networkSavedStore.networkSavedStore.syncFromLive(updatedEvent);
177
186
  this.notify(updatedEvent);
178
187
  this.notifyArrayListeners();
179
188
  }
@@ -205,6 +205,16 @@ class NetworkListener {
205
205
  */
206
206
  handleXHRResponse(xhr, requestId, cleanUrl, method, requestHeaders, requestData, params, startTime, isError = false, clientType = "xhr") {
207
207
  const duration = startTime ? Date.now() - startTime : 0;
208
+
209
+ // XHR's duration was ALREADY total (measured at readyState 4 / load), so
210
+ // only the split is new. `_ttfb` is stamped at HEADERS_RECEIVED; it is
211
+ // absent for a request that failed before any headers arrived, and the
212
+ // breakdown is then correctly omitted rather than faked as 0.
213
+ const ttfb = xhr._ttfb;
214
+ const timings = ttfb === undefined ? undefined : {
215
+ ttfb,
216
+ download: Math.max(0, duration - ttfb)
217
+ };
208
218
  if (isError || xhr.status === 0) {
209
219
  // Network error or aborted request
210
220
  this.emit({
@@ -345,6 +355,7 @@ class NetworkListener {
345
355
  type: "response",
346
356
  timestamp: new Date(),
347
357
  duration,
358
+ timings,
348
359
  request: {
349
360
  id: requestId || "unknown",
350
361
  url: cleanUrl,
@@ -472,7 +483,11 @@ class NetworkListener {
472
483
  });
473
484
  try {
474
485
  const response = await self.originalFetch(input, init);
475
- const duration = Date.now() - startTime;
486
+ // A fetch promise resolves when the HEADERS arrive, not the body — so
487
+ // this is time-to-first-byte, and it is where `duration` used to stop.
488
+ // That silently excluded download time, making an identical request
489
+ // look faster through fetch than through XHR.
490
+ const ttfb = Date.now() - startTime;
476
491
 
477
492
  // Clone response to read body with size limits
478
493
  const responseClone = response.clone();
@@ -482,6 +497,33 @@ class NetworkListener {
482
497
  truncated
483
498
  } = await self.processResponseBody(responseClone);
484
499
 
500
+ // Now the body is in — this is the comparable, total number.
501
+ const duration = Date.now() - startTime;
502
+
503
+ /**
504
+ * The split is only real on platforms whose fetch resolves at HEADERS.
505
+ *
506
+ * React Native's does NOT: `fetch` there is the whatwg-fetch polyfill
507
+ * over XMLHttpRequest, and it resolves inside `xhr.onload` — readyState
508
+ * 4, i.e. after the entire body. On device this `ttfb` is therefore the
509
+ * total and the download phase collapses to ~0, so claiming a "0ms
510
+ * download" would be asserting a measurement we never made.
511
+ *
512
+ * Omitting is the right answer rather than a problem: RN's fetch runs
513
+ * through XMLHttpRequest, so the XHR interceptor below sees readyState 2
514
+ * and supplies a genuine TTFB for those requests anyway. Where fetch is
515
+ * native (web, Node), the gap is real and we keep it.
516
+ *
517
+ * Verified both ways against a server that flushes headers then holds
518
+ * the body 250ms: native fetch → 16ms / 251ms; resolve-at-end → the
519
+ * download phase vanishes, which is what this guard catches.
520
+ */
521
+ const download = Math.max(0, duration - ttfb);
522
+ const timings = download > 0 ? {
523
+ ttfb,
524
+ download
525
+ } : undefined;
526
+
485
527
  // Parse response headers
486
528
  const responseHeaders = {};
487
529
  response.headers.forEach((value, key) => {
@@ -493,6 +535,7 @@ class NetworkListener {
493
535
  type: "response",
494
536
  timestamp: new Date(),
495
537
  duration,
538
+ timings,
496
539
  request: {
497
540
  id: requestId || "unknown",
498
541
  url: cleanUrl,
@@ -645,6 +688,16 @@ class NetworkListener {
645
688
  processResponse(true);
646
689
  };
647
690
  const readyStateListener = () => {
691
+ // HEADERS_RECEIVED — the XHR equivalent of a fetch promise resolving.
692
+ // This listener is already attached for the readyState-4 fallback, and
693
+ // readystatechange fires whether or not we look at it, so capturing
694
+ // TTFB here costs one comparison and one clock read.
695
+ if (this.readyState === 2) {
696
+ const xhr = this;
697
+ if (xhr._ttfb === undefined && xhr._startTime) {
698
+ xhr._ttfb = Date.now() - xhr._startTime;
699
+ }
700
+ }
648
701
  if (this.readyState === 4 && !processed) {
649
702
  processResponse(false);
650
703
  }