rotor-framework 0.3.2
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.md +21 -0
- package/README.md +120 -0
- package/package.json +59 -0
- package/src/source/RotorFramework.bs +654 -0
- package/src/source/RotorFrameworkTask.bs +278 -0
- package/src/source/base/BaseModel.bs +52 -0
- package/src/source/base/BasePlugin.bs +48 -0
- package/src/source/base/BaseReducer.bs +184 -0
- package/src/source/base/BaseStack.bs +92 -0
- package/src/source/base/BaseViewModel.bs +124 -0
- package/src/source/base/BaseWidget.bs +104 -0
- package/src/source/base/DispatcherCreator.bs +193 -0
- package/src/source/base/DispatcherExternal.bs +260 -0
- package/src/source/base/ListenerForDispatchers.bs +246 -0
- package/src/source/engine/Constants.bs +74 -0
- package/src/source/engine/animator/Animator.bs +334 -0
- package/src/source/engine/builder/Builder.bs +213 -0
- package/src/source/engine/builder/NodePool.bs +236 -0
- package/src/source/engine/builder/PluginAdapter.bs +139 -0
- package/src/source/engine/builder/PostProcessor.bs +331 -0
- package/src/source/engine/builder/Processor.bs +156 -0
- package/src/source/engine/builder/Tree.bs +278 -0
- package/src/source/engine/builder/TreeBase.bs +313 -0
- package/src/source/engine/builder/WidgetCreate.bs +322 -0
- package/src/source/engine/builder/WidgetRemove.bs +72 -0
- package/src/source/engine/builder/WidgetUpdate.bs +113 -0
- package/src/source/engine/providers/Dispatcher.bs +72 -0
- package/src/source/engine/providers/DispatcherProvider.bs +95 -0
- package/src/source/engine/services/I18n.bs +169 -0
- package/src/source/libs/animate/Animate.bs +753 -0
- package/src/source/libs/animate/LICENSE.txt +21 -0
- package/src/source/plugins/DispatcherProviderPlugin.bs +127 -0
- package/src/source/plugins/FieldsPlugin.bs +180 -0
- package/src/source/plugins/FocusPlugin.bs +1522 -0
- package/src/source/plugins/FontStylePlugin.bs +159 -0
- package/src/source/plugins/ObserverPlugin.bs +548 -0
- package/src/source/utils/ArrayUtils.bs +495 -0
- package/src/source/utils/GeneralUtils.bs +181 -0
- package/src/source/utils/NodeUtils.bs +180 -0
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
namespace Rotor
|
|
2
|
+
|
|
3
|
+
' =====================================================================
|
|
4
|
+
' ViewModel (BaseViewModel) - Base class for ViewModels that extends Widget to provide view lifecycle management
|
|
5
|
+
'
|
|
6
|
+
' ViewModels act as controllers for views, managing:
|
|
7
|
+
' - props: Immutable properties passed from parent (read-only)
|
|
8
|
+
' - viewModelState: Mutable local state (read-write)
|
|
9
|
+
' - template: Declarative UI structure
|
|
10
|
+
' - Lifecycle hooks: onCreateView, onMountView, onUpdateView, onDestroyView
|
|
11
|
+
'
|
|
12
|
+
' Key Features:
|
|
13
|
+
' - Scoped state: props and viewModelState shared across all child widgets
|
|
14
|
+
' - Template system: template() returns widget tree configuration
|
|
15
|
+
' - Lifecycle management: Hooks for view creation, mounting, updates, destruction
|
|
16
|
+
' - No prop drilling: Children access parent ViewModel state directly
|
|
17
|
+
' =====================================================================
|
|
18
|
+
class ViewModel extends Rotor.Widget
|
|
19
|
+
|
|
20
|
+
' =============================================================
|
|
21
|
+
' MEMBER VARIABLES
|
|
22
|
+
' =============================================================
|
|
23
|
+
|
|
24
|
+
isViewModel = true ' Identifies this as a ViewModel
|
|
25
|
+
|
|
26
|
+
viewModelState = {} ' Mutable state shared across ViewModel's widgets
|
|
27
|
+
props = {} ' Immutable props shared across ViewModel's widgets
|
|
28
|
+
|
|
29
|
+
' =============================================================
|
|
30
|
+
' PROPS MANAGEMENT
|
|
31
|
+
' =============================================================
|
|
32
|
+
|
|
33
|
+
' ---------------------------------------------------------------------
|
|
34
|
+
' setProps - Updates props and triggers view update
|
|
35
|
+
'
|
|
36
|
+
' Merges new props into existing props and calls onUpdateView lifecycle hook.
|
|
37
|
+
'
|
|
38
|
+
' @param {object} newProps - New properties to merge
|
|
39
|
+
'
|
|
40
|
+
sub setProps(newProps as object)
|
|
41
|
+
m.props.append(newProps)
|
|
42
|
+
m.onUpdateView()
|
|
43
|
+
end sub
|
|
44
|
+
|
|
45
|
+
' =============================================================
|
|
46
|
+
' LIFECYCLE HOOKS
|
|
47
|
+
' =============================================================
|
|
48
|
+
|
|
49
|
+
' ---------------------------------------------------------------------
|
|
50
|
+
' onCreateView - Called when view is being created
|
|
51
|
+
'
|
|
52
|
+
' Override to perform initialization tasks before template is generated.
|
|
53
|
+
' Use this to set up initial viewModelState or props.
|
|
54
|
+
'
|
|
55
|
+
sub onCreateView()
|
|
56
|
+
end sub
|
|
57
|
+
|
|
58
|
+
' ---------------------------------------------------------------------
|
|
59
|
+
' onTemplateCreated - Called after template has been created
|
|
60
|
+
'
|
|
61
|
+
' Override to access and modify the created template structure.
|
|
62
|
+
' Useful for dynamic template manipulation.
|
|
63
|
+
'
|
|
64
|
+
' @param {object} template - The created template object
|
|
65
|
+
'
|
|
66
|
+
sub onTemplateCreated(template as object)
|
|
67
|
+
end sub
|
|
68
|
+
|
|
69
|
+
' ---------------------------------------------------------------------
|
|
70
|
+
' onMountView - Called when view is mounted to SceneGraph
|
|
71
|
+
'
|
|
72
|
+
' Override to perform post-mount operations like starting animations,
|
|
73
|
+
' fetching data, or setting up observers.
|
|
74
|
+
'
|
|
75
|
+
sub onMountView()
|
|
76
|
+
end sub
|
|
77
|
+
|
|
78
|
+
' ---------------------------------------------------------------------
|
|
79
|
+
' onUpdateView - Called when view needs to update
|
|
80
|
+
'
|
|
81
|
+
' Override to handle state or props changes.
|
|
82
|
+
' Called automatically when setProps() is invoked.
|
|
83
|
+
'
|
|
84
|
+
sub onUpdateView()
|
|
85
|
+
end sub
|
|
86
|
+
|
|
87
|
+
' ---------------------------------------------------------------------
|
|
88
|
+
' onDestroyView - Called when view is being destroyed
|
|
89
|
+
'
|
|
90
|
+
' Override to perform cleanup tasks like removing observers,
|
|
91
|
+
' stopping timers, or clearing resources.
|
|
92
|
+
'
|
|
93
|
+
sub onDestroyView()
|
|
94
|
+
end sub
|
|
95
|
+
|
|
96
|
+
' =============================================================
|
|
97
|
+
' TEMPLATE
|
|
98
|
+
' =============================================================
|
|
99
|
+
|
|
100
|
+
' ---------------------------------------------------------------------
|
|
101
|
+
' template - Returns view template structure
|
|
102
|
+
'
|
|
103
|
+
' Override to define custom view structure using declarative widget config.
|
|
104
|
+
' Return an object with widget configuration including children.
|
|
105
|
+
'
|
|
106
|
+
' @returns {object} Widget tree configuration
|
|
107
|
+
'
|
|
108
|
+
' Example:
|
|
109
|
+
' function template() as object
|
|
110
|
+
' return {
|
|
111
|
+
' nodeType: "Group",
|
|
112
|
+
' children: [
|
|
113
|
+
' { id: "label", nodeType: "Label", fields: { text: m.props.title } }
|
|
114
|
+
' ]
|
|
115
|
+
' }
|
|
116
|
+
' end function
|
|
117
|
+
'
|
|
118
|
+
function template() as object
|
|
119
|
+
return {}
|
|
120
|
+
end function
|
|
121
|
+
|
|
122
|
+
end class
|
|
123
|
+
|
|
124
|
+
end namespace
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
namespace Rotor
|
|
2
|
+
|
|
3
|
+
' =====================================================================
|
|
4
|
+
' Widget (BaseWidget) - Base class for UI components in the Rotor framework
|
|
5
|
+
'
|
|
6
|
+
' Widgets are the fundamental building blocks of the UI tree, providing:
|
|
7
|
+
' - Lifecycle management (mount, update, destroy)
|
|
8
|
+
' - Tree navigation methods
|
|
9
|
+
' - State access (viewModelState, props)
|
|
10
|
+
' - Rendering and updates
|
|
11
|
+
' - Plugin integration
|
|
12
|
+
'
|
|
13
|
+
' IMPORTANT: Many properties and methods are injected/decorated by the
|
|
14
|
+
' framework builder and plugins during widget instantiation.
|
|
15
|
+
'
|
|
16
|
+
' Widget Creation Flow:
|
|
17
|
+
' 1. builder.WidgetTreeBase.add() - Registers widget in tree
|
|
18
|
+
' 2. builder.processor.createWidget() - Decorates with methods
|
|
19
|
+
' =====================================================================
|
|
20
|
+
class Widget
|
|
21
|
+
|
|
22
|
+
' =============================================================
|
|
23
|
+
' CORE PROPERTIES
|
|
24
|
+
' =============================================================
|
|
25
|
+
|
|
26
|
+
id as string ' Widget identifier
|
|
27
|
+
children as object ' Child widgets collection
|
|
28
|
+
|
|
29
|
+
' =============================================================
|
|
30
|
+
' RUNTIME-INJECTED PROPERTIES
|
|
31
|
+
' =============================================================
|
|
32
|
+
' These are populated by the builder during widget creation
|
|
33
|
+
|
|
34
|
+
node as object ' SceneGraph node reference
|
|
35
|
+
parent as object ' Parent widget reference
|
|
36
|
+
plugins as object ' Plugin methods and state
|
|
37
|
+
|
|
38
|
+
viewModelState as object ' Mutable state shared across ViewModel's widgets
|
|
39
|
+
props as object ' Immutable props shared across ViewModel's widgets
|
|
40
|
+
nodeType as string ' SceneGraph node type (e.g., "Group", "Label")
|
|
41
|
+
zIndex as integer ' Z-order for rendering
|
|
42
|
+
|
|
43
|
+
' =============================================================
|
|
44
|
+
' LIFECYCLE CALLBACKS
|
|
45
|
+
' =============================================================
|
|
46
|
+
|
|
47
|
+
onMountWidget as Function ' Called after widget is mounted
|
|
48
|
+
onUpdateWidget as Function ' Called before widget updates
|
|
49
|
+
onDestroyWidget as Function ' Called before widget is destroyed
|
|
50
|
+
|
|
51
|
+
' =============================================================
|
|
52
|
+
' DECORATED METHODS
|
|
53
|
+
' =============================================================
|
|
54
|
+
' These methods are injected by WidgetCreate.bs
|
|
55
|
+
|
|
56
|
+
' Tree Navigation
|
|
57
|
+
getWidget as Function ' Find widget by ID or path (any depth)
|
|
58
|
+
findWidgets as Function ' Find widgets by glob pattern
|
|
59
|
+
getChildrenWidgets as Function ' Get direct children (optionally filtered)
|
|
60
|
+
getSiblingWidget as Function ' Get sibling widget by ID
|
|
61
|
+
getSubtreeClone as Function ' Clone widget subtree
|
|
62
|
+
|
|
63
|
+
' ViewModel Access
|
|
64
|
+
getParentViewModel as Function ' Get parent ViewModel
|
|
65
|
+
getViewModel as Function ' Get owning ViewModel
|
|
66
|
+
getRootWidget as Function ' Get root widget (HID "0")
|
|
67
|
+
|
|
68
|
+
' Rendering
|
|
69
|
+
render as Function ' Render widget updates
|
|
70
|
+
refresh as Function ' Refresh specific properties by key paths
|
|
71
|
+
erase as Function ' Remove widget(s) from tree
|
|
72
|
+
|
|
73
|
+
' Framework Integration
|
|
74
|
+
getFrameworkInstance as Function ' Get framework instance
|
|
75
|
+
getDispatcher as Function ' Get dispatcher facade by ID
|
|
76
|
+
createDispatcher as Function ' Create dispatcher
|
|
77
|
+
animator as Function ' Get animator factory
|
|
78
|
+
|
|
79
|
+
' =============================================================
|
|
80
|
+
' PLUGIN METHODS
|
|
81
|
+
' =============================================================
|
|
82
|
+
' These are optionally injected by plugins
|
|
83
|
+
|
|
84
|
+
' Focus Plugin Methods (from FocusPlugin.bs)
|
|
85
|
+
optional enableFocusNavigation as function ' Enable focus navigation
|
|
86
|
+
optional isFocusNavigationEnabled as function ' Check if focus enabled
|
|
87
|
+
optional setFocus as function ' Set focus on widget
|
|
88
|
+
optional getFocusedWidget as function ' Get currently focused widget
|
|
89
|
+
optional proceedLongPress as function ' Trigger long press
|
|
90
|
+
optional isLongPressActive as function ' Check if long press active
|
|
91
|
+
|
|
92
|
+
' =============================================================
|
|
93
|
+
' PRIVATE PROPERTIES (Engine Use Only)
|
|
94
|
+
' =============================================================
|
|
95
|
+
|
|
96
|
+
private HID as string ' Hierarchical ID (e.g., "0.header.logo")
|
|
97
|
+
private vmHID as string ' Owning ViewModel's HID
|
|
98
|
+
private isRootChild as boolean ' True if direct child of root
|
|
99
|
+
private childrenHIDhash as object ' Hash of child HIDs for fast lookup
|
|
100
|
+
private parentHID as string ' Parent's HID
|
|
101
|
+
|
|
102
|
+
end class
|
|
103
|
+
|
|
104
|
+
end namespace
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
import "ListenerForDispatchers.bs"
|
|
2
|
+
|
|
3
|
+
namespace Rotor
|
|
4
|
+
|
|
5
|
+
' =====================================================================
|
|
6
|
+
' createDispatcher - Factory function for creating dispatchers
|
|
7
|
+
'
|
|
8
|
+
' Creates a dispatcher instance and returns its facade for external use.
|
|
9
|
+
'
|
|
10
|
+
' @param {string} customDispatcherId - Dispatcher identifier
|
|
11
|
+
' @param {Model} modelInstance - Model holding state
|
|
12
|
+
' @param {Reducer} reducerInstance - Reducer for state transitions
|
|
13
|
+
' @returns {Dispatcher} Dispatcher facade instance
|
|
14
|
+
' =====================================================================
|
|
15
|
+
sub createDispatcher(customDispatcherId as string, modelInstance as Model, reducerInstance as Reducer)
|
|
16
|
+
m.dispatcherInstance = new Rotor.DispatcherCreator(customDispatcherId, modelInstance, reducerInstance)
|
|
17
|
+
end sub
|
|
18
|
+
|
|
19
|
+
' =====================================================================
|
|
20
|
+
' DispatcherCreator - Internal dispatcher implementation for MVI pattern state management
|
|
21
|
+
'
|
|
22
|
+
' Responsibilities:
|
|
23
|
+
' - Dispatches intents to reducer
|
|
24
|
+
' - Manages state updates via Model
|
|
25
|
+
' - Notifies listeners of state changes
|
|
26
|
+
' - Handles dispatch queue to prevent reentrancy
|
|
27
|
+
' - Exposes state to render thread via task node field
|
|
28
|
+
'
|
|
29
|
+
' MVI Flow:
|
|
30
|
+
' dispatch(intent) → reducer.reduce() → update state → notify listeners
|
|
31
|
+
' =====================================================================
|
|
32
|
+
class DispatcherCreator extends ListenerForDispatchers
|
|
33
|
+
|
|
34
|
+
' =============================================================
|
|
35
|
+
' MEMBER VARIABLES
|
|
36
|
+
' =============================================================
|
|
37
|
+
|
|
38
|
+
middlewares = [] ' Middleware array (unused, handled by Reducer)
|
|
39
|
+
isExternal = false ' False for internal dispatchers
|
|
40
|
+
isDispatchingInProgress = false ' Prevents reentrant dispatch
|
|
41
|
+
dispatchQueue = [] ' Queue for intents during active dispatch
|
|
42
|
+
dispatcherId as string ' Dispatcher identifier
|
|
43
|
+
taskNode as object ' Task node for cross-thread state exposure
|
|
44
|
+
reducerInstance as object ' Reducer instance
|
|
45
|
+
modelInstance as object ' Model instance holding state
|
|
46
|
+
listeners = [] ' State change listeners
|
|
47
|
+
|
|
48
|
+
' =============================================================
|
|
49
|
+
' CONSTRUCTOR
|
|
50
|
+
' =============================================================
|
|
51
|
+
|
|
52
|
+
' ---------------------------------------------------------------------
|
|
53
|
+
' Constructor - Initializes dispatcher and registers with provider
|
|
54
|
+
'
|
|
55
|
+
' @param {string} dispatcherId - Dispatcher identifier (auto-generated if empty)
|
|
56
|
+
' @param {object} modelInstance - Model instance
|
|
57
|
+
' @param {object} reducerInstance - Reducer instance
|
|
58
|
+
'
|
|
59
|
+
sub new(dispatcherId = "" as string, modelInstance = invalid as object, reducerInstance = invalid as object)
|
|
60
|
+
super()
|
|
61
|
+
if dispatcherId = "" then dispatcherId = Rotor.Utils.getUUIDHex()
|
|
62
|
+
m.dispatcherId = dispatcherId
|
|
63
|
+
|
|
64
|
+
globalScope = GetGlobalAA()
|
|
65
|
+
m.taskNode = globalScope.top
|
|
66
|
+
|
|
67
|
+
' Link reducer to this dispatcher
|
|
68
|
+
m.reducerInstance = reducerInstance
|
|
69
|
+
m.reducerInstance.ownerDispatcher = m
|
|
70
|
+
m.reducerInstance.ownerDispatcherId = dispatcherId
|
|
71
|
+
m.modelInstance = modelInstance
|
|
72
|
+
|
|
73
|
+
' Create field on task node for render thread communication
|
|
74
|
+
m.taskNode.addField(m.dispatcherId, "node", true)
|
|
75
|
+
|
|
76
|
+
' Expose initial state
|
|
77
|
+
m.exposeState()
|
|
78
|
+
|
|
79
|
+
' Register with dispatcher provider
|
|
80
|
+
dispatcherProvider = globalScope.rotor_framework_helper.frameworkInstance.dispatcherProvider
|
|
81
|
+
dispatcherProvider.registerDispatcher(m, dispatcherId)
|
|
82
|
+
end sub
|
|
83
|
+
|
|
84
|
+
' =============================================================
|
|
85
|
+
' STATE MANAGEMENT
|
|
86
|
+
' =============================================================
|
|
87
|
+
|
|
88
|
+
' ---------------------------------------------------------------------
|
|
89
|
+
' exposeState - Exposes current state to render thread via task node field
|
|
90
|
+
'
|
|
91
|
+
' Creates a ContentNode with state fields and sets it on task node.
|
|
92
|
+
' This allows render thread to observe state changes.
|
|
93
|
+
'
|
|
94
|
+
sub exposeState()
|
|
95
|
+
newState = CreateObject("roSGNode", "ContentNode")
|
|
96
|
+
newState.addFields(m.modelInstance.state)
|
|
97
|
+
m.taskNode.setField(m.dispatcherId, newState)
|
|
98
|
+
end sub
|
|
99
|
+
|
|
100
|
+
' ---------------------------------------------------------------------
|
|
101
|
+
' getState - Returns current state (optionally mapped)
|
|
102
|
+
'
|
|
103
|
+
' @param {dynamic} mapStateToProps - Optional mapping function
|
|
104
|
+
' @param {object} callerScope - Caller scope for mapping function
|
|
105
|
+
' @returns {object} Current state
|
|
106
|
+
'
|
|
107
|
+
function getState(mapStateToProps = invalid as Dynamic, callerScope = invalid as object) as object
|
|
108
|
+
state = m.modelInstance.state
|
|
109
|
+
m.runMapStateToProps(state, mapStateToProps, callerScope)
|
|
110
|
+
return state
|
|
111
|
+
end function
|
|
112
|
+
|
|
113
|
+
' =============================================================
|
|
114
|
+
' DISPATCH
|
|
115
|
+
' =============================================================
|
|
116
|
+
|
|
117
|
+
' ---------------------------------------------------------------------
|
|
118
|
+
' dispatch - Dispatches intent to reducer and updates state
|
|
119
|
+
'
|
|
120
|
+
' Process flow:
|
|
121
|
+
' 1. Validate intent payload
|
|
122
|
+
' 2. If not dispatching, execute reducer and notify listeners
|
|
123
|
+
' 3. If dispatching, queue intent for later processing
|
|
124
|
+
' 4. Process queued intents after completion
|
|
125
|
+
'
|
|
126
|
+
' @param {Intent} intent - Intent object to dispatch
|
|
127
|
+
'
|
|
128
|
+
sub dispatch(intent as Intent)
|
|
129
|
+
if intent?.payload <> invalid and intent.payload.Count() > 1 and intent.payload = invalid
|
|
130
|
+
throw "[WARNING] Intent payload is invalid."
|
|
131
|
+
end if
|
|
132
|
+
|
|
133
|
+
if m.isDispatchingInProgress = false
|
|
134
|
+
' Execute reducer
|
|
135
|
+
currentState = m.modelInstance.state
|
|
136
|
+
newState = m.reducerInstance.reduce(currentState, intent)
|
|
137
|
+
|
|
138
|
+
' Middleware cancelled intent
|
|
139
|
+
if newState = invalid then return
|
|
140
|
+
|
|
141
|
+
' Update state and notify
|
|
142
|
+
m.exposeState()
|
|
143
|
+
m.notifyListeners(newState)
|
|
144
|
+
|
|
145
|
+
' Process queued intents
|
|
146
|
+
if m.dispatchQueue.Count() > 0
|
|
147
|
+
intent = m.dispatchQueue.shift()
|
|
148
|
+
m.dispatch(intent)
|
|
149
|
+
end if
|
|
150
|
+
else
|
|
151
|
+
' Queue intent to prevent reentrancy
|
|
152
|
+
m.dispatchQueue.push(intent)
|
|
153
|
+
end if
|
|
154
|
+
end sub
|
|
155
|
+
|
|
156
|
+
' =============================================================
|
|
157
|
+
' ASYNC CALLBACK
|
|
158
|
+
' =============================================================
|
|
159
|
+
|
|
160
|
+
' ---------------------------------------------------------------------
|
|
161
|
+
' asyncReducerCallback - Routes async callback to reducer
|
|
162
|
+
'
|
|
163
|
+
' @param {object} msg - Message from async operation
|
|
164
|
+
'
|
|
165
|
+
sub asyncReducerCallback(msg)
|
|
166
|
+
m.reducerInstance.asyncReducerCallback(msg)
|
|
167
|
+
end sub
|
|
168
|
+
|
|
169
|
+
' =============================================================
|
|
170
|
+
' CLEANUP
|
|
171
|
+
' =============================================================
|
|
172
|
+
|
|
173
|
+
' ---------------------------------------------------------------------
|
|
174
|
+
' destroy - Cleans up dispatcher resources
|
|
175
|
+
'
|
|
176
|
+
' Deregisters from provider and clears all references.
|
|
177
|
+
'
|
|
178
|
+
override sub destroy()
|
|
179
|
+
super.destroy()
|
|
180
|
+
|
|
181
|
+
m.dispatchQueue.clear()
|
|
182
|
+
|
|
183
|
+
frameworkInstance = GetGlobalAA().rotor_framework_helper.frameworkInstance
|
|
184
|
+
frameworkInstance.dispatcherProvider.deregisterDispatcher(m.dispatcherId)
|
|
185
|
+
|
|
186
|
+
m.taskNode = invalid
|
|
187
|
+
m.modelInstance = invalid
|
|
188
|
+
m.reducerInstance = invalid
|
|
189
|
+
end sub
|
|
190
|
+
|
|
191
|
+
end class
|
|
192
|
+
|
|
193
|
+
end namespace
|
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
import "ListenerForDispatchers.bs"
|
|
2
|
+
|
|
3
|
+
namespace Rotor
|
|
4
|
+
|
|
5
|
+
' =====================================================================
|
|
6
|
+
' DispatcherExternal - External dispatcher for cross-thread MVI state management
|
|
7
|
+
'
|
|
8
|
+
' This dispatcher is used when accessing dispatchers from a different thread
|
|
9
|
+
' than where they were created (render thread accessing task thread dispatcher
|
|
10
|
+
' or vice versa).
|
|
11
|
+
'
|
|
12
|
+
' Responsibilities:
|
|
13
|
+
' - Dispatches intents across thread boundaries via rotorSync field
|
|
14
|
+
' - Observes state changes on task node field
|
|
15
|
+
' - Manages cross-thread listener notifications
|
|
16
|
+
' - Lazy observer setup (only when listeners are added)
|
|
17
|
+
' - Auto cleanup when last listener is removed
|
|
18
|
+
'
|
|
19
|
+
' MVI Cross-Thread Flow:
|
|
20
|
+
' Render Thread: dispatch(intent) → rotorSync field → Task Thread
|
|
21
|
+
' Task Thread: reduce state → task node field → Render Thread observer callback
|
|
22
|
+
' =====================================================================
|
|
23
|
+
class DispatcherExternal extends ListenerForDispatchers
|
|
24
|
+
|
|
25
|
+
' =============================================================
|
|
26
|
+
' MEMBER VARIABLES
|
|
27
|
+
' =============================================================
|
|
28
|
+
|
|
29
|
+
dispatcherId as string ' Dispatcher identifier
|
|
30
|
+
taskNode as object ' Task node for cross-thread communication
|
|
31
|
+
threadType as string ' Current thread type (RENDER or TASK)
|
|
32
|
+
isExternal = true ' True for external dispatchers
|
|
33
|
+
isStateObserved = false ' Tracks if observer is active
|
|
34
|
+
|
|
35
|
+
' =============================================================
|
|
36
|
+
' CONSTRUCTOR
|
|
37
|
+
' =============================================================
|
|
38
|
+
|
|
39
|
+
' ---------------------------------------------------------------------
|
|
40
|
+
' Constructor - Initializes external dispatcher
|
|
41
|
+
'
|
|
42
|
+
' @param {string} dispatcherId - Dispatcher identifier
|
|
43
|
+
' @param {object} taskNode - Task node for cross-thread communication
|
|
44
|
+
' @param {string} threadType - Current thread type (RENDER or TASK)
|
|
45
|
+
'
|
|
46
|
+
sub new(dispatcherId as string, taskNode as object, threadType as string)
|
|
47
|
+
super()
|
|
48
|
+
|
|
49
|
+
' Store dispatcher configuration
|
|
50
|
+
m.dispatcherId = dispatcherId
|
|
51
|
+
m.taskNode = taskNode
|
|
52
|
+
m.threadType = threadType
|
|
53
|
+
end sub
|
|
54
|
+
|
|
55
|
+
' =============================================================
|
|
56
|
+
' DISPATCH
|
|
57
|
+
' =============================================================
|
|
58
|
+
|
|
59
|
+
' ---------------------------------------------------------------------
|
|
60
|
+
' dispatch - Sends intent to internal dispatcher via cross-thread sync
|
|
61
|
+
'
|
|
62
|
+
' Process flow:
|
|
63
|
+
' 1. Validate intent payload
|
|
64
|
+
' 2. Package intent with dispatcher ID
|
|
65
|
+
' 3. Set rotorSync field to trigger task thread processing
|
|
66
|
+
'
|
|
67
|
+
' @param {Intent} intent - Intent object to dispatch
|
|
68
|
+
'
|
|
69
|
+
sub dispatch(intent as Intent)
|
|
70
|
+
' Validate intent payload
|
|
71
|
+
if intent?.payload <> invalid and intent.payload.Count() > 1 and intent.payload = invalid
|
|
72
|
+
throw "[WARNING] Intent payload is invalid."
|
|
73
|
+
end if
|
|
74
|
+
|
|
75
|
+
' Send intent to task thread via rotorSync field
|
|
76
|
+
m.taskNode.setField("rotorSync", {
|
|
77
|
+
type: Rotor.Const.ThreadSyncType.DISPATCH,
|
|
78
|
+
payload: {
|
|
79
|
+
dispatcherId: m.dispatcherId,
|
|
80
|
+
intent: intent
|
|
81
|
+
}
|
|
82
|
+
})
|
|
83
|
+
end sub
|
|
84
|
+
|
|
85
|
+
' =============================================================
|
|
86
|
+
' STATE ACCESS
|
|
87
|
+
' =============================================================
|
|
88
|
+
|
|
89
|
+
' ---------------------------------------------------------------------
|
|
90
|
+
' getState - Returns current state from task node field (optionally mapped)
|
|
91
|
+
'
|
|
92
|
+
' Reads state from task node field set by internal dispatcher.
|
|
93
|
+
'
|
|
94
|
+
' @param {dynamic} mapStateToProps - Optional mapping function
|
|
95
|
+
' @param {object} callerScope - Caller scope for mapping function
|
|
96
|
+
' @returns {object} Current state
|
|
97
|
+
'
|
|
98
|
+
function getState(mapStateToProps = invalid as Dynamic, callerScope = invalid as object) as object
|
|
99
|
+
' Read state from task node field
|
|
100
|
+
state = m.taskNode.getField(m.dispatcherId)
|
|
101
|
+
|
|
102
|
+
' Apply optional state mapping
|
|
103
|
+
m.runMapStateToProps(state, mapStateToProps, callerScope)
|
|
104
|
+
|
|
105
|
+
return state
|
|
106
|
+
end function
|
|
107
|
+
|
|
108
|
+
' =============================================================
|
|
109
|
+
' LISTENER MANAGEMENT
|
|
110
|
+
' =============================================================
|
|
111
|
+
|
|
112
|
+
' ---------------------------------------------------------------------
|
|
113
|
+
' addListener - Adds state change listener with lazy observer setup
|
|
114
|
+
'
|
|
115
|
+
' Sets up observer on first listener registration (lazy initialization).
|
|
116
|
+
'
|
|
117
|
+
' @param {object} listenerConfig - Listener configuration
|
|
118
|
+
' @param {string} listenerId - Listener identifier
|
|
119
|
+
' @param {object} listenerScope - Listener scope for callback
|
|
120
|
+
'
|
|
121
|
+
override sub addListener(listenerConfig as object, listenerId as string, listenerScope as object)
|
|
122
|
+
' Setup observer on first listener (lazy initialization)
|
|
123
|
+
if m.isStateObserved = false
|
|
124
|
+
m.setupObserver()
|
|
125
|
+
end if
|
|
126
|
+
|
|
127
|
+
' Register listener with parent class
|
|
128
|
+
super.addListener(listenerConfig, listenerId, listenerScope)
|
|
129
|
+
end sub
|
|
130
|
+
|
|
131
|
+
' ---------------------------------------------------------------------
|
|
132
|
+
' removeAllListenersByListenerId - Removes all listeners with given ID
|
|
133
|
+
'
|
|
134
|
+
' Automatically removes observer when last listener is removed.
|
|
135
|
+
'
|
|
136
|
+
' @param {string} listenerId - Listener identifier to remove
|
|
137
|
+
'
|
|
138
|
+
override sub removeAllListenersByListenerId(listenerId as string)
|
|
139
|
+
' Remove listeners via parent class
|
|
140
|
+
super.removeAllListenersByListenerId(listenerId)
|
|
141
|
+
|
|
142
|
+
' Auto cleanup: remove observer when no listeners remain
|
|
143
|
+
if m.isStateObserved = true and m.listeners.Count() = 0
|
|
144
|
+
m.removeObserver()
|
|
145
|
+
end if
|
|
146
|
+
end sub
|
|
147
|
+
|
|
148
|
+
' =============================================================
|
|
149
|
+
' OBSERVER MANAGEMENT
|
|
150
|
+
' =============================================================
|
|
151
|
+
|
|
152
|
+
' ---------------------------------------------------------------------
|
|
153
|
+
' setupObserver - Sets up observer for state changes on task node field
|
|
154
|
+
'
|
|
155
|
+
' Thread-specific observer setup:
|
|
156
|
+
' - RENDER thread: Uses observeFieldScoped with callback name
|
|
157
|
+
' - TASK thread: Uses framework's observer system
|
|
158
|
+
'
|
|
159
|
+
sub setupObserver()
|
|
160
|
+
if m.threadType = Rotor.Const.ThreadType.RENDER
|
|
161
|
+
' Render thread: use native SceneGraph observer
|
|
162
|
+
m.taskNode.observeFieldScoped(m.dispatcherId, "Rotor_dispatcherStateCallback")
|
|
163
|
+
else
|
|
164
|
+
' Task thread: use framework observer system
|
|
165
|
+
fieldId = m.dispatcherId
|
|
166
|
+
frameworkInstance = GetGlobalAA().rotor_framework_helper.frameworkInstance
|
|
167
|
+
frameworkInstance.addObserver(fieldId, m.taskNode)
|
|
168
|
+
end if
|
|
169
|
+
|
|
170
|
+
m.isStateObserved = true
|
|
171
|
+
end sub
|
|
172
|
+
|
|
173
|
+
' ---------------------------------------------------------------------
|
|
174
|
+
' removeObserver - Removes observer for state changes
|
|
175
|
+
'
|
|
176
|
+
' Thread-specific observer cleanup matching setupObserver.
|
|
177
|
+
'
|
|
178
|
+
sub removeObserver()
|
|
179
|
+
if m.threadType = Rotor.Const.ThreadType.RENDER
|
|
180
|
+
' Render thread: unobserve SceneGraph field
|
|
181
|
+
m.taskNode.unobserveFieldScoped(m.dispatcherId)
|
|
182
|
+
else
|
|
183
|
+
' Task thread: remove framework observer
|
|
184
|
+
fieldId = m.dispatcherId
|
|
185
|
+
frameworkInstance = GetGlobalAA().rotor_framework_helper.frameworkInstance
|
|
186
|
+
frameworkInstance.removeObserver(fieldId, m.taskNode)
|
|
187
|
+
end if
|
|
188
|
+
|
|
189
|
+
m.isStateObserved = false
|
|
190
|
+
end sub
|
|
191
|
+
|
|
192
|
+
' =============================================================
|
|
193
|
+
' CLEANUP
|
|
194
|
+
' =============================================================
|
|
195
|
+
|
|
196
|
+
' ---------------------------------------------------------------------
|
|
197
|
+
' destroy - Cleans up external dispatcher resources
|
|
198
|
+
'
|
|
199
|
+
' Cleanup steps:
|
|
200
|
+
' 1. Call parent cleanup
|
|
201
|
+
' 2. Remove observer if active
|
|
202
|
+
' 3. Clear listeners
|
|
203
|
+
' 4. Deregister from dispatcher provider
|
|
204
|
+
'
|
|
205
|
+
override sub destroy()
|
|
206
|
+
super.destroy()
|
|
207
|
+
|
|
208
|
+
frameworkInstance = GetGlobalAA().rotor_framework_helper.frameworkInstance
|
|
209
|
+
|
|
210
|
+
' Remove observer if active
|
|
211
|
+
if m.isStateObserved = true
|
|
212
|
+
if m.threadType = Rotor.Const.ThreadType.RENDER
|
|
213
|
+
m.taskNode.unobserveFieldScoped(m.dispatcherId)
|
|
214
|
+
else
|
|
215
|
+
fieldId = m.dispatcherId
|
|
216
|
+
frameworkInstance.removeObserver(fieldId, m.taskNode)
|
|
217
|
+
end if
|
|
218
|
+
end if
|
|
219
|
+
|
|
220
|
+
' Clear all references
|
|
221
|
+
m.listeners.Clear()
|
|
222
|
+
m.taskNode = invalid
|
|
223
|
+
|
|
224
|
+
' Deregister from provider
|
|
225
|
+
frameworkInstance.dispatcherProvider.deregisterDispatcher(m.dispatcherId)
|
|
226
|
+
end sub
|
|
227
|
+
|
|
228
|
+
end class
|
|
229
|
+
|
|
230
|
+
end namespace
|
|
231
|
+
|
|
232
|
+
namespace Rotor
|
|
233
|
+
|
|
234
|
+
' =====================================================================
|
|
235
|
+
' dispatcherStateCallback - Global callback function for dispatcher state changes on render thread
|
|
236
|
+
'
|
|
237
|
+
' This function is called by SceneGraph's observeFieldScoped when a dispatcher's
|
|
238
|
+
' state field changes on the task node. It routes the state change to the
|
|
239
|
+
' appropriate dispatcher instance for listener notification.
|
|
240
|
+
'
|
|
241
|
+
' Process flow:
|
|
242
|
+
' 1. Extract dispatcher ID from message field
|
|
243
|
+
' 2. Extract new state from message data
|
|
244
|
+
' 3. Lookup dispatcher instance from provider
|
|
245
|
+
' 4. Notify all registered listeners with new state
|
|
246
|
+
'
|
|
247
|
+
' @param {roSGNodeEvent} msg - SceneGraph observer message
|
|
248
|
+
' =====================================================================
|
|
249
|
+
sub dispatcherStateCallback(msg)
|
|
250
|
+
' Extract dispatcher ID and state from message
|
|
251
|
+
dispatcherId = msg.getField()
|
|
252
|
+
state = msg.getData()
|
|
253
|
+
|
|
254
|
+
' Lookup dispatcher instance and notify listeners
|
|
255
|
+
frameworkInstance = GetGlobalAA().rotor_framework_helper.frameworkInstance
|
|
256
|
+
dispatcherInstance = frameworkInstance.dispatcherProvider.get(dispatcherId)
|
|
257
|
+
dispatcherInstance.notifyListeners(state)
|
|
258
|
+
end sub
|
|
259
|
+
|
|
260
|
+
end namespace
|