@carbonorm/carbonreact 3.3.14 → 3.4.1

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/package.json CHANGED
@@ -1,13 +1,13 @@
1
1
  {
2
2
  "name": "@carbonorm/carbonreact",
3
- "version": "3.3.14",
3
+ "version": "3.4.1",
4
4
  "browser": "dist/index.umd.js",
5
5
  "module": "dist/index.esm.js",
6
6
  "main": "dist/index.cjs.js",
7
7
  "types": "dist/index.d.ts",
8
8
  "type": "module",
9
9
  "dependencies": {
10
- "@carbonorm/carbonnode": "^1.1.0",
10
+ "@carbonorm/carbonnode": "^1.2.4",
11
11
  "@fortawesome/fontawesome-svg-core": "^6.4.0",
12
12
  "@fortawesome/free-solid-svg-icons": "^6.4.0",
13
13
  "@fortawesome/react-fontawesome": "^0.2.0",
@@ -70,16 +70,19 @@
70
70
  "scripts": {
71
71
  "prepublishOnly": "npm run build",
72
72
  "c6": "wget https://raw.githubusercontent.com/RichardTMiles/CarbonPHP/lts/view/C6.tsx -O src/variables/C6.tsx",
73
- "build": "rm -rf ./dist/ && npm run build:css && npm run build:index && rollup -c && tsc compileValidSQL.tsx && mv compileValidSQL.js compileValidSQL.cjs",
73
+ "build": "rm -rf ./dist/ && npm run build:css && npm run build:index && rollup -c && npm run build:compileValidSQL",
74
74
  "dev": "rollup -c -w",
75
75
  "test": "node test/test.js",
76
76
  "pretest": "npm run build",
77
77
  "build:index": "npx barrelsby --directory ./src --delete --exclude '(jestHoc|\\.test|\\.d).(js|tsx?)$' --exportDefault --verbose --mode top",
78
78
  "build:css": "sass ./src:./src && typed-scss-modules 'src/**/*.?(s)css' --nameFormat all --exportType default src",
79
- "build:compileValidSQL": "ts-node --experimental-specifier-resolution=node compileValidSQL.tsx"
79
+ "build:compileValidSQL": "tsc compileValidSQL.tsx && mv compileValidSQL.js compileValidSQL.cjs"
80
80
  },
81
81
  "files": [
82
82
  "compileValidSQL.tsx",
83
+ "compileValidSQL.cjs",
84
+ "generateRestBindings.tsx",
85
+ "generateRestBindings.cjs",
83
86
  "dist",
84
87
  "src"
85
88
  ]
@@ -9,6 +9,7 @@ import 'react-toastify/dist/ReactToastify.min.css';
9
9
  import BackendThrowable from 'components/Errors/BackendThrowable';
10
10
  import Nest from 'components/Nest/Nest';
11
11
  import {initialRestfulObjectsState, iRestfulObjectArrayTypes} from "variables/C6";
12
+ import CarbonWebSocket from "./components/WebSocket/CarbonWebSocket";
12
13
 
13
14
 
14
15
 
@@ -33,6 +34,16 @@ export const initialCarbonReactState: iCarbonReactState & iRestfulObjectArrayTyp
33
34
  ...initialRestfulObjectsState,
34
35
  }
35
36
 
37
+ // @link https://stackoverflow.com/questions/3710204/how-to-check-if-a-string-is-a-valid-json-string
38
+ export function isJsonString(str) {
39
+ try {
40
+ JSON.parse(str);
41
+ } catch (e) {
42
+ return false;
43
+ }
44
+ return true;
45
+ }
46
+
36
47
  const CarbonReact= class <P = {}, S = {}> extends Component<{
37
48
  children?: ReactNode | ReactNode[],
38
49
  } & P, S & iCarbonReactState> {
@@ -42,6 +53,9 @@ const CarbonReact= class <P = {}, S = {}> extends Component<{
42
53
  } & any, any & iCarbonReactState>;
43
54
 
44
55
  static lastLocation = window.location.pathname;
56
+ static websocketUrl = (window.location.protocol === 'https:' ? 'wss://' : 'ws://') + window.location.host + ':8888/ws';
57
+ static websocketTimeoutSeconds : number = 250;
58
+ static websocketHeartbeatSeconds : number = 250;
45
59
 
46
60
  // @link https://github.com/welldone-software/why-did-you-render
47
61
  // noinspection JSUnusedGlobalSymbols
@@ -71,8 +85,6 @@ const CarbonReact= class <P = {}, S = {}> extends Component<{
71
85
  return CarbonReact.instance.state;
72
86
  }
73
87
 
74
- websocketTimeout = 5000;
75
-
76
88
  shouldComponentUpdate(
77
89
  nextProps: Readonly<any>,
78
90
  nextState: Readonly<iCarbonReactState>,
@@ -117,6 +129,7 @@ const CarbonReact= class <P = {}, S = {}> extends Component<{
117
129
 
118
130
  return <BrowserRouter>
119
131
  <GlobalHistory/>
132
+ <CarbonWebSocket />
120
133
  {this.props.children}
121
134
  <ToastContainer/>
122
135
  </BrowserRouter>;
@@ -0,0 +1,33 @@
1
+ import { useEffect, useRef, useState } from 'react';
2
+
3
+ // @link https://dev.to/ag-grid/react-18-avoiding-use-effect-getting-called-twice-4i9e
4
+ export const useEffectOnce = (effect: () => void | (() => void)) => {
5
+ const effectFn = useRef<() => void | (() => void)>(effect);
6
+ const destroyFn = useRef<void | (() => void)>();
7
+ const effectCalled = useRef(false);
8
+ const rendered = useRef(false);
9
+ const [, setVal] = useState<number>(0);
10
+
11
+ if (effectCalled.current) {
12
+ rendered.current = true;
13
+ }
14
+
15
+ useEffect(() => {
16
+ if (!effectCalled.current) {
17
+ destroyFn.current = effectFn.current();
18
+ effectCalled.current = true;
19
+ }
20
+
21
+ setVal(val => val + 1);
22
+
23
+ return () => {
24
+ if (!rendered.current) {
25
+ return;
26
+ }
27
+
28
+ if (destroyFn.current) {
29
+ destroyFn.current();
30
+ }
31
+ };
32
+ }, []);
33
+ }
@@ -18,16 +18,6 @@ export interface iAlertButtonOptions {
18
18
  color: "default" | "primary" | "secondary" | "inherit" | "danger" | "info" | "success" | "warning" | undefined,
19
19
  }
20
20
 
21
- export function addAlert(props: iAlert) {
22
-
23
- CarbonReact.instance.setState(previousState => ({
24
- alertsWaiting: previousState.alertsWaiting.length === 0
25
- ? [props]
26
- : [...previousState.alertsWaiting, props]
27
- }))
28
-
29
- }
30
-
31
21
  export interface iAlert {
32
22
  title: string,
33
23
  text: string,
@@ -38,11 +28,20 @@ export interface iAlert {
38
28
  then?: (value: string | undefined) => void,
39
29
  timeout?: number,
40
30
  footerText?: string,
41
-
42
31
  intercept?: boolean,
43
32
  backendThrowable?: { [key: string]: any },
44
33
  }
45
34
 
35
+ export function addAlert(props: iAlert) {
36
+
37
+ CarbonReact.instance.setState(previousState => ({
38
+ alertsWaiting: previousState.alertsWaiting.length === 0
39
+ ? [props]
40
+ : [...previousState.alertsWaiting, props]
41
+ }))
42
+
43
+ }
44
+
46
45
  export default function Alert() {
47
46
 
48
47
  const {alertsWaiting, backendThrowable} = CarbonReact.instance.state
@@ -73,7 +72,7 @@ export default function Alert() {
73
72
  title: "Oh no! An issue occurred!",
74
73
  text: backendThrowable?.['DropInGaming\\PHP\\Errors\\DropException'] ?? 'An unknown issue occurred. Please try again.',
75
74
  timeout: 0,
76
- footerText: hideExpandInformation ? '' : 'These alert footer options are only shown to admins and in development environments. Click "Expand" to see more details.',
75
+ footerText: hideExpandInformation ? '' : 'These alert footer options are only shown in development environments. Click "Expand" to see more details.',
77
76
  buttons: buttons,
78
77
  // backendThrowable has its own custom component that can be expanded (called in the Bootstrap component)
79
78
  // our then function will be called when the user clicks on the button providing the option to expand or close
@@ -1,5 +1,5 @@
1
1
  import styles from './style.module.scss';
2
- //import OutsideClickHandler from 'react-outside-click-handler';
2
+ import OutsideClickHandler from 'react-outside-click-handler';
3
3
  import CarbonReact from "../../CarbonReact";
4
4
  import {ReactElement} from "react";
5
5
 
@@ -13,7 +13,31 @@ export default () : ReactElement => {
13
13
 
14
14
  return <div className={styles.maintenanceHero}>
15
15
  <h1 className={styles.httpStatusCode}>{currentThrowable?.status || 500}</h1>
16
+ <OutsideClickHandler
17
+ onOutsideClick={() => bootstrap.setState(currentState => ({ backendThrowable: currentState.backendThrowable.slice(1) }))}>
18
+ <div className={styles.centeredContainer}>
19
+ {Object.keys(currentThrowable).map((key, index) => {
16
20
 
21
+ const valueIsString = typeof currentThrowable[key] === 'string';
22
+
23
+ const valueIsCode = 'THROWN NEAR' === key;
24
+
25
+ return <div key={index}>
26
+ <div className={styles.errorTextGeneral}> &gt; <span className={styles.errorKeys}>{key}</span>:
27
+ {valueIsString
28
+ ? (valueIsCode ? <div
29
+ style={{ backgroundColor: 'black', fontSize: 'xx-small' }}
30
+ dangerouslySetInnerHTML={{ __html: currentThrowable[key] }} /> :
31
+ <i className={styles.errorValues}>&quot;{currentThrowable[key]}&quot;</i>)
32
+ : ''}
33
+ </div>
34
+ {valueIsString
35
+ ? ''
36
+ : <pre className={styles.errorPre}>{JSON.stringify(currentThrowable[key], undefined, 4)}</pre>}
37
+ </div>;
38
+ })}
39
+ </div>
40
+ </OutsideClickHandler>
17
41
  </div>;
18
42
 
19
43
  }
@@ -0,0 +1,181 @@
1
+ import CarbonReact, {isJsonString} from "CarbonReact";
2
+ import {addAlert} from "../Alert/Alert";
3
+ import {useEffectOnce} from "../../api/hoc/useEffectOnce";
4
+ import {tC6Tables} from "@carbonorm/carbonnode";
5
+
6
+
7
+ /**
8
+ * @function connect
9
+ * This function establishes a connection with the websocket and also ensures constant reconnection if connection closes
10
+ **/
11
+ export function initiateWebsocket({TABLES = undefined}: {TABLES?: tC6Tables} = {}) {
12
+
13
+ const {websocket} = CarbonReact.instance.state;
14
+
15
+ if (!("WebSocket" in window)) {
16
+
17
+ // todo - store that this has been shown in the state
18
+ addAlert({
19
+ title: 'Browser does not support websockets, live updates will fail. You may need to refresh the page to see the newest content.',
20
+ text: 'Please use a modern browser.',
21
+ icon: 'warning',
22
+ })
23
+
24
+ }
25
+
26
+ if (false === (undefined === websocket || null === websocket)) {
27
+
28
+ return;
29
+
30
+ }
31
+
32
+
33
+ let connectInterval;
34
+
35
+ const connection = new WebSocket(CarbonReact.websocketUrl);
36
+
37
+ console.log("Connecting websocket url", CarbonReact.websocketUrl);
38
+
39
+ CarbonReact.instance.setState({
40
+ websocket: connection
41
+ }, () => {
42
+
43
+ connection.onopen = () => {
44
+
45
+ console.log('WebSocket Client Connected To :: ' + CarbonReact.websocketUrl);
46
+
47
+ clearTimeout(connectInterval); // clear Interval on open of websocket connection
48
+
49
+ function heartbeat() {
50
+
51
+ const {websocket} = CarbonReact.instance.state;
52
+
53
+ if (!websocket) return;
54
+
55
+ if (websocket.readyState !== 1) return;
56
+
57
+ websocket.send("ping");
58
+
59
+ setTimeout(heartbeat, CarbonReact.websocketHeartbeatSeconds * 1000);
60
+
61
+ }
62
+
63
+ heartbeat();
64
+
65
+ };
66
+
67
+ connection.onmessage = (message: MessageEvent<string> ) => {
68
+
69
+ const parsedData = isJsonString(message?.data) ? JSON.parse(message?.data) : message?.data;
70
+
71
+ CarbonReact.instance.setState((prevState: Readonly<any>) => ({
72
+ websocketEvents: prevState.websocketEvents.concat(message),
73
+ websocketData: prevState.websocketData.concat(parsedData), // JSON.parse no good - base64?
74
+ }));
75
+
76
+ console.log('going to impl TABLES', TABLES)
77
+
78
+ /*if (undefined !== TABLES) {
79
+
80
+ TABLES.
81
+
82
+
83
+ }*/
84
+
85
+ };
86
+
87
+ window.addEventListener("focus", () => initiateWebsocket());
88
+
89
+ // websocket onclose event listener
90
+ connection.addEventListener('close', event => {
91
+
92
+ let reason;
93
+
94
+ console.log(
95
+ `Socket is closed.`,
96
+ event.reason, event);
97
+
98
+ const retry = () => {
99
+
100
+ const retrySeconds = Math.min(5000, (CarbonReact.websocketTimeoutSeconds + CarbonReact.websocketTimeoutSeconds) * 1000)
101
+
102
+ CarbonReact.websocketTimeoutSeconds = retrySeconds;
103
+
104
+ console.log(`WebSocket reconnect will be attempted in ${retrySeconds} second(s).`)
105
+
106
+ connectInterval = setTimeout(() => initiateWebsocket(), retrySeconds);
107
+
108
+ }
109
+
110
+ // See https://www.rfc-editor.org/rfc/rfc6455#section-7.4.1
111
+ switch (event.code) {
112
+ case 1000:
113
+ reason = "Normal closure, meaning that the purpose for which the connection was established has been fulfilled.";
114
+ break;
115
+ case 1001:
116
+ retry(); //call check function after timeout
117
+ reason = "An endpoint is \"going away\", such as a server going down or a browser having navigated away from a page.";
118
+ break;
119
+ case 1002:
120
+ reason = "An endpoint is terminating the connection due to a protocol error";
121
+ break;
122
+ case 1003:
123
+ reason = "An endpoint is terminating the connection because it has received a type of data it cannot accept (e.g., an endpoint that understands only text data MAY send this if it receives a binary message).";
124
+ break;
125
+ case 1004:
126
+ reason = "Reserved. The specific meaning might be defined in the future.";
127
+ break;
128
+ case 1005:
129
+ reason = "No status code was actually present.";
130
+ break;
131
+ case 1006:
132
+ retry();
133
+ reason = "The connection was closed abnormally, e.g., without sending or receiving a close control frame";
134
+ break;
135
+ case 1007:
136
+ reason = "An endpoint is terminating the connection because it has received data within a message that was not consistent with the type of the message (e.g., non-UTF-8 [https://www.rfc-editor.org/rfc/rfc3629] data within a text message).";
137
+ break;
138
+ case 1008:
139
+ reason = "An endpoint is terminating the connection because it has received a message that \"violates its policy\". This reason is given either if there is no other suitable reason, or if there is a need to hide specific details about the policy.";
140
+ break;
141
+ case 1009:
142
+ reason = "An endpoint is terminating the connection because it has received a message that is too big for it to process.";
143
+ break;
144
+ case 1010:
145
+ reason = "An endpoint (client) is terminating the connection because it has expected the server to negotiate one or more extension, but the server didn't return them in the response message of the WebSocket handshake. <br /> Specifically, the extensions that are needed are: " + event.reason;
146
+ break;
147
+ case 1011:
148
+ reason = "A server is terminating the connection because it encountered an un expected condition that prevented it from fulfilling the request.";
149
+ break;
150
+ case 1015:
151
+ reason = "The connection was closed due to a failure to perform a TLS handshake (e.g., the server certificate can't be verified).";
152
+ break;
153
+ default:
154
+ reason = "Unknown reason";
155
+ }
156
+
157
+ console.log("The connection was closed for reason: " + reason);
158
+
159
+ });
160
+
161
+ // websocket onerror event listener
162
+ connection.addEventListener('websocket error', (e: Event) => {
163
+ console.error("Socket encountered error: ", e, JSON.stringify(e));
164
+ connection.close();
165
+ });
166
+
167
+ });
168
+
169
+ }
170
+
171
+ export default function () {
172
+
173
+ useEffectOnce(() => {
174
+
175
+ initiateWebsocket()
176
+
177
+ })
178
+
179
+ return null
180
+
181
+ }
package/src/index.ts CHANGED
@@ -4,6 +4,7 @@
4
4
 
5
5
  export { default as CarbonReact } from "./CarbonReact";
6
6
  export * from "./CarbonReact";
7
+ export * from "./api/hoc/useEffectOnce";
7
8
  export { default as Alert } from "./components/Alert/Alert";
8
9
  export * from "./components/Alert/Alert";
9
10
  export { default as AccessDenied } from "./components/Errors/AccessDenied";
@@ -20,6 +21,8 @@ export { default as Nest } from "./components/Nest/Nest";
20
21
  export * from "./components/Nest/Nest";
21
22
  export { default as Popup } from "./components/Popup/Popup";
22
23
  export * from "./components/Popup/Popup";
24
+ export { default as CarbonWebSocket } from "./components/WebSocket/CarbonWebSocket";
25
+ export * from "./components/WebSocket/CarbonWebSocket";
23
26
  export * from "./hoc/GlobalHistory";
24
27
  export * from "./hoc/KeysMatching";
25
28
  export { default as addValidSQL } from "./hoc/addValidSQL";