dreaction-react 1.0.0

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/src/hooks.ts ADDED
@@ -0,0 +1,129 @@
1
+ import { useState, useEffect, useRef } from 'react';
2
+ import { dreaction } from './dreaction';
3
+
4
+ const STORAGE_KEY_HOST = 'DREACTION_host';
5
+ const STORAGE_KEY_PORT = 'DREACTION_port';
6
+ const STORAGE_KEY_AUTO_CONNECT = 'DREACTION_autoConnect';
7
+
8
+ /**
9
+ * Hook to manage DReaction connection status
10
+ */
11
+ export function useConnectionStatus() {
12
+ const [isConnected, setIsConnected] = useState(dreaction.connected);
13
+
14
+ useEffect(() => {
15
+ const checkConnection = () => {
16
+ setIsConnected(dreaction.connected);
17
+ };
18
+
19
+ // Check connection status periodically
20
+ const interval = setInterval(checkConnection, 1000);
21
+
22
+ return () => clearInterval(interval);
23
+ }, []);
24
+
25
+ return isConnected;
26
+ }
27
+
28
+ /**
29
+ * Hook to manage DReaction configuration
30
+ */
31
+ export function useDReactionConfig() {
32
+ const [host, setHost] = useState(() => {
33
+ if (typeof window !== 'undefined' && window.localStorage) {
34
+ return window.localStorage.getItem(STORAGE_KEY_HOST) || 'localhost';
35
+ }
36
+ return 'localhost';
37
+ });
38
+
39
+ const [port, setPort] = useState(() => {
40
+ if (typeof window !== 'undefined' && window.localStorage) {
41
+ const stored = window.localStorage.getItem(STORAGE_KEY_PORT);
42
+ return stored ? parseInt(stored, 10) : 9600;
43
+ }
44
+ return 9600;
45
+ });
46
+
47
+ const [isConnected, setIsConnected] = useState(dreaction.connected);
48
+ const hasAutoConnected = useRef(false);
49
+
50
+ // Auto-connect on mount if previously connected
51
+ useEffect(() => {
52
+ if (hasAutoConnected.current) return;
53
+
54
+ if (typeof window !== 'undefined' && window.localStorage) {
55
+ const shouldAutoConnect = window.localStorage.getItem(
56
+ STORAGE_KEY_AUTO_CONNECT
57
+ );
58
+ if (shouldAutoConnect === 'true' && !dreaction.connected) {
59
+ hasAutoConnected.current = true;
60
+ // Auto-reconnect with saved settings
61
+ dreaction.configure({
62
+ host,
63
+ port,
64
+ });
65
+ dreaction.connect();
66
+ setIsConnected(true);
67
+ }
68
+ }
69
+ }, [host, port]);
70
+
71
+ useEffect(() => {
72
+ const checkConnection = () => {
73
+ setIsConnected(dreaction.connected);
74
+ };
75
+
76
+ // Check connection status periodically
77
+ const interval = setInterval(checkConnection, 1000);
78
+
79
+ return () => clearInterval(interval);
80
+ }, []);
81
+
82
+ const updateHost = (newHost: string) => {
83
+ setHost(newHost);
84
+ if (typeof window !== 'undefined' && window.localStorage) {
85
+ window.localStorage.setItem(STORAGE_KEY_HOST, newHost);
86
+ }
87
+ };
88
+
89
+ const updatePort = (newPort: number) => {
90
+ setPort(newPort);
91
+ if (typeof window !== 'undefined' && window.localStorage) {
92
+ window.localStorage.setItem(STORAGE_KEY_PORT, newPort.toString());
93
+ }
94
+ };
95
+
96
+ const connect = () => {
97
+ dreaction.configure({
98
+ host,
99
+ port,
100
+ });
101
+ dreaction.connect();
102
+ setIsConnected(true);
103
+
104
+ // Save auto-connect preference
105
+ if (typeof window !== 'undefined' && window.localStorage) {
106
+ window.localStorage.setItem(STORAGE_KEY_AUTO_CONNECT, 'true');
107
+ }
108
+ };
109
+
110
+ const disconnect = () => {
111
+ dreaction.close();
112
+ setIsConnected(false);
113
+
114
+ // Clear auto-connect preference
115
+ if (typeof window !== 'undefined' && window.localStorage) {
116
+ window.localStorage.setItem(STORAGE_KEY_AUTO_CONNECT, 'false');
117
+ }
118
+ };
119
+
120
+ return {
121
+ host,
122
+ port,
123
+ isConnected,
124
+ updateHost,
125
+ updatePort,
126
+ connect,
127
+ disconnect,
128
+ };
129
+ }
package/src/index.ts ADDED
@@ -0,0 +1,17 @@
1
+ export { dreaction, reactCorePlugins } from './dreaction';
2
+ export type { DReactionReact, UseReactOptions } from './dreaction';
3
+ export { ConfigPanel } from './components/ConfigPanel';
4
+ export type { ConfigPanelProps } from './components/ConfigPanel';
5
+ export { useConnectionStatus, useDReactionConfig } from './hooks';
6
+
7
+ // Plugin exports
8
+ export { default as networking } from './plugins/networking';
9
+ export type { NetworkingOptions } from './plugins/networking';
10
+ export { default as localStorage } from './plugins/localStorage';
11
+ export type { LocalStorageOptions } from './plugins/localStorage';
12
+ export { default as trackGlobalLogs } from './plugins/trackGlobalLogs';
13
+ export { default as trackGlobalErrors } from './plugins/trackGlobalErrors';
14
+ export type {
15
+ TrackGlobalErrorsOptions,
16
+ ErrorStackFrame,
17
+ } from './plugins/trackGlobalErrors';
@@ -0,0 +1,100 @@
1
+ import type { DReactionCore, Plugin } from 'dreaction-client-core';
2
+
3
+ export interface LocalStorageOptions {
4
+ ignore?: string[];
5
+ }
6
+
7
+ const PLUGIN_DEFAULTS: LocalStorageOptions = {
8
+ ignore: [],
9
+ };
10
+
11
+ const localStorage =
12
+ (options?: LocalStorageOptions) => (dreaction: DReactionCore) => {
13
+ // setup configuration
14
+ const config = Object.assign({}, PLUGIN_DEFAULTS, options || {});
15
+ const ignore = config.ignore || PLUGIN_DEFAULTS.ignore;
16
+
17
+ let originalSetItem: typeof Storage.prototype.setItem;
18
+ let originalRemoveItem: typeof Storage.prototype.removeItem;
19
+ let originalClear: typeof Storage.prototype.clear;
20
+ let isIntercepted = false;
21
+
22
+ const sendToDReaction = (action: string, data?: any) => {
23
+ dreaction.send('asyncStorage.mutation', { action: action as any, data });
24
+ };
25
+
26
+ const setItem = function (this: Storage, key: string, value: string): void {
27
+ try {
28
+ if (ignore!.indexOf(key) < 0) {
29
+ sendToDReaction('setItem', { key, value });
30
+ }
31
+ } catch (e) {
32
+ // ignore errors
33
+ }
34
+ return originalSetItem.call(this, key, value);
35
+ };
36
+
37
+ const removeItem = function (this: Storage, key: string): void {
38
+ try {
39
+ if (ignore!.indexOf(key) < 0) {
40
+ sendToDReaction('removeItem', { key });
41
+ }
42
+ } catch (e) {
43
+ // ignore errors
44
+ }
45
+ return originalRemoveItem.call(this, key);
46
+ };
47
+
48
+ const clear = function (this: Storage): void {
49
+ try {
50
+ sendToDReaction('clear');
51
+ } catch (e) {
52
+ // ignore errors
53
+ }
54
+ return originalClear.call(this);
55
+ };
56
+
57
+ /**
58
+ * Hijacks the localStorage API.
59
+ */
60
+ const trackLocalStorage = () => {
61
+ if (isIntercepted) return;
62
+ if (typeof window === 'undefined' || !window.localStorage) return;
63
+
64
+ originalSetItem = Storage.prototype.setItem;
65
+ Storage.prototype.setItem = setItem;
66
+
67
+ originalRemoveItem = Storage.prototype.removeItem;
68
+ Storage.prototype.removeItem = removeItem;
69
+
70
+ originalClear = Storage.prototype.clear;
71
+ Storage.prototype.clear = clear;
72
+
73
+ isIntercepted = true;
74
+ };
75
+
76
+ const untrackLocalStorage = () => {
77
+ if (!isIntercepted) return;
78
+
79
+ Storage.prototype.setItem = originalSetItem;
80
+ Storage.prototype.removeItem = originalRemoveItem;
81
+ Storage.prototype.clear = originalClear;
82
+
83
+ isIntercepted = false;
84
+ };
85
+
86
+ return {
87
+ onConnect: () => {
88
+ trackLocalStorage();
89
+ },
90
+ onDisconnect: () => {
91
+ untrackLocalStorage();
92
+ },
93
+ features: {
94
+ trackLocalStorage,
95
+ untrackLocalStorage,
96
+ },
97
+ } satisfies Plugin<DReactionCore>;
98
+ };
99
+
100
+ export default localStorage;
@@ -0,0 +1,333 @@
1
+ import type { DReactionCore, Plugin } from 'dreaction-client-core';
2
+
3
+ /**
4
+ * Don't include the response bodies for images by default.
5
+ */
6
+ const DEFAULT_CONTENT_TYPES_RX = /^(image)\/.*$/i;
7
+
8
+ export interface NetworkingOptions {
9
+ ignoreContentTypes?: RegExp;
10
+ ignoreUrls?: RegExp;
11
+ }
12
+
13
+ const DEFAULTS: NetworkingOptions = {
14
+ ignoreUrls: /symbolicate/,
15
+ };
16
+
17
+ const networking =
18
+ (pluginConfig: NetworkingOptions = {}) =>
19
+ (dreaction: DReactionCore) => {
20
+ const options = Object.assign({}, DEFAULTS, pluginConfig);
21
+
22
+ // a RegExp to suppress adding the body cuz it costs a lot to serialize
23
+ const ignoreContentTypes =
24
+ options.ignoreContentTypes || DEFAULT_CONTENT_TYPES_RX;
25
+
26
+ // a XHR call tracker
27
+ let dreactionCounter = 1000;
28
+
29
+ // a temporary cache to hold requests so we can match up the data
30
+ const requestCache: Record<number, any> = {};
31
+
32
+ // Store original functions
33
+ let originalXHROpen: typeof XMLHttpRequest.prototype.open;
34
+ let originalXHRSend: typeof XMLHttpRequest.prototype.send;
35
+ let originalFetch: typeof window.fetch;
36
+
37
+ /**
38
+ * Intercept XMLHttpRequest
39
+ */
40
+ const interceptXHR = () => {
41
+ originalXHROpen = XMLHttpRequest.prototype.open;
42
+ originalXHRSend = XMLHttpRequest.prototype.send;
43
+
44
+ XMLHttpRequest.prototype.open = function (
45
+ method: string,
46
+ url: string | URL,
47
+ ...rest: any[]
48
+ ) {
49
+ (this as any)._method = method;
50
+ (this as any)._url = url.toString();
51
+ return originalXHROpen.apply(this, [method, url, ...rest] as any);
52
+ };
53
+
54
+ XMLHttpRequest.prototype.send = function (data?: any) {
55
+ const xhr = this as any;
56
+
57
+ if (options.ignoreUrls && options.ignoreUrls.test(xhr._url)) {
58
+ xhr._skipDReaction = true;
59
+ return originalXHRSend.apply(this, [data]);
60
+ }
61
+
62
+ // bump the counter
63
+ dreactionCounter++;
64
+
65
+ // tag
66
+ xhr._trackingName = dreactionCounter;
67
+
68
+ // cache
69
+ requestCache[dreactionCounter] = {
70
+ data,
71
+ xhr,
72
+ stopTimer: dreaction.startTimer(),
73
+ };
74
+
75
+ // Setup listener for response
76
+ const originalOnReadyStateChange = xhr.onreadystatechange;
77
+ xhr.onreadystatechange = function () {
78
+ if (originalOnReadyStateChange) {
79
+ originalOnReadyStateChange.apply(this, arguments as any);
80
+ }
81
+
82
+ if (xhr.readyState === 4) {
83
+ handleXHRResponse(xhr);
84
+ }
85
+ };
86
+
87
+ return originalXHRSend.apply(this, [data]);
88
+ };
89
+ };
90
+
91
+ /**
92
+ * Handle XHR response
93
+ */
94
+ const handleXHRResponse = (xhr: any) => {
95
+ if (xhr._skipDReaction) {
96
+ return;
97
+ }
98
+
99
+ const url = xhr._url;
100
+ let params = null;
101
+ const queryParamIdx = url ? url.indexOf('?') : -1;
102
+ if (queryParamIdx > -1) {
103
+ params = {} as Record<string, string>;
104
+ url
105
+ .substr(queryParamIdx + 1)
106
+ .split('&')
107
+ .forEach((pair: string) => {
108
+ const [key, value] = pair.split('=');
109
+ if (key && value !== undefined) {
110
+ params![key] = decodeURIComponent(value.replace(/\+/g, ' '));
111
+ }
112
+ });
113
+ }
114
+
115
+ // fetch and clear the request data from the cache
116
+ const rid = xhr._trackingName;
117
+ const cachedRequest = requestCache[rid] || { xhr };
118
+ requestCache[rid] = null;
119
+
120
+ // assemble the request object
121
+ const { data, stopTimer } = cachedRequest;
122
+
123
+ // Parse request headers
124
+ let requestHeaders: Record<string, string> = {};
125
+ try {
126
+ const requestHeaderString = xhr._requestHeaders;
127
+ if (requestHeaderString) {
128
+ requestHeaders = requestHeaderString;
129
+ }
130
+ } catch (e) {
131
+ // ignore
132
+ }
133
+
134
+ const tronRequest = {
135
+ url: url || cachedRequest.xhr._url,
136
+ method: xhr._method || null,
137
+ data,
138
+ headers: requestHeaders || null,
139
+ params,
140
+ };
141
+
142
+ // Parse response headers
143
+ let responseHeaders: Record<string, string> = {};
144
+ try {
145
+ const headersString = xhr.getAllResponseHeaders();
146
+ if (headersString) {
147
+ headersString.split('\r\n').forEach((line: string) => {
148
+ const parts = line.split(': ');
149
+ if (parts.length === 2) {
150
+ responseHeaders[parts[0]] = parts[1];
151
+ }
152
+ });
153
+ }
154
+ } catch (e) {
155
+ // ignore
156
+ }
157
+
158
+ // what type of content is this?
159
+ const contentType =
160
+ responseHeaders['content-type'] ||
161
+ responseHeaders['Content-Type'] ||
162
+ '';
163
+
164
+ let body = `~~~ skipped ~~~`;
165
+ const response = xhr.response || xhr.responseText;
166
+
167
+ // can we use the real response?
168
+ const useRealResponse =
169
+ (typeof response === 'string' || typeof response === 'object') &&
170
+ !ignoreContentTypes.test(contentType || '');
171
+
172
+ if (useRealResponse && response) {
173
+ try {
174
+ // Try to parse JSON
175
+ if (typeof response === 'string') {
176
+ body = JSON.parse(response);
177
+ } else {
178
+ body = response;
179
+ }
180
+ } catch (e) {
181
+ body = response;
182
+ }
183
+ }
184
+
185
+ const tronResponse = {
186
+ body,
187
+ status: xhr.status,
188
+ headers: responseHeaders || null,
189
+ };
190
+
191
+ (dreaction as any).apiResponse(
192
+ tronRequest,
193
+ tronResponse,
194
+ stopTimer ? stopTimer() : null
195
+ );
196
+ };
197
+
198
+ /**
199
+ * Intercept fetch API
200
+ */
201
+ const interceptFetch = () => {
202
+ originalFetch = window.fetch;
203
+
204
+ window.fetch = function (
205
+ input: RequestInfo | URL,
206
+ init?: RequestInit
207
+ ): Promise<Response> {
208
+ const url =
209
+ typeof input === 'string'
210
+ ? input
211
+ : input instanceof Request
212
+ ? input.url
213
+ : input.toString();
214
+
215
+ if (options.ignoreUrls && options.ignoreUrls.test(url)) {
216
+ return originalFetch.call(this, input as RequestInfo, init);
217
+ }
218
+
219
+ // bump the counter
220
+ dreactionCounter++;
221
+ const requestId = dreactionCounter;
222
+
223
+ const stopTimer = dreaction.startTimer();
224
+
225
+ // Parse URL and params
226
+ let params = null;
227
+ const queryParamIdx = url.indexOf('?');
228
+ if (queryParamIdx > -1) {
229
+ params = {} as Record<string, string>;
230
+ url
231
+ .substr(queryParamIdx + 1)
232
+ .split('&')
233
+ .forEach((pair: string) => {
234
+ const [key, value] = pair.split('=');
235
+ if (key && value !== undefined) {
236
+ params![key] = decodeURIComponent(value.replace(/\+/g, ' '));
237
+ }
238
+ });
239
+ }
240
+
241
+ const tronRequest = {
242
+ url,
243
+ method: init?.method || 'GET',
244
+ data: init?.body || null,
245
+ headers: init?.headers || {},
246
+ params,
247
+ };
248
+
249
+ return originalFetch
250
+ .call(this, input as RequestInfo, init)
251
+ .then(async (response) => {
252
+ const contentType = response.headers.get('content-type') || '';
253
+
254
+ // Clone the response so we can read it
255
+ const clonedResponse = response.clone();
256
+
257
+ let body = `~~~ skipped ~~~`;
258
+ const useRealResponse = !ignoreContentTypes.test(contentType);
259
+
260
+ if (useRealResponse) {
261
+ try {
262
+ if (contentType.includes('application/json')) {
263
+ body = await clonedResponse.json();
264
+ } else {
265
+ body = await clonedResponse.text();
266
+ }
267
+ } catch (e) {
268
+ // ignore parsing errors
269
+ }
270
+ }
271
+
272
+ // Parse response headers
273
+ const responseHeaders: Record<string, string> = {};
274
+ response.headers.forEach((value: string, key: string) => {
275
+ responseHeaders[key] = value;
276
+ });
277
+
278
+ const tronResponse = {
279
+ body,
280
+ status: response.status,
281
+ headers: responseHeaders,
282
+ };
283
+
284
+ (dreaction as any).apiResponse(
285
+ tronRequest,
286
+ tronResponse,
287
+ stopTimer()
288
+ );
289
+
290
+ return response;
291
+ });
292
+ };
293
+ };
294
+
295
+ /**
296
+ * Restore original functions
297
+ */
298
+ const restoreXHR = () => {
299
+ if (originalXHROpen) {
300
+ XMLHttpRequest.prototype.open = originalXHROpen;
301
+ }
302
+ if (originalXHRSend) {
303
+ XMLHttpRequest.prototype.send = originalXHRSend;
304
+ }
305
+ };
306
+
307
+ const restoreFetch = () => {
308
+ if (originalFetch) {
309
+ window.fetch = originalFetch;
310
+ }
311
+ };
312
+
313
+ return {
314
+ onConnect: () => {
315
+ // register our interceptors
316
+ if (typeof XMLHttpRequest !== 'undefined') {
317
+ interceptXHR();
318
+ }
319
+ if (
320
+ typeof window !== 'undefined' &&
321
+ typeof window.fetch === 'function'
322
+ ) {
323
+ interceptFetch();
324
+ }
325
+ },
326
+ onDisconnect: () => {
327
+ restoreXHR();
328
+ restoreFetch();
329
+ },
330
+ } satisfies Plugin<DReactionCore>;
331
+ };
332
+
333
+ export default networking;