ota-hub-reactjs 0.0.16 → 0.0.18

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.
@@ -13,6 +13,10 @@ export type DeviceConnectionState = {
13
13
  uuid: string;
14
14
  deviceMac?: string;
15
15
  name: string;
16
+ /**
17
+ * Default device send helper.
18
+ * This is a utility function which may better be replaced with transport specific send functions (e.g. MQTT has publish, Serial has write etc.)
19
+ */
16
20
  send: (data: string | Uint8Array) => void | Promise<void>;
17
21
  onReceive?: (data: string | Uint8Array) => void;
18
22
  onConnect?: () => void | Promise<void>;
@@ -29,6 +33,8 @@ export type DeviceWhispererProps<T extends DeviceConnectionState> = {
29
33
  createInitialConnectionState?: (uuid: string) => Partial<T>;
30
34
  };
31
35
  export declare function MultiDeviceWhisperer<T extends DeviceConnectionState>({ createInitialConnectionState, }?: DeviceWhispererProps<T>): {
36
+ focussedConnection: string | undefined;
37
+ setFocussedConnection: import("react").Dispatch<import("react").SetStateAction<string | undefined>>;
32
38
  connections: T[];
33
39
  addConnection: ({ uuid, propCreator }: AddConnectionProps<T>) => Promise<string>;
34
40
  removeConnection: (uuid: string) => void;
@@ -21,6 +21,7 @@ export function MultiDeviceWhisperer({ createInitialConnectionState = createDefa
21
21
  const [connections, setConnections] = useState([]);
22
22
  const connectionsRef = useRef(new Map());
23
23
  const [isReady, setIsReady] = useState(false);
24
+ const [focussedConnection, setFocussedConnection] = useState();
24
25
  // ---------- GET ----------
25
26
  const getConnection = (uuid) => connectionsRef.current.get(uuid);
26
27
  // ---------- UPDATE ----------
@@ -67,6 +68,8 @@ export function MultiDeviceWhisperer({ createInitialConnectionState = createDefa
67
68
  setConnections(Array.from(connectionsRef.current.values()));
68
69
  }, []);
69
70
  return {
71
+ focussedConnection,
72
+ setFocussedConnection,
70
73
  connections,
71
74
  addConnection,
72
75
  removeConnection,
@@ -19,6 +19,8 @@ export declare function ProtobufMultiDeviceWhisperer<AppLayer extends DeviceConn
19
19
  addConnection: ({ uuid, propCreator }: AddConnectionProps<AppLayer>) => Promise<string>;
20
20
  sendProtobuf: (uuid: string, message: MessageTX) => void;
21
21
  protoBufOnReceiveHandler: (uuid: string, data: string | Uint8Array) => void;
22
+ focussedConnection: string | undefined;
23
+ setFocussedConnection: import("react").Dispatch<import("react").SetStateAction<string | undefined>>;
22
24
  connections: AppLayer[];
23
25
  removeConnection: (uuid: string) => void;
24
26
  connect: (_uuid: string) => void;
@@ -27,6 +27,8 @@ export declare function ESP32MultiDeviceWhisperer<AppOrMessageLayer extends ESP3
27
27
  blob: Blob;
28
28
  address: number;
29
29
  }[]) => Promise<void>;
30
+ focussedConnection: string | undefined;
31
+ setFocussedConnection: import("react").Dispatch<import("react").SetStateAction<string | undefined>>;
30
32
  connections: AppOrMessageLayer[];
31
33
  updateConnection: (uuid: string, updater: (c: AppOrMessageLayer) => AppOrMessageLayer) => void;
32
34
  updateConnectionName: (uuid: string, name: string) => void;
@@ -1,5 +1,15 @@
1
1
  import { DeviceConnectionState, AddConnectionProps, DeviceWhispererProps } from "../base/device-whisperer.js";
2
+ type MQTTPayload = string | Uint8Array | ArrayBuffer;
3
+ type MQTTTopicCallback = (payload: MQTTPayload, topic: string) => void;
2
4
  export type MQTTConnectionState = DeviceConnectionState & {
5
+ /**
6
+ * Publishes a payload to any explicit MQTT topic.
7
+ */
8
+ publish?: (topic: string, payload: MQTTPayload) => void | Promise<void>;
9
+ /**
10
+ * Subscribes a callback for a specific topic. Returns an unsubscribe function.
11
+ */
12
+ subscribeCallbackToTopic?: (topic: string, callback: MQTTTopicCallback) => () => void;
3
13
  pingFunction?: (props?: any) => void;
4
14
  touchHeartbeat?: () => void;
5
15
  };
@@ -21,6 +31,8 @@ export declare function MQTTMultiDeviceWhisperer<AppOrMessageLayer extends MQTTC
21
31
  disconnect: (uuid: string) => Promise<void>;
22
32
  reconnectAll: () => Promise<void>;
23
33
  connectToMQTTServer: () => (() => void) | undefined;
34
+ focussedConnection: string | undefined;
35
+ setFocussedConnection: import("react").Dispatch<import("react").SetStateAction<string | undefined>>;
24
36
  connections: AppOrMessageLayer[];
25
37
  updateConnection: (uuid: string, updater: (c: AppOrMessageLayer) => AppOrMessageLayer) => void;
26
38
  updateConnectionName: (uuid: string, name: string) => void;
@@ -30,3 +42,4 @@ export declare function MQTTMultiDeviceWhisperer<AppOrMessageLayer extends MQTTC
30
42
  setIsReady: import("react").Dispatch<import("react").SetStateAction<boolean>>;
31
43
  createInitialConnectionState: (uuid: string) => Partial<AppOrMessageLayer>;
32
44
  };
45
+ export {};
@@ -7,6 +7,83 @@ export function MQTTMultiDeviceWhisperer({ serverUrl, uuidFromMessage, subTopicF
7
7
  const isUnmountedRef = useRef(false);
8
8
  const watchdogTimers = useRef({});
9
9
  const addingConnections = useRef(new Set());
10
+ const topicCallbacksRef = useRef(new Map());
11
+ const hasCallbackForTopic = (topic) => {
12
+ const callbacks = topicCallbacksRef.current.get(topic);
13
+ return !!callbacks && callbacks.size > 0;
14
+ };
15
+ const hasOtherConnectionUsingTopic = (topic, excludingUuid) => {
16
+ return base.connections.some((connection) => {
17
+ if (excludingUuid && connection.uuid === excludingUuid)
18
+ return false;
19
+ const connectionTopic = subTopicFromUuid?.(connection.uuid) ?? connection.uuid;
20
+ return connectionTopic === topic;
21
+ });
22
+ };
23
+ const normalizePayload = (payload) => {
24
+ if (typeof payload === "string") {
25
+ return new TextEncoder().encode(payload);
26
+ }
27
+ if (payload instanceof ArrayBuffer) {
28
+ return new Uint8Array(payload);
29
+ }
30
+ return payload;
31
+ };
32
+ const publish = async (topic, payload, uuidForLogs) => {
33
+ const client = clientRef.current;
34
+ if (!client || isUnmountedRef.current)
35
+ return;
36
+ const bytes = normalizePayload(payload);
37
+ if (uuidForLogs) {
38
+ base.appendLog(uuidForLogs, {
39
+ level: 5,
40
+ message: bytes,
41
+ });
42
+ }
43
+ await new Promise((resolve, reject) => {
44
+ client.publish(topic, bytes, { qos: 1 }, (err) => {
45
+ if (err) {
46
+ reject(err);
47
+ return;
48
+ }
49
+ resolve();
50
+ });
51
+ });
52
+ };
53
+ const subscribeCallbackToTopic = (topic, callback) => {
54
+ if (!topic)
55
+ return () => { };
56
+ const topicCallbacks = topicCallbacksRef.current.get(topic) ?? new Set();
57
+ const topicAlreadyRegistered = topicCallbacksRef.current.has(topic);
58
+ topicCallbacks.add(callback);
59
+ topicCallbacksRef.current.set(topic, topicCallbacks);
60
+ const client = clientRef.current;
61
+ if (client?.connected && !client.disconnecting && !topicAlreadyRegistered) {
62
+ client.subscribe(topic, { qos: 1 }, (err) => {
63
+ if (err)
64
+ console.error("Topic callback subscribe failed:", err);
65
+ });
66
+ }
67
+ return () => {
68
+ const currentCallbacks = topicCallbacksRef.current.get(topic);
69
+ if (!currentCallbacks)
70
+ return;
71
+ currentCallbacks.delete(callback);
72
+ if (currentCallbacks.size > 0)
73
+ return;
74
+ topicCallbacksRef.current.delete(topic);
75
+ // Keep topic subscribed when at least one managed connection still uses it.
76
+ if (hasOtherConnectionUsingTopic(topic))
77
+ return;
78
+ const latestClient = clientRef.current;
79
+ if (latestClient?.connected && !latestClient.disconnecting) {
80
+ latestClient.unsubscribe(topic, (err) => {
81
+ if (err)
82
+ console.error("Topic callback unsubscribe failed:", err);
83
+ });
84
+ }
85
+ };
86
+ };
10
87
  const connectToMQTTServer = () => {
11
88
  isUnmountedRef.current = false;
12
89
  if (clientRef.current) {
@@ -29,6 +106,13 @@ export function MQTTMultiDeviceWhisperer({ serverUrl, uuidFromMessage, subTopicF
29
106
  new_client.on("connect", () => {
30
107
  console.log("MQTT Whisperer Connected");
31
108
  base.setIsReady(true);
109
+ // Re-subscribe custom topic callbacks after reconnect.
110
+ topicCallbacksRef.current.forEach((_callbacks, topic) => {
111
+ new_client.subscribe(topic, { qos: 1 }, (err) => {
112
+ if (err)
113
+ console.error("Topic callback subscribe failed:", err);
114
+ });
115
+ });
32
116
  });
33
117
  new_client.on("reconnect", () => {
34
118
  console.log("MQTT Whisperer Reconnecting...");
@@ -47,6 +131,15 @@ export function MQTTMultiDeviceWhisperer({ serverUrl, uuidFromMessage, subTopicF
47
131
  return;
48
132
  const uuid = uuidFromMessage(topic, payload);
49
133
  const bytes = payload instanceof Uint8Array ? payload : new Uint8Array(payload);
134
+ const topicCallbacks = topicCallbacksRef.current.get(topic);
135
+ topicCallbacks?.forEach((cb) => {
136
+ try {
137
+ cb(bytes, topic);
138
+ }
139
+ catch (err) {
140
+ console.error("Topic callback failed:", err);
141
+ }
142
+ });
50
143
  if (!uuid)
51
144
  return;
52
145
  const conn = base.getConnection(uuid);
@@ -75,6 +168,7 @@ export function MQTTMultiDeviceWhisperer({ serverUrl, uuidFromMessage, subTopicF
75
168
  }
76
169
  });
77
170
  watchdogTimers.current = {};
171
+ topicCallbacksRef.current.clear();
78
172
  if (clientRef.current) {
79
173
  clientRef.current.removeAllListeners();
80
174
  clientRef.current.end(true);
@@ -105,6 +199,10 @@ export function MQTTMultiDeviceWhisperer({ serverUrl, uuidFromMessage, subTopicF
105
199
  if (!clientRef.current || isUnmountedRef.current)
106
200
  return;
107
201
  const topic = subTopicFromUuid?.(uuid) ?? uuid;
202
+ // Keep topic subscribed if callbacks or other device-connections still depend on it.
203
+ if (hasCallbackForTopic(topic) || hasOtherConnectionUsingTopic(topic, uuid)) {
204
+ return;
205
+ }
108
206
  if (clientRef.current.connected && !clientRef.current.disconnecting) {
109
207
  clientRef.current.unsubscribe(topic, async (err) => {
110
208
  if (err)
@@ -173,14 +271,8 @@ export function MQTTMultiDeviceWhisperer({ serverUrl, uuidFromMessage, subTopicF
173
271
  const conn = base.getConnection(uuid);
174
272
  if (!conn)
175
273
  return;
176
- const payload = typeof data === 'string'
177
- ? new TextEncoder().encode(data)
178
- : data;
179
- base.appendLog(uuid, {
180
- level: 5,
181
- message: payload,
182
- });
183
- clientRef.current?.publish(pubTopicFromUuid?.(uuid) ?? uuid, payload); // TS is wrong here!
274
+ const topic = pubTopicFromUuid?.(uuid) ?? subTopicFromUuid?.(uuid) ?? uuid;
275
+ await publish(topic, data, uuid);
184
276
  };
185
277
  const addConnection = async ({ uuid, propCreator }) => {
186
278
  if (!clientRef.current || isUnmountedRef.current)
@@ -199,6 +291,8 @@ export function MQTTMultiDeviceWhisperer({ serverUrl, uuidFromMessage, subTopicF
199
291
  return {
200
292
  // Defaults, may be overridden by props
201
293
  send: (d) => defaultSend(id, d),
294
+ publish: (topic, payload) => publish(topic, payload, id),
295
+ subscribeCallbackToTopic,
202
296
  onReceive: (d) => defaultOnReceive(id, d),
203
297
  touchHeartbeat: () => touchHeartbeat(id),
204
298
  // Initial connection state
@@ -46,6 +46,8 @@ export declare function SerialMultiDeviceWhisperer<AppOrMessageLayer extends Ser
46
46
  reconnectAll: (...connectionProps: any) => Promise<void>;
47
47
  releasePortByDefault: boolean;
48
48
  setReleasePortByDefault: import("react").Dispatch<import("react").SetStateAction<boolean>>;
49
+ focussedConnection: string | undefined;
50
+ setFocussedConnection: import("react").Dispatch<import("react").SetStateAction<string | undefined>>;
49
51
  connections: AppOrMessageLayer[];
50
52
  updateConnection: (uuid: string, updater: (c: AppOrMessageLayer) => AppOrMessageLayer) => void;
51
53
  updateConnectionName: (uuid: string, name: string) => void;
@@ -19,6 +19,8 @@ export declare function WebsocketMultiDeviceWhisperer<AppOrMessageLayer extends
19
19
  disconnect: (uuid: string) => Promise<void>;
20
20
  checkForNewDevices: () => Promise<DeviceObjectResponse[]>;
21
21
  reconnectAll: (...connectionProps: any) => Promise<void>;
22
+ focussedConnection: string | undefined;
23
+ setFocussedConnection: import("react").Dispatch<import("react").SetStateAction<string | undefined>>;
22
24
  connections: AppOrMessageLayer[];
23
25
  updateConnection: (uuid: string, updater: (c: AppOrMessageLayer) => AppOrMessageLayer) => void;
24
26
  updateConnectionName: (uuid: string, name: string) => void;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ota-hub-reactjs",
3
- "version": "0.0.16",
3
+ "version": "0.0.18",
4
4
  "description": "ReactJS tools for building web apps to flash MCU devices such as esp32, brought to you by OTA Hub.",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",