@transitive-sdk/utils-web 0.10.3 → 0.11.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/client/hooks.jsx +84 -64
- package/client/react-web-component/index.js +12 -0
- package/client/shared.jsx +84 -20
- package/dist/utils-web.js +1538 -1
- package/dist/utils-web.js.map +4 -4
- package/docs/client.md +62 -21
- package/esbuild.js +3 -3
- package/package.json +1 -1
package/client/hooks.jsx
CHANGED
|
@@ -10,15 +10,17 @@ log.setLevel('info');
|
|
|
10
10
|
log.setLevel('debug'); // #DEBUG
|
|
11
11
|
|
|
12
12
|
/** Hook for using MqttSync in React.
|
|
13
|
-
@returns {object} An object `{data, mqttSync, ready, StatusComponent, status}`
|
|
14
|
-
where:
|
|
15
|
-
`data` is a reactive data source in React containing all the data received by
|
|
16
|
-
mqttsync,
|
|
17
|
-
`mqttSync` is the MqttSync object itself,
|
|
18
|
-
`ready` indicates when mqttSync is ready to be used (connected and received
|
|
19
|
-
successfully subscribed to mqtt system heartbeats)
|
|
20
|
-
|
|
21
|
-
export const useMqttSync = ({jwt, id, mqttUrl}) => {
|
|
13
|
+
* @returns {object} An object `{data, mqttSync, ready, StatusComponent, status}`
|
|
14
|
+
* where:
|
|
15
|
+
* `data` is a reactive data source in React containing all the data received by
|
|
16
|
+
* mqttsync,
|
|
17
|
+
* `mqttSync` is the MqttSync object itself,
|
|
18
|
+
* `ready` indicates when mqttSync is ready to be used (connected and received
|
|
19
|
+
* successfully subscribed to mqtt system heartbeats)
|
|
20
|
+
*/
|
|
21
|
+
export const useMqttSync = ({jwt, id, mqttUrl, appReact}) => {
|
|
22
|
+
const { useState, useRef, useEffect } = appReact || React;
|
|
23
|
+
|
|
22
24
|
const [status, setStatus] = useState('connecting');
|
|
23
25
|
const [mqttSync, setMqttSync] = useState();
|
|
24
26
|
const [data, setData] = useState({});
|
|
@@ -33,6 +35,10 @@ export const useMqttSync = ({jwt, id, mqttUrl}) => {
|
|
|
33
35
|
});
|
|
34
36
|
|
|
35
37
|
client.on('connect', () => {
|
|
38
|
+
// if (mqttSync) { // #WIP
|
|
39
|
+
// log.debug('reconnected');
|
|
40
|
+
// return;
|
|
41
|
+
// }
|
|
36
42
|
log.debug('connected');
|
|
37
43
|
const mqttSyncClient = new MqttSync({
|
|
38
44
|
mqttClient: client,
|
|
@@ -74,49 +80,54 @@ export const useMqttSync = ({jwt, id, mqttUrl}) => {
|
|
|
74
80
|
};
|
|
75
81
|
|
|
76
82
|
/** Hook for using Transitive in React. Connects to MQTT, establishes sync, and
|
|
77
|
-
exposes reactive `data` state variable. */
|
|
78
|
-
export const useTransitive =
|
|
83
|
+
* exposes reactive `data` state variable. */
|
|
84
|
+
export const useTransitive =
|
|
85
|
+
({jwt, id, capability, versionNS, appReact,
|
|
86
|
+
host = 'transitiverobotics.com', ssl = true }) => {
|
|
79
87
|
|
|
80
|
-
|
|
88
|
+
const [scope, capabilityName] = capability.split('/');
|
|
81
89
|
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
90
|
+
const { device } = decodeJWT(jwt);
|
|
91
|
+
const prefixPath = [id, device, scope, capabilityName];
|
|
92
|
+
const prefix = pathToTopic(prefixPath);
|
|
93
|
+
const prefixPathVersion = [...prefixPath, versionNS];
|
|
94
|
+
const prefixVersion = pathToTopic(prefixPathVersion);
|
|
87
95
|
|
|
88
|
-
|
|
89
|
-
|
|
96
|
+
const mqttUrl = `${ssl && JSON.parse(ssl) ? 'wss' : 'ws'}://mqtt.${host}`;
|
|
97
|
+
const fromMqttSync = useMqttSync({ jwt, id, mqttUrl, appReact });
|
|
90
98
|
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
};
|
|
99
|
+
return {...fromMqttSync, device, prefixPath, prefix, prefixPathVersion,
|
|
100
|
+
prefixVersion};
|
|
101
|
+
};
|
|
94
102
|
|
|
95
103
|
|
|
96
104
|
/** Subscribe to MqttSync topics using the provided JWT. This will
|
|
97
|
-
automatically find which version of the capability named in the JWT is running
|
|
98
|
-
on the device of the JWT and get the data for that version.
|
|
99
|
-
|
|
100
|
-
Example usage (with webrtc-video):
|
|
101
|
-
|
|
102
|
-
```js
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
```
|
|
108
|
-
|
|
109
|
-
@param {object} options An object containing:
|
|
110
|
-
`JWT`: A list of subtopics of the capability named in the JWT.
|
|
111
|
-
|
|
112
|
-
@returns {object} An object `{data, mqttSync, ready, agentStatus, topicData}`
|
|
113
|
-
where:
|
|
114
|
-
|
|
115
|
-
heartbeat and runningPackages, and
|
|
116
|
-
|
|
105
|
+
* automatically find which version of the capability named in the JWT is running
|
|
106
|
+
* on the device of the JWT and get the data for that version.
|
|
107
|
+
*
|
|
108
|
+
* Example usage (with webrtc-video):
|
|
109
|
+
*
|
|
110
|
+
* ```js
|
|
111
|
+
* const { agentStatus, topicData } = useTopics({ jwt, topics: [
|
|
112
|
+
* '/options/videoSource',
|
|
113
|
+
* '/stats/+/log/'
|
|
114
|
+
* ]});
|
|
115
|
+
* ```
|
|
116
|
+
*
|
|
117
|
+
* @param {object} options An object containing:
|
|
118
|
+
* `JWT`: A list of subtopics of the capability named in the JWT.
|
|
119
|
+
* `topics`: A list of subtopics of the capability named in the JWT.
|
|
120
|
+
* @returns {object} An object `{data, mqttSync, ready, agentStatus, topicData}`
|
|
121
|
+
* where:
|
|
122
|
+
* `agentStatus` is the `status` field of the running robot agent, including
|
|
123
|
+
* heartbeat and runningPackages, and
|
|
124
|
+
* `topicData` is the data for the selected topics of the capability
|
|
117
125
|
*/
|
|
118
126
|
export const useTopics = ({jwt, host = 'transitiverobotics.com', ssl = true,
|
|
119
|
-
topics = []}) => {
|
|
127
|
+
topics = [], appReact}) => {
|
|
128
|
+
|
|
129
|
+
log.debug({appReact});
|
|
130
|
+
const { useState, useEffect } = appReact || React;
|
|
120
131
|
|
|
121
132
|
// We need to make sure we don't resubscribe (below) when this function
|
|
122
133
|
// is called with the same content of `topics` but a different object.
|
|
@@ -132,7 +143,7 @@ export const useTopics = ({jwt, host = 'transitiverobotics.com', ssl = true,
|
|
|
132
143
|
const agentPrefix = `/${id}/${device}/@transitive-robotics/_robot-agent/+/status`;
|
|
133
144
|
|
|
134
145
|
const {mqttSync, data, status, ready, StatusComponent} =
|
|
135
|
-
useMqttSync({jwt, id, mqttUrl: `ws${ssl ? 's' : ''}://mqtt.${host}
|
|
146
|
+
useMqttSync({jwt, id, mqttUrl: `ws${ssl ? 's' : ''}://mqtt.${host}`, appReact});
|
|
136
147
|
|
|
137
148
|
useEffect(() => {
|
|
138
149
|
if (ready) {
|
|
@@ -170,20 +181,21 @@ export const useTopics = ({jwt, host = 'transitiverobotics.com', ssl = true,
|
|
|
170
181
|
const listeners = {};
|
|
171
182
|
const loadedModules = {};
|
|
172
183
|
/** Hook to load a Transitive capability. Besides loading the custom element,
|
|
173
|
-
this hook also returns any functions and objects the component exports in
|
|
174
|
-
`loadedModule`. Example:
|
|
175
|
-
```js
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
```
|
|
184
|
+
* this hook also returns any functions and objects the component exports in
|
|
185
|
+
* `loadedModule`. Example:
|
|
186
|
+
* ```js
|
|
187
|
+
* const {loaded, loadedModule} = useCapability({
|
|
188
|
+
* capability: '@transitive-robotics/terminal',
|
|
189
|
+
* name: 'mock-device',
|
|
190
|
+
* userId: 'user123',
|
|
191
|
+
* deviceId: 'd_mydevice123',
|
|
192
|
+
* });
|
|
193
|
+
* ```
|
|
183
194
|
*/
|
|
184
195
|
export const useCapability = ({ capability, name, userId, deviceId,
|
|
185
|
-
host = 'transitiverobotics.com', ssl = true
|
|
196
|
+
host = 'transitiverobotics.com', ssl = true, appReact
|
|
186
197
|
}) => {
|
|
198
|
+
const { useState, useEffect } = appReact || React;
|
|
187
199
|
|
|
188
200
|
const [returns, setReturns] = useState({ loaded: false });
|
|
189
201
|
|
|
@@ -206,7 +218,8 @@ export const useCapability = ({ capability, name, userId, deviceId,
|
|
|
206
218
|
if (listeners[name]) {
|
|
207
219
|
log.debug('already loading');
|
|
208
220
|
// get notified when loading completes
|
|
209
|
-
|
|
221
|
+
listeners[name].push(done);
|
|
222
|
+
return;
|
|
210
223
|
}
|
|
211
224
|
listeners[name] = [done];
|
|
212
225
|
|
|
@@ -215,15 +228,22 @@ export const useCapability = ({ capability, name, userId, deviceId,
|
|
|
215
228
|
// filename without extension as we'll try multiple
|
|
216
229
|
const fileBasename = `${baseUrl}/running/${capability}/dist/${name}`;
|
|
217
230
|
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
231
|
+
/* Since some users use webpack and webpack is stupid, we need to use
|
|
232
|
+
this magic comment for it to ignore these (remote) requests, see:
|
|
233
|
+
https://webpack.js.org/api/module-methods/#webpackignore. */
|
|
234
|
+
import(/* webpackIgnore: true */
|
|
235
|
+
`${fileBasename}.esm.js?${params.toString()}`).then(
|
|
236
|
+
esm => notifyListeners('loaded esm', esm),
|
|
237
|
+
error => {
|
|
238
|
+
log.warn(`No ESM module found for ${name}, loading iife`, error);
|
|
239
|
+
import(/* webpackIgnore: true */
|
|
240
|
+
`${fileBasename}.js?${params.toString()}`).then(
|
|
241
|
+
iife => notifyListeners('loaded iife', iife),
|
|
242
|
+
error => log.error(`Failed to load ${name} iife`, error));
|
|
243
|
+
});
|
|
226
244
|
}, [capability, name, userId, deviceId]);
|
|
227
245
|
|
|
228
246
|
return returns;
|
|
229
247
|
};
|
|
248
|
+
|
|
249
|
+
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
const React = require('react');
|
|
2
2
|
const ReactDOM = require('react-dom');
|
|
3
|
+
// const { createRoot } = require('react-dom/client'); // react 18; wip
|
|
3
4
|
const retargetEvents = require('react-shadow-dom-retarget-events');
|
|
4
5
|
const getStyleElementsFromReactWebComponentStyleLoader = require('./getStyleElementsFromReactWebComponentStyleLoader');
|
|
5
6
|
const extractAttributes = require('./extractAttributes');
|
|
@@ -69,6 +70,17 @@ module.exports = {
|
|
|
69
70
|
self.callConstructorHook();
|
|
70
71
|
self.callLifeCycleHook('connectedCallback');
|
|
71
72
|
});
|
|
73
|
+
|
|
74
|
+
// WIP: trying to upgrade to react 18, which lacks a render callback
|
|
75
|
+
// --> See https://github.com/reactwg/react-18/discussions/5 on how to
|
|
76
|
+
// do this in react 18
|
|
77
|
+
// createRoot(mountPoint).render(
|
|
78
|
+
// // This is where we instantiate the actual component (in its wrapper)
|
|
79
|
+
// React.createElement(wrapper, extractAttributes(self)),
|
|
80
|
+
// );
|
|
81
|
+
// self.instance = this;
|
|
82
|
+
// self.callConstructorHook();
|
|
83
|
+
// self.callLifeCycleHook('connectedCallback');
|
|
72
84
|
}
|
|
73
85
|
|
|
74
86
|
disconnectedCallback() {
|
package/client/shared.jsx
CHANGED
|
@@ -105,17 +105,17 @@ export const Timer = ({duration, onTimeout, onStart, setOnDisconnect, children})
|
|
|
105
105
|
|
|
106
106
|
|
|
107
107
|
/** Dynamically load and use the Transitive web component specified in the JWT.
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
```
|
|
113
|
-
<TransitiveCapability jwt={jwt}
|
|
114
|
-
myconfig={{a: 1, b: 2}}
|
|
115
|
-
onData={(data) => setData(data)}
|
|
116
|
-
onclick={() => { console.log('custom click handler'); }}
|
|
117
|
-
|
|
118
|
-
```
|
|
108
|
+
* Embedding Transitive components this way also enables the use of functional
|
|
109
|
+
* and object properties, which get lost when using the custom element (Web
|
|
110
|
+
* Component) because HTML attributes are strings.
|
|
111
|
+
* Example:
|
|
112
|
+
* ```jsx
|
|
113
|
+
* <TransitiveCapability jwt={jwt}
|
|
114
|
+
* myconfig={{a: 1, b: 2}}
|
|
115
|
+
* onData={(data) => setData(data)}
|
|
116
|
+
* onclick={() => { console.log('custom click handler'); }}
|
|
117
|
+
* />
|
|
118
|
+
* ```
|
|
119
119
|
*/
|
|
120
120
|
export const TransitiveCapability = ({
|
|
121
121
|
jwt, host = 'transitiverobotics.com', ssl = true, ...config
|
|
@@ -154,11 +154,11 @@ export const TransitiveCapability = ({
|
|
|
154
154
|
|
|
155
155
|
|
|
156
156
|
/** A simple error boundary. Usage:
|
|
157
|
-
```jsx
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
```
|
|
157
|
+
* ```jsx
|
|
158
|
+
* <ErrorBoundary message="Something went wrong">
|
|
159
|
+
* <SomeFlakyComponent />
|
|
160
|
+
* </ErrorBoundary>
|
|
161
|
+
* ```
|
|
162
162
|
*/
|
|
163
163
|
export class ErrorBoundary extends React.Component {
|
|
164
164
|
constructor(props) {
|
|
@@ -182,6 +182,70 @@ export class ErrorBoundary extends React.Component {
|
|
|
182
182
|
};
|
|
183
183
|
|
|
184
184
|
|
|
185
|
+
export const CapabilityContext = React.createContext({});
|
|
186
|
+
|
|
187
|
+
/* Only used internally: the actual context provider, given the loaded module */
|
|
188
|
+
const LoadedCapabilityContextProvider = (props) => {
|
|
189
|
+
const {children, jwt, id, host, ssl, loadedModule} = props;
|
|
190
|
+
|
|
191
|
+
const context = loadedModule.provideContext?.({
|
|
192
|
+
jwt, id, host, ssl, appReact: React
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
return <CapabilityContext.Provider value={{ ...context }}>
|
|
196
|
+
{children}
|
|
197
|
+
</CapabilityContext.Provider>;
|
|
198
|
+
};
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* Context provider for capabilities. Use this to access the front-end API
|
|
202
|
+
* provided by some capabilities. Example:
|
|
203
|
+
* ```jsx
|
|
204
|
+
* <CapabilityContextProvider jwt={jwt}>
|
|
205
|
+
* <MyROSComponent />
|
|
206
|
+
* </CapabilityContextProvider>
|
|
207
|
+
* ```
|
|
208
|
+
* where `jwt` is a JWT for a capability that exposes a front-end API. Then use
|
|
209
|
+
* `useContext` in `MyROSComponent` to get the exposed data and functions, e.g.:
|
|
210
|
+
* ```jsx
|
|
211
|
+
* const MyROSComponent = () => {
|
|
212
|
+
* const { ready, subscribe, data } = useContext(CapabilityContext);
|
|
213
|
+
* // When ready, subscribe to the `/odom` topic in ROS1
|
|
214
|
+
* useEffect(() => { ready && subscribe(1, '/odom'); }, [ready]);
|
|
215
|
+
* return <pre>{JSON.stringify(data, true, 2)}</pre>;
|
|
216
|
+
* }
|
|
217
|
+
* ```
|
|
218
|
+
* Where `ready`, `subscribe`, and `data` are reactive variables and functions
|
|
219
|
+
* exposed by the capability of the provided JWT. In this example, the latest
|
|
220
|
+
* message from the subscribed ROS topics will be available in the capabilities
|
|
221
|
+
* namespace in `data`.
|
|
222
|
+
* @param {object} props
|
|
223
|
+
*/
|
|
224
|
+
export const CapabilityContextProvider =
|
|
225
|
+
({children, jwt, host = undefined, ssl = undefined}) => {
|
|
226
|
+
|
|
227
|
+
const {id, device, capability} = decodeJWT(jwt);
|
|
228
|
+
const type = device == '_fleet' ? 'fleet' : 'device';
|
|
229
|
+
const capName = capability.split('/')[1];
|
|
230
|
+
const name = `${capName}-${type}`;
|
|
231
|
+
|
|
232
|
+
const {loaded, loadedModule} = useCapability({
|
|
233
|
+
capability,
|
|
234
|
+
name,
|
|
235
|
+
userId: id,
|
|
236
|
+
deviceId: device,
|
|
237
|
+
appReact: React,
|
|
238
|
+
host,
|
|
239
|
+
ssl
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
if (!loadedModule) return <div>Loading {capability}</div>;
|
|
243
|
+
return <LoadedCapabilityContextProvider {...{jwt, id, host, ssl, loadedModule}}>
|
|
244
|
+
{children}
|
|
245
|
+
</LoadedCapabilityContextProvider>;
|
|
246
|
+
};
|
|
247
|
+
|
|
248
|
+
|
|
185
249
|
/* whether or not the given react component allows refs, i.e., is either
|
|
186
250
|
* a functional component wrapped with forwardRef or a class component */
|
|
187
251
|
const componentPermitsRefs = (Component) =>
|
|
@@ -190,10 +254,10 @@ const componentPermitsRefs = (Component) =>
|
|
|
190
254
|
|
|
191
255
|
|
|
192
256
|
/** Create a WebComponent from the given react component and name that is
|
|
193
|
-
|
|
194
|
-
```js
|
|
195
|
-
|
|
196
|
-
```
|
|
257
|
+
* reactive to all attributes. Used in web capabilities. Example:
|
|
258
|
+
* ```js
|
|
259
|
+
* createWebComponent(Diagnostics, 'health-monitoring-device', TR_PKG_VERSION);
|
|
260
|
+
* ```
|
|
197
261
|
*/
|
|
198
262
|
export const createWebComponent = (Component, name, version = '0.0.0',
|
|
199
263
|
options = {}) => {
|