1 | import XHRInterceptor from "react-native/Libraries/Network/XHRInterceptor"
|
2 | import type { ReactotronCore, Plugin } from "reactotron-core-client"
|
3 |
|
4 |
|
5 |
|
6 |
|
7 | const DEFAULT_CONTENT_TYPES_RX = /^(image)\/.*$/i
|
8 |
|
9 | export interface NetworkingOptions {
|
10 | ignoreContentTypes?: RegExp
|
11 | ignoreUrls?: RegExp
|
12 | }
|
13 |
|
14 | const DEFAULTS: NetworkingOptions = {}
|
15 |
|
16 | const networking =
|
17 | (pluginConfig: NetworkingOptions = {}) =>
|
18 | (reactotron: ReactotronCore) => {
|
19 | const options = Object.assign({}, DEFAULTS, pluginConfig)
|
20 |
|
21 |
|
22 | const ignoreContentTypes = options.ignoreContentTypes || DEFAULT_CONTENT_TYPES_RX
|
23 |
|
24 |
|
25 | let reactotronCounter = 1000
|
26 |
|
27 |
|
28 | const requestCache = {}
|
29 |
|
30 | |
31 |
|
32 |
|
33 |
|
34 |
|
35 |
|
36 | function onSend(data, xhr) {
|
37 | if (options.ignoreUrls && options.ignoreUrls.test(xhr._url)) {
|
38 | xhr._skipReactotron = true
|
39 | return
|
40 | }
|
41 |
|
42 |
|
43 | reactotronCounter++
|
44 |
|
45 |
|
46 | xhr._trackingName = reactotronCounter
|
47 |
|
48 |
|
49 | requestCache[reactotronCounter] = {
|
50 | data,
|
51 | xhr,
|
52 | stopTimer: reactotron.startTimer(),
|
53 | }
|
54 | }
|
55 |
|
56 | |
57 |
|
58 |
|
59 |
|
60 |
|
61 |
|
62 |
|
63 |
|
64 |
|
65 |
|
66 | function onResponse(status, timeout, response, url, type, xhr) {
|
67 | if (xhr._skipReactotron) {
|
68 | return
|
69 | }
|
70 |
|
71 | let params = null
|
72 | const queryParamIdx = url ? url.indexOf("?") : -1
|
73 | if (queryParamIdx > -1) {
|
74 | params = {}
|
75 | url
|
76 | .substr(queryParamIdx + 1)
|
77 | .split("&")
|
78 | .forEach((pair) => {
|
79 | const [key, value] = pair.split("=")
|
80 | if (key && value !== undefined) {
|
81 | params[key] = decodeURIComponent(value.replace(/\+/g, " "))
|
82 | }
|
83 | })
|
84 | }
|
85 |
|
86 |
|
87 | const rid = xhr._trackingName
|
88 | const cachedRequest = requestCache[rid] || { xhr }
|
89 | requestCache[rid] = null
|
90 |
|
91 |
|
92 | const { data, stopTimer } = cachedRequest
|
93 | const tronRequest = {
|
94 | url: url || cachedRequest.xhr._url,
|
95 | method: xhr._method || null,
|
96 | data,
|
97 | headers: xhr._headers || null,
|
98 | params,
|
99 | }
|
100 |
|
101 |
|
102 | const contentType =
|
103 | (xhr.responseHeaders && xhr.responseHeaders["content-type"]) ||
|
104 | (xhr.responseHeaders && xhr.responseHeaders["Content-Type"]) ||
|
105 | ""
|
106 |
|
107 | const sendResponse = (responseBodyText) => {
|
108 | let body = `~~~ skipped ~~~`
|
109 | if (responseBodyText) {
|
110 | try {
|
111 |
|
112 | body = JSON.parse(responseBodyText)
|
113 | } catch (boom) {
|
114 | body = response
|
115 | }
|
116 | }
|
117 | const tronResponse = {
|
118 | body,
|
119 | status,
|
120 | headers: xhr.responseHeaders || null,
|
121 | }
|
122 | ;(reactotron as any).apiResponse(tronRequest, tronResponse, stopTimer ? stopTimer() : null)
|
123 | }
|
124 |
|
125 |
|
126 | const useRealResponse =
|
127 | (typeof response === "string" || typeof response === "object") &&
|
128 | !ignoreContentTypes.test(contentType || "")
|
129 |
|
130 |
|
131 | if (useRealResponse) {
|
132 | if (type === "blob" && typeof FileReader !== "undefined" && response) {
|
133 |
|
134 |
|
135 | const bReader = new FileReader()
|
136 | const brListener = () => {
|
137 | sendResponse(bReader.result)
|
138 | bReader.removeEventListener("loadend", brListener)
|
139 | }
|
140 | bReader.addEventListener("loadend", brListener)
|
141 | bReader.readAsText(response)
|
142 | } else {
|
143 | sendResponse(response)
|
144 | }
|
145 | } else {
|
146 | sendResponse("")
|
147 | }
|
148 | }
|
149 |
|
150 | return {
|
151 | onConnect: () => {
|
152 |
|
153 | XHRInterceptor.setSendCallback(onSend)
|
154 | XHRInterceptor.setResponseCallback(onResponse)
|
155 | XHRInterceptor.enableInterception()
|
156 | },
|
157 | } satisfies Plugin<ReactotronCore>
|
158 | }
|
159 | export default networking
|