@buoy-gg/network 7.0.13 → 7.0.15
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/lib/commonjs/index.js +39 -0
- package/lib/commonjs/network/components/NetworkEventDetailView.js +30 -3
- package/lib/commonjs/network/components/NetworkEventItemCompact.js +4 -0
- package/lib/commonjs/network/components/NetworkHeaderLive.js +26 -6
- package/lib/commonjs/network/components/NetworkHeaderMenu.js +183 -0
- package/lib/commonjs/network/components/NetworkListScreen.js +126 -5
- package/lib/commonjs/network/components/NetworkModal.js +136 -29
- package/lib/commonjs/network/components/NetworkNumberPrompt.js +178 -0
- package/lib/commonjs/network/components/NetworkOverrideEditor.js +797 -0
- package/lib/commonjs/network/components/NetworkOverrideHeaderButton.js +102 -0
- package/lib/commonjs/network/components/NetworkOverridesButton.js +88 -0
- package/lib/commonjs/network/components/NetworkOverridesScreen.js +489 -0
- package/lib/commonjs/network/components/response-editor/Explorer.js +973 -0
- package/lib/commonjs/network/components/response-editor/deleteNestedDataByPath.js +56 -0
- package/lib/commonjs/network/components/response-editor/editorContext.js +33 -0
- package/lib/commonjs/network/components/response-editor/updateNestedDataByPath.js +61 -0
- package/lib/commonjs/network/hooks/useOverrideRules.js +36 -0
- package/lib/commonjs/network/overrides/applyToFetch.js +80 -0
- package/lib/commonjs/network/overrides/applyToXhr.js +190 -0
- package/lib/commonjs/network/overrides/engine.js +70 -0
- package/lib/commonjs/network/overrides/matchRule.js +81 -0
- package/lib/commonjs/network/overrides/overrideRulesStore.js +550 -0
- package/lib/commonjs/network/overrides/presets.js +70 -0
- package/lib/commonjs/network/overrides/resolveOverride.js +158 -0
- package/lib/commonjs/network/overrides/types.js +85 -0
- package/lib/commonjs/network/sync/networkSyncAdapter.js +230 -1
- package/lib/commonjs/network/types/index.js +5 -1
- package/lib/commonjs/network/utils/networkEventStore.js +52 -6
- package/lib/commonjs/network/utils/networkListener.js +90 -10
- package/lib/commonjs/preset.js +11 -0
- package/lib/module/index.js +15 -0
- package/lib/module/network/components/NetworkEventDetailView.js +31 -4
- package/lib/module/network/components/NetworkEventItemCompact.js +5 -1
- package/lib/module/network/components/NetworkHeaderLive.js +26 -6
- package/lib/module/network/components/NetworkHeaderMenu.js +177 -0
- package/lib/module/network/components/NetworkListScreen.js +127 -6
- package/lib/module/network/components/NetworkModal.js +138 -31
- package/lib/module/network/components/NetworkNumberPrompt.js +174 -0
- package/lib/module/network/components/NetworkOverrideEditor.js +794 -0
- package/lib/module/network/components/NetworkOverrideHeaderButton.js +99 -0
- package/lib/module/network/components/NetworkOverridesButton.js +85 -0
- package/lib/module/network/components/NetworkOverridesScreen.js +485 -0
- package/lib/module/network/components/response-editor/Explorer.js +972 -0
- package/lib/module/network/components/response-editor/deleteNestedDataByPath.js +51 -0
- package/lib/module/network/components/response-editor/editorContext.js +29 -0
- package/lib/module/network/components/response-editor/updateNestedDataByPath.js +56 -0
- package/lib/module/network/hooks/useOverrideRules.js +32 -0
- package/lib/module/network/overrides/applyToFetch.js +72 -0
- package/lib/module/network/overrides/applyToXhr.js +185 -0
- package/lib/module/network/overrides/engine.js +64 -0
- package/lib/module/network/overrides/matchRule.js +75 -0
- package/lib/module/network/overrides/overrideRulesStore.js +547 -0
- package/lib/module/network/overrides/presets.js +65 -0
- package/lib/module/network/overrides/resolveOverride.js +152 -0
- package/lib/module/network/overrides/types.js +80 -0
- package/lib/module/network/sync/networkSyncAdapter.js +230 -1
- package/lib/module/network/types/index.js +3 -1
- package/lib/module/network/utils/networkEventStore.js +52 -6
- package/lib/module/network/utils/networkListener.js +91 -10
- package/lib/module/preset.js +13 -1
- package/lib/typescript/index.d.ts +8 -0
- package/lib/typescript/index.d.ts.map +1 -1
- package/lib/typescript/network/components/NetworkEventDetailView.d.ts.map +1 -1
- package/lib/typescript/network/components/NetworkEventItemCompact.d.ts +6 -0
- package/lib/typescript/network/components/NetworkEventItemCompact.d.ts.map +1 -1
- package/lib/typescript/network/components/NetworkHeaderLive.d.ts +8 -1
- package/lib/typescript/network/components/NetworkHeaderLive.d.ts.map +1 -1
- package/lib/typescript/network/components/NetworkHeaderMenu.d.ts +46 -0
- package/lib/typescript/network/components/NetworkHeaderMenu.d.ts.map +1 -0
- package/lib/typescript/network/components/NetworkListScreen.d.ts.map +1 -1
- package/lib/typescript/network/components/NetworkModal.d.ts.map +1 -1
- package/lib/typescript/network/components/NetworkNumberPrompt.d.ts +33 -0
- package/lib/typescript/network/components/NetworkNumberPrompt.d.ts.map +1 -0
- package/lib/typescript/network/components/NetworkOverrideEditor.d.ts +17 -0
- package/lib/typescript/network/components/NetworkOverrideEditor.d.ts.map +1 -0
- package/lib/typescript/network/components/NetworkOverrideHeaderButton.d.ts +27 -0
- package/lib/typescript/network/components/NetworkOverrideHeaderButton.d.ts.map +1 -0
- package/lib/typescript/network/components/NetworkOverridesButton.d.ts +17 -0
- package/lib/typescript/network/components/NetworkOverridesButton.d.ts.map +1 -0
- package/lib/typescript/network/components/NetworkOverridesScreen.d.ts +29 -0
- package/lib/typescript/network/components/NetworkOverridesScreen.d.ts.map +1 -0
- package/lib/typescript/network/components/response-editor/Explorer.d.ts +17 -0
- package/lib/typescript/network/components/response-editor/Explorer.d.ts.map +1 -0
- package/lib/typescript/network/components/response-editor/deleteNestedDataByPath.d.ts +9 -0
- package/lib/typescript/network/components/response-editor/deleteNestedDataByPath.d.ts.map +1 -0
- package/lib/typescript/network/components/response-editor/editorContext.d.ts +29 -0
- package/lib/typescript/network/components/response-editor/editorContext.d.ts.map +1 -0
- package/lib/typescript/network/components/response-editor/updateNestedDataByPath.d.ts +10 -0
- package/lib/typescript/network/components/response-editor/updateNestedDataByPath.d.ts.map +1 -0
- package/lib/typescript/network/hooks/useOverrideRules.d.ts +29 -0
- package/lib/typescript/network/hooks/useOverrideRules.d.ts.map +1 -0
- package/lib/typescript/network/overrides/__tests__/applyToFetch.test.d.ts +16 -0
- package/lib/typescript/network/overrides/__tests__/applyToFetch.test.d.ts.map +1 -0
- package/lib/typescript/network/overrides/__tests__/applyToXhr.test.d.ts +19 -0
- package/lib/typescript/network/overrides/__tests__/applyToXhr.test.d.ts.map +1 -0
- package/lib/typescript/network/overrides/__tests__/resolveOverride.test.d.ts +22 -0
- package/lib/typescript/network/overrides/__tests__/resolveOverride.test.d.ts.map +1 -0
- package/lib/typescript/network/overrides/applyToFetch.d.ts +50 -0
- package/lib/typescript/network/overrides/applyToFetch.d.ts.map +1 -0
- package/lib/typescript/network/overrides/applyToXhr.d.ts +43 -0
- package/lib/typescript/network/overrides/applyToXhr.d.ts.map +1 -0
- package/lib/typescript/network/overrides/engine.d.ts +47 -0
- package/lib/typescript/network/overrides/engine.d.ts.map +1 -0
- package/lib/typescript/network/overrides/matchRule.d.ts +22 -0
- package/lib/typescript/network/overrides/matchRule.d.ts.map +1 -0
- package/lib/typescript/network/overrides/overrideRulesStore.d.ts +205 -0
- package/lib/typescript/network/overrides/overrideRulesStore.d.ts.map +1 -0
- package/lib/typescript/network/overrides/presets.d.ts +38 -0
- package/lib/typescript/network/overrides/presets.d.ts.map +1 -0
- package/lib/typescript/network/overrides/resolveOverride.d.ts +46 -0
- package/lib/typescript/network/overrides/resolveOverride.d.ts.map +1 -0
- package/lib/typescript/network/overrides/types.d.ts +169 -0
- package/lib/typescript/network/overrides/types.d.ts.map +1 -0
- package/lib/typescript/network/sync/networkSyncAdapter.d.ts +73 -0
- package/lib/typescript/network/sync/networkSyncAdapter.d.ts.map +1 -1
- package/lib/typescript/network/types/index.d.ts +10 -0
- package/lib/typescript/network/types/index.d.ts.map +1 -1
- package/lib/typescript/network/utils/networkEventStore.d.ts +14 -0
- package/lib/typescript/network/utils/networkEventStore.d.ts.map +1 -1
- package/lib/typescript/network/utils/networkListener.d.ts +3 -0
- package/lib/typescript/network/utils/networkListener.d.ts.map +1 -1
- package/lib/typescript/preset.d.ts.map +1 -1
- package/package.json +2 -2
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Deletes nested data by path
|
|
5
|
+
*
|
|
6
|
+
* @param {unknown} oldData Data to be updated
|
|
7
|
+
* @param {Array<string>} deletePath Path to the data to be deleted
|
|
8
|
+
* @returns newData without the deleted items by path
|
|
9
|
+
*/
|
|
10
|
+
export const deleteNestedDataByPath = (oldData, deletePath) => {
|
|
11
|
+
if (oldData instanceof Map) {
|
|
12
|
+
const newData = new Map(oldData);
|
|
13
|
+
if (deletePath.length === 1) {
|
|
14
|
+
newData.delete(deletePath[0]);
|
|
15
|
+
return newData;
|
|
16
|
+
}
|
|
17
|
+
const [head, ...tail] = deletePath;
|
|
18
|
+
newData.set(head, deleteNestedDataByPath(newData.get(head), tail));
|
|
19
|
+
return newData;
|
|
20
|
+
}
|
|
21
|
+
if (oldData instanceof Set) {
|
|
22
|
+
const setAsArray = deleteNestedDataByPath(Array.from(oldData), deletePath);
|
|
23
|
+
return new Set(setAsArray);
|
|
24
|
+
}
|
|
25
|
+
if (Array.isArray(oldData)) {
|
|
26
|
+
const newData = [...oldData];
|
|
27
|
+
if (deletePath.length === 1) {
|
|
28
|
+
return newData.filter((_, idx) => idx.toString() !== deletePath[0]);
|
|
29
|
+
}
|
|
30
|
+
const [head, ...tail] = deletePath;
|
|
31
|
+
|
|
32
|
+
// @ts-expect-error NAS
|
|
33
|
+
newData[head] = deleteNestedDataByPath(newData[head], tail);
|
|
34
|
+
return newData;
|
|
35
|
+
}
|
|
36
|
+
if (oldData instanceof Object) {
|
|
37
|
+
const newData = {
|
|
38
|
+
...oldData
|
|
39
|
+
};
|
|
40
|
+
if (deletePath.length === 1) {
|
|
41
|
+
// @ts-expect-error NAS
|
|
42
|
+
delete newData[deletePath[0]];
|
|
43
|
+
return newData;
|
|
44
|
+
}
|
|
45
|
+
const [head, ...tail] = deletePath;
|
|
46
|
+
// @ts-expect-error NAS
|
|
47
|
+
newData[head] = deleteNestedDataByPath(newData[head], tail);
|
|
48
|
+
return newData;
|
|
49
|
+
}
|
|
50
|
+
return oldData;
|
|
51
|
+
};
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
import { createContext, useContext } from "react";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* What the ported Explorer needs in place of React Query's cache.
|
|
7
|
+
*
|
|
8
|
+
* In the original, every edit ends at
|
|
9
|
+
* `queryClient.setQueryData(activeQuery.queryKey, newData)` and `activeQuery`
|
|
10
|
+
* doubles as the "is this editable" gate — so the whole tree is wired to one
|
|
11
|
+
* specific store. Here that becomes two values in context: the ROOT document
|
|
12
|
+
* (every op rebuilds the whole thing from the root, so each row needs it) and a
|
|
13
|
+
* single `onChange`.
|
|
14
|
+
*
|
|
15
|
+
* Context rather than props because Explorer recurses per node and already
|
|
16
|
+
* threads `activeQuery` down every level; swapping one drilled prop for another
|
|
17
|
+
* would have meant the same plumbing at every call site, and a row deep in a
|
|
18
|
+
* response is exactly where you don't want a missed prop.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
export const ResponseEditorContext = /*#__PURE__*/createContext(null);
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Null outside a provider, which is how the tree stays READ-ONLY by default —
|
|
25
|
+
* the same role `activeQuery === undefined` played in the original.
|
|
26
|
+
*/
|
|
27
|
+
export function useResponseEditor() {
|
|
28
|
+
return useContext(ResponseEditorContext);
|
|
29
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* updates nested data by path
|
|
5
|
+
*
|
|
6
|
+
* @param {JsonValue} oldData Data to be updated
|
|
7
|
+
* @param {Array<string>} updatePath Path to the data to be updated
|
|
8
|
+
* @param {JsonValue} value New value
|
|
9
|
+
*/
|
|
10
|
+
export const updateNestedDataByPath = (oldData, updatePath, value) => {
|
|
11
|
+
if (updatePath.length === 0) {
|
|
12
|
+
return value;
|
|
13
|
+
}
|
|
14
|
+
if (oldData instanceof Map) {
|
|
15
|
+
const newData = new Map(oldData);
|
|
16
|
+
if (updatePath.length === 1) {
|
|
17
|
+
newData.set(updatePath[0], value);
|
|
18
|
+
return newData;
|
|
19
|
+
}
|
|
20
|
+
const [head, ...tail] = updatePath;
|
|
21
|
+
const currentValue = newData.get(head);
|
|
22
|
+
newData.set(head, updateNestedDataByPath(currentValue ?? null, tail, value));
|
|
23
|
+
return newData;
|
|
24
|
+
}
|
|
25
|
+
if (oldData instanceof Set) {
|
|
26
|
+
const setAsArray = updateNestedDataByPath(Array.from(oldData), updatePath, value);
|
|
27
|
+
return new Set(Array.isArray(setAsArray) ? setAsArray : []);
|
|
28
|
+
}
|
|
29
|
+
if (Array.isArray(oldData)) {
|
|
30
|
+
const newData = [...oldData];
|
|
31
|
+
if (updatePath.length === 1) {
|
|
32
|
+
// @ts-expect-error NAS
|
|
33
|
+
newData[updatePath[0]] = value;
|
|
34
|
+
return newData;
|
|
35
|
+
}
|
|
36
|
+
const [head, ...tail] = updatePath;
|
|
37
|
+
// @ts-expect-error NAS
|
|
38
|
+
newData[head] = updateNestedDataByPath(newData[head], tail, value);
|
|
39
|
+
return newData;
|
|
40
|
+
}
|
|
41
|
+
if (oldData instanceof Object) {
|
|
42
|
+
const newData = {
|
|
43
|
+
...oldData
|
|
44
|
+
};
|
|
45
|
+
if (updatePath.length === 1) {
|
|
46
|
+
// @ts-expect-error NAS
|
|
47
|
+
newData[updatePath[0]] = value;
|
|
48
|
+
return newData;
|
|
49
|
+
}
|
|
50
|
+
const [head, ...tail] = updatePath;
|
|
51
|
+
// @ts-expect-error NAS
|
|
52
|
+
newData[head] = updateNestedDataByPath(newData[head], tail, value);
|
|
53
|
+
return newData;
|
|
54
|
+
}
|
|
55
|
+
return oldData;
|
|
56
|
+
};
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
import { useCallback, useSyncExternalStore } from "react";
|
|
4
|
+
import { overrideRulesStore } from "../overrides/overrideRulesStore";
|
|
5
|
+
import { isSpent } from "../overrides/resolveOverride";
|
|
6
|
+
/**
|
|
7
|
+
* Read + mutate the override rule list.
|
|
8
|
+
*
|
|
9
|
+
* `useSyncExternalStore` over the store's own subscribe/getState, which already
|
|
10
|
+
* hands out a stable state object per mutation (see `rebuild`) — so this can't
|
|
11
|
+
* tear and doesn't need a selector to stay cheap.
|
|
12
|
+
*/
|
|
13
|
+
export function useOverrideRules() {
|
|
14
|
+
const state = useSyncExternalStore(useCallback(listener => overrideRulesStore.subscribe(listener), []), () => overrideRulesStore.getState(), () => overrideRulesStore.getState());
|
|
15
|
+
const setEnabled = useCallback(enabled => overrideRulesStore.setEnabled(enabled), []);
|
|
16
|
+
const upsertRule = useCallback(draft => overrideRulesStore.upsertRule(draft), []);
|
|
17
|
+
const setRuleEnabled = useCallback((id, enabled) => overrideRulesStore.setRuleEnabled(id, enabled), []);
|
|
18
|
+
const removeRule = useCallback(id => overrideRulesStore.removeRule(id), []);
|
|
19
|
+
const clearRules = useCallback(() => overrideRulesStore.clearRules(), []);
|
|
20
|
+
return {
|
|
21
|
+
enabled: state.enabled,
|
|
22
|
+
rules: state.rules,
|
|
23
|
+
autoPaused: state.autoPaused,
|
|
24
|
+
activeCount: overrideRulesStore.activeCount(),
|
|
25
|
+
setEnabled,
|
|
26
|
+
upsertRule,
|
|
27
|
+
setRuleEnabled,
|
|
28
|
+
removeRule,
|
|
29
|
+
clearRules,
|
|
30
|
+
isRuleSpent: isSpent
|
|
31
|
+
};
|
|
32
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Perform an override outcome against `fetch`.
|
|
5
|
+
*
|
|
6
|
+
* WHY THIS EXISTS — and why "XHR is the single choke point" is WRONG:
|
|
7
|
+
*
|
|
8
|
+
* In bare React Native, `Libraries/Network/fetch.js` is `require('whatwg-fetch')`,
|
|
9
|
+
* so `fetch` really is a polyfill over XMLHttpRequest and hooking XHR alone
|
|
10
|
+
* would cover everything. But **Expo replaces it**: `expo/src/winter/runtime.native.ts`
|
|
11
|
+
* installs `expo/fetch` — a NATIVE fetch over `ExpoFetchModule` — as
|
|
12
|
+
* `globalThis.fetch` unless `EXPO_PUBLIC_USE_RN_FETCH=1`. Those requests never
|
|
13
|
+
* construct an XMLHttpRequest, so an XHR-only override silently does nothing on
|
|
14
|
+
* the majority of real apps.
|
|
15
|
+
*
|
|
16
|
+
* Verified the hard way on Expo SDK 56 / RN 0.85.3: rule matched, engine
|
|
17
|
+
* healthy, interceptor installed, and the request still came back a real 200.
|
|
18
|
+
*
|
|
19
|
+
* So overrides hook BOTH layers. `axios` still needs the XHR path (it uses
|
|
20
|
+
* XMLHttpRequest on every platform), and this covers `fetch` on both flavours.
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
export const sleep = ms => ms > 0 ? new Promise(resolve => setTimeout(resolve, ms)) : Promise.resolve();
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Can we build a `Response` in this runtime?
|
|
27
|
+
*
|
|
28
|
+
* React Native installs the whatwg-fetch `Response` global via `setUpXHR`, and
|
|
29
|
+
* Expo deliberately keeps it (it only swaps `fetch`). Feature-detected anyway:
|
|
30
|
+
* without it, the honest thing is to let the request run for real.
|
|
31
|
+
*/
|
|
32
|
+
export function canSynthesizeResponse() {
|
|
33
|
+
return typeof Response === "function";
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Build the fake `Response` an overridden `fetch` resolves with.
|
|
38
|
+
*
|
|
39
|
+
* Note the status range: whatwg-fetch's `Response` constructor THROWS
|
|
40
|
+
* `RangeError` outside [200, 599], which would crash the caller instead of
|
|
41
|
+
* overriding it. `resolveOverride` clamps before we ever get here.
|
|
42
|
+
*/
|
|
43
|
+
export function synthesizeResponse(url, outcome) {
|
|
44
|
+
const response = new Response(outcome.body, {
|
|
45
|
+
status: outcome.status,
|
|
46
|
+
statusText: outcome.statusText,
|
|
47
|
+
headers: outcome.headers
|
|
48
|
+
});
|
|
49
|
+
// `Response.url` is not settable through the constructor, and consumers do
|
|
50
|
+
// read it (redirect checks, cache keys). whatwg-fetch stores it as a plain
|
|
51
|
+
// property, so assigning is both possible and correct.
|
|
52
|
+
try {
|
|
53
|
+
Object.defineProperty(response, "url", {
|
|
54
|
+
value: url,
|
|
55
|
+
configurable: true
|
|
56
|
+
});
|
|
57
|
+
} catch {
|
|
58
|
+
// Sealed implementation — an empty url is cosmetic, not worth failing over.
|
|
59
|
+
}
|
|
60
|
+
return response;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* The error a failed `fetch` rejects with.
|
|
65
|
+
*
|
|
66
|
+
* These are the exact messages whatwg-fetch produces for a dead connection and
|
|
67
|
+
* a timeout, so app code that string-matches on them (plenty does) behaves the
|
|
68
|
+
* same way it would against a real failure.
|
|
69
|
+
*/
|
|
70
|
+
export function synthesizeFetchError(outcome) {
|
|
71
|
+
return new TypeError(outcome.failKind === "timeout" ? "Network request timed out" : "Network request failed");
|
|
72
|
+
}
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Perform an override outcome against a React Native XMLHttpRequest.
|
|
5
|
+
*
|
|
6
|
+
* WHY THIS IS POSSIBLE AT ALL: React Native's XMLHttpRequest
|
|
7
|
+
* (`react-native/Libraries/Network/XMLHttpRequest.js`) is pure JavaScript.
|
|
8
|
+
* `readyState`, `status`, `responseHeaders` and `responseURL` are plain
|
|
9
|
+
* writable fields, and `setResponseHeaders()` / `setReadyState()` are real
|
|
10
|
+
* methods — `setReadyState(DONE)` dispatches the correct terminal event off
|
|
11
|
+
* internal flags (`_aborted` → abort, `_hasError` + `_timedOut` → timeout,
|
|
12
|
+
* `_hasError` → error, otherwise load; always loadend). So a response can be
|
|
13
|
+
* synthesized end to end without touching native code.
|
|
14
|
+
*
|
|
15
|
+
* WHY XHR IS THE ONLY PLACE WE HOOK: `react-native/Libraries/Network/fetch.js`
|
|
16
|
+
* is `require('whatwg-fetch')` — RN's `fetch` IS the polyfill over
|
|
17
|
+
* XMLHttpRequest. Overriding here therefore covers `fetch`, `axios` and raw
|
|
18
|
+
* XHR through one code path, and the fake response flows back up through the
|
|
19
|
+
* polyfill so the tool records it without any extra plumbing.
|
|
20
|
+
*
|
|
21
|
+
* Verified field-for-field on React Native 0.81.4 and 0.85.3.
|
|
22
|
+
*
|
|
23
|
+
* Four non-obvious behaviours are handled below; each one is a real bug if you
|
|
24
|
+
* skip it, and each is commented at the line that fixes it.
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
const HEADERS_RECEIVED = 2;
|
|
28
|
+
const LOADING = 3;
|
|
29
|
+
const DONE = 4;
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* The private surface of RN's XMLHttpRequest that we drive.
|
|
33
|
+
*
|
|
34
|
+
* Typed explicitly rather than reached for with `any` so that if a future React
|
|
35
|
+
* Native renames one of these, the compiler points at every use — and so the
|
|
36
|
+
* blast radius of "we depend on RN internals" is one interface.
|
|
37
|
+
*/
|
|
38
|
+
|
|
39
|
+
const internalsOf = xhr => xhr;
|
|
40
|
+
let warnedUnsupported = false;
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Can this XMLHttpRequest be driven by hand?
|
|
44
|
+
*
|
|
45
|
+
* The whole technique rests on RN internals, so it is feature-detected rather
|
|
46
|
+
* than assumed: on a browser XMLHttpRequest (react-native-web) these methods
|
|
47
|
+
* don't exist and `status` is a read-only getter. There, overrides no-op and
|
|
48
|
+
* the request goes out for real — degraded, but never broken.
|
|
49
|
+
*/
|
|
50
|
+
export function canSynthesize(xhr) {
|
|
51
|
+
const internals = internalsOf(xhr);
|
|
52
|
+
return typeof internals.setReadyState === "function" && typeof internals.setResponseHeaders === "function";
|
|
53
|
+
}
|
|
54
|
+
function warnUnsupported() {
|
|
55
|
+
if (warnedUnsupported) return;
|
|
56
|
+
warnedUnsupported = true;
|
|
57
|
+
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
58
|
+
console.warn("[Buoy] Network overrides need React Native's XMLHttpRequest and this " + "environment provides a different one — requests will run normally.");
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** Synthesize a successful response. */
|
|
63
|
+
function respond(xhr, url, outcome) {
|
|
64
|
+
const internals = internalsOf(xhr);
|
|
65
|
+
|
|
66
|
+
// `_reset()` does `delete this.responseURL`, so `'responseURL' in xhr` is
|
|
67
|
+
// FALSE on a fresh request — and whatwg-fetch reads exactly that to populate
|
|
68
|
+
// `Response.url` (fetch.js:558). Without this line every overridden fetch
|
|
69
|
+
// response would silently carry `url: ""`.
|
|
70
|
+
internals.responseURL = url;
|
|
71
|
+
internals.status = outcome.status;
|
|
72
|
+
// RN has no `statusText` field at all (real requests report `undefined`), so
|
|
73
|
+
// this is a new own property. Harmless, and it makes the tool's list read
|
|
74
|
+
// correctly instead of showing a bare number.
|
|
75
|
+
internals.statusText = outcome.statusText;
|
|
76
|
+
internals.setResponseHeaders?.(outcome.headers);
|
|
77
|
+
// HEADERS_RECEIVED also stamps the interceptor's existing `_ttfb`, so an
|
|
78
|
+
// overridden request reports its injected latency like a real one.
|
|
79
|
+
internals.setReadyState?.(HEADERS_RECEIVED);
|
|
80
|
+
if (internals._aborted) return;
|
|
81
|
+
|
|
82
|
+
// whatwg-fetch sets `responseType = 'blob'` before every send
|
|
83
|
+
// (fetch.js:599-606), which would make RN's `response` getter demand a native
|
|
84
|
+
// Blob descriptor from BlobManager. But the polyfill reads the response
|
|
85
|
+
// exactly once — `'response' in xhr ? xhr.response : xhr.responseText`
|
|
86
|
+
// (fetch.js:559) — and never re-reads `responseType`. Handing back text is
|
|
87
|
+
// therefore both sufficient and strictly simpler: no Blob, no FileReader, no
|
|
88
|
+
// native round trip, and `.json()` becomes a synchronous JSON.parse.
|
|
89
|
+
//
|
|
90
|
+
// Assign the FIELD, not the property: the `responseType` setter throws once
|
|
91
|
+
// `_sent` is true (XMLHttpRequest.js:215), and we set `_sent` above.
|
|
92
|
+
internals._responseType = "text";
|
|
93
|
+
internals._response = outcome.body;
|
|
94
|
+
internals._cachedResponse = undefined; // force the getter to recompute
|
|
95
|
+
internals.setReadyState?.(LOADING);
|
|
96
|
+
if (internals._aborted) return;
|
|
97
|
+
internals.setReadyState?.(DONE); // dispatches 'load' then 'loadend'
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/** Synthesize a transport failure — offline or timeout. */
|
|
101
|
+
function fail(xhr, outcome) {
|
|
102
|
+
const internals = internalsOf(xhr);
|
|
103
|
+
const timedOut = outcome.failKind === "timeout";
|
|
104
|
+
|
|
105
|
+
// These two flags are the entire difference between the 'error' and
|
|
106
|
+
// 'timeout' events — see `setReadyState` in XMLHttpRequest.js. whatwg-fetch
|
|
107
|
+
// turns them into `TypeError: Network request failed` and
|
|
108
|
+
// `TypeError: Network request timed out` respectively, which is exactly what
|
|
109
|
+
// the app would see from a real dead connection.
|
|
110
|
+
internals._hasError = true;
|
|
111
|
+
internals._timedOut = timedOut;
|
|
112
|
+
internals._responseType = "text";
|
|
113
|
+
internals._response = timedOut ? "Request timed out (Buoy override)" : "Network request failed (Buoy override)";
|
|
114
|
+
internals._cachedResponse = undefined;
|
|
115
|
+
internals.setReadyState?.(DONE);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Apply an outcome to a request that has NOT been sent yet.
|
|
120
|
+
*
|
|
121
|
+
* Returns `false` when this environment can't be driven by hand, in which case
|
|
122
|
+
* the caller must send the request normally.
|
|
123
|
+
*
|
|
124
|
+
* @param passThrough Sends the real request. Only used by `kind: "delay"`.
|
|
125
|
+
*/
|
|
126
|
+
export function runOverride(xhr, url, outcome, passThrough) {
|
|
127
|
+
if (!canSynthesize(xhr)) {
|
|
128
|
+
warnUnsupported();
|
|
129
|
+
return false;
|
|
130
|
+
}
|
|
131
|
+
const internals = internalsOf(xhr);
|
|
132
|
+
|
|
133
|
+
// Claim the request as sent, for all three kinds.
|
|
134
|
+
//
|
|
135
|
+
// We skip RN's own `send()`, which is what normally sets this — and `abort()`
|
|
136
|
+
// branches on it: with `_sent` false it treats the request as never started,
|
|
137
|
+
// silently `_reset()`s, and dispatches NOTHING. The app's fetch promise would
|
|
138
|
+
// then never settle (a hang), and for a synthesized response our own timer
|
|
139
|
+
// would afterwards fire 'load' on a reset object, resolving a fetch the app
|
|
140
|
+
// had already aborted. Claiming it means abort() dispatches DONE + 'abort'
|
|
141
|
+
// and whatwg-fetch rejects with AbortError, as it should.
|
|
142
|
+
internals._sent = true;
|
|
143
|
+
if (outcome.kind !== "delay") {
|
|
144
|
+
// Buoy's own `open()` swizzle overwrites RN's private `_requestId` with a
|
|
145
|
+
// string id. RN normally clobbers it back with the native numeric id inside
|
|
146
|
+
// `send()` — which we're skipping — so it would survive here, and
|
|
147
|
+
// `abort()` would hand that string to `RCTNetworking.abortRequest()`.
|
|
148
|
+
// Nulling it makes abort a clean no-op on the native side.
|
|
149
|
+
internals._requestId = null;
|
|
150
|
+
}
|
|
151
|
+
let timer = null;
|
|
152
|
+
const onAbort = () => {
|
|
153
|
+
if (timer !== null) {
|
|
154
|
+
clearTimeout(timer);
|
|
155
|
+
timer = null;
|
|
156
|
+
}
|
|
157
|
+
};
|
|
158
|
+
xhr.addEventListener("abort", onAbort);
|
|
159
|
+
const fire = () => {
|
|
160
|
+
timer = null;
|
|
161
|
+
xhr.removeEventListener("abort", onAbort);
|
|
162
|
+
// The real guard. `_aborted` is set unconditionally by `abort()` and
|
|
163
|
+
// survives its `_reset()`, so this is true whether or not the 'abort' event
|
|
164
|
+
// was ever dispatched.
|
|
165
|
+
if (internals._aborted) return;
|
|
166
|
+
if (outcome.kind === "delay") {
|
|
167
|
+
// Hand the state machine back before RN's `send()` sees it: it throws
|
|
168
|
+
// 'Request has already been sent' on a truthy `_sent`.
|
|
169
|
+
internals._sent = false;
|
|
170
|
+
passThrough();
|
|
171
|
+
return;
|
|
172
|
+
}
|
|
173
|
+
if (outcome.kind === "fail") {
|
|
174
|
+
fail(xhr, outcome);
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
177
|
+
respond(xhr, url, outcome);
|
|
178
|
+
};
|
|
179
|
+
|
|
180
|
+
// ALWAYS asynchronous, even at 0ms. Callers legitimately attach handlers
|
|
181
|
+
// after `send()` returns (axios does), and a synchronous response would fire
|
|
182
|
+
// 'load' before they exist.
|
|
183
|
+
timer = setTimeout(fire, outcome.delayMs);
|
|
184
|
+
return true;
|
|
185
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* The seam between the interceptor and the rule store.
|
|
5
|
+
*
|
|
6
|
+
* It exists to keep the import graph acyclic: `networkListener` needs rules,
|
|
7
|
+
* and the rule store needs the listener (to keep the swizzle installed while a
|
|
8
|
+
* rule is active). Rather than have them import each other, the store REGISTERS
|
|
9
|
+
* itself here and the listener asks here. Both sides import only this leaf.
|
|
10
|
+
*
|
|
11
|
+
* It's also the single place the production gate lives — see `overrideForRequest`.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { findMatchingRule, shouldApply, toOutcome } from "./resolveOverride";
|
|
15
|
+
let hooks = null;
|
|
16
|
+
export function setOverrideHooks(next) {
|
|
17
|
+
hooks = next;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Diagnostics for the sync adapter's `debugOverrides` action.
|
|
22
|
+
*
|
|
23
|
+
* Reads the engine's own view of the world — crucially `hooks`, which is
|
|
24
|
+
* MODULE state here: if the interceptor and the rule store ever ended up with
|
|
25
|
+
* separate copies of this file, this is the only place that shows it.
|
|
26
|
+
*/
|
|
27
|
+
export function inspectEngine(url, method) {
|
|
28
|
+
return {
|
|
29
|
+
hooksInstalled: hooks !== null,
|
|
30
|
+
devFlag: typeof __DEV__ === "undefined" ? "undefined" : __DEV__,
|
|
31
|
+
ruleCount: hooks ? hooks.getRules().length : 0,
|
|
32
|
+
// Non-recording probe — a diagnostic must not burn a rule's `times` budget
|
|
33
|
+
// or nudge an alternating rule's phase.
|
|
34
|
+
matches: hooks ? findMatchingRule(url, method, hooks.getRules()) !== null : false
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Resolve an override for one outgoing request, recording the hit.
|
|
40
|
+
*
|
|
41
|
+
* NEVER ACTIVE IN A PRODUCTION BUILD. A shipped app that synthesizes 500s or
|
|
42
|
+
* silently swallows requests is a catastrophe of a different order than a
|
|
43
|
+
* broken dev tool, so the gate is here — at the single entry point — rather
|
|
44
|
+
* than trusted to every call site.
|
|
45
|
+
*/
|
|
46
|
+
export function overrideForRequest(url, method) {
|
|
47
|
+
if (!hooks) return null;
|
|
48
|
+
// `__DEV__` is undefined outside React Native (unit tests, the desktop
|
|
49
|
+
// dashboard's bundle); only an explicit `false` — a real release build —
|
|
50
|
+
// disables overrides.
|
|
51
|
+
if (typeof __DEV__ !== "undefined" && !__DEV__) return null;
|
|
52
|
+
const rules = hooks.getRules();
|
|
53
|
+
if (rules.length === 0) return null;
|
|
54
|
+
const rule = findMatchingRule(url, method, rules);
|
|
55
|
+
if (!rule) return null;
|
|
56
|
+
|
|
57
|
+
// Phase is read BEFORE the match is recorded, so a fresh alternating rule
|
|
58
|
+
// (`seen === 0`) lets this first request through and takes the next one.
|
|
59
|
+
const applies = shouldApply(rule);
|
|
60
|
+
hooks.recordMatch(rule.id);
|
|
61
|
+
if (!applies) return null;
|
|
62
|
+
hooks.recordHit(rule.id);
|
|
63
|
+
return toOutcome(rule);
|
|
64
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Glob matching for override rules — a faithful port of Chrome DevTools'.
|
|
5
|
+
*
|
|
6
|
+
* Ported from `NetworkPersistenceManager.ts:1051` + `StringUtilities.ts:9` in
|
|
7
|
+
* the devtools-frontend source. Ported rather than reinvented on purpose: `*`
|
|
8
|
+
* globs are the syntax developers already have in their fingers from Chrome's
|
|
9
|
+
* "Override headers" `applyTo` field, and a pattern that behaves differently
|
|
10
|
+
* here than it does in the browser would be a quiet trap.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Chrome's escape set, character for character.
|
|
15
|
+
*
|
|
16
|
+
* `*` is deliberately ABSENT — it survives escaping so it can become `.*`
|
|
17
|
+
* below. Everything else that means something to a regex is neutered.
|
|
18
|
+
*/
|
|
19
|
+
const SPECIAL_CHARACTERS = "[]{}()\\.^$+|-,?";
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Escape a glob into regex source: neutralize regex metacharacters, then
|
|
23
|
+
* promote `*` to `.*`.
|
|
24
|
+
*
|
|
25
|
+
* `split`/`join` rather than `replaceAll` — same result, no reliance on how
|
|
26
|
+
* complete a given Hermes build's ES2021 surface is.
|
|
27
|
+
*/
|
|
28
|
+
export function escapeRegex(pattern) {
|
|
29
|
+
let result = "";
|
|
30
|
+
for (let i = 0; i < pattern.length; i++) {
|
|
31
|
+
const char = pattern.charAt(i);
|
|
32
|
+
if (SPECIAL_CHARACTERS.indexOf(char) !== -1) {
|
|
33
|
+
result += "\\";
|
|
34
|
+
}
|
|
35
|
+
result += char;
|
|
36
|
+
}
|
|
37
|
+
return result.split("*").join(".*");
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Compiled patterns, cached by source.
|
|
42
|
+
*
|
|
43
|
+
* `resolveOverride` runs on EVERY request the app makes, against every rule.
|
|
44
|
+
* Recompiling a RegExp per request per rule is pure waste in the one place
|
|
45
|
+
* this feature is allowed to cost nothing.
|
|
46
|
+
*/
|
|
47
|
+
const compiled = new Map();
|
|
48
|
+
export function compilePattern(pattern) {
|
|
49
|
+
const cached = compiled.get(pattern);
|
|
50
|
+
if (cached) return cached;
|
|
51
|
+
// Anchored, like Chrome's `^${...}$` — a pattern describes the whole URL, so
|
|
52
|
+
// `pokeapi.co` matches nothing and `*pokeapi.co*` matches everything on that
|
|
53
|
+
// host. Predictable beats clever; the rule editor writes full URLs for you.
|
|
54
|
+
const regex = new RegExp(`^${escapeRegex(pattern)}$`);
|
|
55
|
+
// Unbounded growth isn't possible in practice (patterns come from a capped
|
|
56
|
+
// rule list), but a user editing a pattern character by character churns
|
|
57
|
+
// entries, so keep the cache from becoming a leak.
|
|
58
|
+
if (compiled.size > 200) compiled.clear();
|
|
59
|
+
compiled.set(pattern, regex);
|
|
60
|
+
return regex;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** Does this rule apply to this request? Ignores `enabled` — callers check it. */
|
|
64
|
+
export function ruleMatches(rule, url, method) {
|
|
65
|
+
if (rule.methods && rule.methods.length > 0) {
|
|
66
|
+
if (!rule.methods.includes(method.toUpperCase())) return false;
|
|
67
|
+
}
|
|
68
|
+
try {
|
|
69
|
+
return compilePattern(rule.urlPattern).test(url);
|
|
70
|
+
} catch {
|
|
71
|
+
// A pattern that can't compile matches nothing rather than throwing inside
|
|
72
|
+
// the app's `send()`. The rule editor validates; this is the backstop.
|
|
73
|
+
return false;
|
|
74
|
+
}
|
|
75
|
+
}
|