@adcops/autocore-react 3.0.39 → 3.0.40

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.
@@ -2,14 +2,21 @@
2
2
  * Copyright (C) 2024 Automated Design Corp. All Rights Reserved.
3
3
  * Created Date: 2024-01-17 11:45:10
4
4
  * -----
5
- * Last Modified: 2024-04-25 15:24:53
5
+ * Last Modified: 2025-08-30 06:54:17
6
6
  * Modified By: ADC
7
7
  * -----
8
- *
8
+ *
9
9
  */
10
10
 
11
-
12
- import React, { createContext, ReactNode, useState, useMemo, useCallback } from 'react';
11
+ import React, {
12
+ createContext,
13
+ ReactNode,
14
+ useState,
15
+ useMemo,
16
+ useCallback,
17
+ useRef,
18
+ useEffect,
19
+ } from "react";
13
20
  import { createHub, Hub } from "../hub";
14
21
  import { CommandMessageResult } from "../hub/CommandMessage";
15
22
 
@@ -19,26 +26,25 @@ export { Hub };
19
26
  * Represents an event subsription.
20
27
  */
21
28
  export interface Subscription {
22
- /** ID of the subscription used for unsubscription. */
23
- id: number;
24
- /** Callback function. */
25
- callback: React.Dispatch<any>;
29
+ /** ID of the subscription used for unsubscription. */
30
+ id: number;
31
+ /** Callback function. */
32
+ callback: React.Dispatch<any>;
26
33
  }
27
34
 
28
-
29
35
  /**
30
36
  * Represents the payload data associated with an event.
31
37
  */
32
38
  export interface State {
33
- /**
34
- * The optional data payload of the event.
35
- */
36
- eventData?: any;
37
- /** Callback subscription. A list of callback subscriptions matched to a topic. */
38
- subscriptions: Record<string, Subscription[]>;
39
-
40
- /** Tracks the next subscription ID that will be assigned. */
41
- nextSubscriptionId: number;
39
+ /**
40
+ * The optional data payload of the event.
41
+ */
42
+ eventData?: any;
43
+ /** Callback subscription. A list of callback subscriptions matched to a topic. */
44
+ subscriptions: Record<string, Subscription[]>;
45
+
46
+ /** Tracks the next subscription ID that will be assigned. */
47
+ nextSubscriptionId: number;
42
48
  }
43
49
 
44
50
  /**
@@ -47,37 +53,35 @@ export interface State {
47
53
  * a value of any type.
48
54
  */
49
55
  export interface Action {
50
-
51
- /**
52
- * The topic or identifier of the event.
53
- */
54
- topic: string;
55
-
56
- /**
57
- * The optional data payload associated with the event.
58
- */
59
- payload?: any;
56
+ /**
57
+ * The topic or identifier of the event.
58
+ */
59
+ topic: string;
60
+
61
+ /**
62
+ * The optional data payload associated with the event.
63
+ */
64
+ payload?: any;
60
65
  }
61
66
 
62
- /**
67
+ /**
63
68
  * Type declaration for the EventEmitter dispatch function, which
64
69
  * publishes an Action globally throughout the EventEmitterContext.
65
70
  */
66
71
  export type EmitterDispatchFunction = (action: Action) => void;
67
72
 
68
- /**
73
+ /**
69
74
  * Type declaration for the EventEmitter dispatch function, which
70
75
  * receives an Action on a specific topic broadcast through the EventEmitterContext.
71
76
  */
72
77
  export type EmitterSubscribeFunction = (action: Action) => void;
73
78
 
74
- /**
79
+ /**
75
80
  * Type declaration for the EventEmitter unsubscribe function, which
76
81
  * receives an Action on a specific topic broadcast through the EventEmitterContext.
77
82
  */
78
83
  export type EmitterUnsubscribeFunction = (action: Action) => void;
79
84
 
80
-
81
85
  /**
82
86
  * Defines the context for an event emitter used throughout a front-end application to manage and dispatch events.
83
87
  * This interface includes methods for managing the application's state, handling global actions, communicating with the back end,
@@ -86,64 +90,66 @@ export type EmitterUnsubscribeFunction = (action: Action) => void;
86
90
  * components and the back end, as well as among components themselves.
87
91
  */
88
92
  export interface EventEmitterContextType {
89
- /**
90
- * The current state of the event emitter, containing the latest event data.
91
- */
92
- state: State;
93
-
94
- /**
95
- * A function to dispatch actions globally throughout the front-end,
96
- * triggering state updates and events.
97
- *
98
- * @param action The action to dispatch, containing topic and optional payload.
99
- */
100
- dispatch: (action: Action) => void;
101
-
102
-
103
- /**
104
- * Invoke/send a message to the back end.
105
- * This does NOT get published to the front end.
106
- */
107
- invoke(domain: string, fname: string, payload?: object): Promise<CommandMessageResult>;
108
-
109
- /**
110
- * Subscribe to events identified by the topic.
111
- * @param topic The subscription topic.
112
- * @param callback The callback to signal.
113
- * @returns number Subscription ID used to unsubscribe later.
114
- */
115
- subscribe: (topic: string, callback: React.Dispatch<any>) => number;
116
-
117
- /**
118
- * Unsubscribe to events.
119
- * @param subscriptionId The id of the subscription returned by the subscribe method.
120
- * @returns
121
- */
122
- unsubscribe: (subscriptionId: number) => void;
123
-
124
-
125
- /**
126
- * Global hub for publishing and receiving events throughout the interface, and for exchanging
127
- * data with the backend.
128
- */
129
- hub: Hub | null;
130
-
131
- /**
132
- * Retrieves the current subscriptions. Used for debugging purposes.
133
- * @param topic Optional. The topic to retrieve subscriptions for. If omitted, returns all subscriptions.
134
- * @returns An object containing the current subscriptions, optionally filtered by topic.
135
- */
136
- getSubscriptions: (topic?: string) => Record<string, Subscription[]> | Subscription[];
137
-
138
-
139
- /**
140
- * Returns true if the Hub in use is connected to its source.
141
- * @returns boolean
142
- */
143
- isConnected: () => boolean;
93
+ /**
94
+ * The current state of the event emitter, containing the latest event data.
95
+ */
96
+ state: State;
97
+
98
+ /**
99
+ * A function to dispatch actions globally throughout the front-end,
100
+ * triggering state updates and events.
101
+ *
102
+ * @param action The action to dispatch, containing topic and optional payload.
103
+ */
104
+ dispatch: (action: Action) => void;
105
+
106
+ /**
107
+ * Invoke/send a message to the back end.
108
+ * This does NOT get published to the front end.
109
+ */
110
+ invoke(
111
+ domain: string,
112
+ fname: string,
113
+ payload?: object
114
+ ): Promise<CommandMessageResult>;
115
+
116
+ /**
117
+ * Subscribe to events identified by the topic.
118
+ * @param topic The subscription topic.
119
+ * @param callback The callback to signal.
120
+ * @returns number Subscription ID used to unsubscribe later.
121
+ */
122
+ subscribe: (topic: string, callback: React.Dispatch<any>) => number;
123
+
124
+ /**
125
+ * Unsubscribe to events.
126
+ * @param subscriptionId The id of the subscription returned by the subscribe method.
127
+ * @returns
128
+ */
129
+ unsubscribe: (subscriptionId: number) => void;
130
+
131
+ /**
132
+ * Global hub for publishing and receiving events throughout the interface, and for exchanging
133
+ * data with the backend.
134
+ */
135
+ hub: Hub | null;
136
+
137
+ /**
138
+ * Retrieves the current subscriptions. Used for debugging purposes.
139
+ * @param topic Optional. The topic to retrieve subscriptions for. If omitted, returns all subscriptions.
140
+ * @returns An object containing the current subscriptions, optionally filtered by topic.
141
+ */
142
+ getSubscriptions: (
143
+ topic?: string
144
+ ) => Record<string, Subscription[]> | Subscription[];
145
+
146
+ /**
147
+ * Returns true if the Hub in use is connected to its source.
148
+ * @returns boolean
149
+ */
150
+ isConnected: () => boolean;
144
151
  }
145
152
 
146
-
147
153
  let globalSubscriptionId = 1;
148
154
 
149
155
  /**
@@ -153,7 +159,7 @@ let globalSubscriptionId = 1;
153
159
  * in a React application. It serves as a global event bus that components can subscribe to or emit events, allowing for
154
160
  * a loosely coupled architecture. Additionally, it provides a mechanism for invoking backend functions and managing
155
161
  * subscriptions, making it easier to integrate React components with backend services.
156
- *
162
+ *
157
163
  * The context includes several key functionalities:
158
164
  * - `state`: Maintains the current state of subscriptions and their identifiers.
159
165
  * - `dispatch`: Allows components to emit events with specific topics and payloads, which can be listened to by other components.
@@ -161,21 +167,21 @@ let globalSubscriptionId = 1;
161
167
  * - `unsubscribe`: Provides a way for components to stop listening to events, helping prevent memory leaks and unnecessary updates.
162
168
  * - `invoke`: Facilitates calling backend functions with specific arguments and handling their responses asynchronously.
163
169
  * - `getSubscriptions`: Offers insight into current active subscriptions, useful for debugging purposes.
164
- *
170
+ *
165
171
  * This context is essential for applications that require a high degree of inter-component communication or need to
166
172
  * interact with a backend efficiently.
167
- *
173
+ *
168
174
  * For more information, see [Additional Documentation](../additional-docs/GlobalEventEmitter.md).
169
- *
175
+ *
170
176
  * ## Usage
171
- *
177
+ *
172
178
  * The entire application should be wrapped in the EventEmitterProvider.
173
- *
179
+ *
174
180
  * App.tsx
175
181
  * ```
176
182
  * import { EventEmitterProvider } from "@adcops/autocore-react/core/EventEmitterContext.js";
177
183
  * function App() {
178
- *
184
+ *
179
185
  * return(
180
186
  * <EventEmitterProvider>
181
187
  * <PrimeReactProvider>
@@ -187,31 +193,31 @@ let globalSubscriptionId = 1;
187
193
  * </PrimeReactProvider>
188
194
  * </EventEmitterProvider>
189
195
  * );
190
- *
196
+ *
191
197
  * }
192
- *
198
+ *
193
199
  * ```
194
- *
200
+ *
195
201
  * ### Catching and receiving events
196
202
  * The EventEmitterContext creates an appropriate instance of the hub, which is derived from HubBase.
197
203
  * That hub can be used to publish and subscribe to
198
204
  * topics globally in the front-end, regardless of being connected to any backend.
199
205
  * Usage within a component is simple.
200
- *
206
+ *
201
207
  * ```
202
208
  * const {dispatch, subscribe, unsubscribe} = useContext(EventEmitterContext);
203
209
  * const [controlPower, setControlPower] = useState(false);
204
210
  * useEffect(() => {
205
211
  * const unsubscribeControlPower = subscribe('value-simulator-bBit1', (value) => {
206
212
  * setControlPower(value);
207
- * });
213
+ * });
208
214
  *
209
215
  *
210
216
  * return () => {
211
217
  * unsubscribe(unsubscribeControlPower);
212
218
  * }
213
219
  * }, [] );
214
- *
220
+ *
215
221
  * const onPbPressed = () => {
216
222
  * let count = 1;
217
223
  * dispatch({
@@ -219,30 +225,30 @@ let globalSubscriptionId = 1;
219
225
  * payload: count
220
226
  * });
221
227
  * }
222
- *
223
- * ```
228
+ *
229
+ * ```
224
230
  * The hub should also be used for invoking events in the backend.
225
231
  * This example will call the function "update_count" in the backend, passing
226
232
  * the expected argument "count". Details of the interaction between the Hub and
227
233
  * the backend will be handled by the appropriate HubBase sub-class, and should
228
234
  * be transparent to the front end.
229
- *
235
+ *
230
236
  * ```
231
237
  * const {invoke} = useContext(EventEmitterContext);
232
238
  * const incrementCount = () => {
233
239
  * count += 1;
234
- * invoke('update_count', {"count": count});
240
+ * invoke('update_count', {"count": count});
235
241
  * };
236
- *
242
+ *
237
243
  * Subscribing to a topic is simple. The type of value received is specific
238
244
  * to the topic.
239
- *
245
+ *
240
246
  * Example: Listen to an event 'xarm-position':
241
247
  * ```
242
248
  * const {subscribe, unsubscribe} = useContext(EventEmitterContext);
243
249
  * useEffect(() => {
244
250
  * const unsubscripeMp = subscribe('xarm-position', (value) => {
245
- * // The publisher sent a JSON object of 3D position values.
251
+ * // The publisher sent a JSON object of 3D position values.
246
252
  * setX(value.x);
247
253
  * setY(value.y);
248
254
  * setZ(value.z);
@@ -250,49 +256,55 @@ let globalSubscriptionId = 1;
250
256
  * setB(value.yaw);
251
257
  * setC(value.pitch);
252
258
  * });
253
- *
259
+ *
254
260
  * return () => {
255
261
  * unsubscribe(unsubscripeMp);
256
262
  * }
257
- *
263
+ *
258
264
  * }, [] );
259
265
  *
260
266
  * ```
261
- *
267
+ *
262
268
  * For applications that need to access the instance of the hub, get the current instance
263
269
  * from the EventEmitterContext:
264
- *
270
+ *
265
271
  * ```
266
272
  * const {hub} = useContext(EventEmitterContext);
267
273
  * * ```
268
- *
269
- *
274
+ *
275
+ *
270
276
  */
271
277
  export const EventEmitterContext = createContext<EventEmitterContextType>({
272
- state: { subscriptions: {}, nextSubscriptionId: 1 },
273
- dispatch: () => { },
274
- subscribe: () => { return 0; }, // Placeholder for subscription logic
275
- invoke: async (domain: string, fname: string, payload?: object) => {
276
- domain;
277
- fname;
278
- payload;
279
- let ret: CommandMessageResult = {
280
- data: {},
281
- success: false,
282
- error_message: ""
283
- };
284
- // Placeholder for invoke logic
285
- // Implement the logic to send a message to the backend and return a promise
286
- return Promise.resolve(ret); // Example placeholder, replace with actual implementation
287
- },
288
- unsubscribe: (subscriptionId: number) => { subscriptionId; }, // Placeholder for unsubscription logic
289
- hub: null,
290
- getSubscriptions: () => { return []; },
291
- isConnected: () => { return false }
292
-
278
+ state: { subscriptions: {}, nextSubscriptionId: 1 },
279
+ dispatch: () => {},
280
+ subscribe: () => {
281
+ return 0;
282
+ }, // Placeholder for subscription logic
283
+ invoke: async (domain: string, fname: string, payload?: object) => {
284
+ domain;
285
+ fname;
286
+ payload;
287
+ let ret: CommandMessageResult = {
288
+ data: {},
289
+ success: false,
290
+ error_message: "",
291
+ };
292
+ // Placeholder for invoke logic
293
+ // Implement the logic to send a message to the backend and return a promise
294
+ return Promise.resolve(ret); // Example placeholder, replace with actual implementation
295
+ },
296
+ unsubscribe: (subscriptionId: number) => {
297
+ subscriptionId;
298
+ }, // Placeholder for unsubscription logic
299
+ hub: null,
300
+ getSubscriptions: () => {
301
+ return [];
302
+ },
303
+ isConnected: () => {
304
+ return false;
305
+ },
293
306
  });
294
307
 
295
-
296
308
  /**
297
309
  * A React component that provides the EventEmitterContext to its children.
298
310
  *
@@ -300,16 +312,16 @@ export const EventEmitterContext = createContext<EventEmitterContextType>({
300
312
  * with the event emitter.
301
313
  *
302
314
  * @param children The child components to be wrapped in the context.
303
- *
315
+ *
304
316
  * ## Usage
305
- *
317
+ *
306
318
  * The entire application should be wrapped in the EventEmitterProvider.
307
- *
319
+ *
308
320
  * App.tsx
309
321
  * ```
310
322
  * import { EventEmitterProvider } from "@adcops/autocore-react/core/EventEmitterContext.js";
311
323
  * function App() {
312
- *
324
+ *
313
325
  * return(
314
326
  * <EventEmitterProvider>
315
327
  * <PrimeReactProvider>
@@ -321,89 +333,120 @@ export const EventEmitterContext = createContext<EventEmitterContextType>({
321
333
  * </PrimeReactProvider>
322
334
  * </EventEmitterProvider>
323
335
  * );
324
- *
336
+ *
325
337
  * }
326
- *
327
- * ```
338
+ *
339
+ * ```
328
340
  */
329
- export const EventEmitterProvider: React.FC<{ children: ReactNode }> = ({ children }) => {
330
- const [state, setState] = useState<State>({ subscriptions: {}, nextSubscriptionId: 1 });
331
-
332
- // Memoize the hub instance so it's only created once
333
- const hub = useMemo(() => createHub(), []);
334
-
335
- const dispatch = useCallback((action: Action) => {
336
- const { topic, payload } = action;
337
-
338
- setState(prevState => {
339
- const callbacks = prevState.subscriptions[topic] || [];
340
- callbacks.forEach(sub => sub.callback(payload));
341
- return { ...prevState, eventData: payload };
342
- });
343
-
344
- }, []);
345
-
346
- const subscribe = useCallback((topic: string, callback: React.Dispatch<any>): number => {
347
-
348
- globalSubscriptionId += 1;
349
- const subscriptionId = globalSubscriptionId; // state.nextSubscriptionId;
350
-
351
- setState(prevState => ({
352
- ...prevState,
353
- subscriptions: {
354
- ...prevState.subscriptions,
355
- [topic]: [...(prevState.subscriptions[topic] || []), { id: subscriptionId, callback }]
356
- },
357
- nextSubscriptionId: globalSubscriptionId + 1 // prevState.nextSubscriptionId + 1
358
- }));
359
-
360
- return subscriptionId;
361
- }, []);
362
-
363
-
364
- const unsubscribe = useCallback((subscriptionId: number) => {
365
- setState(prevState => {
366
- const newSubscriptions = { ...prevState.subscriptions };
367
- Object.keys(newSubscriptions).forEach(topic => {
368
- newSubscriptions[topic] = newSubscriptions[topic].filter(sub => sub.id !== subscriptionId);
369
- if (newSubscriptions[topic].length === 0) {
370
- delete newSubscriptions[topic];
371
- }
372
- });
373
- return { ...prevState, subscriptions: newSubscriptions };
374
- });
375
- }, []);
376
-
377
-
378
- const getSubscriptions = useCallback((topic?: string): Record<string, Subscription[]> | Subscription[] => {
379
- if (topic) {
380
- // Return subscriptions for the provided topic, or an empty array if the topic doesn't exist
381
- return state.subscriptions[topic] || [];
382
- } else {
383
- // Return all subscriptions
384
- return state.subscriptions;
385
- }
386
- }, []);
387
-
388
- // Provide the memoized hub instance in the context value
389
- const contextValue = useMemo(() => ({
390
- state,
391
- dispatch,
392
- subscribe,
393
- unsubscribe,
394
- invoke: hub.invoke,
395
- hub,
396
- getSubscriptions,
397
- isConnected: hub.isConnected
398
- }), [state, hub]);
399
-
400
-
401
- hub.setContext(contextValue);
402
-
403
- return (
404
- <EventEmitterContext.Provider value={contextValue}>
405
- {children}
406
- </EventEmitterContext.Provider>
407
- );
408
-
341
+ export const EventEmitterProvider: React.FC<{ children: ReactNode }> = ({
342
+ children,
343
+ }) => {
344
+ const [state, setState] = useState<State>({
345
+ subscriptions: {},
346
+ nextSubscriptionId: 1,
347
+ });
348
+
349
+ // Memoize the hub instance so it's only created once
350
+ const hub = useMemo(() => createHub(), []);
351
+
352
+ // <-- New: the source of truth for subscriptions lives in a ref
353
+ const subsRef = useRef<Record<string, Subscription[]>>({});
354
+
355
+ // Keep the ref in sync for debugging / external reads
356
+ useEffect(() => {
357
+ subsRef.current = state.subscriptions;
358
+ }, [state.subscriptions]);
359
+
360
+ const dispatch = useCallback((action: Action) => {
361
+ const { topic, payload } = action;
362
+
363
+ // Read once, outside setState, so it can't be double-invoked by StrictMode
364
+ const listeners = subsRef.current[topic]?.slice() ?? [];
365
+ for (const sub of listeners) {
366
+ try {
367
+ sub.callback(payload);
368
+ } catch (e) {
369
+ console.error("[EventBus] listener error", e);
370
+ }
371
+ }
372
+
373
+ // Optional: keep last payload in state (pure)
374
+ setState((prev) => ({ ...prev, eventData: payload }));
375
+ }, []);
376
+
377
+ const subscribe = useCallback(
378
+ (topic: string, callback: React.Dispatch<any>): number => {
379
+ const id = ++globalSubscriptionId;
380
+
381
+ // Mutate ref
382
+ const prev = subsRef.current[topic] ?? [];
383
+ const next = [...prev, { id, callback }];
384
+ subsRef.current[topic] = next;
385
+
386
+ // Reflect in state (for debugging/inspection)
387
+ setState((prevState) => ({
388
+ ...prevState,
389
+ subscriptions: { ...prevState.subscriptions, [topic]: next },
390
+ nextSubscriptionId: globalSubscriptionId + 1,
391
+ }));
392
+
393
+ return id;
394
+ },
395
+ []
396
+ );
397
+
398
+ const unsubscribe = useCallback((subscriptionId: number) => {
399
+ const map = subsRef.current;
400
+ for (const t of Object.keys(map)) {
401
+ const next = map[t].filter((s) => s.id !== subscriptionId);
402
+ if (next.length) map[t] = next;
403
+ else delete map[t];
404
+ }
405
+
406
+ setState((prev) => ({ ...prev, subscriptions: { ...map } }));
407
+ }, []);
408
+
409
+ // (Nice-to-have) Handle HMR so listeners don’t leak across hot updates
410
+ if (import.meta && (import.meta as any).hot) {
411
+ (import.meta as any).hot.dispose(() => {
412
+ subsRef.current = {};
413
+ });
414
+ }
415
+
416
+ // const getSubscriptions = useCallback(
417
+ // (topic?: string): Record<string, Subscription[]> | Subscription[] => {
418
+ // if (topic) {
419
+ // // Return subscriptions for the provided topic, or an empty array if the topic doesn't exist
420
+ // return state.subscriptions[topic] || [];
421
+ // } else {
422
+ // // Return all subscriptions
423
+ // return state.subscriptions;
424
+ // }
425
+ // },
426
+ // []
427
+ // );
428
+
429
+ // Provide the memoized hub instance in the context value
430
+ const contextValue = useMemo(
431
+ () => ({
432
+ state,
433
+ dispatch,
434
+ subscribe,
435
+ unsubscribe,
436
+ invoke: hub.invoke,
437
+ hub,
438
+ getSubscriptions: (topic?: string) =>
439
+ topic ? subsRef.current[topic] ?? [] : subsRef.current,
440
+ isConnected: hub.isConnected,
441
+ }),
442
+ [state, hub, dispatch, subscribe, unsubscribe]
443
+ );
444
+
445
+ hub.setContext(contextValue);
446
+
447
+ return (
448
+ <EventEmitterContext.Provider value={contextValue}>
449
+ {children}
450
+ </EventEmitterContext.Provider>
451
+ );
409
452
  };