@transitive-sdk/utils-web 0.10.2 → 0.11.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/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({});
@@ -74,49 +76,53 @@ export const useMqttSync = ({jwt, id, mqttUrl}) => {
74
76
  };
75
77
 
76
78
  /** 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}) => {
79
+ * exposes reactive `data` state variable. */
80
+ export const useTransitive =
81
+ ({jwt, id, host, ssl, capability, versionNS, appReact}) => {
79
82
 
80
- const [scope, capabilityName] = capability.split('/');
83
+ const [scope, capabilityName] = capability.split('/');
81
84
 
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);
85
+ const { device } = decodeJWT(jwt);
86
+ const prefixPath = [id, device, scope, capabilityName];
87
+ const prefix = pathToTopic(prefixPath);
88
+ const prefixPathVersion = [...prefixPath, versionNS];
89
+ const prefixVersion = pathToTopic(prefixPathVersion);
87
90
 
88
- const mqttUrl = `${ssl && JSON.parse(ssl) ? 'wss' : 'ws'}://mqtt.${host}`;
89
- const fromMqttSync = useMqttSync({ jwt, id, mqttUrl });
91
+ const mqttUrl = `${ssl && JSON.parse(ssl) ? 'wss' : 'ws'}://mqtt.${host}`;
92
+ const fromMqttSync = useMqttSync({ jwt, id, mqttUrl, appReact });
90
93
 
91
- return {...fromMqttSync, device, prefixPath, prefix, prefixPathVersion,
92
- prefixVersion};
93
- };
94
+ return {...fromMqttSync, device, prefixPath, prefix, prefixPathVersion,
95
+ prefixVersion};
96
+ };
94
97
 
95
98
 
96
99
  /** 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
100
+ * automatically find which version of the capability named in the JWT is running
101
+ * on the device of the JWT and get the data for that version.
102
+ *
103
+ * Example usage (with webrtc-video):
104
+ *
105
+ * ```js
106
+ * const { agentStatus, topicData } = useTopics({ jwt, topics: [
107
+ * '/options/videoSource',
108
+ * '/stats/+/log/'
109
+ * ]});
110
+ * ```
111
+ *
112
+ * @param {object} options An object containing:
113
+ * `JWT`: A list of subtopics of the capability named in the JWT.
114
+ * `topics`: A list of subtopics of the capability named in the JWT.
115
+ * @returns {object} An object `{data, mqttSync, ready, agentStatus, topicData}`
116
+ * where:
117
+ * `agentStatus` is the `status` field of the running robot agent, including
118
+ * heartbeat and runningPackages, and
119
+ * `topicData` is the data for the selected topics of the capability
117
120
  */
118
121
  export const useTopics = ({jwt, host = 'transitiverobotics.com', ssl = true,
119
- topics = []}) => {
122
+ topics = [], appReact}) => {
123
+
124
+ log.debug({appReact});
125
+ const { useState, useEffect } = appReact || React;
120
126
 
121
127
  // We need to make sure we don't resubscribe (below) when this function
122
128
  // is called with the same content of `topics` but a different object.
@@ -132,7 +138,7 @@ export const useTopics = ({jwt, host = 'transitiverobotics.com', ssl = true,
132
138
  const agentPrefix = `/${id}/${device}/@transitive-robotics/_robot-agent/+/status`;
133
139
 
134
140
  const {mqttSync, data, status, ready, StatusComponent} =
135
- useMqttSync({jwt, id, mqttUrl: `ws${ssl ? 's' : ''}://mqtt.${host}`});
141
+ useMqttSync({jwt, id, mqttUrl: `ws${ssl ? 's' : ''}://mqtt.${host}`, appReact});
136
142
 
137
143
  useEffect(() => {
138
144
  if (ready) {
@@ -170,20 +176,21 @@ export const useTopics = ({jwt, host = 'transitiverobotics.com', ssl = true,
170
176
  const listeners = {};
171
177
  const loadedModules = {};
172
178
  /** 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
- ```
179
+ * this hook also returns any functions and objects the component exports in
180
+ * `loadedModule`. Example:
181
+ * ```js
182
+ * const {loaded, loadedModule} = useCapability({
183
+ * capability: '@transitive-robotics/terminal',
184
+ * name: 'mock-device',
185
+ * userId: 'user123',
186
+ * deviceId: 'd_mydevice123',
187
+ * });
188
+ * ```
183
189
  */
184
190
  export const useCapability = ({ capability, name, userId, deviceId,
185
- host = 'transitiverobotics.com', ssl = true
191
+ host = 'transitiverobotics.com', ssl = true, appReact
186
192
  }) => {
193
+ const { useState, useEffect } = appReact || React;
187
194
 
188
195
  const [returns, setReturns] = useState({ loaded: false });
189
196
 
@@ -206,7 +213,8 @@ export const useCapability = ({ capability, name, userId, deviceId,
206
213
  if (listeners[name]) {
207
214
  log.debug('already loading');
208
215
  // get notified when loading completes
209
- return listeners[name].push(done);
216
+ listeners[name].push(done);
217
+ return;
210
218
  }
211
219
  listeners[name] = [done];
212
220
 
@@ -215,15 +223,22 @@ export const useCapability = ({ capability, name, userId, deviceId,
215
223
  // filename without extension as we'll try multiple
216
224
  const fileBasename = `${baseUrl}/running/${capability}/dist/${name}`;
217
225
 
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
- });
226
+ /* Since some users use webpack and webpack is stupid, we need to use
227
+ this magic comment for it to ignore these (remote) requests, see:
228
+ https://webpack.js.org/api/module-methods/#webpackignore. */
229
+ import(/* webpackIgnore: true */
230
+ `${fileBasename}.esm.js?${params.toString()}`).then(
231
+ esm => notifyListeners('loaded esm', esm),
232
+ error => {
233
+ log.warn(`No ESM module found for ${name}, loading iife`, error);
234
+ import(/* webpackIgnore: true */
235
+ `${fileBasename}.js?${params.toString()}`).then(
236
+ iife => notifyListeners('loaded iife', iife),
237
+ error => log.error(`Failed to load ${name} iife`, error));
238
+ });
226
239
  }, [capability, name, userId, deviceId]);
227
240
 
228
241
  return returns;
229
242
  };
243
+
244
+
@@ -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 = {}) => {