@adcops/autocore-react 3.3.9 → 3.3.10
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/LICENSE +58 -58
- package/additional-docs/AutoCoreTagContext.md +441 -441
- package/additional-docs/ButtonApiSpecs.md +48 -48
- package/additional-docs/GlobalEventEmitter.md +243 -243
- package/additional-docs/general_recommendations.md +22 -22
- package/additional-docs/react_performance_notes.md +94 -94
- package/dist/assets/svg/blockly_logo.svg +82 -82
- package/dist/assets/svg/distance.svg +40 -40
- package/dist/assets/svg/python_logo.svg +246 -246
- package/dist/assets/svg/rotation_ccw.svg +50 -50
- package/dist/assets/svg/rotation_ccw_a.svg +57 -57
- package/dist/assets/svg/rotation_ccw_b.svg +57 -57
- package/dist/assets/svg/rotation_ccw_c.svg +57 -57
- package/dist/assets/svg/rotation_cw.svg +49 -49
- package/dist/assets/svg/rotation_cw_a.svg +30 -30
- package/dist/assets/svg/rotation_cw_b.svg +30 -30
- package/dist/assets/svg/rotation_cw_c.svg +30 -30
- package/dist/assets/svg/speed.svg +39 -39
- package/dist/components/BlocklyEditor.css +93 -93
- package/dist/components/JogPanel.css +41 -41
- package/dist/components/ProgressBarWithValue.css +27 -27
- package/dist/components/ValueIndicator.css +31 -31
- package/dist/components/osk.css +123 -123
- package/dist/core/AutoCoreTagContext.d.ts.map +1 -1
- package/dist/core/AutoCoreTagContext.js +1 -1
- package/dist/hub/HubBase.d.ts +3 -3
- package/dist/hub/HubBase.d.ts.map +1 -1
- package/dist/hub/HubBase.js +1 -1
- package/package.json +104 -104
- package/readme.md +343 -343
- package/src/assets/BlocklyLogo.tsx +27 -27
- package/src/assets/Distance.tsx +18 -18
- package/src/assets/JogLong.tsx +13 -13
- package/src/assets/JogMedium.tsx +13 -13
- package/src/assets/JogShort.tsx +13 -13
- package/src/assets/PythonLogo.tsx +83 -83
- package/src/assets/Rotation3D.tsx +13 -13
- package/src/assets/RotationCcw.tsx +33 -33
- package/src/assets/RotationCcwA.tsx +45 -45
- package/src/assets/RotationCcwB.tsx +45 -45
- package/src/assets/RotationCcwC.tsx +45 -45
- package/src/assets/RotationCw.tsx +31 -31
- package/src/assets/RotationCwA.tsx +42 -42
- package/src/assets/RotationCwB.tsx +42 -42
- package/src/assets/RotationCwC.tsx +42 -42
- package/src/assets/Run.tsx +13 -13
- package/src/assets/Speed.tsx +18 -18
- package/src/assets/SpeedFast.tsx +13 -13
- package/src/assets/SpeedMedium.tsx +13 -13
- package/src/assets/SpeedNone.tsx +13 -13
- package/src/assets/SpeedSlow.tsx +13 -13
- package/src/assets/Walk.tsx +13 -13
- package/src/assets/index.ts +22 -22
- package/src/assets/svg/blockly_logo.svg +82 -82
- package/src/assets/svg/distance.svg +40 -40
- package/src/assets/svg/python_logo.svg +246 -246
- package/src/assets/svg/rotation_ccw.svg +50 -50
- package/src/assets/svg/rotation_ccw_a.svg +57 -57
- package/src/assets/svg/rotation_ccw_b.svg +57 -57
- package/src/assets/svg/rotation_ccw_c.svg +57 -57
- package/src/assets/svg/rotation_cw.svg +49 -49
- package/src/assets/svg/rotation_cw_a.svg +30 -30
- package/src/assets/svg/rotation_cw_b.svg +30 -30
- package/src/assets/svg/rotation_cw_c.svg +30 -30
- package/src/assets/svg/speed.svg +39 -39
- package/src/components/AutoCoreDevPanel.tsx +414 -414
- package/src/components/BlocklyEditor.css +93 -93
- package/src/components/BlocklyEditor.tsx +609 -609
- package/src/components/CodeEditor.tsx +155 -155
- package/src/components/FileList.tsx +390 -390
- package/src/components/FileSelect.tsx +128 -128
- package/src/components/FitText.tsx +35 -35
- package/src/components/Indicator.tsx +188 -188
- package/src/components/IndicatorButton.tsx +214 -214
- package/src/components/IndicatorRect.tsx +172 -172
- package/src/components/JogPanel.css +41 -41
- package/src/components/JogPanel.tsx +461 -461
- package/src/components/Lamp.tsx +243 -243
- package/src/components/Osk.tsx +192 -192
- package/src/components/OskDialog.tsx +164 -164
- package/src/components/ProgressBarWithValue.css +27 -27
- package/src/components/ProgressBarWithValue.tsx +48 -48
- package/src/components/TextInput.tsx +195 -195
- package/src/components/ToggleGroup.tsx +322 -322
- package/src/components/ValueDisplay.tsx +236 -236
- package/src/components/ValueIndicator.css +31 -31
- package/src/components/ValueIndicator.tsx +135 -135
- package/src/components/ValueInput.tsx +368 -368
- package/src/components/osk.css +123 -123
- package/src/core/ActionMode.ts +19 -19
- package/src/core/AutoCoreTagContext.tsx +625 -614
- package/src/core/AutoCoreTagTypes.ts +334 -334
- package/src/core/CoreStreamTypes.ts +512 -512
- package/src/core/EventEmitterContext.tsx +434 -434
- package/src/core/IndicatorButtonState.ts +34 -34
- package/src/core/IndicatorColor.ts +35 -35
- package/src/core/MaskPatterns.ts +87 -87
- package/src/core/NumerableTypes.ts +80 -80
- package/src/core/PositionContext.ts +59 -59
- package/src/core/UniqueId.ts +41 -41
- package/src/core/ValueSimulator.ts +166 -166
- package/src/core/hoc.tsx +65 -65
- package/src/hooks/adsHooks.tsx +287 -287
- package/src/hooks/commandHooks.tsx +300 -300
- package/src/hooks/index.ts +12 -12
- package/src/hooks/useAutoCoreTag.ts +103 -103
- package/src/hooks/useScaledValue.tsx +99 -99
- package/src/hub/CommandMessage.ts +89 -89
- package/src/hub/DebugPanel.ts +307 -307
- package/src/hub/HubBase.ts +249 -236
- package/src/hub/HubSimulate.ts +124 -124
- package/src/hub/HubTauri.ts +140 -140
- package/src/hub/HubWebSocket.ts +250 -250
- package/src/hub/debug.ts +211 -211
- package/src/hub/index.ts +81 -81
- package/src/themes/adc-dark/_extensions.scss +166 -166
- package/src/themes/adc-dark/_variables.scss +913 -913
- package/src/themes/adc-dark/blue/_fonts.scss +23 -23
- package/src/themes/adc-dark/blue/adc_theme.scss +31 -31
- package/src/themes/adc-dark/blue/theme.scss +14 -14
- package/src/themes/theme-base/_colors.scss +17 -17
- package/src/themes/theme-base/_common.scss +74 -74
- package/src/themes/theme-base/_components.scss +111 -111
- package/src/themes/theme-base/_mixins.scss +243 -243
- package/src/themes/theme-base/components/button/_button.scss +644 -644
- package/src/themes/theme-base/components/button/_speeddial.scss +91 -91
- package/src/themes/theme-base/components/button/_splitbutton.scss +358 -358
- package/src/themes/theme-base/components/data/_carousel.scss +39 -39
- package/src/themes/theme-base/components/data/_datascroller.scss +47 -47
- package/src/themes/theme-base/components/data/_datatable.scss +388 -388
- package/src/themes/theme-base/components/data/_dataview.scss +47 -47
- package/src/themes/theme-base/components/data/_filter.scss +137 -137
- package/src/themes/theme-base/components/data/_orderlist.scss +86 -86
- package/src/themes/theme-base/components/data/_organizationchart.scss +50 -50
- package/src/themes/theme-base/components/data/_paginator.scss +91 -91
- package/src/themes/theme-base/components/data/_picklist.scss +73 -73
- package/src/themes/theme-base/components/data/_timeline.scss +38 -38
- package/src/themes/theme-base/components/data/_tree.scss +184 -184
- package/src/themes/theme-base/components/data/_treetable.scss +431 -431
- package/src/themes/theme-base/components/file/_fileupload.scss +41 -41
- package/src/themes/theme-base/components/input/_autocomplete.scss +94 -94
- package/src/themes/theme-base/components/input/_calendar.scss +251 -251
- package/src/themes/theme-base/components/input/_cascadeselect.scss +107 -107
- package/src/themes/theme-base/components/input/_checkbox.scss +181 -181
- package/src/themes/theme-base/components/input/_chips.scss +102 -102
- package/src/themes/theme-base/components/input/_colorpicker.scss +17 -17
- package/src/themes/theme-base/components/input/_dropdown.scss +252 -252
- package/src/themes/theme-base/components/input/_editor.scss +122 -122
- package/src/themes/theme-base/components/input/_iconfield.scss +9 -9
- package/src/themes/theme-base/components/input/_inputgroup.scss +74 -74
- package/src/themes/theme-base/components/input/_inputicon.scss +14 -14
- package/src/themes/theme-base/components/input/_inputnumber.scss +4 -4
- package/src/themes/theme-base/components/input/_inputotp.scss +10 -10
- package/src/themes/theme-base/components/input/_inputswitch.scss +99 -99
- package/src/themes/theme-base/components/input/_inputtext.scss +101 -101
- package/src/themes/theme-base/components/input/_listbox.scss +138 -138
- package/src/themes/theme-base/components/input/_mention.scss +30 -30
- package/src/themes/theme-base/components/input/_multiselect.scss +278 -278
- package/src/themes/theme-base/components/input/_password.scss +32 -32
- package/src/themes/theme-base/components/input/_radiobutton.scss +169 -169
- package/src/themes/theme-base/components/input/_rating.scss +80 -80
- package/src/themes/theme-base/components/input/_selectbutton.scss +49 -49
- package/src/themes/theme-base/components/input/_slider.scss +49 -49
- package/src/themes/theme-base/components/input/_togglebutton.scss +99 -99
- package/src/themes/theme-base/components/input/_treeselect.scss +151 -151
- package/src/themes/theme-base/components/input/_tristatecheckbox.scss +46 -46
- package/src/themes/theme-base/components/menu/_breadcrumb.scss +42 -42
- package/src/themes/theme-base/components/menu/_contextmenu.scss +39 -39
- package/src/themes/theme-base/components/menu/_dock.scss +109 -109
- package/src/themes/theme-base/components/menu/_megamenu.scss +141 -141
- package/src/themes/theme-base/components/menu/_menu.scss +33 -33
- package/src/themes/theme-base/components/menu/_menubar.scss +216 -216
- package/src/themes/theme-base/components/menu/_panelmenu.scss +153 -153
- package/src/themes/theme-base/components/menu/_slidemenu.scss +60 -60
- package/src/themes/theme-base/components/menu/_steps.scss +57 -57
- package/src/themes/theme-base/components/menu/_tabmenu.scss +50 -50
- package/src/themes/theme-base/components/menu/_tieredmenu.scss +43 -43
- package/src/themes/theme-base/components/messages/_inlinemessage.scss +69 -69
- package/src/themes/theme-base/components/messages/_message.scss +107 -107
- package/src/themes/theme-base/components/messages/_toast.scss +100 -100
- package/src/themes/theme-base/components/misc/_avatar.scss +33 -33
- package/src/themes/theme-base/components/misc/_badge.scss +76 -76
- package/src/themes/theme-base/components/misc/_chip.scss +38 -38
- package/src/themes/theme-base/components/misc/_inplace.scss +17 -17
- package/src/themes/theme-base/components/misc/_metergroup.scss +80 -80
- package/src/themes/theme-base/components/misc/_progressbar.scss +17 -17
- package/src/themes/theme-base/components/misc/_scrolltop.scss +24 -24
- package/src/themes/theme-base/components/misc/_skeleton.scss +7 -7
- package/src/themes/theme-base/components/misc/_tag.scss +39 -39
- package/src/themes/theme-base/components/misc/_terminal.scss +12 -12
- package/src/themes/theme-base/components/multimedia/_galleria.scss +153 -153
- package/src/themes/theme-base/components/multimedia/_image.scss +53 -53
- package/src/themes/theme-base/components/overlay/_confirmpopup.scss +72 -72
- package/src/themes/theme-base/components/overlay/_dialog.scss +78 -78
- package/src/themes/theme-base/components/overlay/_overlaypanel.scss +64 -64
- package/src/themes/theme-base/components/overlay/_sidebar.scss +23 -23
- package/src/themes/theme-base/components/overlay/_tooltip.scss +33 -33
- package/src/themes/theme-base/components/panel/_accordion.scss +118 -118
- package/src/themes/theme-base/components/panel/_card.scss +30 -30
- package/src/themes/theme-base/components/panel/_divider.scss +30 -30
- package/src/themes/theme-base/components/panel/_fieldset.scss +47 -47
- package/src/themes/theme-base/components/panel/_panel.scss +47 -47
- package/src/themes/theme-base/components/panel/_scrollpanel.scss +10 -10
- package/src/themes/theme-base/components/panel/_splitter.scss +23 -23
- package/src/themes/theme-base/components/panel/_stepper.scss +136 -136
- package/src/themes/theme-base/components/panel/_tabview.scss +147 -147
- package/src/themes/theme-base/components/panel/_toolbar.scss +11 -11
- package/terser.config.cjs +25 -25
- package/todo.md +18 -18
- package/tools/build-themes.cjs +65 -65
- package/tools/copy-distribution-files.cjs +77 -77
- package/tools/minify.cjs +55 -55
- package/tsconfig.json +48 -48
- package/typedoc.json +12 -12
- package/.claude/settings.local.json +0 -7
|
@@ -1,435 +1,435 @@
|
|
|
1
|
-
/*
|
|
2
|
-
* Copyright (C) 2024 Automated Design Corp. All Rights Reserved.
|
|
3
|
-
* Created Date: 2024-01-17 11:45:10
|
|
4
|
-
* -----
|
|
5
|
-
* Last Modified: 2026-01-29 09:32:29
|
|
6
|
-
* Modified By: ADC
|
|
7
|
-
* -----
|
|
8
|
-
*
|
|
9
|
-
*/
|
|
10
|
-
|
|
11
|
-
/**
|
|
12
|
-
* @fileoverview EventEmitterContext - Global Event Bus for AutoCore React Applications
|
|
13
|
-
*
|
|
14
|
-
* The EventEmitterContext provides a comprehensive event-driven communication system for React applications,
|
|
15
|
-
* enabling seamless interaction between components and backend services. It serves as the foundational layer
|
|
16
|
-
* for AutoCore's real-time data flow and component communication architecture.
|
|
17
|
-
*
|
|
18
|
-
* ## Core Features
|
|
19
|
-
*
|
|
20
|
-
* - **Global Event Bus**: Publish and subscribe to events across all components.
|
|
21
|
-
* - **Backend Integration**: Direct communication with AutoCore server via Hub abstraction.
|
|
22
|
-
* - **Type-Safe Subscriptions**: Strongly-typed event handling with automatic cleanup.
|
|
23
|
-
* - **Performance Optimized**: Uses stable context values and direct callbacks to avoid global re-renders.
|
|
24
|
-
* - **Connection Management**: Automatic reconnection and state synchronization.
|
|
25
|
-
*
|
|
26
|
-
* ## Architecture
|
|
27
|
-
*
|
|
28
|
-
* The system consists of three main components:
|
|
29
|
-
* 1. **EventEmitterProvider**: React context provider that manages global state and event routing.
|
|
30
|
-
* 2. **Hub**: Abstraction layer for backend communication (WebSocket, HTTP, etc.).
|
|
31
|
-
* 3. **Subscription Manager**: Handles event routing and lifecycle management.
|
|
32
|
-
*
|
|
33
|
-
* ## Performance Notes
|
|
34
|
-
*
|
|
35
|
-
* To ensure high performance with high-frequency data (e.g., 50Hz sensor updates):
|
|
36
|
-
* 1. **No State Updates on Dispatch**: The `dispatch` function does NOT update React state. It calls listeners directly.
|
|
37
|
-
* This prevents the entire component tree from re-rendering on every message.
|
|
38
|
-
* 2. **Stable Context Value**: The `contextValue` object is memoized and rarely changes.
|
|
39
|
-
* 3. **Direct Subscriptions**: Components subscribe via `useEffect` and receive updates via callbacks, not props.
|
|
40
|
-
*
|
|
41
|
-
* @module core/EventEmitterContext
|
|
42
|
-
* @version 3.0.42
|
|
43
|
-
* @author Automated Design Corp.
|
|
44
|
-
*/
|
|
45
|
-
|
|
46
|
-
import React, {
|
|
47
|
-
createContext,
|
|
48
|
-
useState,
|
|
49
|
-
useMemo,
|
|
50
|
-
useCallback,
|
|
51
|
-
useRef,
|
|
52
|
-
useEffect,
|
|
53
|
-
} from "react";
|
|
54
|
-
|
|
55
|
-
import type {ReactNode} from "react";
|
|
56
|
-
import { createHub, Hub } from "../hub";
|
|
57
|
-
import type { CommandMessage } from "../hub";
|
|
58
|
-
import { MessageType } from "../hub/CommandMessage";
|
|
59
|
-
import { enableDebugPanel } from "../hub/DebugPanel";
|
|
60
|
-
|
|
61
|
-
export { Hub };
|
|
62
|
-
|
|
63
|
-
/**
|
|
64
|
-
* Represents an active event subscription.
|
|
65
|
-
*/
|
|
66
|
-
export interface Subscription {
|
|
67
|
-
/** Unique ID of the subscription used for unsubscription. */
|
|
68
|
-
id: number;
|
|
69
|
-
/** Callback function to execute when the event fires. */
|
|
70
|
-
callback: React.Dispatch<any>;
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
/**
|
|
74
|
-
* Represents the internal state of the EventEmitter.
|
|
75
|
-
* Mostly used for debugging and dev tools inspection.
|
|
76
|
-
*/
|
|
77
|
-
export interface State {
|
|
78
|
-
/**
|
|
79
|
-
* The optional data payload of the last event (DEBUGGING ONLY).
|
|
80
|
-
* Note: This is no longer updated in production for performance reasons.
|
|
81
|
-
*/
|
|
82
|
-
eventData?: any;
|
|
83
|
-
/** Active subscriptions grouped by topic. */
|
|
84
|
-
subscriptions: Record<string, Subscription[]>;
|
|
85
|
-
|
|
86
|
-
/** Tracks the next subscription ID that will be assigned. */
|
|
87
|
-
nextSubscriptionId: number;
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
/**
|
|
91
|
-
* An action event published by the EventEmitterContext.
|
|
92
|
-
*/
|
|
93
|
-
export interface Action {
|
|
94
|
-
/** The topic or identifier of the event. */
|
|
95
|
-
topic: string;
|
|
96
|
-
/** The optional data payload associated with the event. */
|
|
97
|
-
payload?: any;
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
export type EmitterDispatchFunction = (action: Action) => void;
|
|
101
|
-
|
|
102
|
-
/**
|
|
103
|
-
* Defines the context for the global event emitter.
|
|
104
|
-
* Provides methods for dispatching events, subscribing to topics, and communicating with the backend.
|
|
105
|
-
*/
|
|
106
|
-
export interface EventEmitterContextType {
|
|
107
|
-
/**
|
|
108
|
-
* The current state of the event emitter (mostly for debugging).
|
|
109
|
-
*/
|
|
110
|
-
state: State;
|
|
111
|
-
|
|
112
|
-
/**
|
|
113
|
-
* Dispatch an action globally throughout the front-end.
|
|
114
|
-
* Triggers callbacks for all subscribers of the topic.
|
|
115
|
-
*
|
|
116
|
-
* @param action The action to dispatch.
|
|
117
|
-
*/
|
|
118
|
-
dispatch: EmitterDispatchFunction;
|
|
119
|
-
|
|
120
|
-
/**
|
|
121
|
-
* Send a message to the backend (Low-level).
|
|
122
|
-
*
|
|
123
|
-
* @param topic The FQDN of the resource (e.g., "ads.plc1.gio.bControlPowerOk")
|
|
124
|
-
* @param messageType The action to perform (Read, Write, Subscribe, etc.)
|
|
125
|
-
* @param payload Optional data payload
|
|
126
|
-
* @returns Promise resolving to the CommandMessage response
|
|
127
|
-
*/
|
|
128
|
-
invoke(
|
|
129
|
-
topic: string,
|
|
130
|
-
messageType: MessageType,
|
|
131
|
-
payload?: object
|
|
132
|
-
): Promise<CommandMessage>;
|
|
133
|
-
|
|
134
|
-
/**
|
|
135
|
-
* Read a value from the backend.
|
|
136
|
-
*
|
|
137
|
-
* @param topic The FQDN of the resource to read
|
|
138
|
-
* @param payload Optional additional parameters
|
|
139
|
-
* @returns Promise resolving to the CommandMessage response with the value in `data`
|
|
140
|
-
*/
|
|
141
|
-
read(topic: string, payload?: object): Promise<CommandMessage>;
|
|
142
|
-
|
|
143
|
-
/**
|
|
144
|
-
* Write a value to the backend.
|
|
145
|
-
*
|
|
146
|
-
* @param topic The FQDN of the resource to write
|
|
147
|
-
* @param value The value to write
|
|
148
|
-
* @returns Promise resolving to the CommandMessage response
|
|
149
|
-
*/
|
|
150
|
-
write(topic: string, value: any): Promise<CommandMessage>;
|
|
151
|
-
|
|
152
|
-
/**
|
|
153
|
-
* Subscribe to value changes on the backend.
|
|
154
|
-
*
|
|
155
|
-
* Tells the server to start broadcasting updates for a specific topic.
|
|
156
|
-
* Also registers a mapping between the local `tagName` and the remote `topic` (FQDN)
|
|
157
|
-
* in the Hub, so that incoming broadcasts are routed correctly.
|
|
158
|
-
*
|
|
159
|
-
* @param tagName The local tag name to map the topic to
|
|
160
|
-
* @param topic The FQDN of the resource to watch
|
|
161
|
-
* @param payload Optional subscription parameters (e.g., update rate)
|
|
162
|
-
* @returns Promise resolving to the CommandMessage response
|
|
163
|
-
*/
|
|
164
|
-
serverSubscribe(tagName: string, topic: string, payload?: object): Promise<CommandMessage>;
|
|
165
|
-
|
|
166
|
-
/**
|
|
167
|
-
* Unsubscribe from value changes on the backend.
|
|
168
|
-
*
|
|
169
|
-
* @param tagName The local tag name to stop watching
|
|
170
|
-
* @param payload Optional parameters
|
|
171
|
-
* @returns Promise resolving to the CommandMessage response
|
|
172
|
-
*/
|
|
173
|
-
serverUnsubscribe(tagName: string, payload?: object): Promise<CommandMessage>;
|
|
174
|
-
|
|
175
|
-
/**
|
|
176
|
-
* Subscribe to **local** frontend events.
|
|
177
|
-
*
|
|
178
|
-
* This registers a callback to listen for events dispatched on the local event bus.
|
|
179
|
-
* This includes both locally dispatched events AND broadcast messages received
|
|
180
|
-
* from the server (which the Hub dispatches to the local bus).
|
|
181
|
-
*
|
|
182
|
-
* @param topic The subscription topic (or tagName).
|
|
183
|
-
* @param callback The callback to signal.
|
|
184
|
-
* @returns number Subscription ID used to unsubscribe later.
|
|
185
|
-
*/
|
|
186
|
-
subscribe: (topic: string, callback: React.Dispatch<any>) => number;
|
|
187
|
-
|
|
188
|
-
/**
|
|
189
|
-
* Unsubscribe from **local** frontend events.
|
|
190
|
-
*
|
|
191
|
-
* @param subscriptionId The id returned by the subscribe method.
|
|
192
|
-
*/
|
|
193
|
-
unsubscribe: (subscriptionId: number) => void;
|
|
194
|
-
|
|
195
|
-
/**
|
|
196
|
-
* Global hub instance for backend communication.
|
|
197
|
-
*/
|
|
198
|
-
hub: Hub | null;
|
|
199
|
-
|
|
200
|
-
/**
|
|
201
|
-
* Retrieves the current subscriptions. Used for debugging purposes.
|
|
202
|
-
*/
|
|
203
|
-
getSubscriptions: (
|
|
204
|
-
topic?: string
|
|
205
|
-
) => Record<string, Subscription[]> | Subscription[];
|
|
206
|
-
|
|
207
|
-
/**
|
|
208
|
-
* Returns true if the Hub is connected to the backend.
|
|
209
|
-
*/
|
|
210
|
-
isConnected: () => boolean;
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
// Default placeholder CommandMessage for context initialization
|
|
214
|
-
const placeholderResponse: CommandMessage = {
|
|
215
|
-
data: {},
|
|
216
|
-
success: false,
|
|
217
|
-
error_message: "Context not initialized",
|
|
218
|
-
transaction_id: 0,
|
|
219
|
-
timecode: 0,
|
|
220
|
-
topic: "",
|
|
221
|
-
message_type: MessageType.NoOp
|
|
222
|
-
};
|
|
223
|
-
|
|
224
|
-
export const EventEmitterContext = createContext<EventEmitterContextType>({
|
|
225
|
-
state: { subscriptions: {}, nextSubscriptionId: 1 },
|
|
226
|
-
dispatch: () => {},
|
|
227
|
-
subscribe: () => 0,
|
|
228
|
-
unsubscribe: () => {},
|
|
229
|
-
invoke: async () => placeholderResponse,
|
|
230
|
-
read: async () => placeholderResponse,
|
|
231
|
-
write: async () => placeholderResponse,
|
|
232
|
-
serverSubscribe: async (_tagName: string, _topic: string, _payload?: object) => placeholderResponse,
|
|
233
|
-
serverUnsubscribe: async (_tagName: string, _payload?: object) => placeholderResponse,
|
|
234
|
-
hub: null,
|
|
235
|
-
getSubscriptions: () => [],
|
|
236
|
-
isConnected: () => false,
|
|
237
|
-
});
|
|
238
|
-
|
|
239
|
-
/**
|
|
240
|
-
* A React component that provides the EventEmitterContext to its children.
|
|
241
|
-
*
|
|
242
|
-
* Wraps child components with the context, enabling them to access and interact
|
|
243
|
-
* with the event emitter.
|
|
244
|
-
*
|
|
245
|
-
* @param children The child components to be wrapped in the context.
|
|
246
|
-
*/
|
|
247
|
-
export const EventEmitterProvider: React.FC<{ children: ReactNode; debug?: boolean }> = ({
|
|
248
|
-
children,
|
|
249
|
-
debug = false,
|
|
250
|
-
}) => {
|
|
251
|
-
console.log("[EventEmitterProvider] Rendering...");
|
|
252
|
-
|
|
253
|
-
// Enable the debug panel if requested (must run before Hub creation)
|
|
254
|
-
if (debug) {
|
|
255
|
-
enableDebugPanel();
|
|
256
|
-
}
|
|
257
|
-
|
|
258
|
-
const [state, setState] = useState<State>({
|
|
259
|
-
subscriptions: {},
|
|
260
|
-
nextSubscriptionId: 1,
|
|
261
|
-
});
|
|
262
|
-
|
|
263
|
-
// PERFORMANCE: Memoize the hub instance so it's only created once.
|
|
264
|
-
// This ensures the WebSocket connection persists across re-renders.
|
|
265
|
-
const hub = useMemo(() => {
|
|
266
|
-
console.log("[EventEmitterProvider] Creating Hub instance");
|
|
267
|
-
return createHub();
|
|
268
|
-
}, []);
|
|
269
|
-
|
|
270
|
-
// Use ref for subscription ID management (avoids global state issues)
|
|
271
|
-
const nextIdRef = useRef(1);
|
|
272
|
-
|
|
273
|
-
// Subscription storage - this is the single source of truth for local listeners
|
|
274
|
-
const subsRef = useRef<Record<string, Subscription[]>>({});
|
|
275
|
-
|
|
276
|
-
// Sync state with ref for debugging/inspection purposes only (on mount)
|
|
277
|
-
useEffect(() => {
|
|
278
|
-
setState(prev => ({
|
|
279
|
-
...prev,
|
|
280
|
-
subscriptions: { ...subsRef.current },
|
|
281
|
-
nextSubscriptionId: nextIdRef.current
|
|
282
|
-
}));
|
|
283
|
-
}, []);
|
|
284
|
-
|
|
285
|
-
/**
|
|
286
|
-
* Dispatches an event to all listeners.
|
|
287
|
-
*
|
|
288
|
-
* PERFORMANCE CRITICAL: This function iterates through listeners and calls them directly.
|
|
289
|
-
* It intentionally does NOT update React state (setState) to avoid triggering a
|
|
290
|
-
* full app re-render on every high-frequency event.
|
|
291
|
-
*/
|
|
292
|
-
const dispatch = useCallback((action: Action) => {
|
|
293
|
-
const { topic, payload } = action;
|
|
294
|
-
|
|
295
|
-
// Read from ref (single source of truth) and create a defensive copy
|
|
296
|
-
const listeners = subsRef.current[topic]?.slice() ?? [];
|
|
297
|
-
|
|
298
|
-
// Execute callbacks synchronously to avoid timing issues
|
|
299
|
-
for (const sub of listeners) {
|
|
300
|
-
try {
|
|
301
|
-
sub.callback(payload);
|
|
302
|
-
} catch (e) {
|
|
303
|
-
console.error("[EventBus] listener error", e);
|
|
304
|
-
}
|
|
305
|
-
}
|
|
306
|
-
|
|
307
|
-
// NOTE: State update removed for performance.
|
|
308
|
-
// Uncommenting this will cause the entire Provider to re-render on every event!
|
|
309
|
-
// setState((prev) => ({ ...prev, eventData: payload }));
|
|
310
|
-
}, []);
|
|
311
|
-
|
|
312
|
-
const subscribe = useCallback(
|
|
313
|
-
(topic: string, callback: React.Dispatch<any>): number => {
|
|
314
|
-
const id = nextIdRef.current++;
|
|
315
|
-
|
|
316
|
-
// Create new subscription entry
|
|
317
|
-
const newSub: Subscription = { id, callback };
|
|
318
|
-
|
|
319
|
-
// Atomically update the ref with a complete new structure
|
|
320
|
-
const currentSubs = subsRef.current[topic] ?? [];
|
|
321
|
-
const newSubs = [...currentSubs, newSub];
|
|
322
|
-
|
|
323
|
-
subsRef.current = {
|
|
324
|
-
...subsRef.current,
|
|
325
|
-
[topic]: newSubs
|
|
326
|
-
};
|
|
327
|
-
|
|
328
|
-
// Update state for debugging/inspection (less frequent than dispatch, so acceptable)
|
|
329
|
-
setState(prevState => ({
|
|
330
|
-
...prevState,
|
|
331
|
-
subscriptions: { ...subsRef.current },
|
|
332
|
-
nextSubscriptionId: nextIdRef.current,
|
|
333
|
-
}));
|
|
334
|
-
|
|
335
|
-
return id;
|
|
336
|
-
},
|
|
337
|
-
[]
|
|
338
|
-
);
|
|
339
|
-
|
|
340
|
-
const unsubscribe = useCallback((subscriptionId: number) => {
|
|
341
|
-
// Create a completely new structure to avoid mutation issues
|
|
342
|
-
const newSubscriptions: Record<string, Subscription[]> = {};
|
|
343
|
-
|
|
344
|
-
for (const [topic, subs] of Object.entries(subsRef.current)) {
|
|
345
|
-
const filteredSubs = subs.filter(s => s.id !== subscriptionId);
|
|
346
|
-
if (filteredSubs.length > 0) {
|
|
347
|
-
newSubscriptions[topic] = filteredSubs;
|
|
348
|
-
}
|
|
349
|
-
}
|
|
350
|
-
|
|
351
|
-
// Atomically replace the entire structure
|
|
352
|
-
subsRef.current = newSubscriptions;
|
|
353
|
-
|
|
354
|
-
// Update state for debugging/inspection
|
|
355
|
-
setState(prev => ({
|
|
356
|
-
...prev,
|
|
357
|
-
subscriptions: { ...newSubscriptions }
|
|
358
|
-
}));
|
|
359
|
-
}, []);
|
|
360
|
-
|
|
361
|
-
// Clean up on unmount
|
|
362
|
-
useEffect(() => {
|
|
363
|
-
return () => {
|
|
364
|
-
subsRef.current = {};
|
|
365
|
-
nextIdRef.current = 1;
|
|
366
|
-
};
|
|
367
|
-
}, []);
|
|
368
|
-
|
|
369
|
-
// Handle HMR so listeners don't leak across hot updates
|
|
370
|
-
if (import.meta && (import.meta as any).hot) {
|
|
371
|
-
(import.meta as any).hot.dispose(() => {
|
|
372
|
-
subsRef.current = {};
|
|
373
|
-
nextIdRef.current = 1;
|
|
374
|
-
});
|
|
375
|
-
}
|
|
376
|
-
|
|
377
|
-
// Memoize hub methods separately so they don't change when state changes.
|
|
378
|
-
// This prevents downstream useEffects from re-running due to function reference changes.
|
|
379
|
-
const invoke = useCallback(
|
|
380
|
-
(topic: string, messageType: MessageType, payload?: object) => hub.invoke(topic, messageType, payload),
|
|
381
|
-
[hub]
|
|
382
|
-
);
|
|
383
|
-
const read = useCallback(
|
|
384
|
-
(topic: string, payload?: object) => hub.read(topic, payload),
|
|
385
|
-
[hub]
|
|
386
|
-
);
|
|
387
|
-
const write = useCallback(
|
|
388
|
-
(topic: string, value: any) => hub.write(topic, value),
|
|
389
|
-
[hub]
|
|
390
|
-
);
|
|
391
|
-
const serverSubscribe = useCallback(
|
|
392
|
-
(tagName: string, topic: string, payload?: object) => hub.subscribe(tagName, topic, payload),
|
|
393
|
-
[hub]
|
|
394
|
-
);
|
|
395
|
-
const serverUnsubscribe = useCallback(
|
|
396
|
-
(tagName: string, payload?: object) => hub.unsubscribe(tagName, payload),
|
|
397
|
-
[hub]
|
|
398
|
-
);
|
|
399
|
-
const isConnected = useCallback(() => hub.isConnected(), [hub]);
|
|
400
|
-
|
|
401
|
-
// Provide the memoized context value
|
|
402
|
-
// PERFORMANCE: This object is memoized and its dependencies (dispatch, subscribe, etc.)
|
|
403
|
-
// are also memoized/stable. This means 'contextValue' reference rarely changes,
|
|
404
|
-
// preventing consumers (like App) from re-rendering unless necessary.
|
|
405
|
-
const contextValue = useMemo(
|
|
406
|
-
() => ({
|
|
407
|
-
state,
|
|
408
|
-
dispatch,
|
|
409
|
-
subscribe,
|
|
410
|
-
unsubscribe,
|
|
411
|
-
invoke,
|
|
412
|
-
read,
|
|
413
|
-
write,
|
|
414
|
-
serverSubscribe,
|
|
415
|
-
serverUnsubscribe,
|
|
416
|
-
hub,
|
|
417
|
-
getSubscriptions: (topic?: string) =>
|
|
418
|
-
topic ? subsRef.current[topic] ?? [] : subsRef.current,
|
|
419
|
-
isConnected,
|
|
420
|
-
}),
|
|
421
|
-
[state, hub, dispatch, subscribe, unsubscribe, invoke, read, write, serverSubscribe, serverUnsubscribe, isConnected]
|
|
422
|
-
);
|
|
423
|
-
|
|
424
|
-
// Set the context on the hub instance so it can dispatch back to us.
|
|
425
|
-
// Must be in useEffect to avoid side-effects during render phase.
|
|
426
|
-
useEffect(() => {
|
|
427
|
-
hub.setContext(contextValue);
|
|
428
|
-
}, [hub, contextValue]);
|
|
429
|
-
|
|
430
|
-
return (
|
|
431
|
-
<EventEmitterContext.Provider value={contextValue}>
|
|
432
|
-
{children}
|
|
433
|
-
</EventEmitterContext.Provider>
|
|
434
|
-
);
|
|
1
|
+
/*
|
|
2
|
+
* Copyright (C) 2024 Automated Design Corp. All Rights Reserved.
|
|
3
|
+
* Created Date: 2024-01-17 11:45:10
|
|
4
|
+
* -----
|
|
5
|
+
* Last Modified: 2026-01-29 09:32:29
|
|
6
|
+
* Modified By: ADC
|
|
7
|
+
* -----
|
|
8
|
+
*
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* @fileoverview EventEmitterContext - Global Event Bus for AutoCore React Applications
|
|
13
|
+
*
|
|
14
|
+
* The EventEmitterContext provides a comprehensive event-driven communication system for React applications,
|
|
15
|
+
* enabling seamless interaction between components and backend services. It serves as the foundational layer
|
|
16
|
+
* for AutoCore's real-time data flow and component communication architecture.
|
|
17
|
+
*
|
|
18
|
+
* ## Core Features
|
|
19
|
+
*
|
|
20
|
+
* - **Global Event Bus**: Publish and subscribe to events across all components.
|
|
21
|
+
* - **Backend Integration**: Direct communication with AutoCore server via Hub abstraction.
|
|
22
|
+
* - **Type-Safe Subscriptions**: Strongly-typed event handling with automatic cleanup.
|
|
23
|
+
* - **Performance Optimized**: Uses stable context values and direct callbacks to avoid global re-renders.
|
|
24
|
+
* - **Connection Management**: Automatic reconnection and state synchronization.
|
|
25
|
+
*
|
|
26
|
+
* ## Architecture
|
|
27
|
+
*
|
|
28
|
+
* The system consists of three main components:
|
|
29
|
+
* 1. **EventEmitterProvider**: React context provider that manages global state and event routing.
|
|
30
|
+
* 2. **Hub**: Abstraction layer for backend communication (WebSocket, HTTP, etc.).
|
|
31
|
+
* 3. **Subscription Manager**: Handles event routing and lifecycle management.
|
|
32
|
+
*
|
|
33
|
+
* ## Performance Notes
|
|
34
|
+
*
|
|
35
|
+
* To ensure high performance with high-frequency data (e.g., 50Hz sensor updates):
|
|
36
|
+
* 1. **No State Updates on Dispatch**: The `dispatch` function does NOT update React state. It calls listeners directly.
|
|
37
|
+
* This prevents the entire component tree from re-rendering on every message.
|
|
38
|
+
* 2. **Stable Context Value**: The `contextValue` object is memoized and rarely changes.
|
|
39
|
+
* 3. **Direct Subscriptions**: Components subscribe via `useEffect` and receive updates via callbacks, not props.
|
|
40
|
+
*
|
|
41
|
+
* @module core/EventEmitterContext
|
|
42
|
+
* @version 3.0.42
|
|
43
|
+
* @author Automated Design Corp.
|
|
44
|
+
*/
|
|
45
|
+
|
|
46
|
+
import React, {
|
|
47
|
+
createContext,
|
|
48
|
+
useState,
|
|
49
|
+
useMemo,
|
|
50
|
+
useCallback,
|
|
51
|
+
useRef,
|
|
52
|
+
useEffect,
|
|
53
|
+
} from "react";
|
|
54
|
+
|
|
55
|
+
import type {ReactNode} from "react";
|
|
56
|
+
import { createHub, Hub } from "../hub";
|
|
57
|
+
import type { CommandMessage } from "../hub";
|
|
58
|
+
import { MessageType } from "../hub/CommandMessage";
|
|
59
|
+
import { enableDebugPanel } from "../hub/DebugPanel";
|
|
60
|
+
|
|
61
|
+
export { Hub };
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Represents an active event subscription.
|
|
65
|
+
*/
|
|
66
|
+
export interface Subscription {
|
|
67
|
+
/** Unique ID of the subscription used for unsubscription. */
|
|
68
|
+
id: number;
|
|
69
|
+
/** Callback function to execute when the event fires. */
|
|
70
|
+
callback: React.Dispatch<any>;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Represents the internal state of the EventEmitter.
|
|
75
|
+
* Mostly used for debugging and dev tools inspection.
|
|
76
|
+
*/
|
|
77
|
+
export interface State {
|
|
78
|
+
/**
|
|
79
|
+
* The optional data payload of the last event (DEBUGGING ONLY).
|
|
80
|
+
* Note: This is no longer updated in production for performance reasons.
|
|
81
|
+
*/
|
|
82
|
+
eventData?: any;
|
|
83
|
+
/** Active subscriptions grouped by topic. */
|
|
84
|
+
subscriptions: Record<string, Subscription[]>;
|
|
85
|
+
|
|
86
|
+
/** Tracks the next subscription ID that will be assigned. */
|
|
87
|
+
nextSubscriptionId: number;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* An action event published by the EventEmitterContext.
|
|
92
|
+
*/
|
|
93
|
+
export interface Action {
|
|
94
|
+
/** The topic or identifier of the event. */
|
|
95
|
+
topic: string;
|
|
96
|
+
/** The optional data payload associated with the event. */
|
|
97
|
+
payload?: any;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export type EmitterDispatchFunction = (action: Action) => void;
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Defines the context for the global event emitter.
|
|
104
|
+
* Provides methods for dispatching events, subscribing to topics, and communicating with the backend.
|
|
105
|
+
*/
|
|
106
|
+
export interface EventEmitterContextType {
|
|
107
|
+
/**
|
|
108
|
+
* The current state of the event emitter (mostly for debugging).
|
|
109
|
+
*/
|
|
110
|
+
state: State;
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Dispatch an action globally throughout the front-end.
|
|
114
|
+
* Triggers callbacks for all subscribers of the topic.
|
|
115
|
+
*
|
|
116
|
+
* @param action The action to dispatch.
|
|
117
|
+
*/
|
|
118
|
+
dispatch: EmitterDispatchFunction;
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Send a message to the backend (Low-level).
|
|
122
|
+
*
|
|
123
|
+
* @param topic The FQDN of the resource (e.g., "ads.plc1.gio.bControlPowerOk")
|
|
124
|
+
* @param messageType The action to perform (Read, Write, Subscribe, etc.)
|
|
125
|
+
* @param payload Optional data payload
|
|
126
|
+
* @returns Promise resolving to the CommandMessage response
|
|
127
|
+
*/
|
|
128
|
+
invoke(
|
|
129
|
+
topic: string,
|
|
130
|
+
messageType: MessageType,
|
|
131
|
+
payload?: object
|
|
132
|
+
): Promise<CommandMessage>;
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Read a value from the backend.
|
|
136
|
+
*
|
|
137
|
+
* @param topic The FQDN of the resource to read
|
|
138
|
+
* @param payload Optional additional parameters
|
|
139
|
+
* @returns Promise resolving to the CommandMessage response with the value in `data`
|
|
140
|
+
*/
|
|
141
|
+
read(topic: string, payload?: object): Promise<CommandMessage>;
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Write a value to the backend.
|
|
145
|
+
*
|
|
146
|
+
* @param topic The FQDN of the resource to write
|
|
147
|
+
* @param value The value to write
|
|
148
|
+
* @returns Promise resolving to the CommandMessage response
|
|
149
|
+
*/
|
|
150
|
+
write(topic: string, value: any): Promise<CommandMessage>;
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Subscribe to value changes on the backend.
|
|
154
|
+
*
|
|
155
|
+
* Tells the server to start broadcasting updates for a specific topic.
|
|
156
|
+
* Also registers a mapping between the local `tagName` and the remote `topic` (FQDN)
|
|
157
|
+
* in the Hub, so that incoming broadcasts are routed correctly.
|
|
158
|
+
*
|
|
159
|
+
* @param tagName The local tag name to map the topic to
|
|
160
|
+
* @param topic The FQDN of the resource to watch
|
|
161
|
+
* @param payload Optional subscription parameters (e.g., update rate)
|
|
162
|
+
* @returns Promise resolving to the CommandMessage response
|
|
163
|
+
*/
|
|
164
|
+
serverSubscribe(tagName: string, topic: string, payload?: object): Promise<CommandMessage>;
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Unsubscribe from value changes on the backend.
|
|
168
|
+
*
|
|
169
|
+
* @param tagName The local tag name to stop watching
|
|
170
|
+
* @param payload Optional parameters
|
|
171
|
+
* @returns Promise resolving to the CommandMessage response
|
|
172
|
+
*/
|
|
173
|
+
serverUnsubscribe(tagName: string, payload?: object): Promise<CommandMessage>;
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* Subscribe to **local** frontend events.
|
|
177
|
+
*
|
|
178
|
+
* This registers a callback to listen for events dispatched on the local event bus.
|
|
179
|
+
* This includes both locally dispatched events AND broadcast messages received
|
|
180
|
+
* from the server (which the Hub dispatches to the local bus).
|
|
181
|
+
*
|
|
182
|
+
* @param topic The subscription topic (or tagName).
|
|
183
|
+
* @param callback The callback to signal.
|
|
184
|
+
* @returns number Subscription ID used to unsubscribe later.
|
|
185
|
+
*/
|
|
186
|
+
subscribe: (topic: string, callback: React.Dispatch<any>) => number;
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* Unsubscribe from **local** frontend events.
|
|
190
|
+
*
|
|
191
|
+
* @param subscriptionId The id returned by the subscribe method.
|
|
192
|
+
*/
|
|
193
|
+
unsubscribe: (subscriptionId: number) => void;
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* Global hub instance for backend communication.
|
|
197
|
+
*/
|
|
198
|
+
hub: Hub | null;
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* Retrieves the current subscriptions. Used for debugging purposes.
|
|
202
|
+
*/
|
|
203
|
+
getSubscriptions: (
|
|
204
|
+
topic?: string
|
|
205
|
+
) => Record<string, Subscription[]> | Subscription[];
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* Returns true if the Hub is connected to the backend.
|
|
209
|
+
*/
|
|
210
|
+
isConnected: () => boolean;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// Default placeholder CommandMessage for context initialization
|
|
214
|
+
const placeholderResponse: CommandMessage = {
|
|
215
|
+
data: {},
|
|
216
|
+
success: false,
|
|
217
|
+
error_message: "Context not initialized",
|
|
218
|
+
transaction_id: 0,
|
|
219
|
+
timecode: 0,
|
|
220
|
+
topic: "",
|
|
221
|
+
message_type: MessageType.NoOp
|
|
222
|
+
};
|
|
223
|
+
|
|
224
|
+
export const EventEmitterContext = createContext<EventEmitterContextType>({
|
|
225
|
+
state: { subscriptions: {}, nextSubscriptionId: 1 },
|
|
226
|
+
dispatch: () => {},
|
|
227
|
+
subscribe: () => 0,
|
|
228
|
+
unsubscribe: () => {},
|
|
229
|
+
invoke: async () => placeholderResponse,
|
|
230
|
+
read: async () => placeholderResponse,
|
|
231
|
+
write: async () => placeholderResponse,
|
|
232
|
+
serverSubscribe: async (_tagName: string, _topic: string, _payload?: object) => placeholderResponse,
|
|
233
|
+
serverUnsubscribe: async (_tagName: string, _payload?: object) => placeholderResponse,
|
|
234
|
+
hub: null,
|
|
235
|
+
getSubscriptions: () => [],
|
|
236
|
+
isConnected: () => false,
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* A React component that provides the EventEmitterContext to its children.
|
|
241
|
+
*
|
|
242
|
+
* Wraps child components with the context, enabling them to access and interact
|
|
243
|
+
* with the event emitter.
|
|
244
|
+
*
|
|
245
|
+
* @param children The child components to be wrapped in the context.
|
|
246
|
+
*/
|
|
247
|
+
export const EventEmitterProvider: React.FC<{ children: ReactNode; debug?: boolean }> = ({
|
|
248
|
+
children,
|
|
249
|
+
debug = false,
|
|
250
|
+
}) => {
|
|
251
|
+
console.log("[EventEmitterProvider] Rendering...");
|
|
252
|
+
|
|
253
|
+
// Enable the debug panel if requested (must run before Hub creation)
|
|
254
|
+
if (debug) {
|
|
255
|
+
enableDebugPanel();
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
const [state, setState] = useState<State>({
|
|
259
|
+
subscriptions: {},
|
|
260
|
+
nextSubscriptionId: 1,
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
// PERFORMANCE: Memoize the hub instance so it's only created once.
|
|
264
|
+
// This ensures the WebSocket connection persists across re-renders.
|
|
265
|
+
const hub = useMemo(() => {
|
|
266
|
+
console.log("[EventEmitterProvider] Creating Hub instance");
|
|
267
|
+
return createHub();
|
|
268
|
+
}, []);
|
|
269
|
+
|
|
270
|
+
// Use ref for subscription ID management (avoids global state issues)
|
|
271
|
+
const nextIdRef = useRef(1);
|
|
272
|
+
|
|
273
|
+
// Subscription storage - this is the single source of truth for local listeners
|
|
274
|
+
const subsRef = useRef<Record<string, Subscription[]>>({});
|
|
275
|
+
|
|
276
|
+
// Sync state with ref for debugging/inspection purposes only (on mount)
|
|
277
|
+
useEffect(() => {
|
|
278
|
+
setState(prev => ({
|
|
279
|
+
...prev,
|
|
280
|
+
subscriptions: { ...subsRef.current },
|
|
281
|
+
nextSubscriptionId: nextIdRef.current
|
|
282
|
+
}));
|
|
283
|
+
}, []);
|
|
284
|
+
|
|
285
|
+
/**
|
|
286
|
+
* Dispatches an event to all listeners.
|
|
287
|
+
*
|
|
288
|
+
* PERFORMANCE CRITICAL: This function iterates through listeners and calls them directly.
|
|
289
|
+
* It intentionally does NOT update React state (setState) to avoid triggering a
|
|
290
|
+
* full app re-render on every high-frequency event.
|
|
291
|
+
*/
|
|
292
|
+
const dispatch = useCallback((action: Action) => {
|
|
293
|
+
const { topic, payload } = action;
|
|
294
|
+
|
|
295
|
+
// Read from ref (single source of truth) and create a defensive copy
|
|
296
|
+
const listeners = subsRef.current[topic]?.slice() ?? [];
|
|
297
|
+
|
|
298
|
+
// Execute callbacks synchronously to avoid timing issues
|
|
299
|
+
for (const sub of listeners) {
|
|
300
|
+
try {
|
|
301
|
+
sub.callback(payload);
|
|
302
|
+
} catch (e) {
|
|
303
|
+
console.error("[EventBus] listener error", e);
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
// NOTE: State update removed for performance.
|
|
308
|
+
// Uncommenting this will cause the entire Provider to re-render on every event!
|
|
309
|
+
// setState((prev) => ({ ...prev, eventData: payload }));
|
|
310
|
+
}, []);
|
|
311
|
+
|
|
312
|
+
const subscribe = useCallback(
|
|
313
|
+
(topic: string, callback: React.Dispatch<any>): number => {
|
|
314
|
+
const id = nextIdRef.current++;
|
|
315
|
+
|
|
316
|
+
// Create new subscription entry
|
|
317
|
+
const newSub: Subscription = { id, callback };
|
|
318
|
+
|
|
319
|
+
// Atomically update the ref with a complete new structure
|
|
320
|
+
const currentSubs = subsRef.current[topic] ?? [];
|
|
321
|
+
const newSubs = [...currentSubs, newSub];
|
|
322
|
+
|
|
323
|
+
subsRef.current = {
|
|
324
|
+
...subsRef.current,
|
|
325
|
+
[topic]: newSubs
|
|
326
|
+
};
|
|
327
|
+
|
|
328
|
+
// Update state for debugging/inspection (less frequent than dispatch, so acceptable)
|
|
329
|
+
setState(prevState => ({
|
|
330
|
+
...prevState,
|
|
331
|
+
subscriptions: { ...subsRef.current },
|
|
332
|
+
nextSubscriptionId: nextIdRef.current,
|
|
333
|
+
}));
|
|
334
|
+
|
|
335
|
+
return id;
|
|
336
|
+
},
|
|
337
|
+
[]
|
|
338
|
+
);
|
|
339
|
+
|
|
340
|
+
const unsubscribe = useCallback((subscriptionId: number) => {
|
|
341
|
+
// Create a completely new structure to avoid mutation issues
|
|
342
|
+
const newSubscriptions: Record<string, Subscription[]> = {};
|
|
343
|
+
|
|
344
|
+
for (const [topic, subs] of Object.entries(subsRef.current)) {
|
|
345
|
+
const filteredSubs = subs.filter(s => s.id !== subscriptionId);
|
|
346
|
+
if (filteredSubs.length > 0) {
|
|
347
|
+
newSubscriptions[topic] = filteredSubs;
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
// Atomically replace the entire structure
|
|
352
|
+
subsRef.current = newSubscriptions;
|
|
353
|
+
|
|
354
|
+
// Update state for debugging/inspection
|
|
355
|
+
setState(prev => ({
|
|
356
|
+
...prev,
|
|
357
|
+
subscriptions: { ...newSubscriptions }
|
|
358
|
+
}));
|
|
359
|
+
}, []);
|
|
360
|
+
|
|
361
|
+
// Clean up on unmount
|
|
362
|
+
useEffect(() => {
|
|
363
|
+
return () => {
|
|
364
|
+
subsRef.current = {};
|
|
365
|
+
nextIdRef.current = 1;
|
|
366
|
+
};
|
|
367
|
+
}, []);
|
|
368
|
+
|
|
369
|
+
// Handle HMR so listeners don't leak across hot updates
|
|
370
|
+
if (import.meta && (import.meta as any).hot) {
|
|
371
|
+
(import.meta as any).hot.dispose(() => {
|
|
372
|
+
subsRef.current = {};
|
|
373
|
+
nextIdRef.current = 1;
|
|
374
|
+
});
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
// Memoize hub methods separately so they don't change when state changes.
|
|
378
|
+
// This prevents downstream useEffects from re-running due to function reference changes.
|
|
379
|
+
const invoke = useCallback(
|
|
380
|
+
(topic: string, messageType: MessageType, payload?: object) => hub.invoke(topic, messageType, payload),
|
|
381
|
+
[hub]
|
|
382
|
+
);
|
|
383
|
+
const read = useCallback(
|
|
384
|
+
(topic: string, payload?: object) => hub.read(topic, payload),
|
|
385
|
+
[hub]
|
|
386
|
+
);
|
|
387
|
+
const write = useCallback(
|
|
388
|
+
(topic: string, value: any) => hub.write(topic, value),
|
|
389
|
+
[hub]
|
|
390
|
+
);
|
|
391
|
+
const serverSubscribe = useCallback(
|
|
392
|
+
(tagName: string, topic: string, payload?: object) => hub.subscribe(tagName, topic, payload),
|
|
393
|
+
[hub]
|
|
394
|
+
);
|
|
395
|
+
const serverUnsubscribe = useCallback(
|
|
396
|
+
(tagName: string, payload?: object) => hub.unsubscribe(tagName, payload),
|
|
397
|
+
[hub]
|
|
398
|
+
);
|
|
399
|
+
const isConnected = useCallback(() => hub.isConnected(), [hub]);
|
|
400
|
+
|
|
401
|
+
// Provide the memoized context value
|
|
402
|
+
// PERFORMANCE: This object is memoized and its dependencies (dispatch, subscribe, etc.)
|
|
403
|
+
// are also memoized/stable. This means 'contextValue' reference rarely changes,
|
|
404
|
+
// preventing consumers (like App) from re-rendering unless necessary.
|
|
405
|
+
const contextValue = useMemo(
|
|
406
|
+
() => ({
|
|
407
|
+
state,
|
|
408
|
+
dispatch,
|
|
409
|
+
subscribe,
|
|
410
|
+
unsubscribe,
|
|
411
|
+
invoke,
|
|
412
|
+
read,
|
|
413
|
+
write,
|
|
414
|
+
serverSubscribe,
|
|
415
|
+
serverUnsubscribe,
|
|
416
|
+
hub,
|
|
417
|
+
getSubscriptions: (topic?: string) =>
|
|
418
|
+
topic ? subsRef.current[topic] ?? [] : subsRef.current,
|
|
419
|
+
isConnected,
|
|
420
|
+
}),
|
|
421
|
+
[state, hub, dispatch, subscribe, unsubscribe, invoke, read, write, serverSubscribe, serverUnsubscribe, isConnected]
|
|
422
|
+
);
|
|
423
|
+
|
|
424
|
+
// Set the context on the hub instance so it can dispatch back to us.
|
|
425
|
+
// Must be in useEffect to avoid side-effects during render phase.
|
|
426
|
+
useEffect(() => {
|
|
427
|
+
hub.setContext(contextValue);
|
|
428
|
+
}, [hub, contextValue]);
|
|
429
|
+
|
|
430
|
+
return (
|
|
431
|
+
<EventEmitterContext.Provider value={contextValue}>
|
|
432
|
+
{children}
|
|
433
|
+
</EventEmitterContext.Provider>
|
|
434
|
+
);
|
|
435
435
|
};
|