@buoy-gg/network 7.0.36 → 7.0.39
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 +45 -12
- package/lib/commonjs/index.js +26 -1
- package/lib/commonjs/index.web.js +26 -1
- package/lib/commonjs/network/components/NetworkEventDetailView.js +6 -1
- package/lib/commonjs/network/components/NetworkHeaderMenu.js +4 -0
- package/lib/commonjs/network/components/NetworkModal.js +24 -8
- package/lib/commonjs/network/conditions/NetworkConditionsControl.js +235 -0
- package/lib/commonjs/network/conditions/NetworkConditionsControl.test.js +136 -0
- package/lib/commonjs/network/conditions/NetworkConditionsIcon.js +33 -0
- package/lib/commonjs/network/conditions/NetworkFloatingTool.js +310 -0
- package/lib/commonjs/network/conditions/NetworkFloatingTool.test.js +219 -0
- package/lib/commonjs/network/conditions/store.js +75 -0
- package/lib/commonjs/network/conditions/wait.js +43 -0
- package/lib/commonjs/network/conditions/xhr.js +110 -0
- package/lib/commonjs/network/hooks/useNetworkEvents.js +1 -1
- package/lib/commonjs/network/overrides/applyToXhr.js +10 -8
- package/lib/commonjs/network/overrides/engine.js +2 -1
- package/lib/commonjs/network/sync/networkSyncAdapter.js +26 -1
- package/lib/commonjs/network/utils/generateNetworkCopyText.js +2 -0
- package/lib/commonjs/network/utils/networkEventStore.js +5 -77
- package/lib/commonjs/network/utils/networkListener.js +96 -51
- package/lib/commonjs/network/utils/networkSavedStore.js +2 -2
- package/lib/commonjs/network/utils/selectSavedEvents.js +2 -2
- package/lib/commonjs/preset.js +2 -1
- package/lib/module/index.js +2 -1
- package/lib/module/index.web.js +2 -1
- package/lib/module/network/components/NetworkEventDetailView.js +6 -1
- package/lib/module/network/components/NetworkHeaderMenu.js +4 -0
- package/lib/module/network/components/NetworkModal.js +25 -9
- package/lib/module/network/conditions/NetworkConditionsControl.js +231 -0
- package/lib/module/network/conditions/NetworkConditionsControl.test.js +135 -0
- package/lib/module/network/conditions/NetworkConditionsIcon.js +29 -0
- package/lib/module/network/conditions/NetworkFloatingTool.js +304 -0
- package/lib/module/network/conditions/NetworkFloatingTool.test.js +218 -0
- package/lib/module/network/conditions/store.js +65 -0
- package/lib/module/network/conditions/wait.js +38 -0
- package/lib/module/network/conditions/xhr.js +106 -0
- package/lib/module/network/hooks/useNetworkEvents.js +1 -1
- package/lib/module/network/overrides/applyToXhr.js +9 -8
- package/lib/module/network/overrides/engine.js +2 -1
- package/lib/module/network/sync/networkSyncAdapter.js +26 -1
- package/lib/module/network/utils/generateNetworkCopyText.js +2 -0
- package/lib/module/network/utils/networkEventStore.js +6 -77
- package/lib/module/network/utils/networkListener.js +97 -52
- package/lib/module/network/utils/networkSavedStore.js +2 -2
- package/lib/module/network/utils/selectSavedEvents.js +2 -2
- package/lib/module/preset.js +3 -2
- package/lib/typescript/index.d.ts +2 -0
- package/lib/typescript/index.d.ts.map +1 -1
- package/lib/typescript/index.web.d.ts +2 -0
- package/lib/typescript/index.web.d.ts.map +1 -1
- package/lib/typescript/network/components/NetworkEventDetailView.d.ts.map +1 -1
- package/lib/typescript/network/components/NetworkHeaderMenu.d.ts +2 -1
- package/lib/typescript/network/components/NetworkHeaderMenu.d.ts.map +1 -1
- package/lib/typescript/network/components/NetworkModal.d.ts +3 -0
- package/lib/typescript/network/components/NetworkModal.d.ts.map +1 -1
- package/lib/typescript/network/conditions/NetworkConditionsControl.d.ts +12 -0
- package/lib/typescript/network/conditions/NetworkConditionsControl.d.ts.map +1 -0
- package/lib/typescript/network/conditions/NetworkConditionsControl.test.d.ts +2 -0
- package/lib/typescript/network/conditions/NetworkConditionsControl.test.d.ts.map +1 -0
- package/lib/typescript/network/conditions/NetworkConditionsIcon.d.ts +4 -0
- package/lib/typescript/network/conditions/NetworkConditionsIcon.d.ts.map +1 -0
- package/lib/typescript/network/conditions/NetworkFloatingTool.d.ts +15 -0
- package/lib/typescript/network/conditions/NetworkFloatingTool.d.ts.map +1 -0
- package/lib/typescript/network/conditions/NetworkFloatingTool.test.d.ts +2 -0
- package/lib/typescript/network/conditions/NetworkFloatingTool.test.d.ts.map +1 -0
- package/lib/typescript/network/conditions/store.d.ts +30 -0
- package/lib/typescript/network/conditions/store.d.ts.map +1 -0
- package/lib/typescript/network/conditions/wait.d.ts +4 -0
- package/lib/typescript/network/conditions/wait.d.ts.map +1 -0
- package/lib/typescript/network/conditions/xhr.d.ts +5 -0
- package/lib/typescript/network/conditions/xhr.d.ts.map +1 -0
- package/lib/typescript/network/hooks/useNetworkEvents.d.ts +1 -1
- package/lib/typescript/network/overrides/applyToXhr.d.ts +2 -0
- package/lib/typescript/network/overrides/applyToXhr.d.ts.map +1 -1
- package/lib/typescript/network/overrides/engine.d.ts +0 -10
- package/lib/typescript/network/overrides/engine.d.ts.map +1 -1
- package/lib/typescript/network/sync/networkSyncAdapter.d.ts +17 -0
- package/lib/typescript/network/sync/networkSyncAdapter.d.ts.map +1 -1
- package/lib/typescript/network/types/index.d.ts +1 -0
- package/lib/typescript/network/types/index.d.ts.map +1 -1
- package/lib/typescript/network/utils/generateNetworkCopyText.d.ts.map +1 -1
- package/lib/typescript/network/utils/networkEventStore.d.ts +0 -15
- package/lib/typescript/network/utils/networkEventStore.d.ts.map +1 -1
- package/lib/typescript/network/utils/networkListener.d.ts +2 -1
- package/lib/typescript/network/utils/networkListener.d.ts.map +1 -1
- package/lib/typescript/network/utils/networkSavedStore.d.ts +2 -2
- package/lib/typescript/network/utils/selectSavedEvents.d.ts +2 -2
- package/lib/typescript/preset.d.ts +0 -16
- package/lib/typescript/preset.d.ts.map +1 -1
- package/package.json +2 -2
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
// @vitest-environment jsdom
|
|
4
|
+
import { act, createElement } from "react";
|
|
5
|
+
import { createRoot } from "react-dom/client";
|
|
6
|
+
import { afterEach, beforeEach, expect, it, vi } from "vitest";
|
|
7
|
+
import { NetworkFloatingTool, createRemoteConditionsSource } from "./NetworkFloatingTool";
|
|
8
|
+
const state = vi.hoisted(() => ({
|
|
9
|
+
profile: "normal",
|
|
10
|
+
available: true,
|
|
11
|
+
listeners: new Set(),
|
|
12
|
+
set: vi.fn()
|
|
13
|
+
}));
|
|
14
|
+
vi.mock("./store", () => ({
|
|
15
|
+
getNetworkCondition: () => state.profile,
|
|
16
|
+
conditionsCanApply: () => state.available,
|
|
17
|
+
subscribeNetworkConditions: listener => {
|
|
18
|
+
state.listeners.add(listener);
|
|
19
|
+
return () => state.listeners.delete(listener);
|
|
20
|
+
},
|
|
21
|
+
setNetworkCondition: profile => {
|
|
22
|
+
state.set(profile);
|
|
23
|
+
state.profile = profile;
|
|
24
|
+
state.listeners.forEach(listener => listener());
|
|
25
|
+
}
|
|
26
|
+
}));
|
|
27
|
+
vi.mock("@buoy-gg/shared-ui", async () => ({
|
|
28
|
+
night: (await vi.importActual("../../../../shared/src/ui/night/nightTheme")).night,
|
|
29
|
+
ChevronDown: () => null,
|
|
30
|
+
Check: () => null,
|
|
31
|
+
openFloatingTool: vi.fn(),
|
|
32
|
+
FloatingTool: ({
|
|
33
|
+
children,
|
|
34
|
+
accessory
|
|
35
|
+
}) => /*#__PURE__*/createElement("div", null, children, accessory)
|
|
36
|
+
}));
|
|
37
|
+
vi.mock("react-native", () => ({
|
|
38
|
+
StyleSheet: {
|
|
39
|
+
create: value => value
|
|
40
|
+
},
|
|
41
|
+
useWindowDimensions: () => ({
|
|
42
|
+
width: 390,
|
|
43
|
+
height: 844
|
|
44
|
+
}),
|
|
45
|
+
View: ({
|
|
46
|
+
children,
|
|
47
|
+
testID
|
|
48
|
+
}) => /*#__PURE__*/createElement("div", {
|
|
49
|
+
"data-testid": testID
|
|
50
|
+
}, children),
|
|
51
|
+
Text: ({
|
|
52
|
+
children,
|
|
53
|
+
testID
|
|
54
|
+
}) => /*#__PURE__*/createElement("span", {
|
|
55
|
+
"data-testid": testID
|
|
56
|
+
}, children),
|
|
57
|
+
ActivityIndicator: () => null,
|
|
58
|
+
TouchableOpacity: ({
|
|
59
|
+
children,
|
|
60
|
+
onPress,
|
|
61
|
+
disabled,
|
|
62
|
+
testID,
|
|
63
|
+
accessibilityLabel
|
|
64
|
+
}) => /*#__PURE__*/createElement("button", {
|
|
65
|
+
onClick: onPress,
|
|
66
|
+
disabled,
|
|
67
|
+
"data-testid": testID,
|
|
68
|
+
"aria-label": accessibilityLabel
|
|
69
|
+
}, children)
|
|
70
|
+
}));
|
|
71
|
+
let root;
|
|
72
|
+
let container;
|
|
73
|
+
const dismiss = vi.fn();
|
|
74
|
+
beforeEach(() => {
|
|
75
|
+
globalThis.IS_REACT_ACT_ENVIRONMENT = true;
|
|
76
|
+
state.profile = "normal";
|
|
77
|
+
state.available = true;
|
|
78
|
+
state.set.mockClear();
|
|
79
|
+
dismiss.mockClear();
|
|
80
|
+
container = document.createElement("div");
|
|
81
|
+
root = createRoot(container);
|
|
82
|
+
});
|
|
83
|
+
afterEach(() => act(() => root.unmount()));
|
|
84
|
+
const button = id => container.querySelector(`[data-testid="${id}"]`);
|
|
85
|
+
const tap = async id => act(async () => button(id).click());
|
|
86
|
+
const status = () => container.querySelector('[data-testid="throttling-status"]')?.textContent;
|
|
87
|
+
function render(source) {
|
|
88
|
+
act(() => root.render(/*#__PURE__*/createElement(NetworkFloatingTool, {
|
|
89
|
+
id: "network-throttling",
|
|
90
|
+
revealToken: 1,
|
|
91
|
+
dismiss,
|
|
92
|
+
source
|
|
93
|
+
})));
|
|
94
|
+
}
|
|
95
|
+
async function choose(value) {
|
|
96
|
+
const order = ["normal", "slow", "verySlow", "offline"];
|
|
97
|
+
const count = (order.indexOf(value) - order.indexOf(state.profile) + order.length) % order.length;
|
|
98
|
+
for (let i = 0; i < count; i++) await tap("throttling-cycle");
|
|
99
|
+
}
|
|
100
|
+
it("cycles and applies every profile immediately, including wrapping to normal", async () => {
|
|
101
|
+
render();
|
|
102
|
+
for (const [profile, label] of [["slow", "+500 ms"], ["verySlow", "+2000 ms"], ["offline", "Blocked"], ["normal", "0 ms"]]) {
|
|
103
|
+
await tap("throttling-cycle");
|
|
104
|
+
expect(state.profile).toBe(profile);
|
|
105
|
+
expect(container.querySelector(`[data-testid="throttling-signal-${profile}"]`)).not.toBeNull();
|
|
106
|
+
expect(status()).toBe(label);
|
|
107
|
+
}
|
|
108
|
+
expect(state.set).toHaveBeenCalledTimes(4);
|
|
109
|
+
});
|
|
110
|
+
it("keeps the profile label informational when tapped", async () => {
|
|
111
|
+
render();
|
|
112
|
+
await tap("throttling-profile");
|
|
113
|
+
expect(state.set).not.toHaveBeenCalled();
|
|
114
|
+
expect(state.profile).toBe("normal");
|
|
115
|
+
expect(button("throttling-profile").closest("button")).toBeNull();
|
|
116
|
+
});
|
|
117
|
+
it("changes active profiles and stops before dismissal", async () => {
|
|
118
|
+
state.profile = "slow";
|
|
119
|
+
render();
|
|
120
|
+
await choose("offline");
|
|
121
|
+
expect(state.profile).toBe("offline");
|
|
122
|
+
await tap("throttling-close");
|
|
123
|
+
expect(state.profile).toBe("normal");
|
|
124
|
+
expect(dismiss).toHaveBeenCalledOnce();
|
|
125
|
+
});
|
|
126
|
+
it("reflects external changes and disables cycling when account admission is revoked", () => {
|
|
127
|
+
render();
|
|
128
|
+
act(() => {
|
|
129
|
+
state.profile = "offline";
|
|
130
|
+
state.listeners.forEach(listener => listener());
|
|
131
|
+
});
|
|
132
|
+
expect(status()).toBe("Blocked");
|
|
133
|
+
act(() => {
|
|
134
|
+
state.profile = "normal";
|
|
135
|
+
state.available = false;
|
|
136
|
+
state.listeners.forEach(listener => listener());
|
|
137
|
+
});
|
|
138
|
+
expect(status()).toBe("Unavailable");
|
|
139
|
+
expect(button("throttling-cycle").disabled).toBe(true);
|
|
140
|
+
});
|
|
141
|
+
it("keeps remote failures visible and does not claim a profile was applied", async () => {
|
|
142
|
+
const source = createRemoteConditionsSource();
|
|
143
|
+
const onChange = vi.fn().mockRejectedValue(new Error("Connection lost"));
|
|
144
|
+
source.update({
|
|
145
|
+
profile: "normal",
|
|
146
|
+
available: true,
|
|
147
|
+
onChange
|
|
148
|
+
});
|
|
149
|
+
render(source);
|
|
150
|
+
await choose("slow");
|
|
151
|
+
expect(container.textContent).toContain("Connection lost");
|
|
152
|
+
expect(status()).toBe("0 ms");
|
|
153
|
+
expect(state.set).not.toHaveBeenCalled();
|
|
154
|
+
});
|
|
155
|
+
it("prevents duplicate remote operations and follows confirmed snapshots", async () => {
|
|
156
|
+
const source = createRemoteConditionsSource();
|
|
157
|
+
let resolve;
|
|
158
|
+
const onChange = vi.fn(() => new Promise(done => {
|
|
159
|
+
resolve = done;
|
|
160
|
+
}));
|
|
161
|
+
source.update({
|
|
162
|
+
profile: "normal",
|
|
163
|
+
available: true,
|
|
164
|
+
onChange
|
|
165
|
+
});
|
|
166
|
+
render(source);
|
|
167
|
+
await tap("throttling-cycle");
|
|
168
|
+
await tap("throttling-cycle");
|
|
169
|
+
expect(onChange).toHaveBeenCalledOnce();
|
|
170
|
+
expect(status()).toBe("Applying…");
|
|
171
|
+
await act(async () => resolve());
|
|
172
|
+
expect(status()).toBe("0 ms");
|
|
173
|
+
act(() => source.update({
|
|
174
|
+
profile: "slow",
|
|
175
|
+
available: true,
|
|
176
|
+
onChange
|
|
177
|
+
}));
|
|
178
|
+
expect(status()).toBe("+500 ms");
|
|
179
|
+
act(() => source.update(undefined));
|
|
180
|
+
expect(status()).toBe("Unavailable");
|
|
181
|
+
});
|
|
182
|
+
it("ignores a previous device's completion after switching devices", async () => {
|
|
183
|
+
const source = createRemoteConditionsSource();
|
|
184
|
+
let reject;
|
|
185
|
+
source.update({
|
|
186
|
+
targetId: "first",
|
|
187
|
+
profile: "normal",
|
|
188
|
+
available: true,
|
|
189
|
+
onChange: () => new Promise((_resolve, fail) => {
|
|
190
|
+
reject = fail;
|
|
191
|
+
})
|
|
192
|
+
});
|
|
193
|
+
render(source);
|
|
194
|
+
await choose("slow");
|
|
195
|
+
act(() => source.update({
|
|
196
|
+
targetId: "second",
|
|
197
|
+
profile: "normal",
|
|
198
|
+
available: true,
|
|
199
|
+
onChange: vi.fn()
|
|
200
|
+
}));
|
|
201
|
+
await act(async () => reject(new Error("Old device failed")));
|
|
202
|
+
expect(container.textContent).not.toContain("Old device failed");
|
|
203
|
+
expect(status()).toBe("0 ms");
|
|
204
|
+
expect(button("throttling-profile").textContent).toBe("No throttling");
|
|
205
|
+
});
|
|
206
|
+
it("keeps the strip open when stopping a remote device fails", async () => {
|
|
207
|
+
const source = createRemoteConditionsSource();
|
|
208
|
+
source.update({
|
|
209
|
+
profile: "offline",
|
|
210
|
+
available: true,
|
|
211
|
+
onChange: vi.fn().mockRejectedValue(new Error("Could not stop"))
|
|
212
|
+
});
|
|
213
|
+
render(source);
|
|
214
|
+
await tap("throttling-close");
|
|
215
|
+
expect(dismiss).not.toHaveBeenCalled();
|
|
216
|
+
expect(status()).toBe("Blocked");
|
|
217
|
+
expect(container.textContent).toContain("Could not stop");
|
|
218
|
+
});
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
import { hasAccountAccess, subscribeAccountAccess } from "@buoy-gg/shared-ui";
|
|
4
|
+
import { addNetworkListener, startNetworkListener, getNetworkListenerCount, stopNetworkListener } from "../utils/networkListener";
|
|
5
|
+
export const NETWORK_CONDITIONS = {
|
|
6
|
+
normal: {
|
|
7
|
+
label: "Normal",
|
|
8
|
+
delayMs: 0
|
|
9
|
+
},
|
|
10
|
+
offline: {
|
|
11
|
+
label: "Offline (requests)",
|
|
12
|
+
delayMs: 0
|
|
13
|
+
},
|
|
14
|
+
slow: {
|
|
15
|
+
label: "Slow (+500 ms)",
|
|
16
|
+
delayMs: 500
|
|
17
|
+
},
|
|
18
|
+
verySlow: {
|
|
19
|
+
label: "Very slow (+2000 ms)",
|
|
20
|
+
delayMs: 2000
|
|
21
|
+
}
|
|
22
|
+
};
|
|
23
|
+
export const networkConditionsRuntimeId = `${Date.now()}-${Math.random().toString(36).slice(2)}`;
|
|
24
|
+
let profile = "normal";
|
|
25
|
+
let keepAlive;
|
|
26
|
+
const listeners = new Set();
|
|
27
|
+
export const conditionsCanApply = () => typeof __DEV__ !== "undefined" && __DEV__ && hasAccountAccess();
|
|
28
|
+
export const getNetworkCondition = () => profile;
|
|
29
|
+
export const subscribeNetworkConditions = listener => {
|
|
30
|
+
listeners.add(listener);
|
|
31
|
+
return () => {
|
|
32
|
+
listeners.delete(listener);
|
|
33
|
+
};
|
|
34
|
+
};
|
|
35
|
+
export function setNetworkCondition(value) {
|
|
36
|
+
if (typeof value !== "string" || !Object.prototype.hasOwnProperty.call(NETWORK_CONDITIONS, value)) {
|
|
37
|
+
throw new Error("Choose normal, offline, slow, or verySlow.");
|
|
38
|
+
}
|
|
39
|
+
if (value !== "normal" && !conditionsCanApply()) {
|
|
40
|
+
throw new Error("Network conditions require an admitted account and a development build.");
|
|
41
|
+
}
|
|
42
|
+
if (value === profile) return profile;
|
|
43
|
+
if (value !== "normal" && !keepAlive) {
|
|
44
|
+
startNetworkListener();
|
|
45
|
+
keepAlive = addNetworkListener(() => {});
|
|
46
|
+
} else if (value === "normal") {
|
|
47
|
+
keepAlive?.();
|
|
48
|
+
keepAlive = undefined;
|
|
49
|
+
if (getNetworkListenerCount() === 0) stopNetworkListener();
|
|
50
|
+
}
|
|
51
|
+
profile = value;
|
|
52
|
+
listeners.forEach(listener => listener());
|
|
53
|
+
return profile;
|
|
54
|
+
}
|
|
55
|
+
export function snapshotNetworkCondition(url) {
|
|
56
|
+
if (profile === "normal" || !conditionsCanApply() || !/^https?:\/\//i.test(url)) return;
|
|
57
|
+
return Object.freeze({
|
|
58
|
+
profile,
|
|
59
|
+
delayMs: NETWORK_CONDITIONS[profile].delayMs
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
subscribeAccountAccess(() => {
|
|
63
|
+
if (!hasAccountAccess()) setNetworkCondition("normal");
|
|
64
|
+
listeners.forEach(listener => listener());
|
|
65
|
+
});
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
export function throwIfAborted(signal) {
|
|
4
|
+
if (!signal?.aborted) return;
|
|
5
|
+
if (signal.reason !== undefined) throw signal.reason;
|
|
6
|
+
const error = new Error("The request was aborted.");
|
|
7
|
+
error.name = "AbortError";
|
|
8
|
+
throw error;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/** Wait before dispatch, releasing the timer and listener on cancellation. */
|
|
12
|
+
export function waitBeforeDispatch(ms, signal) {
|
|
13
|
+
return new Promise((resolve, reject) => {
|
|
14
|
+
try {
|
|
15
|
+
throwIfAborted(signal);
|
|
16
|
+
} catch (error) {
|
|
17
|
+
reject(error);
|
|
18
|
+
return;
|
|
19
|
+
}
|
|
20
|
+
const cleanup = () => signal?.removeEventListener("abort", abort);
|
|
21
|
+
const abort = () => {
|
|
22
|
+
clearTimeout(timer);
|
|
23
|
+
cleanup();
|
|
24
|
+
try {
|
|
25
|
+
throwIfAborted(signal);
|
|
26
|
+
} catch (error) {
|
|
27
|
+
reject(error);
|
|
28
|
+
}
|
|
29
|
+
};
|
|
30
|
+
const timer = setTimeout(() => {
|
|
31
|
+
cleanup();
|
|
32
|
+
resolve();
|
|
33
|
+
}, ms);
|
|
34
|
+
signal?.addEventListener("abort", abort, {
|
|
35
|
+
once: true
|
|
36
|
+
});
|
|
37
|
+
});
|
|
38
|
+
}
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
import { canSynthesize, completeNativeOverride } from "../overrides/applyToXhr";
|
|
4
|
+
/** Active simulation uses a send-to-completion deadline, including the wait. */
|
|
5
|
+
export function runConditionXhr(target, url, condition, outcome, dispatch) {
|
|
6
|
+
if (!canSynthesize(target)) throw new Error("Network conditions require React Native XMLHttpRequest.");
|
|
7
|
+
const xhr = target;
|
|
8
|
+
if (xhr.readyState !== 1 || xhr._sent) throw new Error("Request is not open or has already been sent.");
|
|
9
|
+
const started = Date.now();
|
|
10
|
+
let timeout = xhr.timeout;
|
|
11
|
+
let settled = false;
|
|
12
|
+
let dispatching = false;
|
|
13
|
+
let deadline;
|
|
14
|
+
let pending;
|
|
15
|
+
const originalAbort = xhr.abort;
|
|
16
|
+
const originalOpen = xhr.open;
|
|
17
|
+
const abortDescriptor = Object.getOwnPropertyDescriptor(xhr, "abort");
|
|
18
|
+
const openDescriptor = Object.getOwnPropertyDescriptor(xhr, "open");
|
|
19
|
+
const timeoutDescriptor = Object.getOwnPropertyDescriptor(xhr, "timeout");
|
|
20
|
+
const restore = (key, descriptor) => {
|
|
21
|
+
if (descriptor) Object.defineProperty(xhr, key, descriptor);else delete xhr[key];
|
|
22
|
+
};
|
|
23
|
+
const cleanup = () => {
|
|
24
|
+
if (settled) return;
|
|
25
|
+
settled = true;
|
|
26
|
+
clearTimeout(pending);
|
|
27
|
+
clearTimeout(deadline);
|
|
28
|
+
xhr.removeEventListener("loadend", cleanup);
|
|
29
|
+
restore("abort", abortDescriptor);
|
|
30
|
+
restore("open", openDescriptor);
|
|
31
|
+
restore("timeout", timeoutDescriptor);
|
|
32
|
+
xhr.timeout = timeout;
|
|
33
|
+
};
|
|
34
|
+
const fail = timedOut => {
|
|
35
|
+
if (settled) return;
|
|
36
|
+
// DONE suppresses abort events while the native transport is cancelled.
|
|
37
|
+
Object.assign(xhr, {
|
|
38
|
+
readyState: 4
|
|
39
|
+
});
|
|
40
|
+
originalAbort.call(xhr);
|
|
41
|
+
xhr._aborted = false;
|
|
42
|
+
xhr._hasError = true;
|
|
43
|
+
xhr._timedOut = timedOut;
|
|
44
|
+
xhr._response = timedOut ? "Request timed out (Buoy conditions)" : "Network request failed (Buoy Offline)";
|
|
45
|
+
xhr.setReadyState(4);
|
|
46
|
+
cleanup();
|
|
47
|
+
};
|
|
48
|
+
const expired = () => timeout > 0 && Date.now() - started >= timeout;
|
|
49
|
+
const scheduleDeadline = () => {
|
|
50
|
+
clearTimeout(deadline);
|
|
51
|
+
if (timeout > 0) deadline = setTimeout(() => fail(true), Math.max(0, timeout - (Date.now() - started)));
|
|
52
|
+
};
|
|
53
|
+
Object.defineProperty(xhr, "timeout", {
|
|
54
|
+
configurable: true,
|
|
55
|
+
get: () => dispatching ? 0 : timeout,
|
|
56
|
+
set: value => {
|
|
57
|
+
timeout = Math.max(0, Number(value) || 0);
|
|
58
|
+
scheduleDeadline();
|
|
59
|
+
}
|
|
60
|
+
});
|
|
61
|
+
Object.defineProperty(xhr, "abort", {
|
|
62
|
+
configurable: true,
|
|
63
|
+
value: () => {
|
|
64
|
+
if (!settled) {
|
|
65
|
+
originalAbort.call(xhr);
|
|
66
|
+
cleanup();
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
});
|
|
70
|
+
Object.defineProperty(xhr, "open", {
|
|
71
|
+
configurable: true,
|
|
72
|
+
value: (...args) => {
|
|
73
|
+
const result = Reflect.apply(originalOpen, xhr, args);
|
|
74
|
+
cleanup();
|
|
75
|
+
return result;
|
|
76
|
+
}
|
|
77
|
+
});
|
|
78
|
+
xhr._sent = true;
|
|
79
|
+
xhr._requestId = null;
|
|
80
|
+
xhr.addEventListener("loadend", cleanup);
|
|
81
|
+
scheduleDeadline();
|
|
82
|
+
pending = setTimeout(() => {
|
|
83
|
+
if (settled) return;
|
|
84
|
+
if (expired()) {
|
|
85
|
+
fail(true);
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
if (condition.profile === "offline") {
|
|
89
|
+
fail(false);
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
if (outcome && outcome.kind !== "delay") {
|
|
93
|
+
completeNativeOverride(xhr, url, outcome);
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
xhr._sent = false;
|
|
97
|
+
dispatching = true;
|
|
98
|
+
try {
|
|
99
|
+
dispatch();
|
|
100
|
+
} catch {
|
|
101
|
+
fail(false);
|
|
102
|
+
} finally {
|
|
103
|
+
dispatching = false;
|
|
104
|
+
}
|
|
105
|
+
}, condition.delayMs + (outcome?.delayMs ?? 0));
|
|
106
|
+
}
|
|
@@ -14,7 +14,7 @@ import { searchGraphQLVariables } from "../utils/formatGraphQLVariables";
|
|
|
14
14
|
|
|
15
15
|
/**
|
|
16
16
|
* @deprecated Read the live limit with `useLimit("network.requests")` — this
|
|
17
|
-
* only reports the FREE tier's value and
|
|
17
|
+
* only reports the FREE tier's value and ignores the Weekend Pass. Kept
|
|
18
18
|
* because it is part of the package's public exports.
|
|
19
19
|
*/
|
|
20
20
|
export const FREE_TIER_REQUEST_LIMIT = TIER_LIMITS["network.requests"].free;
|
|
@@ -400,16 +400,12 @@ export function runOverride(xhr, url, outcome, passThrough) {
|
|
|
400
400
|
// had already aborted. Claiming it means abort() dispatches DONE + 'abort'
|
|
401
401
|
// and whatwg-fetch rejects with AbortError, as it should.
|
|
402
402
|
internals._sent = true;
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
// `send()` — which we're skipping — so it would survive here, and
|
|
407
|
-
// `abort()` would hand that string to `RCTNetworking.abortRequest()`.
|
|
408
|
-
// Nulling it makes abort a clean no-op on the native side.
|
|
409
|
-
internals._requestId = null;
|
|
410
|
-
}
|
|
403
|
+
|
|
404
|
+
// There is no native request to cancel until a delayed send is dispatched.
|
|
405
|
+
internals._requestId = null;
|
|
411
406
|
let timer = null;
|
|
412
407
|
const onAbort = () => {
|
|
408
|
+
xhr.removeEventListener("abort", onAbort);
|
|
413
409
|
if (timer !== null) {
|
|
414
410
|
clearTimeout(timer);
|
|
415
411
|
timer = null;
|
|
@@ -442,4 +438,9 @@ export function runOverride(xhr, url, outcome, passThrough) {
|
|
|
442
438
|
// 'load' before they exist.
|
|
443
439
|
timer = setTimeout(fire, outcome.delayMs);
|
|
444
440
|
return true;
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
/** Complete an already scheduled native outcome without adding another timer. */
|
|
444
|
+
export function completeNativeOverride(xhr, url, outcome) {
|
|
445
|
+
if (outcome.kind === "fail") fail(xhr, outcome);else if (outcome.kind === "respond") respond(xhr, url, outcome);
|
|
445
446
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
|
|
3
|
+
import { hasAccountAccess } from "@buoy-gg/shared-ui";
|
|
3
4
|
/**
|
|
4
5
|
* The seam between the interceptor and the rule store.
|
|
5
6
|
*
|
|
@@ -34,7 +35,7 @@ export function setOverrideHooks(next) {
|
|
|
34
35
|
export function overridesCanApply() {
|
|
35
36
|
// `__DEV__` is undefined outside React Native (unit tests, the desktop
|
|
36
37
|
// bundle); only an explicit `false` — a real release build — disables it.
|
|
37
|
-
return !(typeof __DEV__ !== "undefined" && !__DEV__);
|
|
38
|
+
return hasAccountAccess() && !(typeof __DEV__ !== "undefined" && !__DEV__);
|
|
38
39
|
}
|
|
39
40
|
|
|
40
41
|
/** Why overrides are inert, for a caller that has to explain it to a person. */
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
|
|
3
|
+
import { getNetworkCondition, setNetworkCondition, subscribeNetworkConditions, conditionsCanApply, networkConditionsRuntimeId } from "../conditions/store";
|
|
3
4
|
import { networkEventStore } from "../utils/networkEventStore";
|
|
4
5
|
import { flagsForEventId, networkSavedStore } from "../utils/networkSavedStore";
|
|
5
6
|
import { overrideRulesStore } from "../overrides/overrideRulesStore";
|
|
@@ -474,10 +475,13 @@ function baseFromRequest(params) {
|
|
|
474
475
|
* `NetworkPanel`/`NetworkInline` accept either shape.
|
|
475
476
|
*/
|
|
476
477
|
export const networkSyncAdapter = {
|
|
478
|
+
dispose: () => {
|
|
479
|
+
overrideRulesStore.setEnabled(false);
|
|
480
|
+
},
|
|
477
481
|
// v5: honest request/responseSize + explicit request/responseBodyOmitted
|
|
478
482
|
// flags, and a total snapshot budget so the list degrades instead of the
|
|
479
483
|
// whole panel being dropped at the emit layer.
|
|
480
|
-
version:
|
|
484
|
+
version: 6,
|
|
481
485
|
getSnapshot: () => {
|
|
482
486
|
// Piggyback the interception health check on the watch loop: while a
|
|
483
487
|
// dashboard/MCP watch is open this runs continuously, so a clobbered
|
|
@@ -486,6 +490,12 @@ export const networkSyncAdapter = {
|
|
|
486
490
|
// reads when healthy.
|
|
487
491
|
networkEventStore.ensureInterception();
|
|
488
492
|
return {
|
|
493
|
+
conditions: {
|
|
494
|
+
version: 1,
|
|
495
|
+
runtimeId: networkConditionsRuntimeId,
|
|
496
|
+
profile: getNetworkCondition(),
|
|
497
|
+
available: conditionsCanApply()
|
|
498
|
+
},
|
|
489
499
|
events: fitEventsToBudget(networkEventStore.getEvents()),
|
|
490
500
|
/**
|
|
491
501
|
* Response-override rules live on the DEVICE — that's the only place they
|
|
@@ -506,17 +516,32 @@ export const networkSyncAdapter = {
|
|
|
506
516
|
// Pinning with no traffic in flight still has to reach the dashboard, so
|
|
507
517
|
// both stores drive the snapshot.
|
|
508
518
|
const unsubscribeEvents = networkEventStore.subscribeToEvents(onChange);
|
|
519
|
+
const unsubscribeConditions = subscribeNetworkConditions(onChange);
|
|
509
520
|
const unsubscribeSaved = networkSavedStore.subscribe(onChange);
|
|
510
521
|
// Same reasoning for rules: editing one with no traffic in flight must
|
|
511
522
|
// still reach the dashboard. Subscribing also hydrates the rule list.
|
|
512
523
|
const unsubscribeOverrides = overrideRulesStore.subscribe(onChange);
|
|
513
524
|
return () => {
|
|
525
|
+
unsubscribeConditions();
|
|
514
526
|
unsubscribeEvents();
|
|
515
527
|
unsubscribeSaved();
|
|
516
528
|
unsubscribeOverrides();
|
|
517
529
|
};
|
|
518
530
|
},
|
|
519
531
|
actions: {
|
|
532
|
+
getNetworkConditions: () => ({
|
|
533
|
+
runtimeId: networkConditionsRuntimeId,
|
|
534
|
+
profile: getNetworkCondition(),
|
|
535
|
+
available: conditionsCanApply()
|
|
536
|
+
}),
|
|
537
|
+
setNetworkConditions: params => {
|
|
538
|
+
const profile = params && typeof params === "object" ? params.profile : undefined;
|
|
539
|
+
return {
|
|
540
|
+
runtimeId: networkConditionsRuntimeId,
|
|
541
|
+
profile: setNetworkCondition(profile),
|
|
542
|
+
available: conditionsCanApply()
|
|
543
|
+
};
|
|
544
|
+
},
|
|
520
545
|
clearEvents: () => {
|
|
521
546
|
networkEventStore.clearEvents();
|
|
522
547
|
},
|
|
@@ -77,6 +77,7 @@ export function generateNetworkCopyText(events, copySettings) {
|
|
|
77
77
|
obj.statusText = event.statusText;
|
|
78
78
|
}
|
|
79
79
|
if (copySettings.includeDuration) obj.duration = event.duration;
|
|
80
|
+
if (event.condition) obj.condition = event.condition;
|
|
80
81
|
if (copySettings.includeTimestamp) obj.timestamp = event.timestamp;
|
|
81
82
|
if (copySettings.includeClient) obj.requestClient = event.requestClient;
|
|
82
83
|
if (copySettings.includeSizes) {
|
|
@@ -115,6 +116,7 @@ export function generateNetworkCopyText(events, copySettings) {
|
|
|
115
116
|
if (copySettings.includeClient && event.requestClient) {
|
|
116
117
|
md += `**Client:** ${event.requestClient}\n`;
|
|
117
118
|
}
|
|
119
|
+
if (event.condition) md += `Conditions: ${event.condition.profile} (+${event.condition.delayMs} ms)\n`;
|
|
118
120
|
if (copySettings.includeDuration && event.duration) {
|
|
119
121
|
md += `**Duration:** ${event.duration}ms\n`;
|
|
120
122
|
}
|