@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.
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 +26 -1
  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 +96 -51
  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 +26 -1
  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 +97 -52
  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 +17 -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 +2 -1
  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
@@ -0,0 +1,231 @@
1
+ "use strict";
2
+
3
+ import { useEffect, useRef, useState, useSyncExternalStore } from "react";
4
+ import { ActivityIndicator, StyleSheet, Text, TouchableOpacity, View } from "react-native";
5
+ import { ChevronDown, Check, night } from "@buoy-gg/shared-ui";
6
+ import { conditionsCanApply, getNetworkCondition, setNetworkCondition, subscribeNetworkConditions } from "./store";
7
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
8
+ const options = [{
9
+ profile: "normal",
10
+ label: "No throttling"
11
+ }, {
12
+ profile: "slow",
13
+ label: "Slow"
14
+ }, {
15
+ profile: "verySlow",
16
+ label: "Very slow"
17
+ }, {
18
+ profile: "offline",
19
+ label: "Offline"
20
+ }];
21
+ const summaries = {
22
+ normal: "No added delay",
23
+ slow: "+500 ms before each new request",
24
+ verySlow: "+2 seconds before each new request",
25
+ offline: "New requests fail before sending"
26
+ };
27
+ export function NetworkConditionsControl({
28
+ remote
29
+ }) {
30
+ const local = useSyncExternalStore(subscribeNetworkConditions, getNetworkCondition, getNetworkCondition);
31
+ const localAvailable = useSyncExternalStore(subscribeNetworkConditions, conditionsCanApply, conditionsCanApply);
32
+ const [expanded, setExpanded] = useState(false);
33
+ const [error, setError] = useState();
34
+ const [pending, setPending] = useState(false);
35
+ const busy = useRef(false);
36
+ const mounted = useRef(true);
37
+ useEffect(() => {
38
+ mounted.current = true;
39
+ return () => {
40
+ mounted.current = false;
41
+ };
42
+ }, []);
43
+ const profile = remote ? remote.profile : local;
44
+ const available = remote ? remote.available : localAvailable;
45
+ const active = profile !== null && profile !== "normal";
46
+ const tint = profile === "offline" ? night.color.danger : active ? night.color.warning : night.color.accent;
47
+ async function select(value) {
48
+ if (!available || busy.current) return;
49
+ if (value === profile) {
50
+ setExpanded(false);
51
+ return;
52
+ }
53
+ busy.current = true;
54
+ setPending(true);
55
+ setError(undefined);
56
+ try {
57
+ if (remote) await remote.onChange(value);else setNetworkCondition(value);
58
+ if (mounted.current) setExpanded(false);
59
+ } catch (failure) {
60
+ if (mounted.current) setError(failure instanceof Error ? failure.message : String(failure));
61
+ } finally {
62
+ busy.current = false;
63
+ if (mounted.current) setPending(false);
64
+ }
65
+ }
66
+ const label = options.find(option => option.profile === profile)?.label ?? "Unavailable";
67
+ return /*#__PURE__*/_jsxs(View, {
68
+ style: styles.container,
69
+ testID: "buoy-network-throttling-screen",
70
+ children: [/*#__PURE__*/_jsx(Text, {
71
+ style: styles.heading,
72
+ children: "Connection profile"
73
+ }), /*#__PURE__*/_jsxs(TouchableOpacity, {
74
+ testID: "buoy-network-conditions",
75
+ accessibilityRole: "button",
76
+ accessibilityLabel: `Network throttling: ${label}`,
77
+ accessibilityState: {
78
+ expanded,
79
+ disabled: !available || pending
80
+ },
81
+ disabled: !available || pending,
82
+ onPress: () => setExpanded(value => !value),
83
+ style: [styles.dropdown, expanded && styles.dropdownOpen, !available && styles.disabled],
84
+ children: [/*#__PURE__*/_jsx(Text, {
85
+ style: [styles.value, active && {
86
+ color: tint
87
+ }],
88
+ children: label
89
+ }), pending ? /*#__PURE__*/_jsx(ActivityIndicator, {
90
+ size: "small",
91
+ color: night.color.textSecondary
92
+ }) : /*#__PURE__*/_jsx(ChevronDown, {
93
+ size: 16,
94
+ color: night.color.textSecondary
95
+ })]
96
+ }), expanded && /*#__PURE__*/_jsx(View, {
97
+ accessibilityRole: "radiogroup",
98
+ accessibilityLabel: "Connection profile",
99
+ style: styles.options,
100
+ children: options.map(option => {
101
+ const selected = profile === option.profile;
102
+ return /*#__PURE__*/_jsxs(TouchableOpacity, {
103
+ testID: `buoy-network-condition-${option.profile}`,
104
+ accessibilityRole: "radio",
105
+ accessibilityLabel: option.label,
106
+ accessibilityState: {
107
+ checked: selected,
108
+ disabled: !available || pending
109
+ },
110
+ disabled: !available || pending,
111
+ onPress: () => void select(option.profile),
112
+ style: [styles.option, selected && styles.selected],
113
+ children: [/*#__PURE__*/_jsxs(View, {
114
+ style: styles.optionBody,
115
+ children: [/*#__PURE__*/_jsx(Text, {
116
+ style: [styles.optionTitle, selected && {
117
+ color: tint
118
+ }],
119
+ children: option.label
120
+ }), /*#__PURE__*/_jsx(Text, {
121
+ style: styles.caption,
122
+ children: summaries[option.profile]
123
+ })]
124
+ }), selected && /*#__PURE__*/_jsx(Check, {
125
+ size: 16,
126
+ color: tint
127
+ })]
128
+ }, option.profile);
129
+ })
130
+ }), /*#__PURE__*/_jsx(Text, {
131
+ style: styles.caption,
132
+ accessibilityLiveRegion: "polite",
133
+ children: pending ? "Applying to device…" : !available ? remote ? "Conditions unavailable on this device" : "Requires a signed-in account and a development build" : profile ? summaries[profile] : "Waiting for device status"
134
+ }), !!error && /*#__PURE__*/_jsx(Text, {
135
+ accessibilityRole: "alert",
136
+ style: styles.error,
137
+ children: error
138
+ }), /*#__PURE__*/_jsxs(View, {
139
+ style: styles.help,
140
+ children: [/*#__PURE__*/_jsx(Text, {
141
+ style: styles.heading,
142
+ children: "How it works"
143
+ }), /*#__PURE__*/_jsx(Text, {
144
+ style: styles.caption,
145
+ children: "Affects new HTTP requests through global fetch and XHR. Slow adds a wait before sending; download speed stays unchanged."
146
+ }), /*#__PURE__*/_jsx(Text, {
147
+ style: styles.caption,
148
+ children: "Stays active when you close this tool or pause capture. Choose No throttling to stop. Reloading the app resets it."
149
+ })]
150
+ })]
151
+ });
152
+ }
153
+ const styles = StyleSheet.create({
154
+ container: {
155
+ padding: night.gutter,
156
+ gap: 10
157
+ },
158
+ heading: {
159
+ fontSize: night.font.caption,
160
+ fontWeight: "600",
161
+ color: night.color.textSecondary
162
+ },
163
+ dropdown: {
164
+ flexDirection: "row",
165
+ alignItems: "center",
166
+ justifyContent: "space-between",
167
+ gap: 12,
168
+ minHeight: 44,
169
+ paddingHorizontal: 12,
170
+ paddingVertical: 10,
171
+ borderRadius: night.radius.segment,
172
+ borderWidth: 1,
173
+ borderColor: night.color.separator,
174
+ backgroundColor: night.color.surfaceElevated
175
+ },
176
+ dropdownOpen: {
177
+ borderColor: night.color.accentBorderStrong
178
+ },
179
+ value: {
180
+ flex: 1,
181
+ fontSize: night.font.body,
182
+ color: night.color.text
183
+ },
184
+ options: {
185
+ padding: 4,
186
+ borderRadius: night.radius.row,
187
+ borderWidth: 1,
188
+ borderColor: night.color.border,
189
+ backgroundColor: night.color.surface
190
+ },
191
+ option: {
192
+ flexDirection: "row",
193
+ alignItems: "center",
194
+ gap: 12,
195
+ minHeight: 54,
196
+ padding: 10,
197
+ borderRadius: night.radius.segment
198
+ },
199
+ optionBody: {
200
+ flex: 1,
201
+ gap: 4
202
+ },
203
+ optionTitle: {
204
+ fontSize: night.font.caption,
205
+ fontWeight: "600",
206
+ color: night.color.text
207
+ },
208
+ selected: {
209
+ backgroundColor: night.color.surfaceElevated
210
+ },
211
+ disabled: {
212
+ opacity: 0.45
213
+ },
214
+ caption: {
215
+ fontSize: night.font.caption,
216
+ lineHeight: 19,
217
+ color: night.color.textSecondary
218
+ },
219
+ help: {
220
+ marginTop: 14,
221
+ paddingTop: 16,
222
+ gap: 10,
223
+ borderTopWidth: 1,
224
+ borderTopColor: night.color.separator
225
+ },
226
+ error: {
227
+ fontSize: night.font.caption,
228
+ lineHeight: 18,
229
+ color: night.color.danger
230
+ }
231
+ });
@@ -0,0 +1,135 @@
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 { NetworkConditionsControl } from "./NetworkConditionsControl";
8
+ vi.mock("@buoy-gg/shared-ui", async () => ({
9
+ ChevronDown: () => null,
10
+ Check: () => null,
11
+ night: (await vi.importActual("../../../../shared/src/ui/night/nightTheme")).night
12
+ }));
13
+ vi.mock("react-native", () => ({
14
+ StyleSheet: {
15
+ create: value => value
16
+ },
17
+ View: ({
18
+ children
19
+ }) => /*#__PURE__*/createElement("div", null, children),
20
+ Text: ({
21
+ children
22
+ }) => /*#__PURE__*/createElement("span", null, children),
23
+ ActivityIndicator: () => /*#__PURE__*/createElement("span", null, "pending"),
24
+ TouchableOpacity: ({
25
+ children,
26
+ onPress,
27
+ disabled,
28
+ accessibilityLabel,
29
+ accessibilityState,
30
+ testID
31
+ }) => /*#__PURE__*/createElement("button", {
32
+ onClick: onPress,
33
+ disabled,
34
+ "aria-label": accessibilityLabel,
35
+ "aria-checked": accessibilityState?.checked,
36
+ "data-testid": testID
37
+ }, children)
38
+ }));
39
+ vi.mock("./store", () => ({
40
+ NETWORK_CONDITIONS: {
41
+ normal: {
42
+ label: "Normal"
43
+ },
44
+ slow: {
45
+ label: "Slow"
46
+ },
47
+ verySlow: {
48
+ label: "Very slow"
49
+ },
50
+ offline: {
51
+ label: "Offline"
52
+ }
53
+ },
54
+ getNetworkCondition: () => "normal",
55
+ conditionsCanApply: () => true,
56
+ subscribeNetworkConditions: () => () => {},
57
+ setNetworkCondition: vi.fn()
58
+ }));
59
+ let root;
60
+ let container;
61
+ beforeEach(() => {
62
+ globalThis.IS_REACT_ACT_ENVIRONMENT = true;
63
+ container = document.createElement("div");
64
+ root = createRoot(container);
65
+ });
66
+ afterEach(() => act(() => root.unmount()));
67
+ const button = profile => container.querySelector(`[data-testid="buoy-network-condition-${profile}"]`);
68
+ function render(remote) {
69
+ act(() => root.render(/*#__PURE__*/createElement(NetworkConditionsControl, {
70
+ remote
71
+ })));
72
+ }
73
+ const dropdown = () => container.querySelector('[data-testid="buoy-network-conditions"]');
74
+ const open = () => act(() => dropdown().click());
75
+ it("defaults to a closed No throttling dropdown and closes when the current option is chosen", () => {
76
+ const onChange = vi.fn();
77
+ render({
78
+ profile: "normal",
79
+ available: true,
80
+ onChange
81
+ });
82
+ expect(dropdown().textContent).toBe("No throttling");
83
+ expect(button("normal")).toBeNull();
84
+ open();
85
+ expect(["normal", "slow", "verySlow", "offline"].every(value => !!button(value))).toBe(true);
86
+ act(() => button("normal").click());
87
+ expect(button("normal")).toBeNull();
88
+ expect(onChange).not.toHaveBeenCalled();
89
+ });
90
+ it("blocks duplicate remote changes and waits for the device profile before selecting it", async () => {
91
+ let finish;
92
+ const onChange = vi.fn(() => new Promise(resolve => {
93
+ finish = resolve;
94
+ }));
95
+ render({
96
+ profile: "normal",
97
+ available: true,
98
+ onChange
99
+ });
100
+ open();
101
+ act(() => button("slow").click());
102
+ expect(button("offline").disabled).toBe(true);
103
+ expect(button("normal").getAttribute("aria-checked")).toBe("true");
104
+ act(() => button("offline").click());
105
+ expect(onChange).toHaveBeenCalledExactlyOnceWith("slow");
106
+ await act(async () => finish());
107
+ render({
108
+ profile: "slow",
109
+ available: true,
110
+ onChange
111
+ });
112
+ expect(dropdown().textContent).toBe("Slow");
113
+ expect(button("slow")).toBeNull();
114
+ open();
115
+ expect(button("slow").getAttribute("aria-checked")).toBe("true");
116
+ });
117
+ it("reports a remote error, permits retry, and disables unavailable devices", async () => {
118
+ const onChange = vi.fn().mockRejectedValue(new Error("Device disconnected"));
119
+ render({
120
+ profile: "normal",
121
+ available: true,
122
+ onChange
123
+ });
124
+ open();
125
+ await act(async () => button("offline").click());
126
+ expect(container.textContent).toContain("Device disconnected");
127
+ expect(button("offline").disabled).toBe(false);
128
+ render({
129
+ profile: null,
130
+ available: false,
131
+ onChange
132
+ });
133
+ expect(button("normal").disabled).toBe(true);
134
+ expect(container.textContent).toContain("Conditions unavailable on this device");
135
+ });
@@ -0,0 +1,29 @@
1
+ "use strict";
2
+
3
+ import { useSyncExternalStore } from "react";
4
+ import { Text, View } from "react-native";
5
+ import { NetworkIcon } from "@buoy-gg/shared-ui";
6
+ import { getNetworkCondition, subscribeNetworkConditions, NETWORK_CONDITIONS } from "./store";
7
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
8
+ export function NetworkConditionsIcon({
9
+ size
10
+ }) {
11
+ const profile = useSyncExternalStore(subscribeNetworkConditions, getNetworkCondition, getNetworkCondition);
12
+ return /*#__PURE__*/_jsxs(View, {
13
+ accessibilityLabel: profile === "normal" ? "Network" : `Network: ${NETWORK_CONDITIONS[profile].label}`,
14
+ children: [/*#__PURE__*/_jsx(NetworkIcon, {
15
+ size: size
16
+ }), profile !== "normal" && /*#__PURE__*/_jsx(Text, {
17
+ testID: "buoy-network-condition-indicator",
18
+ style: {
19
+ position: "absolute",
20
+ top: -6,
21
+ right: -8,
22
+ color: "#FFCC66",
23
+ fontSize: 10,
24
+ fontWeight: "700"
25
+ },
26
+ children: profile === "offline" ? "OFF" : `+${NETWORK_CONDITIONS[profile].delayMs}`
27
+ })]
28
+ });
29
+ }
@@ -0,0 +1,304 @@
1
+ "use strict";
2
+
3
+ import { useEffect, useRef, useState, useSyncExternalStore } from "react";
4
+ import { ActivityIndicator, StyleSheet, Text, TouchableOpacity, View, useWindowDimensions } from "react-native";
5
+ import { FloatingTool, night, openFloatingTool, persistentStorage, registerPersistentFloatingTool } from "@buoy-gg/shared-ui";
6
+ import { conditionsCanApply, getNetworkCondition, setNetworkCondition, subscribeNetworkConditions } from "./store";
7
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
8
+ /** A live source keeps the desktop strip current after its launching panel closes. */
9
+ export function createRemoteConditionsSource() {
10
+ let value;
11
+ const listeners = new Set();
12
+ return {
13
+ getSnapshot: () => value,
14
+ subscribe: listener => {
15
+ listeners.add(listener);
16
+ return () => {
17
+ listeners.delete(listener);
18
+ };
19
+ },
20
+ update(next) {
21
+ value = next;
22
+ listeners.forEach(listener => listener());
23
+ }
24
+ };
25
+ }
26
+ const noSubscribe = () => () => {};
27
+ const noRemote = () => undefined;
28
+ const options = [{
29
+ id: "normal",
30
+ label: "No throttling",
31
+ detail: "No added delay"
32
+ }, {
33
+ id: "slow",
34
+ label: "Slow",
35
+ detail: "+500 ms before sending"
36
+ }, {
37
+ id: "verySlow",
38
+ label: "Very slow",
39
+ detail: "+2 seconds before sending"
40
+ }, {
41
+ id: "offline",
42
+ label: "Offline",
43
+ detail: "New requests fail"
44
+ }];
45
+ const localRegistration = {
46
+ id: "network-throttling",
47
+ render: context => /*#__PURE__*/_jsx(NetworkFloatingTool, {
48
+ ...context
49
+ })
50
+ };
51
+ if (typeof __DEV__ !== "undefined" && __DEV__ && typeof navigator !== "undefined" && navigator.product === "ReactNative") {
52
+ registerPersistentFloatingTool(localRegistration, persistentStorage);
53
+ }
54
+ export function openNetworkFloatingTool(source) {
55
+ openFloatingTool({
56
+ id: "network-throttling",
57
+ render: context => /*#__PURE__*/_jsx(NetworkFloatingTool, {
58
+ ...context,
59
+ source: source
60
+ })
61
+ });
62
+ }
63
+ export function NetworkFloatingTool({
64
+ id,
65
+ revealToken,
66
+ dismiss,
67
+ source
68
+ }) {
69
+ const local = useSyncExternalStore(subscribeNetworkConditions, getNetworkCondition, getNetworkCondition);
70
+ const localAvailable = useSyncExternalStore(subscribeNetworkConditions, conditionsCanApply, conditionsCanApply);
71
+ const remote = useSyncExternalStore(source?.subscribe ?? noSubscribe, source?.getSnapshot ?? noRemote, noRemote);
72
+ const profile = source ? remote?.profile ?? null : local;
73
+ const available = source ? remote?.available === true : localAvailable;
74
+ const [pending, setPending] = useState(false);
75
+ const [error, setError] = useState();
76
+ const busy = useRef(false);
77
+ const operation = useRef(0);
78
+ const mounted = useRef(true);
79
+ const {
80
+ width
81
+ } = useWindowDimensions();
82
+ useEffect(() => {
83
+ mounted.current = true;
84
+ return () => {
85
+ mounted.current = false;
86
+ };
87
+ }, []);
88
+ useEffect(() => {
89
+ operation.current += 1;
90
+ busy.current = false;
91
+ setPending(false);
92
+ setError(undefined);
93
+ }, [source, remote?.targetId]);
94
+ const active = profile !== null && profile !== "normal";
95
+ const selected = profile ?? "normal";
96
+ const choice = options.find(option => option.id === selected);
97
+ const next = options[(options.findIndex(option => option.id === selected) + 1) % options.length];
98
+ const bars = !available || selected === "offline" ? 0 : selected === "verySlow" ? 1 : selected === "slow" ? 2 : 4;
99
+ const detail = pending ? "Applying…" : !available ? "Unavailable" : selected === "normal" ? "0 ms" : selected === "slow" ? "+500 ms" : selected === "verySlow" ? "+2000 ms" : "Blocked";
100
+ async function apply(next, close = false) {
101
+ if (busy.current) return;
102
+ const currentOperation = ++operation.current;
103
+ busy.current = true;
104
+ setPending(true);
105
+ setError(undefined);
106
+ const startingRemote = source?.getSnapshot();
107
+ try {
108
+ if (source) {
109
+ if (!startingRemote?.available) throw new Error("Device unavailable. Open Network again to reconnect these controls.");
110
+ await startingRemote.onChange(next);
111
+ } else setNetworkCondition(next);
112
+ if (mounted.current && operation.current === currentOperation) {
113
+ if (close) dismiss();
114
+ }
115
+ } catch (failure) {
116
+ if (mounted.current && operation.current === currentOperation) setError(failure instanceof Error ? failure.message : String(failure));
117
+ } finally {
118
+ if (operation.current === currentOperation) {
119
+ busy.current = false;
120
+ if (mounted.current) setPending(false);
121
+ }
122
+ }
123
+ }
124
+ const accessory = error ? /*#__PURE__*/_jsx(View, {
125
+ style: styles.menu,
126
+ children: /*#__PURE__*/_jsx(Text, {
127
+ accessibilityRole: "alert",
128
+ style: styles.error,
129
+ children: error
130
+ })
131
+ }) : undefined;
132
+ return /*#__PURE__*/_jsx(FloatingTool, {
133
+ id: id,
134
+ revealToken: revealToken,
135
+ variant: "strip",
136
+ dragFromSurface: true,
137
+ accessibilityLabel: `Move or hide network throttling. ${active ? "Active" : "Off"}`,
138
+ accessory: accessory,
139
+ children: /*#__PURE__*/_jsxs(View, {
140
+ style: [styles.row, {
141
+ width: Math.max(220, Math.min(292, width - 64))
142
+ }],
143
+ children: [/*#__PURE__*/_jsx(TouchableOpacity, {
144
+ testID: "throttling-cycle",
145
+ accessibilityRole: "button",
146
+ accessibilityLabel: `Network: ${choice.label}. Switch to ${next.label}`,
147
+ accessibilityState: {
148
+ disabled: pending || !available
149
+ },
150
+ disabled: pending || !available,
151
+ onPress: () => void apply(next.id),
152
+ style: styles.signalButton,
153
+ children: pending ? /*#__PURE__*/_jsx(ActivityIndicator, {
154
+ size: "small",
155
+ color: night.color.accent
156
+ }) : /*#__PURE__*/_jsxs(View, {
157
+ testID: `throttling-signal-${selected}`,
158
+ style: styles.signal,
159
+ children: [[7, 12, 18, 24].map((height, index) => /*#__PURE__*/_jsx(View, {
160
+ style: {
161
+ width: 4,
162
+ height,
163
+ borderRadius: 2,
164
+ backgroundColor: index < bars ? selected === "offline" ? night.color.danger : night.color.accent : night.color.fillTertiary
165
+ }
166
+ }, height)), selected === "offline" && /*#__PURE__*/_jsx(View, {
167
+ style: styles.slash
168
+ })]
169
+ })
170
+ }), /*#__PURE__*/_jsx(View, {
171
+ style: styles.profile,
172
+ children: /*#__PURE__*/_jsx(Text, {
173
+ testID: "throttling-profile",
174
+ accessibilityLabel: `Connection profile: ${choice.label}`,
175
+ accessibilityLiveRegion: "polite",
176
+ numberOfLines: 1,
177
+ style: styles.profileLabel,
178
+ children: choice.label
179
+ })
180
+ }), /*#__PURE__*/_jsx(Text, {
181
+ testID: "throttling-status",
182
+ numberOfLines: 1,
183
+ accessibilityLiveRegion: "polite",
184
+ style: styles.status,
185
+ children: detail
186
+ }), /*#__PURE__*/_jsx(View, {
187
+ style: styles.separator
188
+ }), /*#__PURE__*/_jsx(TouchableOpacity, {
189
+ testID: "throttling-close",
190
+ accessibilityRole: "button",
191
+ accessibilityLabel: "Close and turn off throttling",
192
+ disabled: pending,
193
+ accessibilityState: {
194
+ disabled: pending
195
+ },
196
+ onPress: () => {
197
+ if (profile === "normal") dismiss();else void apply("normal", true);
198
+ },
199
+ style: [styles.action, pending && styles.disabled],
200
+ children: /*#__PURE__*/_jsx(Text, {
201
+ style: styles.actionText,
202
+ children: "Close"
203
+ })
204
+ })]
205
+ })
206
+ });
207
+ }
208
+ const styles = StyleSheet.create({
209
+ row: {
210
+ flexDirection: "row",
211
+ alignItems: "center",
212
+ gap: 6,
213
+ paddingVertical: 4,
214
+ minHeight: 48
215
+ },
216
+ signalButton: {
217
+ width: 36,
218
+ minHeight: 44,
219
+ alignItems: "center",
220
+ justifyContent: "center"
221
+ },
222
+ signal: {
223
+ flexDirection: "row",
224
+ alignItems: "flex-end",
225
+ gap: 3,
226
+ height: 24
227
+ },
228
+ slash: {
229
+ position: "absolute",
230
+ width: 30,
231
+ height: 2,
232
+ backgroundColor: night.color.danger,
233
+ top: 11,
234
+ left: -2,
235
+ transform: [{
236
+ rotate: "-45deg"
237
+ }]
238
+ },
239
+ separator: {
240
+ height: 20,
241
+ width: 1,
242
+ backgroundColor: night.color.separator,
243
+ marginHorizontal: 2
244
+ },
245
+ status: {
246
+ color: night.color.textSecondary,
247
+ fontSize: 10,
248
+ lineHeight: 15,
249
+ flexShrink: 1
250
+ },
251
+ profile: {
252
+ flex: 1,
253
+ flexDirection: "row",
254
+ alignItems: "center",
255
+ gap: 6,
256
+ minHeight: 44
257
+ },
258
+ profileLabel: {
259
+ color: night.color.text,
260
+ fontSize: 13,
261
+ fontWeight: "500",
262
+ flexShrink: 1
263
+ },
264
+ action: {
265
+ minHeight: 36,
266
+ minWidth: 52,
267
+ borderRadius: night.radius.input,
268
+ borderWidth: 1,
269
+ borderColor: night.color.accentBorderStrong,
270
+ alignItems: "center",
271
+ justifyContent: "center",
272
+ paddingHorizontal: 10
273
+ },
274
+ actionText: {
275
+ color: night.color.accent,
276
+ fontSize: 12,
277
+ fontWeight: "600"
278
+ },
279
+ disabled: {
280
+ opacity: 0.4
281
+ },
282
+ menu: {
283
+ marginVertical: 8,
284
+ padding: 6,
285
+ borderRadius: night.radius.card,
286
+ borderWidth: 1,
287
+ borderColor: night.color.separator,
288
+ backgroundColor: night.color.surface,
289
+ shadowColor: "#000",
290
+ shadowOffset: {
291
+ width: 0,
292
+ height: 6
293
+ },
294
+ shadowOpacity: 0.25,
295
+ shadowRadius: 16,
296
+ elevation: 8
297
+ },
298
+ error: {
299
+ color: night.color.danger,
300
+ fontSize: 12,
301
+ lineHeight: 18,
302
+ padding: 10
303
+ }
304
+ });