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.
@@ -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
+ ⚛️&nbsp;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
+ }
@@ -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
+ };