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/README.md +255 -0
- package/lib/components/ConfigPanel.d.ts +12 -0
- package/lib/components/ConfigPanel.d.ts.map +1 -0
- package/lib/components/ConfigPanel.js +151 -0
- package/lib/dreaction.d.ts +32 -0
- package/lib/dreaction.d.ts.map +1 -0
- package/lib/dreaction.js +110 -0
- package/lib/hooks.d.ts +17 -0
- package/lib/hooks.d.ts.map +1 -0
- package/lib/hooks.js +111 -0
- package/lib/index.d.ts +13 -0
- package/lib/index.d.ts.map +1 -0
- package/lib/index.js +23 -0
- package/lib/plugins/localStorage.d.ts +14 -0
- package/lib/plugins/localStorage.d.ts.map +1 -0
- package/lib/plugins/localStorage.js +85 -0
- package/lib/plugins/networking.d.ts +11 -0
- package/lib/plugins/networking.d.ts.map +1 -0
- package/lib/plugins/networking.js +263 -0
- package/lib/plugins/trackGlobalErrors.d.ts +25 -0
- package/lib/plugins/trackGlobalErrors.d.ts.map +1 -0
- package/lib/plugins/trackGlobalErrors.js +149 -0
- package/lib/plugins/trackGlobalLogs.d.ts +10 -0
- package/lib/plugins/trackGlobalLogs.d.ts.map +1 -0
- package/lib/plugins/trackGlobalLogs.js +42 -0
- package/package.json +35 -0
- package/src/components/ConfigPanel.tsx +247 -0
- package/src/dreaction.ts +181 -0
- package/src/hooks.ts +129 -0
- package/src/index.ts +17 -0
- package/src/plugins/localStorage.ts +100 -0
- package/src/plugins/networking.ts +333 -0
- package/src/plugins/trackGlobalErrors.ts +203 -0
- package/src/plugins/trackGlobalLogs.ts +53 -0
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
/**
|
|
4
|
+
* Provides a global error handler to report errors.
|
|
5
|
+
*/
|
|
6
|
+
const dreaction_client_core_1 = require("dreaction-client-core");
|
|
7
|
+
// defaults
|
|
8
|
+
const PLUGIN_DEFAULTS = {
|
|
9
|
+
veto: undefined,
|
|
10
|
+
};
|
|
11
|
+
const objectifyError = (error) => {
|
|
12
|
+
const objectifiedError = {};
|
|
13
|
+
Object.getOwnPropertyNames(error).forEach((key) => {
|
|
14
|
+
objectifiedError[key] = error[key];
|
|
15
|
+
});
|
|
16
|
+
return objectifiedError;
|
|
17
|
+
};
|
|
18
|
+
/**
|
|
19
|
+
* Parse error stack trace
|
|
20
|
+
*/
|
|
21
|
+
const parseErrorStack = (error) => {
|
|
22
|
+
if (!error.stack) {
|
|
23
|
+
return [];
|
|
24
|
+
}
|
|
25
|
+
const frames = [];
|
|
26
|
+
const lines = error.stack.split('\n');
|
|
27
|
+
// Skip the first line (error message)
|
|
28
|
+
for (let i = 1; i < lines.length; i++) {
|
|
29
|
+
const line = lines[i].trim();
|
|
30
|
+
// Try to match different stack trace formats
|
|
31
|
+
// Format: at functionName (fileName:lineNumber:columnNumber)
|
|
32
|
+
let match = line.match(/at\s+(.+?)\s+\((.+?):(\d+):(\d+)\)/);
|
|
33
|
+
if (match) {
|
|
34
|
+
frames.push({
|
|
35
|
+
functionName: match[1],
|
|
36
|
+
fileName: match[2],
|
|
37
|
+
lineNumber: parseInt(match[3], 10),
|
|
38
|
+
columnNumber: parseInt(match[4], 10),
|
|
39
|
+
});
|
|
40
|
+
continue;
|
|
41
|
+
}
|
|
42
|
+
// Format: at fileName:lineNumber:columnNumber
|
|
43
|
+
match = line.match(/at\s+(.+?):(\d+):(\d+)/);
|
|
44
|
+
if (match) {
|
|
45
|
+
frames.push({
|
|
46
|
+
functionName: '<anonymous>',
|
|
47
|
+
fileName: match[1],
|
|
48
|
+
lineNumber: parseInt(match[2], 10),
|
|
49
|
+
columnNumber: parseInt(match[3], 10),
|
|
50
|
+
});
|
|
51
|
+
continue;
|
|
52
|
+
}
|
|
53
|
+
// Format: functionName@fileName:lineNumber:columnNumber (Firefox)
|
|
54
|
+
match = line.match(/(.+?)@(.+?):(\d+):(\d+)/);
|
|
55
|
+
if (match) {
|
|
56
|
+
frames.push({
|
|
57
|
+
functionName: match[1] || '<anonymous>',
|
|
58
|
+
fileName: match[2],
|
|
59
|
+
lineNumber: parseInt(match[3], 10),
|
|
60
|
+
columnNumber: parseInt(match[4], 10),
|
|
61
|
+
});
|
|
62
|
+
continue;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
return frames;
|
|
66
|
+
};
|
|
67
|
+
/**
|
|
68
|
+
* Track global errors and send them to DReaction logger.
|
|
69
|
+
*/
|
|
70
|
+
const trackGlobalErrors = (options) => (dreaction) => {
|
|
71
|
+
// make sure we have the logger plugin
|
|
72
|
+
(0, dreaction_client_core_1.assertHasLoggerPlugin)(dreaction);
|
|
73
|
+
const client = dreaction;
|
|
74
|
+
// setup configuration
|
|
75
|
+
const config = Object.assign({}, PLUGIN_DEFAULTS, options || {});
|
|
76
|
+
let originalWindowOnError;
|
|
77
|
+
let unhandledRejectionHandler = null;
|
|
78
|
+
// manually fire an error
|
|
79
|
+
function reportError(error, stack) {
|
|
80
|
+
try {
|
|
81
|
+
let prettyStackFrames = stack || parseErrorStack(error);
|
|
82
|
+
// does the dev want us to keep each frame?
|
|
83
|
+
if (config.veto) {
|
|
84
|
+
prettyStackFrames = prettyStackFrames.filter((frame) => config?.veto?.(frame));
|
|
85
|
+
}
|
|
86
|
+
client.error(error.message, prettyStackFrames);
|
|
87
|
+
}
|
|
88
|
+
catch (e) {
|
|
89
|
+
client.error('Unable to parse stack trace from error object', []);
|
|
90
|
+
client.debug(objectifyError(e));
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
// the dreaction plugin interface
|
|
94
|
+
return {
|
|
95
|
+
onConnect: () => {
|
|
96
|
+
if (typeof window === 'undefined')
|
|
97
|
+
return;
|
|
98
|
+
// Intercept window.onerror
|
|
99
|
+
originalWindowOnError = window.onerror;
|
|
100
|
+
window.onerror = function (message, source, lineno, colno, error) {
|
|
101
|
+
if (error) {
|
|
102
|
+
reportError(error);
|
|
103
|
+
}
|
|
104
|
+
else if (typeof message === 'string') {
|
|
105
|
+
const syntheticError = new Error(message);
|
|
106
|
+
const frames = [];
|
|
107
|
+
if (source) {
|
|
108
|
+
frames.push({
|
|
109
|
+
fileName: source,
|
|
110
|
+
functionName: '<unknown>',
|
|
111
|
+
lineNumber: lineno || 0,
|
|
112
|
+
columnNumber: colno || 0,
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
reportError(syntheticError, frames);
|
|
116
|
+
}
|
|
117
|
+
// Call original handler
|
|
118
|
+
if (originalWindowOnError) {
|
|
119
|
+
return originalWindowOnError.apply(this, arguments);
|
|
120
|
+
}
|
|
121
|
+
return false;
|
|
122
|
+
};
|
|
123
|
+
// Intercept unhandled promise rejections
|
|
124
|
+
unhandledRejectionHandler = (event) => {
|
|
125
|
+
const error = event.reason instanceof Error
|
|
126
|
+
? event.reason
|
|
127
|
+
: new Error(String(event.reason));
|
|
128
|
+
reportError(error);
|
|
129
|
+
};
|
|
130
|
+
window.addEventListener('unhandledrejection', unhandledRejectionHandler);
|
|
131
|
+
},
|
|
132
|
+
onDisconnect: () => {
|
|
133
|
+
if (typeof window === 'undefined')
|
|
134
|
+
return;
|
|
135
|
+
// Restore original handlers
|
|
136
|
+
if (originalWindowOnError) {
|
|
137
|
+
window.onerror = originalWindowOnError;
|
|
138
|
+
}
|
|
139
|
+
if (unhandledRejectionHandler) {
|
|
140
|
+
window.removeEventListener('unhandledrejection', unhandledRejectionHandler);
|
|
141
|
+
}
|
|
142
|
+
},
|
|
143
|
+
// attach these functions to the DReaction
|
|
144
|
+
features: {
|
|
145
|
+
reportError,
|
|
146
|
+
},
|
|
147
|
+
};
|
|
148
|
+
};
|
|
149
|
+
exports.default = trackGlobalErrors;
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { DReactionCore } from 'dreaction-client-core';
|
|
2
|
+
/**
|
|
3
|
+
* Track calls to console.log, console.warn, and console.debug and send them to DReaction logger
|
|
4
|
+
*/
|
|
5
|
+
declare const trackGlobalLogs: () => (dreaction: DReactionCore) => {
|
|
6
|
+
onConnect: () => void;
|
|
7
|
+
onDisconnect: () => void;
|
|
8
|
+
};
|
|
9
|
+
export default trackGlobalLogs;
|
|
10
|
+
//# sourceMappingURL=trackGlobalLogs.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"trackGlobalLogs.d.ts","sourceRoot":"","sources":["../../src/plugins/trackGlobalLogs.ts"],"names":[],"mappings":"AAAA,OAAO,EAGL,aAAa,EAGd,MAAM,uBAAuB,CAAC;AAE/B;;GAEG;AACH,QAAA,MAAM,eAAe,oBAAqB,aAAa;;;CAuCtD,CAAC;AAEF,eAAe,eAAe,CAAC"}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const dreaction_client_core_1 = require("dreaction-client-core");
|
|
4
|
+
/**
|
|
5
|
+
* Track calls to console.log, console.warn, and console.debug and send them to DReaction logger
|
|
6
|
+
*/
|
|
7
|
+
const trackGlobalLogs = () => (dreaction) => {
|
|
8
|
+
(0, dreaction_client_core_1.assertHasLoggerPlugin)(dreaction);
|
|
9
|
+
const client = dreaction;
|
|
10
|
+
const originalConsoleLog = console.log;
|
|
11
|
+
const originalConsoleWarn = console.warn;
|
|
12
|
+
const originalConsoleDebug = console.debug;
|
|
13
|
+
const originalConsoleInfo = console.info;
|
|
14
|
+
return {
|
|
15
|
+
onConnect: () => {
|
|
16
|
+
console.log = (...args) => {
|
|
17
|
+
originalConsoleLog(...args);
|
|
18
|
+
client.log(...args);
|
|
19
|
+
};
|
|
20
|
+
console.info = (...args) => {
|
|
21
|
+
originalConsoleInfo(...args);
|
|
22
|
+
client.log(...args);
|
|
23
|
+
};
|
|
24
|
+
console.warn = (...args) => {
|
|
25
|
+
originalConsoleWarn(...args);
|
|
26
|
+
client.warn(args[0]);
|
|
27
|
+
};
|
|
28
|
+
console.debug = (...args) => {
|
|
29
|
+
originalConsoleDebug(...args);
|
|
30
|
+
client.debug(args[0]);
|
|
31
|
+
};
|
|
32
|
+
// console.error is taken care of by ./trackGlobalErrors.ts
|
|
33
|
+
},
|
|
34
|
+
onDisconnect: () => {
|
|
35
|
+
console.log = originalConsoleLog;
|
|
36
|
+
console.warn = originalConsoleWarn;
|
|
37
|
+
console.debug = originalConsoleDebug;
|
|
38
|
+
console.info = originalConsoleInfo;
|
|
39
|
+
},
|
|
40
|
+
};
|
|
41
|
+
};
|
|
42
|
+
exports.default = trackGlobalLogs;
|
package/package.json
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "dreaction-react",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"private": false,
|
|
5
|
+
"description": "DReaction client for React web applications",
|
|
6
|
+
"main": "lib/index.js",
|
|
7
|
+
"files": [
|
|
8
|
+
"lib",
|
|
9
|
+
"src"
|
|
10
|
+
],
|
|
11
|
+
"keywords": [
|
|
12
|
+
"dreaction",
|
|
13
|
+
"react",
|
|
14
|
+
"debug"
|
|
15
|
+
],
|
|
16
|
+
"author": "moonrailgun <moonrailgun@gmail.com>",
|
|
17
|
+
"license": "MIT",
|
|
18
|
+
"dependencies": {
|
|
19
|
+
"dreaction-client-core": "1.2.0",
|
|
20
|
+
"dreaction-protocol": "1.0.8"
|
|
21
|
+
},
|
|
22
|
+
"devDependencies": {
|
|
23
|
+
"@types/react": "^18.0.0",
|
|
24
|
+
"@types/react-dom": "^18.0.0",
|
|
25
|
+
"typescript": "^5.4.5"
|
|
26
|
+
},
|
|
27
|
+
"peerDependencies": {
|
|
28
|
+
"react": ">=18",
|
|
29
|
+
"react-dom": ">=18"
|
|
30
|
+
},
|
|
31
|
+
"scripts": {
|
|
32
|
+
"dev": "tsc --watch",
|
|
33
|
+
"build": "tsc"
|
|
34
|
+
}
|
|
35
|
+
}
|
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
import React, { useState } from 'react';
|
|
2
|
+
import { useDReactionConfig } from '../hooks';
|
|
3
|
+
|
|
4
|
+
export interface ConfigPanelProps {
|
|
5
|
+
/**
|
|
6
|
+
* Initial collapsed state
|
|
7
|
+
*/
|
|
8
|
+
defaultCollapsed?: boolean;
|
|
9
|
+
/**
|
|
10
|
+
* Position of the panel
|
|
11
|
+
*/
|
|
12
|
+
position?: 'top-right' | 'top-left' | 'bottom-right' | 'bottom-left';
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function ConfigPanel({
|
|
16
|
+
defaultCollapsed = true,
|
|
17
|
+
position = 'top-right',
|
|
18
|
+
}: ConfigPanelProps) {
|
|
19
|
+
const [isCollapsed, setIsCollapsed] = useState(defaultCollapsed);
|
|
20
|
+
const {
|
|
21
|
+
host,
|
|
22
|
+
port,
|
|
23
|
+
isConnected,
|
|
24
|
+
updateHost,
|
|
25
|
+
updatePort,
|
|
26
|
+
connect,
|
|
27
|
+
disconnect,
|
|
28
|
+
} = useDReactionConfig();
|
|
29
|
+
|
|
30
|
+
const [localHost, setLocalHost] = useState(host);
|
|
31
|
+
const [localPort, setLocalPort] = useState(port.toString());
|
|
32
|
+
|
|
33
|
+
const handleConnect = () => {
|
|
34
|
+
updateHost(localHost);
|
|
35
|
+
updatePort(parseInt(localPort, 10));
|
|
36
|
+
connect();
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
const handleDisconnect = () => {
|
|
40
|
+
disconnect();
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
const positionStyles = {
|
|
44
|
+
'top-right': { top: '12px', right: '12px' },
|
|
45
|
+
'top-left': { top: '12px', left: '12px' },
|
|
46
|
+
'bottom-right': { bottom: '12px', right: '12px' },
|
|
47
|
+
'bottom-left': { bottom: '12px', left: '12px' },
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
const panelStyle: React.CSSProperties = {
|
|
51
|
+
position: 'fixed',
|
|
52
|
+
...positionStyles[position],
|
|
53
|
+
backgroundColor: '#1a1a1a',
|
|
54
|
+
color: '#ffffff',
|
|
55
|
+
borderRadius: isCollapsed ? '6px' : '8px',
|
|
56
|
+
boxShadow: isCollapsed
|
|
57
|
+
? '0 2px 8px rgba(0, 0, 0, 0.2)'
|
|
58
|
+
: '0 4px 12px rgba(0, 0, 0, 0.3)',
|
|
59
|
+
zIndex: 9999,
|
|
60
|
+
fontFamily: 'system-ui, -apple-system, sans-serif',
|
|
61
|
+
fontSize: '14px',
|
|
62
|
+
width: isCollapsed ? '120px' : '280px',
|
|
63
|
+
transition:
|
|
64
|
+
'width 0.2s ease-in-out, border-radius 0.2s ease-in-out, box-shadow 0.2s ease-in-out',
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
const headerStyle: React.CSSProperties = {
|
|
68
|
+
padding: '2px 6px',
|
|
69
|
+
borderBottom: isCollapsed ? '1px solid transparent' : '1px solid #333',
|
|
70
|
+
display: 'flex',
|
|
71
|
+
justifyContent: 'space-between',
|
|
72
|
+
alignItems: 'center',
|
|
73
|
+
cursor: 'pointer',
|
|
74
|
+
userSelect: 'none',
|
|
75
|
+
overflow: 'hidden',
|
|
76
|
+
whiteSpace: 'nowrap',
|
|
77
|
+
transition: 'border-bottom 0.2s ease-in-out',
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
const bodyStyle: React.CSSProperties = {
|
|
81
|
+
padding: isCollapsed ? '0 16px' : '16px',
|
|
82
|
+
maxHeight: isCollapsed ? '0' : '400px',
|
|
83
|
+
overflow: 'hidden',
|
|
84
|
+
opacity: isCollapsed ? 0 : 1,
|
|
85
|
+
transition:
|
|
86
|
+
'max-height 0.2s ease-in-out, opacity 0.2s ease-in-out, padding 0.2s ease-in-out',
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
const inputStyle: React.CSSProperties = {
|
|
90
|
+
width: '100%',
|
|
91
|
+
padding: '8px 12px',
|
|
92
|
+
marginTop: '4px',
|
|
93
|
+
backgroundColor: '#2a2a2a',
|
|
94
|
+
border: '1px solid #444',
|
|
95
|
+
borderRadius: '4px',
|
|
96
|
+
color: '#ffffff',
|
|
97
|
+
fontSize: '14px',
|
|
98
|
+
outline: 'none',
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
const buttonStyle: React.CSSProperties = {
|
|
102
|
+
width: '100%',
|
|
103
|
+
padding: '10px',
|
|
104
|
+
marginTop: '12px',
|
|
105
|
+
backgroundColor: isConnected ? '#dc2626' : '#10b981',
|
|
106
|
+
border: 'none',
|
|
107
|
+
borderRadius: '4px',
|
|
108
|
+
color: '#ffffff',
|
|
109
|
+
fontSize: '14px',
|
|
110
|
+
fontWeight: '600',
|
|
111
|
+
cursor: 'pointer',
|
|
112
|
+
transition: 'background-color 0.2s',
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
const statusStyle: React.CSSProperties = {
|
|
116
|
+
display: 'flex',
|
|
117
|
+
alignItems: 'center',
|
|
118
|
+
padding: '8px 12px',
|
|
119
|
+
marginTop: '8px',
|
|
120
|
+
backgroundColor: isConnected ? '#065f46' : '#7c2d12',
|
|
121
|
+
borderRadius: '4px',
|
|
122
|
+
fontSize: '13px',
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
const dotStyle: React.CSSProperties = {
|
|
126
|
+
width: '8px',
|
|
127
|
+
height: '8px',
|
|
128
|
+
borderRadius: '50%',
|
|
129
|
+
backgroundColor: isConnected ? '#10b981' : '#ef4444',
|
|
130
|
+
marginRight: '8px',
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
const labelStyle: React.CSSProperties = {
|
|
134
|
+
marginTop: '12px',
|
|
135
|
+
marginBottom: '4px',
|
|
136
|
+
fontSize: '13px',
|
|
137
|
+
fontWeight: '500',
|
|
138
|
+
color: '#a0a0a0',
|
|
139
|
+
};
|
|
140
|
+
|
|
141
|
+
const statusIndicatorStyle: React.CSSProperties = {
|
|
142
|
+
width: '6px',
|
|
143
|
+
height: '6px',
|
|
144
|
+
borderRadius: '50%',
|
|
145
|
+
backgroundColor: isConnected ? '#10b981' : '#ef4444',
|
|
146
|
+
marginLeft: '8px',
|
|
147
|
+
flexShrink: 0,
|
|
148
|
+
transition: 'background-color 0.2s ease-in-out',
|
|
149
|
+
boxShadow: isConnected
|
|
150
|
+
? '0 0 8px rgba(16, 185, 129, 0.6)'
|
|
151
|
+
: '0 0 12px rgba(239, 68, 68, 0.8)',
|
|
152
|
+
animation: isConnected
|
|
153
|
+
? 'none'
|
|
154
|
+
: 'pulse 1.5s cubic-bezier(0.4, 0, 0.6, 1) infinite',
|
|
155
|
+
};
|
|
156
|
+
|
|
157
|
+
return (
|
|
158
|
+
<>
|
|
159
|
+
<style>
|
|
160
|
+
{`
|
|
161
|
+
@keyframes pulse {
|
|
162
|
+
0%, 100% {
|
|
163
|
+
opacity: 1;
|
|
164
|
+
transform: scale(1);
|
|
165
|
+
}
|
|
166
|
+
50% {
|
|
167
|
+
opacity: 0.3;
|
|
168
|
+
transform: scale(1.3);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
`}
|
|
172
|
+
</style>
|
|
173
|
+
<div style={panelStyle}>
|
|
174
|
+
<div style={headerStyle} onClick={() => setIsCollapsed(!isCollapsed)}>
|
|
175
|
+
<div
|
|
176
|
+
style={{
|
|
177
|
+
display: 'flex',
|
|
178
|
+
alignItems: 'center',
|
|
179
|
+
flex: 1,
|
|
180
|
+
overflow: 'hidden',
|
|
181
|
+
}}
|
|
182
|
+
>
|
|
183
|
+
<div
|
|
184
|
+
style={{
|
|
185
|
+
fontWeight: '600',
|
|
186
|
+
fontSize: '10px',
|
|
187
|
+
}}
|
|
188
|
+
>
|
|
189
|
+
⚛️ DReaction
|
|
190
|
+
</div>
|
|
191
|
+
{isCollapsed && <div style={statusIndicatorStyle}></div>}
|
|
192
|
+
</div>
|
|
193
|
+
<div
|
|
194
|
+
style={{
|
|
195
|
+
fontSize: '14px',
|
|
196
|
+
marginLeft: '4px',
|
|
197
|
+
transform: isCollapsed ? 'rotate(0deg)' : 'rotate(180deg)',
|
|
198
|
+
transition: 'transform 0.2s ease-in-out',
|
|
199
|
+
}}
|
|
200
|
+
>
|
|
201
|
+
▼
|
|
202
|
+
</div>
|
|
203
|
+
</div>
|
|
204
|
+
|
|
205
|
+
<div style={bodyStyle}>
|
|
206
|
+
<div style={statusStyle}>
|
|
207
|
+
<div style={dotStyle}></div>
|
|
208
|
+
<span>{isConnected ? 'Connected' : 'Disconnected'}</span>
|
|
209
|
+
</div>
|
|
210
|
+
|
|
211
|
+
<div style={labelStyle}>Host</div>
|
|
212
|
+
<input
|
|
213
|
+
type="text"
|
|
214
|
+
value={localHost}
|
|
215
|
+
onChange={(e) => setLocalHost(e.target.value)}
|
|
216
|
+
placeholder="localhost"
|
|
217
|
+
style={inputStyle}
|
|
218
|
+
disabled={isConnected}
|
|
219
|
+
/>
|
|
220
|
+
|
|
221
|
+
<div style={labelStyle}>Port</div>
|
|
222
|
+
<input
|
|
223
|
+
type="number"
|
|
224
|
+
value={localPort}
|
|
225
|
+
onChange={(e) => setLocalPort(e.target.value)}
|
|
226
|
+
placeholder="9600"
|
|
227
|
+
style={inputStyle}
|
|
228
|
+
disabled={isConnected}
|
|
229
|
+
/>
|
|
230
|
+
|
|
231
|
+
<button
|
|
232
|
+
style={buttonStyle}
|
|
233
|
+
onClick={isConnected ? handleDisconnect : handleConnect}
|
|
234
|
+
onMouseOver={(e) => {
|
|
235
|
+
e.currentTarget.style.opacity = '0.9';
|
|
236
|
+
}}
|
|
237
|
+
onMouseOut={(e) => {
|
|
238
|
+
e.currentTarget.style.opacity = '1';
|
|
239
|
+
}}
|
|
240
|
+
>
|
|
241
|
+
{isConnected ? 'Disconnect' : 'Connect'}
|
|
242
|
+
</button>
|
|
243
|
+
</div>
|
|
244
|
+
</div>
|
|
245
|
+
</>
|
|
246
|
+
);
|
|
247
|
+
}
|
package/src/dreaction.ts
ADDED
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
import { createClient } from 'dreaction-client-core';
|
|
2
|
+
import type {
|
|
3
|
+
ClientOptions,
|
|
4
|
+
DReaction,
|
|
5
|
+
DReactionCore,
|
|
6
|
+
InferFeaturesFromPlugins,
|
|
7
|
+
PluginCreator,
|
|
8
|
+
} from 'dreaction-client-core';
|
|
9
|
+
import type { DataWatchPayload } from 'dreaction-protocol';
|
|
10
|
+
import { useEffect } from 'react';
|
|
11
|
+
import networking, { NetworkingOptions } from './plugins/networking';
|
|
12
|
+
import localStorage, { LocalStorageOptions } from './plugins/localStorage';
|
|
13
|
+
import trackGlobalLogs from './plugins/trackGlobalLogs';
|
|
14
|
+
import trackGlobalErrors, {
|
|
15
|
+
TrackGlobalErrorsOptions,
|
|
16
|
+
} from './plugins/trackGlobalErrors';
|
|
17
|
+
|
|
18
|
+
export type { ClientOptions };
|
|
19
|
+
|
|
20
|
+
const DREACTION_LOCAL_STORAGE_CLIENT_ID = 'DREACTION_clientId';
|
|
21
|
+
|
|
22
|
+
let tempClientId: string | null = null;
|
|
23
|
+
|
|
24
|
+
export const reactCorePlugins = [
|
|
25
|
+
trackGlobalErrors(),
|
|
26
|
+
trackGlobalLogs(),
|
|
27
|
+
localStorage(),
|
|
28
|
+
networking(),
|
|
29
|
+
] satisfies PluginCreator<DReactionCore>[];
|
|
30
|
+
|
|
31
|
+
export interface UseReactOptions {
|
|
32
|
+
errors?: TrackGlobalErrorsOptions | boolean;
|
|
33
|
+
log?: boolean;
|
|
34
|
+
localStorage?: LocalStorageOptions | boolean;
|
|
35
|
+
networking?: NetworkingOptions | boolean;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
type ReactPluginFeatures = InferFeaturesFromPlugins<
|
|
39
|
+
DReactionCore,
|
|
40
|
+
typeof reactCorePlugins
|
|
41
|
+
>;
|
|
42
|
+
|
|
43
|
+
export interface DReactionReact
|
|
44
|
+
extends DReaction,
|
|
45
|
+
// @ts-ignore
|
|
46
|
+
ReactPluginFeatures {
|
|
47
|
+
useReact: (options?: UseReactOptions) => this;
|
|
48
|
+
registerDataWatcher: <T = unknown>(
|
|
49
|
+
name: string,
|
|
50
|
+
type: DataWatchPayload['type'],
|
|
51
|
+
options?: {
|
|
52
|
+
/**
|
|
53
|
+
* Is data watcher enabled?
|
|
54
|
+
*/
|
|
55
|
+
enabled?: boolean;
|
|
56
|
+
}
|
|
57
|
+
) => {
|
|
58
|
+
currentDebugValue: T | undefined;
|
|
59
|
+
updateDebugValue: (data: unknown) => void;
|
|
60
|
+
useDebugDataWatch: (target: unknown) => void;
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const DEFAULTS: ClientOptions<DReactionReact> = {
|
|
65
|
+
createSocket: (path: string) => new WebSocket(path),
|
|
66
|
+
host: 'localhost',
|
|
67
|
+
port: 9600,
|
|
68
|
+
name: 'React Web App',
|
|
69
|
+
environment: process.env.NODE_ENV || 'development',
|
|
70
|
+
client: {
|
|
71
|
+
dreactionLibraryName: 'dreaction-react',
|
|
72
|
+
dreactionLibraryVersion: '1.0.0',
|
|
73
|
+
platform: 'web',
|
|
74
|
+
userAgent: typeof navigator !== 'undefined' ? navigator.userAgent : '',
|
|
75
|
+
},
|
|
76
|
+
getClientId: async (name: string = '') => {
|
|
77
|
+
if (typeof window !== 'undefined' && window.localStorage) {
|
|
78
|
+
const stored = window.localStorage.getItem(
|
|
79
|
+
DREACTION_LOCAL_STORAGE_CLIENT_ID
|
|
80
|
+
);
|
|
81
|
+
if (stored) {
|
|
82
|
+
return stored;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Generate clientId based on browser info
|
|
87
|
+
tempClientId = [
|
|
88
|
+
name,
|
|
89
|
+
'web',
|
|
90
|
+
typeof navigator !== 'undefined' ? navigator.userAgent : '',
|
|
91
|
+
Date.now(),
|
|
92
|
+
]
|
|
93
|
+
.filter(Boolean)
|
|
94
|
+
.join('-');
|
|
95
|
+
|
|
96
|
+
return tempClientId;
|
|
97
|
+
},
|
|
98
|
+
setClientId: async (clientId: string) => {
|
|
99
|
+
if (typeof window !== 'undefined' && window.localStorage) {
|
|
100
|
+
window.localStorage.setItem(DREACTION_LOCAL_STORAGE_CLIENT_ID, clientId);
|
|
101
|
+
} else {
|
|
102
|
+
tempClientId = clientId;
|
|
103
|
+
}
|
|
104
|
+
},
|
|
105
|
+
proxyHack: true,
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
export const dreaction = createClient<DReactionReact>(DEFAULTS);
|
|
109
|
+
|
|
110
|
+
function getPluginOptions<T>(options?: T | boolean): T | null {
|
|
111
|
+
return typeof options === 'object' ? options : null;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
dreaction.useReact = (options: UseReactOptions = {}) => {
|
|
115
|
+
if (options.errors !== false) {
|
|
116
|
+
dreaction.use(
|
|
117
|
+
trackGlobalErrors(getPluginOptions(options.errors as any)) as any
|
|
118
|
+
);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
if (options.log !== false) {
|
|
122
|
+
dreaction.use(trackGlobalLogs() as any);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
if (options.localStorage !== false) {
|
|
126
|
+
dreaction.use(
|
|
127
|
+
localStorage(getPluginOptions(options.localStorage) as any) as any
|
|
128
|
+
);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
if (options.networking !== false) {
|
|
132
|
+
dreaction.use(
|
|
133
|
+
networking(getPluginOptions(options.networking) as any) as any
|
|
134
|
+
);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
return dreaction;
|
|
138
|
+
};
|
|
139
|
+
|
|
140
|
+
dreaction.registerDataWatcher = <T = unknown>(
|
|
141
|
+
name: string,
|
|
142
|
+
type: DataWatchPayload['type'],
|
|
143
|
+
options?: {
|
|
144
|
+
/**
|
|
145
|
+
* Is data watcher enabled?
|
|
146
|
+
*/
|
|
147
|
+
enabled?: boolean;
|
|
148
|
+
}
|
|
149
|
+
) => {
|
|
150
|
+
const { enabled = process.env.NODE_ENV === 'development' } = options ?? {};
|
|
151
|
+
if (!enabled) {
|
|
152
|
+
return {
|
|
153
|
+
currentDebugValue: undefined,
|
|
154
|
+
updateDebugValue: () => {},
|
|
155
|
+
useDebugDataWatch: () => {},
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
let prev: T | undefined = undefined;
|
|
160
|
+
|
|
161
|
+
const updateDebugValue = (data: T | ((prev: T | undefined) => T)) => {
|
|
162
|
+
let newData = prev;
|
|
163
|
+
if (typeof data === 'function') {
|
|
164
|
+
newData = (data as (prev: T | undefined) => T)(prev);
|
|
165
|
+
} else {
|
|
166
|
+
newData = data;
|
|
167
|
+
}
|
|
168
|
+
prev = newData;
|
|
169
|
+
dreaction.send('dataWatch', { name, type, data: newData });
|
|
170
|
+
};
|
|
171
|
+
|
|
172
|
+
return {
|
|
173
|
+
currentDebugValue: prev,
|
|
174
|
+
updateDebugValue,
|
|
175
|
+
useDebugDataWatch: (target: T) => {
|
|
176
|
+
useEffect(() => {
|
|
177
|
+
updateDebugValue(target);
|
|
178
|
+
}, [target]);
|
|
179
|
+
},
|
|
180
|
+
};
|
|
181
|
+
};
|