@buoy-gg/network 7.0.35 → 7.0.38
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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 +224 -3
- 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 +245 -46
- 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 +225 -3
- 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 +246 -47
- 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 +29 -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 +13 -28
- 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,75 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
Object.defineProperty(exports, "__esModule", {
|
|
4
|
+
value: true
|
|
5
|
+
});
|
|
6
|
+
exports.networkConditionsRuntimeId = exports.getNetworkCondition = exports.conditionsCanApply = exports.NETWORK_CONDITIONS = void 0;
|
|
7
|
+
exports.setNetworkCondition = setNetworkCondition;
|
|
8
|
+
exports.snapshotNetworkCondition = snapshotNetworkCondition;
|
|
9
|
+
exports.subscribeNetworkConditions = void 0;
|
|
10
|
+
var _sharedUi = require("@buoy-gg/shared-ui");
|
|
11
|
+
var _networkListener = require("../utils/networkListener");
|
|
12
|
+
const NETWORK_CONDITIONS = exports.NETWORK_CONDITIONS = {
|
|
13
|
+
normal: {
|
|
14
|
+
label: "Normal",
|
|
15
|
+
delayMs: 0
|
|
16
|
+
},
|
|
17
|
+
offline: {
|
|
18
|
+
label: "Offline (requests)",
|
|
19
|
+
delayMs: 0
|
|
20
|
+
},
|
|
21
|
+
slow: {
|
|
22
|
+
label: "Slow (+500 ms)",
|
|
23
|
+
delayMs: 500
|
|
24
|
+
},
|
|
25
|
+
verySlow: {
|
|
26
|
+
label: "Very slow (+2000 ms)",
|
|
27
|
+
delayMs: 2000
|
|
28
|
+
}
|
|
29
|
+
};
|
|
30
|
+
const networkConditionsRuntimeId = exports.networkConditionsRuntimeId = `${Date.now()}-${Math.random().toString(36).slice(2)}`;
|
|
31
|
+
let profile = "normal";
|
|
32
|
+
let keepAlive;
|
|
33
|
+
const listeners = new Set();
|
|
34
|
+
const conditionsCanApply = () => typeof __DEV__ !== "undefined" && __DEV__ && (0, _sharedUi.hasAccountAccess)();
|
|
35
|
+
exports.conditionsCanApply = conditionsCanApply;
|
|
36
|
+
const getNetworkCondition = () => profile;
|
|
37
|
+
exports.getNetworkCondition = getNetworkCondition;
|
|
38
|
+
const subscribeNetworkConditions = listener => {
|
|
39
|
+
listeners.add(listener);
|
|
40
|
+
return () => {
|
|
41
|
+
listeners.delete(listener);
|
|
42
|
+
};
|
|
43
|
+
};
|
|
44
|
+
exports.subscribeNetworkConditions = subscribeNetworkConditions;
|
|
45
|
+
function setNetworkCondition(value) {
|
|
46
|
+
if (typeof value !== "string" || !Object.prototype.hasOwnProperty.call(NETWORK_CONDITIONS, value)) {
|
|
47
|
+
throw new Error("Choose normal, offline, slow, or verySlow.");
|
|
48
|
+
}
|
|
49
|
+
if (value !== "normal" && !conditionsCanApply()) {
|
|
50
|
+
throw new Error("Network conditions require an admitted account and a development build.");
|
|
51
|
+
}
|
|
52
|
+
if (value === profile) return profile;
|
|
53
|
+
if (value !== "normal" && !keepAlive) {
|
|
54
|
+
(0, _networkListener.startNetworkListener)();
|
|
55
|
+
keepAlive = (0, _networkListener.addNetworkListener)(() => {});
|
|
56
|
+
} else if (value === "normal") {
|
|
57
|
+
keepAlive?.();
|
|
58
|
+
keepAlive = undefined;
|
|
59
|
+
if ((0, _networkListener.getNetworkListenerCount)() === 0) (0, _networkListener.stopNetworkListener)();
|
|
60
|
+
}
|
|
61
|
+
profile = value;
|
|
62
|
+
listeners.forEach(listener => listener());
|
|
63
|
+
return profile;
|
|
64
|
+
}
|
|
65
|
+
function snapshotNetworkCondition(url) {
|
|
66
|
+
if (profile === "normal" || !conditionsCanApply() || !/^https?:\/\//i.test(url)) return;
|
|
67
|
+
return Object.freeze({
|
|
68
|
+
profile,
|
|
69
|
+
delayMs: NETWORK_CONDITIONS[profile].delayMs
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
(0, _sharedUi.subscribeAccountAccess)(() => {
|
|
73
|
+
if (!(0, _sharedUi.hasAccountAccess)()) setNetworkCondition("normal");
|
|
74
|
+
listeners.forEach(listener => listener());
|
|
75
|
+
});
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
Object.defineProperty(exports, "__esModule", {
|
|
4
|
+
value: true
|
|
5
|
+
});
|
|
6
|
+
exports.throwIfAborted = throwIfAborted;
|
|
7
|
+
exports.waitBeforeDispatch = waitBeforeDispatch;
|
|
8
|
+
function throwIfAborted(signal) {
|
|
9
|
+
if (!signal?.aborted) return;
|
|
10
|
+
if (signal.reason !== undefined) throw signal.reason;
|
|
11
|
+
const error = new Error("The request was aborted.");
|
|
12
|
+
error.name = "AbortError";
|
|
13
|
+
throw error;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/** Wait before dispatch, releasing the timer and listener on cancellation. */
|
|
17
|
+
function waitBeforeDispatch(ms, signal) {
|
|
18
|
+
return new Promise((resolve, reject) => {
|
|
19
|
+
try {
|
|
20
|
+
throwIfAborted(signal);
|
|
21
|
+
} catch (error) {
|
|
22
|
+
reject(error);
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
const cleanup = () => signal?.removeEventListener("abort", abort);
|
|
26
|
+
const abort = () => {
|
|
27
|
+
clearTimeout(timer);
|
|
28
|
+
cleanup();
|
|
29
|
+
try {
|
|
30
|
+
throwIfAborted(signal);
|
|
31
|
+
} catch (error) {
|
|
32
|
+
reject(error);
|
|
33
|
+
}
|
|
34
|
+
};
|
|
35
|
+
const timer = setTimeout(() => {
|
|
36
|
+
cleanup();
|
|
37
|
+
resolve();
|
|
38
|
+
}, ms);
|
|
39
|
+
signal?.addEventListener("abort", abort, {
|
|
40
|
+
once: true
|
|
41
|
+
});
|
|
42
|
+
});
|
|
43
|
+
}
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
Object.defineProperty(exports, "__esModule", {
|
|
4
|
+
value: true
|
|
5
|
+
});
|
|
6
|
+
exports.runConditionXhr = runConditionXhr;
|
|
7
|
+
var _applyToXhr = require("../overrides/applyToXhr");
|
|
8
|
+
/** Active simulation uses a send-to-completion deadline, including the wait. */
|
|
9
|
+
function runConditionXhr(target, url, condition, outcome, dispatch) {
|
|
10
|
+
if (!(0, _applyToXhr.canSynthesize)(target)) throw new Error("Network conditions require React Native XMLHttpRequest.");
|
|
11
|
+
const xhr = target;
|
|
12
|
+
if (xhr.readyState !== 1 || xhr._sent) throw new Error("Request is not open or has already been sent.");
|
|
13
|
+
const started = Date.now();
|
|
14
|
+
let timeout = xhr.timeout;
|
|
15
|
+
let settled = false;
|
|
16
|
+
let dispatching = false;
|
|
17
|
+
let deadline;
|
|
18
|
+
let pending;
|
|
19
|
+
const originalAbort = xhr.abort;
|
|
20
|
+
const originalOpen = xhr.open;
|
|
21
|
+
const abortDescriptor = Object.getOwnPropertyDescriptor(xhr, "abort");
|
|
22
|
+
const openDescriptor = Object.getOwnPropertyDescriptor(xhr, "open");
|
|
23
|
+
const timeoutDescriptor = Object.getOwnPropertyDescriptor(xhr, "timeout");
|
|
24
|
+
const restore = (key, descriptor) => {
|
|
25
|
+
if (descriptor) Object.defineProperty(xhr, key, descriptor);else delete xhr[key];
|
|
26
|
+
};
|
|
27
|
+
const cleanup = () => {
|
|
28
|
+
if (settled) return;
|
|
29
|
+
settled = true;
|
|
30
|
+
clearTimeout(pending);
|
|
31
|
+
clearTimeout(deadline);
|
|
32
|
+
xhr.removeEventListener("loadend", cleanup);
|
|
33
|
+
restore("abort", abortDescriptor);
|
|
34
|
+
restore("open", openDescriptor);
|
|
35
|
+
restore("timeout", timeoutDescriptor);
|
|
36
|
+
xhr.timeout = timeout;
|
|
37
|
+
};
|
|
38
|
+
const fail = timedOut => {
|
|
39
|
+
if (settled) return;
|
|
40
|
+
// DONE suppresses abort events while the native transport is cancelled.
|
|
41
|
+
Object.assign(xhr, {
|
|
42
|
+
readyState: 4
|
|
43
|
+
});
|
|
44
|
+
originalAbort.call(xhr);
|
|
45
|
+
xhr._aborted = false;
|
|
46
|
+
xhr._hasError = true;
|
|
47
|
+
xhr._timedOut = timedOut;
|
|
48
|
+
xhr._response = timedOut ? "Request timed out (Buoy conditions)" : "Network request failed (Buoy Offline)";
|
|
49
|
+
xhr.setReadyState(4);
|
|
50
|
+
cleanup();
|
|
51
|
+
};
|
|
52
|
+
const expired = () => timeout > 0 && Date.now() - started >= timeout;
|
|
53
|
+
const scheduleDeadline = () => {
|
|
54
|
+
clearTimeout(deadline);
|
|
55
|
+
if (timeout > 0) deadline = setTimeout(() => fail(true), Math.max(0, timeout - (Date.now() - started)));
|
|
56
|
+
};
|
|
57
|
+
Object.defineProperty(xhr, "timeout", {
|
|
58
|
+
configurable: true,
|
|
59
|
+
get: () => dispatching ? 0 : timeout,
|
|
60
|
+
set: value => {
|
|
61
|
+
timeout = Math.max(0, Number(value) || 0);
|
|
62
|
+
scheduleDeadline();
|
|
63
|
+
}
|
|
64
|
+
});
|
|
65
|
+
Object.defineProperty(xhr, "abort", {
|
|
66
|
+
configurable: true,
|
|
67
|
+
value: () => {
|
|
68
|
+
if (!settled) {
|
|
69
|
+
originalAbort.call(xhr);
|
|
70
|
+
cleanup();
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
});
|
|
74
|
+
Object.defineProperty(xhr, "open", {
|
|
75
|
+
configurable: true,
|
|
76
|
+
value: (...args) => {
|
|
77
|
+
const result = Reflect.apply(originalOpen, xhr, args);
|
|
78
|
+
cleanup();
|
|
79
|
+
return result;
|
|
80
|
+
}
|
|
81
|
+
});
|
|
82
|
+
xhr._sent = true;
|
|
83
|
+
xhr._requestId = null;
|
|
84
|
+
xhr.addEventListener("loadend", cleanup);
|
|
85
|
+
scheduleDeadline();
|
|
86
|
+
pending = setTimeout(() => {
|
|
87
|
+
if (settled) return;
|
|
88
|
+
if (expired()) {
|
|
89
|
+
fail(true);
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
if (condition.profile === "offline") {
|
|
93
|
+
fail(false);
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
if (outcome && outcome.kind !== "delay") {
|
|
97
|
+
(0, _applyToXhr.completeNativeOverride)(xhr, url, outcome);
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
xhr._sent = false;
|
|
101
|
+
dispatching = true;
|
|
102
|
+
try {
|
|
103
|
+
dispatch();
|
|
104
|
+
} catch {
|
|
105
|
+
fail(false);
|
|
106
|
+
} finally {
|
|
107
|
+
dispatching = false;
|
|
108
|
+
}
|
|
109
|
+
}, condition.delayMs + (outcome?.delayMs ?? 0));
|
|
110
|
+
}
|
|
@@ -18,7 +18,7 @@ var _formatGraphQLVariables = require("../utils/formatGraphQLVariables");
|
|
|
18
18
|
|
|
19
19
|
/**
|
|
20
20
|
* @deprecated Read the live limit with `useLimit("network.requests")` — this
|
|
21
|
-
* only reports the FREE tier's value and
|
|
21
|
+
* only reports the FREE tier's value and ignores the Weekend Pass. Kept
|
|
22
22
|
* because it is part of the package's public exports.
|
|
23
23
|
*/
|
|
24
24
|
const FREE_TIER_REQUEST_LIMIT = exports.FREE_TIER_REQUEST_LIMIT = _sharedUi.TIER_LIMITS["network.requests"].free;
|
|
@@ -5,6 +5,7 @@ Object.defineProperty(exports, "__esModule", {
|
|
|
5
5
|
});
|
|
6
6
|
exports.canSynthesize = canSynthesize;
|
|
7
7
|
exports.canSynthesizeStandard = canSynthesizeStandard;
|
|
8
|
+
exports.completeNativeOverride = completeNativeOverride;
|
|
8
9
|
exports.runOverride = runOverride;
|
|
9
10
|
/**
|
|
10
11
|
* Perform an override outcome against an XMLHttpRequest — React Native's OR a
|
|
@@ -406,16 +407,12 @@ function runOverride(xhr, url, outcome, passThrough) {
|
|
|
406
407
|
// had already aborted. Claiming it means abort() dispatches DONE + 'abort'
|
|
407
408
|
// and whatwg-fetch rejects with AbortError, as it should.
|
|
408
409
|
internals._sent = true;
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
// `send()` — which we're skipping — so it would survive here, and
|
|
413
|
-
// `abort()` would hand that string to `RCTNetworking.abortRequest()`.
|
|
414
|
-
// Nulling it makes abort a clean no-op on the native side.
|
|
415
|
-
internals._requestId = null;
|
|
416
|
-
}
|
|
410
|
+
|
|
411
|
+
// There is no native request to cancel until a delayed send is dispatched.
|
|
412
|
+
internals._requestId = null;
|
|
417
413
|
let timer = null;
|
|
418
414
|
const onAbort = () => {
|
|
415
|
+
xhr.removeEventListener("abort", onAbort);
|
|
419
416
|
if (timer !== null) {
|
|
420
417
|
clearTimeout(timer);
|
|
421
418
|
timer = null;
|
|
@@ -448,4 +445,9 @@ function runOverride(xhr, url, outcome, passThrough) {
|
|
|
448
445
|
// 'load' before they exist.
|
|
449
446
|
timer = setTimeout(fire, outcome.delayMs);
|
|
450
447
|
return true;
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
/** Complete an already scheduled native outcome without adding another timer. */
|
|
451
|
+
function completeNativeOverride(xhr, url, outcome) {
|
|
452
|
+
if (outcome.kind === "fail") fail(xhr, outcome);else if (outcome.kind === "respond") respond(xhr, url, outcome);
|
|
451
453
|
}
|
|
@@ -8,6 +8,7 @@ exports.inspectEngine = inspectEngine;
|
|
|
8
8
|
exports.overrideForRequest = overrideForRequest;
|
|
9
9
|
exports.overridesCanApply = overridesCanApply;
|
|
10
10
|
exports.setOverrideHooks = setOverrideHooks;
|
|
11
|
+
var _sharedUi = require("@buoy-gg/shared-ui");
|
|
11
12
|
var _resolveOverride = require("./resolveOverride");
|
|
12
13
|
/**
|
|
13
14
|
* The seam between the interceptor and the rule store.
|
|
@@ -42,7 +43,7 @@ function setOverrideHooks(next) {
|
|
|
42
43
|
function overridesCanApply() {
|
|
43
44
|
// `__DEV__` is undefined outside React Native (unit tests, the desktop
|
|
44
45
|
// bundle); only an explicit `false` — a real release build — disables it.
|
|
45
|
-
return !(typeof __DEV__ !== "undefined" && !__DEV__);
|
|
46
|
+
return (0, _sharedUi.hasAccountAccess)() && !(typeof __DEV__ !== "undefined" && !__DEV__);
|
|
46
47
|
}
|
|
47
48
|
|
|
48
49
|
/** Why overrides are inert, for a caller that has to explain it to a person. */
|
|
@@ -4,6 +4,7 @@ Object.defineProperty(exports, "__esModule", {
|
|
|
4
4
|
value: true
|
|
5
5
|
});
|
|
6
6
|
exports.networkSyncAdapter = exports.SNAPSHOT_HEADER_VALUE_LIMIT = exports.SNAPSHOT_EVENTS_BUDGET = exports.SNAPSHOT_BODY_INLINE_LIMIT = void 0;
|
|
7
|
+
var _store = require("../conditions/store");
|
|
7
8
|
var _networkEventStore = require("../utils/networkEventStore");
|
|
8
9
|
var _networkSavedStore = require("../utils/networkSavedStore");
|
|
9
10
|
var _overrideRulesStore = require("../overrides/overrideRulesStore");
|
|
@@ -311,13 +312,116 @@ base) {
|
|
|
311
312
|
headers: rule.headers && typeof rule.headers === "object" ? rule.headers : base?.headers,
|
|
312
313
|
// Objects are accepted and serialized so a caller can send JSON directly
|
|
313
314
|
// instead of pre-stringifying it, which is the mistake everyone makes once.
|
|
314
|
-
body:
|
|
315
|
+
body: patchedBody(rule, base),
|
|
315
316
|
failKind: rule.failKind === "timeout" ? "timeout" : undefined,
|
|
316
317
|
delayMs: typeof rule.delayMs === "number" ? rule.delayMs : undefined,
|
|
317
318
|
times: typeof rule.times === "number" ? rule.times : null,
|
|
318
319
|
alternate: rule.alternate === true
|
|
319
320
|
};
|
|
320
321
|
}
|
|
322
|
+
const isPlainObject = v => !!v && typeof v === "object" && !Array.isArray(v);
|
|
323
|
+
/**
|
|
324
|
+
* Objects merge key by key; arrays and primitives are replaced. Collects two
|
|
325
|
+
* dangers instead of writing them: `shrunk` (a non-empty array shorter than
|
|
326
|
+
* the one it replaces) and `breaks` (an array/object field overwritten by null
|
|
327
|
+
* or a primitive — the shape the app renders, changed out from under it). The
|
|
328
|
+
* handler refuses either unless forced.
|
|
329
|
+
*
|
|
330
|
+
* This stays local while the typed-edit RULE is shared: it is the merge itself,
|
|
331
|
+
* not the check, and it reports network-specific dangers the store surfaces do
|
|
332
|
+
* not have. `shapeKind` comes from the shared module so both agree on what
|
|
333
|
+
* counts as an array.
|
|
334
|
+
*/
|
|
335
|
+
function deepMerge(base, patch, path = "", issues = {
|
|
336
|
+
shrunk: [],
|
|
337
|
+
breaks: []
|
|
338
|
+
}) {
|
|
339
|
+
if (Array.isArray(base) && Array.isArray(patch)) {
|
|
340
|
+
if (patch.length > 0 && patch.length < base.length) {
|
|
341
|
+
issues.shrunk.push({
|
|
342
|
+
path: path || "(root)",
|
|
343
|
+
have: base.length,
|
|
344
|
+
sent: patch.length
|
|
345
|
+
});
|
|
346
|
+
}
|
|
347
|
+
return patch;
|
|
348
|
+
}
|
|
349
|
+
if (!isPlainObject(base) || !isPlainObject(patch)) return patch;
|
|
350
|
+
const out = {
|
|
351
|
+
...base
|
|
352
|
+
};
|
|
353
|
+
for (const [k, v] of Object.entries(patch)) {
|
|
354
|
+
const p = path ? `${path}.${k}` : k;
|
|
355
|
+
if (k in base) {
|
|
356
|
+
const from = (0, _sharedUi.shapeKind)(base[k]);
|
|
357
|
+
const to = (0, _sharedUi.shapeKind)(v);
|
|
358
|
+
if ((from === "array" || from === "object") && from !== to) {
|
|
359
|
+
issues.breaks.push({
|
|
360
|
+
path: p,
|
|
361
|
+
from,
|
|
362
|
+
to
|
|
363
|
+
});
|
|
364
|
+
}
|
|
365
|
+
out[k] = deepMerge(base[k], v, p, issues);
|
|
366
|
+
} else {
|
|
367
|
+
out[k] = v;
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
return out;
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
/** Typed-edit violations a bodyPatch would cause against the seed body, or []. */
|
|
374
|
+
function patchViolations(rule, seed) {
|
|
375
|
+
if (rule.body != null || !isPlainObject(rule.bodyPatch) || typeof seed !== "string") return [];
|
|
376
|
+
try {
|
|
377
|
+
return (0, _sharedUi.typedEditViolations)(JSON.parse(seed), rule.bodyPatch, {
|
|
378
|
+
noun: "response body"
|
|
379
|
+
});
|
|
380
|
+
} catch {
|
|
381
|
+
return [];
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
/**
|
|
386
|
+
* The rule's body: explicit `body` wins, else `bodyPatch` deep-merged into the
|
|
387
|
+
* seeded body (from `fromRequestId`, or the rule being replaced), else the seed.
|
|
388
|
+
*
|
|
389
|
+
* `bodyPatch` exists for the same reason `setQueryData` has `merge`: a remote
|
|
390
|
+
* caller reads the real response through a wire that truncates big payloads,
|
|
391
|
+
* edits one field, and pastes the torn object back as `body`. The app then
|
|
392
|
+
* renders a response missing half its fields. With a patch the caller sends
|
|
393
|
+
* `{ stats: [...] }` and the other 30KB stay exactly as the server sent them.
|
|
394
|
+
*/
|
|
395
|
+
function patchedBody(rule, base) {
|
|
396
|
+
if (typeof rule.body === "string") return rule.body;
|
|
397
|
+
if (rule.body != null) return JSON.stringify(rule.body, null, 2);
|
|
398
|
+
const seed = base?.body ?? (typeof rule.id === "string" ? _overrideRulesStore.overrideRulesStore.getState().rules.find(r => r.id === rule.id)?.body : undefined);
|
|
399
|
+
if (isPlainObject(rule.bodyPatch) && typeof seed === "string") {
|
|
400
|
+
try {
|
|
401
|
+
return JSON.stringify(deepMerge(JSON.parse(seed), rule.bodyPatch), null, 2);
|
|
402
|
+
} catch {
|
|
403
|
+
return seed;
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
return seed;
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
/**
|
|
410
|
+
* Replacing a rule by id inherits every field the caller did not send, so a
|
|
411
|
+
* remote driver can flip one thing — `{ id, bodyPatch }`, `{ id, status: 500 }`
|
|
412
|
+
* — without re-sending the urlPattern it never had to know.
|
|
413
|
+
*/
|
|
414
|
+
function existingRule(params) {
|
|
415
|
+
if (!params || typeof params !== "object") return null;
|
|
416
|
+
const raw = params.rule ?? params;
|
|
417
|
+
if (!raw || typeof raw !== "object") return null;
|
|
418
|
+
const id = raw.id;
|
|
419
|
+
if (typeof id !== "string") return null;
|
|
420
|
+
const existing = _overrideRulesStore.overrideRulesStore.getState().rules.find(r => r.id === id);
|
|
421
|
+
return existing ? {
|
|
422
|
+
...existing
|
|
423
|
+
} : null;
|
|
424
|
+
}
|
|
321
425
|
|
|
322
426
|
/**
|
|
323
427
|
* The body a remote surface never received, when it says so.
|
|
@@ -373,10 +477,13 @@ function baseFromRequest(params) {
|
|
|
373
477
|
* `NetworkPanel`/`NetworkInline` accept either shape.
|
|
374
478
|
*/
|
|
375
479
|
const networkSyncAdapter = exports.networkSyncAdapter = {
|
|
480
|
+
dispose: () => {
|
|
481
|
+
_overrideRulesStore.overrideRulesStore.setEnabled(false);
|
|
482
|
+
},
|
|
376
483
|
// v5: honest request/responseSize + explicit request/responseBodyOmitted
|
|
377
484
|
// flags, and a total snapshot budget so the list degrades instead of the
|
|
378
485
|
// whole panel being dropped at the emit layer.
|
|
379
|
-
version:
|
|
486
|
+
version: 6,
|
|
380
487
|
getSnapshot: () => {
|
|
381
488
|
// Piggyback the interception health check on the watch loop: while a
|
|
382
489
|
// dashboard/MCP watch is open this runs continuously, so a clobbered
|
|
@@ -385,6 +492,12 @@ const networkSyncAdapter = exports.networkSyncAdapter = {
|
|
|
385
492
|
// reads when healthy.
|
|
386
493
|
_networkEventStore.networkEventStore.ensureInterception();
|
|
387
494
|
return {
|
|
495
|
+
conditions: {
|
|
496
|
+
version: 1,
|
|
497
|
+
runtimeId: _store.networkConditionsRuntimeId,
|
|
498
|
+
profile: (0, _store.getNetworkCondition)(),
|
|
499
|
+
available: (0, _store.conditionsCanApply)()
|
|
500
|
+
},
|
|
388
501
|
events: fitEventsToBudget(_networkEventStore.networkEventStore.getEvents()),
|
|
389
502
|
/**
|
|
390
503
|
* Response-override rules live on the DEVICE — that's the only place they
|
|
@@ -405,17 +518,32 @@ const networkSyncAdapter = exports.networkSyncAdapter = {
|
|
|
405
518
|
// Pinning with no traffic in flight still has to reach the dashboard, so
|
|
406
519
|
// both stores drive the snapshot.
|
|
407
520
|
const unsubscribeEvents = _networkEventStore.networkEventStore.subscribeToEvents(onChange);
|
|
521
|
+
const unsubscribeConditions = (0, _store.subscribeNetworkConditions)(onChange);
|
|
408
522
|
const unsubscribeSaved = _networkSavedStore.networkSavedStore.subscribe(onChange);
|
|
409
523
|
// Same reasoning for rules: editing one with no traffic in flight must
|
|
410
524
|
// still reach the dashboard. Subscribing also hydrates the rule list.
|
|
411
525
|
const unsubscribeOverrides = _overrideRulesStore.overrideRulesStore.subscribe(onChange);
|
|
412
526
|
return () => {
|
|
527
|
+
unsubscribeConditions();
|
|
413
528
|
unsubscribeEvents();
|
|
414
529
|
unsubscribeSaved();
|
|
415
530
|
unsubscribeOverrides();
|
|
416
531
|
};
|
|
417
532
|
},
|
|
418
533
|
actions: {
|
|
534
|
+
getNetworkConditions: () => ({
|
|
535
|
+
runtimeId: _store.networkConditionsRuntimeId,
|
|
536
|
+
profile: (0, _store.getNetworkCondition)(),
|
|
537
|
+
available: (0, _store.conditionsCanApply)()
|
|
538
|
+
}),
|
|
539
|
+
setNetworkConditions: params => {
|
|
540
|
+
const profile = params && typeof params === "object" ? params.profile : undefined;
|
|
541
|
+
return {
|
|
542
|
+
runtimeId: _store.networkConditionsRuntimeId,
|
|
543
|
+
profile: (0, _store.setNetworkCondition)(profile),
|
|
544
|
+
available: (0, _store.conditionsCanApply)()
|
|
545
|
+
};
|
|
546
|
+
},
|
|
419
547
|
clearEvents: () => {
|
|
420
548
|
_networkEventStore.networkEventStore.clearEvents();
|
|
421
549
|
},
|
|
@@ -511,7 +639,89 @@ const networkSyncAdapter = exports.networkSyncAdapter = {
|
|
|
511
639
|
},
|
|
512
640
|
/** Create a rule, or replace one by id. */
|
|
513
641
|
upsertOverrideRule: params => {
|
|
514
|
-
const base = baseFromRequest(params) ?? preservedBody(params);
|
|
642
|
+
const base = baseFromRequest(params) ?? preservedBody(params) ?? existingRule(params);
|
|
643
|
+
const rawRule = params?.rule ?? params;
|
|
644
|
+
// `bodyPath` + `bodyValue` are sugar over `bodyPatch`, resolved against the
|
|
645
|
+
// real response before anything else looks at the rule — so the keyed
|
|
646
|
+
// expansion, the typed-edit guard and the merge below all run on an
|
|
647
|
+
// ordinary patch and gain no special cases. This is the same form the
|
|
648
|
+
// state tools take; it exists here because a response body is the surface
|
|
649
|
+
// where the caller is LEAST likely to have seen the shape (it read the
|
|
650
|
+
// body over a wire that truncates at 24,000 characters), and therefore
|
|
651
|
+
// most likely to get the nesting wrong.
|
|
652
|
+
if (rawRule && typeof rawRule.bodyPath === "string" && rawRule.body == null) {
|
|
653
|
+
const seed = base?.body ?? (typeof rawRule.id === "string" ? _overrideRulesStore.overrideRulesStore.getState().rules.find(r => r.id === rawRule.id)?.body : undefined);
|
|
654
|
+
if (typeof seed !== "string") {
|
|
655
|
+
return {
|
|
656
|
+
ok: false,
|
|
657
|
+
error: "bodyPath needs something to patch: send fromRequestId (the captured request whose real response it addresses) or the id of an existing rule."
|
|
658
|
+
};
|
|
659
|
+
}
|
|
660
|
+
if (rawRule.bodyValue === undefined) {
|
|
661
|
+
return {
|
|
662
|
+
ok: false,
|
|
663
|
+
error: `Refused: sent \`bodyPath\` without \`bodyValue\`. Say what to set "${rawRule.bodyPath}" to.`
|
|
664
|
+
};
|
|
665
|
+
}
|
|
666
|
+
let parsed;
|
|
667
|
+
try {
|
|
668
|
+
parsed = JSON.parse(seed);
|
|
669
|
+
} catch {
|
|
670
|
+
return {
|
|
671
|
+
ok: false,
|
|
672
|
+
error: "The captured response body is not JSON, so it cannot be addressed by path. Send a whole `body` instead."
|
|
673
|
+
};
|
|
674
|
+
}
|
|
675
|
+
const built = (0, _sharedUi.patchFromPath)(parsed, rawRule.bodyPath, rawRule.bodyValue);
|
|
676
|
+
if (!built.ok) return {
|
|
677
|
+
ok: false,
|
|
678
|
+
error: `Refused: ${built.error}`
|
|
679
|
+
};
|
|
680
|
+
rawRule.bodyPatch = built.patch;
|
|
681
|
+
delete rawRule.bodyPath;
|
|
682
|
+
delete rawRule.bodyValue;
|
|
683
|
+
}
|
|
684
|
+
if (rawRule && isPlainObject(rawRule.bodyPatch) && rawRule.body == null) {
|
|
685
|
+
const seed = base?.body ?? (typeof rawRule.id === "string" ? _overrideRulesStore.overrideRulesStore.getState().rules.find(r => r.id === rawRule.id)?.body : undefined);
|
|
686
|
+
if (typeof seed !== "string") {
|
|
687
|
+
return {
|
|
688
|
+
ok: false,
|
|
689
|
+
error: "bodyPatch needs something to patch: send fromRequestId (the captured request whose real response the patch is merged into) or the id of an existing rule."
|
|
690
|
+
};
|
|
691
|
+
}
|
|
692
|
+
if (rawRule.force !== true) {
|
|
693
|
+
// Address list items in the real response by their own id rather than
|
|
694
|
+
// resending the array: {results:{pikachu:{name:"test123"}}}. Expanded
|
|
695
|
+
// against the seed BEFORE the guard and before the merge, so the
|
|
696
|
+
// typed-edit rule still holds over the result. This is the only form
|
|
697
|
+
// that is correct when the caller read the response through a wire
|
|
698
|
+
// that truncated it — see packages/shared/src/sync/keyedArrays.ts.
|
|
699
|
+
let keyedError;
|
|
700
|
+
try {
|
|
701
|
+
const expanded = (0, _sharedUi.expandKeyedArrays)(JSON.parse(seed), rawRule.bodyPatch);
|
|
702
|
+
keyedError = expanded.error;
|
|
703
|
+
if (expanded.notes.length) rawRule.bodyPatch = expanded.patch;
|
|
704
|
+
} catch {
|
|
705
|
+
/* a non-JSON seed has no lists to address; leave the patch alone */
|
|
706
|
+
}
|
|
707
|
+
// See keyedArrays.ts: an added item whose id disagrees with the key
|
|
708
|
+
// it was sent under is ambiguous, and only this layer can say so.
|
|
709
|
+
if (keyedError) {
|
|
710
|
+
return {
|
|
711
|
+
ok: false,
|
|
712
|
+
error: `Refused: ${keyedError}`
|
|
713
|
+
};
|
|
714
|
+
}
|
|
715
|
+
const violations = patchViolations(rawRule, seed);
|
|
716
|
+
if (violations.length > 0) {
|
|
717
|
+
return {
|
|
718
|
+
ok: false,
|
|
719
|
+
error: (0, _sharedUi.refusalText)(violations, "response body", (0, _sharedUi.suggestedPatch)(rawRule.bodyPatch, violations)),
|
|
720
|
+
violations
|
|
721
|
+
};
|
|
722
|
+
}
|
|
723
|
+
}
|
|
724
|
+
}
|
|
515
725
|
const draft = readRuleDraft(params, base);
|
|
516
726
|
if (!draft) {
|
|
517
727
|
const wanted = params?.fromRequestId;
|
|
@@ -541,8 +751,19 @@ const networkSyncAdapter = exports.networkSyncAdapter = {
|
|
|
541
751
|
enabled: rule.enabled,
|
|
542
752
|
kind: rule.kind,
|
|
543
753
|
status: rule.status,
|
|
754
|
+
times: rule.times,
|
|
544
755
|
bodySize: rule.body ? rule.body.length : 0
|
|
545
756
|
},
|
|
757
|
+
// A rule with no budget overrides EVERY matching request until someone
|
|
758
|
+
// deletes it, and that is rarely what a one-off ask meant. Measured on
|
|
759
|
+
// `api-sim/next-call-fails-once` ("just the next one, then let it work
|
|
760
|
+
// again"): ten of fourteen attempts left the budget off and got a rule
|
|
761
|
+
// that never expires. Saying so is not a nag — it is the difference
|
|
762
|
+
// between a caller that adds `times` on its next turn and one that walks
|
|
763
|
+
// away leaving the app permanently broken.
|
|
764
|
+
...(rule.times === undefined || rule.times === null ? {
|
|
765
|
+
lingers: `This rule has no \`times\` budget, so it overrides EVERY matching request until it is disabled or deleted (id \`${rule.id}\`). If you meant only the next one, set times:1.`
|
|
766
|
+
} : {}),
|
|
546
767
|
...inactiveStamp()
|
|
547
768
|
};
|
|
548
769
|
},
|
|
@@ -81,6 +81,7 @@ function generateNetworkCopyText(events, copySettings) {
|
|
|
81
81
|
obj.statusText = event.statusText;
|
|
82
82
|
}
|
|
83
83
|
if (copySettings.includeDuration) obj.duration = event.duration;
|
|
84
|
+
if (event.condition) obj.condition = event.condition;
|
|
84
85
|
if (copySettings.includeTimestamp) obj.timestamp = event.timestamp;
|
|
85
86
|
if (copySettings.includeClient) obj.requestClient = event.requestClient;
|
|
86
87
|
if (copySettings.includeSizes) {
|
|
@@ -119,6 +120,7 @@ function generateNetworkCopyText(events, copySettings) {
|
|
|
119
120
|
if (copySettings.includeClient && event.requestClient) {
|
|
120
121
|
md += `**Client:** ${event.requestClient}\n`;
|
|
121
122
|
}
|
|
123
|
+
if (event.condition) md += `Conditions: ${event.condition.profile} (+${event.condition.delayMs} ms)\n`;
|
|
122
124
|
if (copySettings.includeDuration && event.duration) {
|
|
123
125
|
md += `**Duration:** ${event.duration}ms\n`;
|
|
124
126
|
}
|