@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.
- package/README.md +2 -1
- package/lib/commonjs/index.js +85 -0
- package/lib/commonjs/network/components/NetworkEventDetailView.js +201 -40
- package/lib/commonjs/network/components/NetworkEventItemCompact.js +52 -5
- package/lib/commonjs/network/components/NetworkHeaderLive.js +132 -0
- package/lib/commonjs/network/components/NetworkListScreen.js +134 -56
- package/lib/commonjs/network/components/NetworkModal.js +154 -36
- package/lib/commonjs/network/components/NetworkSavedScreen.js +245 -0
- package/lib/commonjs/network/components/NetworkUpgradeBanner.js +76 -0
- package/lib/commonjs/network/hooks/useLiveNetworkEvent.js +62 -0
- package/lib/commonjs/network/hooks/useNetworkSaved.js +38 -0
- package/lib/commonjs/network/index.js +84 -0
- package/lib/commonjs/network/sync/networkSyncAdapter.js +85 -7
- package/lib/commonjs/network/utils/generateCurl.js +71 -0
- package/lib/commonjs/network/utils/networkEventStore.js +10 -1
- package/lib/commonjs/network/utils/networkListener.js +54 -1
- package/lib/commonjs/network/utils/networkSavedStore.js +575 -0
- package/lib/commonjs/network/utils/withPinnedFirst.js +32 -0
- package/lib/module/index.js +17 -1
- package/lib/module/network/components/NetworkEventDetailView.js +204 -41
- package/lib/module/network/components/NetworkEventItemCompact.js +53 -6
- package/lib/module/network/components/NetworkHeaderLive.js +132 -2
- package/lib/module/network/components/NetworkListScreen.js +136 -58
- package/lib/module/network/components/NetworkModal.js +156 -39
- package/lib/module/network/components/NetworkSavedScreen.js +241 -0
- package/lib/module/network/components/NetworkUpgradeBanner.js +72 -0
- package/lib/module/network/hooks/useLiveNetworkEvent.js +58 -0
- package/lib/module/network/hooks/useNetworkSaved.js +34 -0
- package/lib/module/network/index.js +7 -1
- package/lib/module/network/sync/networkSyncAdapter.js +85 -7
- package/lib/module/network/utils/generateCurl.js +67 -0
- package/lib/module/network/utils/networkEventStore.js +10 -1
- package/lib/module/network/utils/networkListener.js +54 -1
- package/lib/module/network/utils/networkSavedStore.js +569 -0
- package/lib/module/network/utils/withPinnedFirst.js +28 -0
- package/lib/typescript/index.d.ts +13 -1
- package/lib/typescript/index.d.ts.map +1 -1
- package/lib/typescript/network/components/NetworkEventDetailView.d.ts.map +1 -1
- package/lib/typescript/network/components/NetworkEventItemCompact.d.ts +12 -0
- package/lib/typescript/network/components/NetworkEventItemCompact.d.ts.map +1 -1
- package/lib/typescript/network/components/NetworkHeaderLive.d.ts +28 -0
- package/lib/typescript/network/components/NetworkHeaderLive.d.ts.map +1 -1
- package/lib/typescript/network/components/NetworkListScreen.d.ts.map +1 -1
- package/lib/typescript/network/components/NetworkModal.d.ts +34 -0
- package/lib/typescript/network/components/NetworkModal.d.ts.map +1 -1
- package/lib/typescript/network/components/NetworkSavedScreen.d.ts +29 -0
- package/lib/typescript/network/components/NetworkSavedScreen.d.ts.map +1 -0
- package/lib/typescript/network/components/NetworkUpgradeBanner.d.ts +17 -0
- package/lib/typescript/network/components/NetworkUpgradeBanner.d.ts.map +1 -0
- package/lib/typescript/network/hooks/useLiveNetworkEvent.d.ts +19 -0
- package/lib/typescript/network/hooks/useLiveNetworkEvent.d.ts.map +1 -0
- package/lib/typescript/network/hooks/useNetworkSaved.d.ts +21 -0
- package/lib/typescript/network/hooks/useNetworkSaved.d.ts.map +1 -0
- package/lib/typescript/network/index.d.ts +8 -1
- package/lib/typescript/network/index.d.ts.map +1 -1
- package/lib/typescript/network/sync/networkSyncAdapter.d.ts +52 -3
- package/lib/typescript/network/sync/networkSyncAdapter.d.ts.map +1 -1
- package/lib/typescript/network/types/index.d.ts +43 -0
- package/lib/typescript/network/types/index.d.ts.map +1 -1
- package/lib/typescript/network/utils/generateCurl.d.ts +3 -0
- package/lib/typescript/network/utils/generateCurl.d.ts.map +1 -0
- package/lib/typescript/network/utils/networkEventStore.d.ts.map +1 -1
- package/lib/typescript/network/utils/networkListener.d.ts +4 -0
- package/lib/typescript/network/utils/networkListener.d.ts.map +1 -1
- package/lib/typescript/network/utils/networkSavedStore.d.ts +186 -0
- package/lib/typescript/network/utils/networkSavedStore.d.ts.map +1 -0
- package/lib/typescript/network/utils/withPinnedFirst.d.ts +24 -0
- package/lib/typescript/network/utils/withPinnedFirst.d.ts.map +1 -0
- 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:
|
|
47
|
-
getSnapshot: () =>
|
|
48
|
-
|
|
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.
|
|
56
|
-
*
|
|
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
|
|
88
|
+
const id = readId(params);
|
|
60
89
|
if (!id) return null;
|
|
61
|
-
const event =
|
|
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
|
-
|
|
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
|
}
|