@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 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 = ({jwt, id, host, ssl, capability, versionNS}) => {
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
- const [scope, capabilityName] = capability.split('/');
88
+ const [scope, capabilityName] = capability.split('/');
81
89
 
82
- const { device } = decodeJWT(jwt);
83
- const prefixPath = [id, device, scope, capabilityName];
84
- const prefix = pathToTopic(prefixPath);
85
- const prefixPathVersion = [...prefixPath, versionNS];
86
- const prefixVersion = pathToTopic(prefixPathVersion);
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
- const mqttUrl = `${ssl && JSON.parse(ssl) ? 'wss' : 'ws'}://mqtt.${host}`;
89
- const fromMqttSync = useMqttSync({ jwt, id, mqttUrl });
96
+ const mqttUrl = `${ssl && JSON.parse(ssl) ? 'wss' : 'ws'}://mqtt.${host}`;
97
+ const fromMqttSync = useMqttSync({ jwt, id, mqttUrl, appReact });
90
98
 
91
- return {...fromMqttSync, device, prefixPath, prefix, prefixPathVersion,
92
- prefixVersion};
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
- const { agentStatus, topicData } = useTopics({ jwt, topics: [
104
- '/options/videoSource',
105
- '/stats/+/log/'
106
- ]});
107
- ```
108
-
109
- @param {object} options An object containing:
110
- `JWT`: A list of subtopics of the capability named in the JWT.
111
- `topics`: A list of subtopics of the capability named in the JWT.
112
- @returns {object} An object `{data, mqttSync, ready, agentStatus, topicData}`
113
- where:
114
- * `agentStatus` is the `status` field of the running robot agent, including
115
- heartbeat and runningPackages, and
116
- * `topicData` is the data for the selected topics of the capability
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
- const {loaded, loadedModule} = useCapability({
177
- capability: '@transitive-robotics/terminal',
178
- name: 'mock-device',
179
- userId: 'user123',
180
- deviceId: 'd_mydevice123',
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
- return listeners[name].push(done);
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
- import(`${fileBasename}.esm.js?${params.toString()}`).then(
219
- esm => notifyListeners('loaded esm', esm),
220
- error => {
221
- log.warn(`No ESM module found for ${name}, loading iife`);
222
- import(`${fileBasename}.js?${params.toString()}`).then(
223
- iife => notifyListeners('loaded iife', iife),
224
- error => log.error(`Failed to load ${name} iife`, error));
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
- * 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
- ```js
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
- <ErrorBoundary message="Something went wrong">
159
- <SomeFlakyComponent />
160
- </ErrorBoundary>
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
- reactive to all attributes. Used in web capabilities. Example:
194
- ```js
195
- createWebComponent(Diagnostics, 'health-monitoring-device', TR_PKG_VERSION);
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 = {}) => {