• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

mobalazs / rotor-framework / 20862688693

09 Jan 2026 07:10PM UTC coverage: 85.229% (-0.4%) from 85.617%
20862688693

push

github

web-flow
Feat/network event (#17)

* feat(RotorFrameworkTask): add device info registration and event handling

* fix(BaseWidget): standardize spacing and formatting for focus plugin methods and private properties

1 of 12 new or added lines in 1 file covered. (8.33%)

2008 of 2356 relevant lines covered (85.23%)

1.17 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

52.38
/src/source/RotorFrameworkTask.bs
1
' =========================================================================
2
' ▗▄▄▖  ▗▄▖▗▄▄▄▖▗▄▖ ▗▄▄▖     ▗▄▄▄▖▗▄▄▖  ▗▄▖ ▗▖  ▗▖▗▄▄▄▖▗▖ ▗▖ ▗▄▖ ▗▄▄▖ ▗▖ ▗▖
3
' ▐▌ ▐▌▐▌ ▐▌ █ ▐▌ ▐▌▐▌ ▐▌    ▐▌   ▐▌ ▐▌▐▌ ▐▌▐▛▚▞▜▌▐▌   ▐▌ ▐▌▐▌ ▐▌▐▌ ▐▌▐▌▗▞▘
4
' ▐▛▀▚▖▐▌ ▐▌ █ ▐▌ ▐▌▐▛▀▚▖    ▐▛▀▀▘▐▛▀▚▖▐▛▀▜▌▐▌  ▐▌▐▛▀▀▘▐▌ ▐▌▐▌ ▐▌▐▛▀▚▖▐▛▚▖
5
' ▐▌ ▐▌▝▚▄▞▘ █ ▝▚▄▞▘▐▌ ▐▌    ▐▌   ▐▌ ▐▌▐▌ ▐▌▐▌  ▐▌▐▙▄▄▖▐▙█▟▌▝▚▄▞▘▐▌ ▐▌▐▌ ▐▌
6
' Rotor Framework™
7
' Version 0.7.2
8
' © 2025 Balázs Molnár — Apache License 2.0
9
' =========================================================================
10

11
' constants
12
import "engine/Constants.bs"
13

14
' engine
15
import "engine/providers/DispatcherProvider.bs"
16
import "engine/providers/Dispatcher.bs"
17

18
' base classes
19
import "base/DispatcherCreator.bs"
20
import "base/DispatcherExternal.bs"
21
import "base/BaseReducer.bs"
22
import "base/BaseModel.bs"
23
import "base/BaseStack.bs"
24

25
' utils
26
import "utils/GeneralUtils.bs"
27
import "utils/NodeUtils.bs"
28
import "utils/ArrayUtils.bs"
29

30
namespace Rotor
31
    ' =====================================================================
32
    ' FrameworkTask - Task thread version of Rotor Framework for MVI
33
    '
34
    ' Task thread version of the Rotor Framework that enables cross-thread MVI
35
    ' (Model-View-Intent) architecture. This class manages state and dispatchers
36
    ' on a separate task thread, allowing heavy computations and state management
37
    ' to run off the render thread for better performance.
38
    '
39
    ' Configuration:
40
    '   - tasks (array, optional): List of additional task node names to synchronize with.
41
    '                             Allows multiple task threads to communicate and share
42
    '                             dispatchers across different threads.
43
    '
44
    ' USAGE NOTES:
45
    ' The FrameworkTask must be instantiated in the task's init() function and the sync()
46
    ' method MUST be called at the end of your task function to establish the message loop.
47
    '
48
    ' IMPORTANT: The sync() method creates an infinite loop that handles cross-thread
49
    ' communication and dispatcher synchronization. This call should be the LAST statement
50
    ' in your task function, after all dispatcher initialization.
51
    '
52
    ' Example:
53
    '   File: MyTask.task.bs
54
    '   import "pkg:/source/RotorFrameworkTask.bs"
55
    '   import "pkg:/source/MyDispatcher.bs"
56
    '
57
    '   sub init()
58
    '       m.top.functionName = "task"
59
    '       m.appFw = new Rotor.FrameworkTask({
60
    '           tasks: ["AnotherTask"]
61
    '       })
62
    '   end sub
63
    '
64
    '   sub task()
65
    '       m.fooDispatcher = createFooDispatcher()
66
    '       m.barDispatcher = createBarDispatcher()
67
    '       m.appFw.sync()
68
    '   end sub
69
    ' =====================================================================
70
    class FrameworkTask
71

72
        name = "Rotor Framework"
73
        version = "0.7.2"
74

75
        config = {
76
            tasks: invalid, ' optional
77
            debug: {
78
            }
79
        }
80

81
        threadType = Rotor.Const.ThreadType.TASK
82

83
        keepAlive = true
84

85
        ' helper vars
86
        taskNode as object
87
        dispatcherProvider as object
88
        port as object
89
        asyncTransferRegistry = {} ' transferId -> { dispatcherId, context }
90
        deviceInfoRegistry = {} ' deviceInfoId -> { dispatcherId, context, deviceInfo }
91
        onTick as function
92

93
        ' ---------------------------------------------------------------------
94
        ' new - Initializes the FrameworkTask instance
95
        '
96
        ' Sets up the task thread dispatcher provider, message port, and
97
        ' global framework helper for cross-thread communication.
98
        '
99
        ' @param {object} config - Configuration object (see class documentation)
100
        '
101
        sub new(config = {} as object)
102

103
            Rotor.Utils.deepExtendAA(m.config, config)
1✔
104

105
            globalScope = GetGlobalAA()
1✔
106
            globalScope.rotor_framework_helper = { ' this give to dispatcher instance the possibility to self-register
1✔
107
                threadType: m.threadType,
108
                frameworkInstance: m
109
            }
110
            m.taskNode = globalScope.top
1✔
111

112
            m.dispatcherProvider = new Rotor.DispatcherProvider(m.threadType)
1✔
113

114
            m.taskNode.addField("rotorSync", "assocarray", true)
1✔
115
            m.port = CreateObject("roMessagePort")
1✔
116
            m.taskNode.observeFieldScoped("rotorSync", m.port)
1✔
117

118
        end sub
119

120
        ' =====================================================================
121
        ' PUBLIC API
122
        ' =====================================================================
123

124
        ' ---------------------------------------------------------------------
125
        ' getDispatcher - Gets dispatcher facade by ID
126
        '
127
        ' @param {string} dispatcherId - Dispatcher identifier
128
        ' @returns {object} Dispatcher facade instance
129
        '
130
        public function getDispatcher(dispatcherId as string) as object
131
            return m.dispatcherProvider.getFacade(dispatcherId, GetGlobalAA())
×
132
        end function
133

134
        ' ---------------------------------------------------------------------
135
        ' registerAsyncTransfer - Registers an async transfer with dispatcher context
136
        '
137
        ' This method associates a roUrlTransfer identity with a dispatcher ID and
138
        ' optional context data. When the transfer completes and sends a roUrlEvent,
139
        ' the framework will route it to the correct dispatcher's asyncReducerCallback.
140
        '
141
        ' @param {string} transferId - The transfer.GetIdentity().ToStr() value
142
        ' @param {string} dispatcherId - Dispatcher ID that initiated the transfer
143
        ' @param {object} context - Optional user data to pass back in callback
144
        '
145
        public sub registerAsyncTransfer(transferId as string, dispatcherId as string, context = invalid as dynamic)
146
            m.asyncTransferRegistry[transferId] = {
×
147
                dispatcherId: dispatcherId,
148
                context: context
149
            }
150
        end sub
151

152
        ' ---------------------------------------------------------------------
153
        ' registerDeviceInfo - Registers a roDeviceInfo for event listening
154
        '
155
        ' This method associates a roDeviceInfo object with a dispatcher ID.
156
        ' When device info events occur (linkStatus, etc.), the framework will
157
        ' route them to the correct dispatcher's asyncReducerCallback.
158
        '
159
        ' @param {string} deviceInfoId - Unique identifier for this registration
160
        ' @param {string} dispatcherId - Dispatcher ID that will handle events
161
        ' @param {object} deviceInfo - The roDeviceInfo object (must have SetMessagePort called)
162
        ' @param {object} context - Optional user data to pass back in callback
163
        '
164
        public sub registerDeviceInfo(deviceInfoId as string, dispatcherId as string, deviceInfo as object, context = invalid as dynamic)
NEW
165
            deviceInfo.SetMessagePort(m.port)
×
NEW
166
            m.deviceInfoRegistry[deviceInfoId] = {
×
167
                dispatcherId: dispatcherId,
168
                deviceInfo: deviceInfo,
169
                context: context
170
            }
171
        end sub
172

173
        ' ---------------------------------------------------------------------
174
        ' sync - Starts the message loop for cross-thread communication
175
        '
176
        ' IMPORTANT: This method creates an infinite loop that handles:
177
        '   - Intent dispatching from render thread
178
        '   - External dispatcher registration
179
        '   - State change notifications
180
        '   - Async reducer callbacks
181
        '
182
        ' This method MUST be the last call in your task function, as it
183
        ' blocks execution until the framework is destroyed.
184
        '
185
        sub sync(waitMs = 0 as integer, onTick = invalid as dynamic)
186
            m.notifySyncStatus(Rotor.Const.ThreadSyncType.TASK_SYNCING)
1✔
187

188
            keepAlive = true
1✔
189

190
            ' Initialize tick timer if waitMs > 0
191
            lastTickTime = invalid
1✔
192
            if waitMs > 0
2✔
193
                lastTickTime = CreateObject("roTimespan")
×
194
                lastTickTime.Mark()
×
195
            end if
196

197
            while true and keepAlive = true
1✔
198
                msg = wait(waitMs, m.port)
1✔
199

200
                if msg = invalid
2✔
201
                    ' Timeout - check if tick interval elapsed
202
                    if waitMs > 0 and onTick <> invalid and lastTickTime <> invalid
×
203
                        elapsedMs = lastTickTime.TotalMilliseconds()
×
204
                        if elapsedMs >= waitMs
×
205
                            ' Tick interval elapsed - call callback
206
                            Rotor.Utils.callbackScoped(onTick, GetGlobalAA())
×
207
                            ' Reset tick timer
208
                            lastTickTime.Mark()
×
209
                        end if
210
                    end if
211
                else if msg <> invalid
3✔
212
                    msgType = type(msg)
1✔
213
                    if msgType = "roSGNodeEvent"
3✔
214
                        fieldId = msg.getField()
1✔
215

216
                        if fieldId = "rotorSync"
3✔
217

218
                            sync = msg.getData() ' @type:AA
1✔
219

220
                            if sync.type = Rotor.Const.ThreadSyncType.DISPATCH
2✔
221

222

223
                                dispatcherId = sync.payload.dispatcherId
1✔
224
                                intent = sync.payload.intent
1✔
225
                                dispatcherInstance = m.dispatcherProvider.stack.LookupCI(dispatcherId)
1✔
226

227
                                ' taskIntent = Rotor.Utils.deepCopy(intent)
228
                                dispatcherInstance.dispatch(intent)
1✔
229

230
                            else if sync.type = Rotor.Const.ThreadSyncType.REGISTER_EXTERNAL_DISPATCHER
3✔
231

232
                                for each item in sync.externalDispatcherList
1✔
233
                                    m.dispatcherProvider.registerExternalDispatchers(item.dispatcherId, item.externalTaskNode)
1✔
234
                                end for
235

236
                                m.notifySyncStatus(Rotor.Const.ThreadSyncType.TASK_SYNCED)
1✔
237

238
                            else if sync.type = Rotor.Const.ThreadSyncType.DESTROY
3✔
239

240
                                keepAlive = false
1✔
241

242
                            end if
243
                        else
244

×
245
                            data = msg.getData()
×
246
                            extraInfo = msg.GetInfo() ' Info AA passed during observeFieldScoped
×
247

248
                            if extraInfo?.dispatcherId <> invalid and m.dispatcherProvider.get(extraInfo?.dispatcherId) <> invalid
×
249
                                ' Catch by dispatcherId
250
                                m.dispatcherProvider.get(extraInfo?.dispatcherId).asyncReducerCallback(msg, extraInfo?.context as dynamic)
×
251
                            else
×
252
                                dispatcherId = fieldId
×
253
                                dispatcherInstance = m.dispatcherProvider.get(dispatcherId)
×
254
                                dispatcherInstance.notifyListeners(data)
×
255
                            end if
256

257
                        end if
258
                    else if msgType = "roUrlEvent"
×
259
                        ' Handle async transfer responses
260
                        transferId = msg.GetSourceIdentity().ToStr()
×
261

262
                        if m.asyncTransferRegistry.DoesExist(transferId)
×
263
                            transferData = m.asyncTransferRegistry[transferId]
×
264
                            dispatcherId = transferData.dispatcherId
×
265

266
                            ' Cleanup registry entry (Note: order is important - this make it reusable immediately)
267
                            m.asyncTransferRegistry.delete(transferId)
×
268

269
                            dispatcherInstance = m.dispatcherProvider.get(dispatcherId)
×
270
                            if dispatcherInstance <> invalid
×
271
                                ' Route to dispatcher with wrapped message
272
                                dispatcherInstance.asyncReducerCallback(msg as roUrlEvent, transferData?.context as dynamic)
×
273
                            end if
274

275
                        end if
NEW
276
                    else if msgType = "roDeviceInfoEvent"
×
277
                        ' Handle device info events (ignore appFocused, etc.)
NEW
278
                        if m.deviceInfoRegistry.count() > 0
×
NEW
279
                            eventInfo = msg.GetInfo()
×
280
                            ' Only process linkStatus events, ignore appFocused and other events
NEW
281
                            if eventInfo?.linkStatus <> invalid
×
282
                                ' Route to all registered device info dispatchers
NEW
283
                                for each deviceInfoId in m.deviceInfoRegistry
×
NEW
284
                                    registryEntry = m.deviceInfoRegistry[deviceInfoId]
×
NEW
285
                                    dispatcherInstance = m.dispatcherProvider.get(registryEntry.dispatcherId)
×
NEW
286
                                    if dispatcherInstance <> invalid
×
NEW
287
                                        dispatcherInstance.asyncReducerCallback(eventInfo, registryEntry?.context as dynamic)
×
288
                                    end if
289
                                end for
290
                            end if
291
                        end if
292
                    end if
293
                end if
294
            end while
295
            m.destroy()
1✔
296
        end sub
297

298
        ' =====================================================================
299
        ' INTERNAL METHODS
300
        ' =====================================================================
301

302
        ' ---------------------------------------------------------------------
303
        ' notifySyncStatus - Notifies render thread of sync status
304
        '
305
        ' Sends sync status message to render thread via rotorSync field.
306
        '
307
        ' @param {string} status - Sync status type (TASK_SYNCING or TASK_SYNCED)
308
        '
309
        sub notifySyncStatus(status as string)
310

311
            payload = {
1✔
312
                type: status,
313
                taskNode: m.taskNode
314
            }
315

316
            if status = Rotor.Const.ThreadSyncType.TASK_SYNCING
2✔
317
                payload.append({
1✔
318
                    dispatcherIds: m.dispatcherProvider.stack.Keys(),
319
                    tasks: m.config.tasks
320
                })
321
            end if
322

323
            m.taskNode.rootNode.setField("rotorSync", payload)
1✔
324

325
        end sub
326

327
        ' ---------------------------------------------------------------------
328
        ' addObserver - Adds field observer to task thread message port
329
        '
330
        ' @param {string} fieldId - Field name to observe
331
        ' @param {object} node - SceneGraph node to observe
332
        '
333
        sub addObserver(fieldId as string, node)
334
            node.observeFieldScoped(fieldId, m.port)
×
335
        end sub
336

337
        ' ---------------------------------------------------------------------
338
        ' removeObserver - Removes field observer from node
339
        '
340
        ' @param {string} fieldId - Field name to stop observing
341
        ' @param {object} node - SceneGraph node to unobserve
342
        '
343
        sub removeObserver(fieldId as string, node)
344
            node.unobserveFieldScoped(fieldId)
×
345
        end sub
346

347
        ' =====================================================================
348
        ' CLEANUP
349
        ' =====================================================================
350

351
        ' ---------------------------------------------------------------------
352
        ' destroy - Cleans up task thread resources
353
        '
354
        ' Destroys dispatcher provider and clears global framework helper.
355
        '
356
        public sub destroy()
357
            m.asyncTransferRegistry.clear()
1✔
358
            m.deviceInfoRegistry.clear()
1✔
359
            m.dispatcherProvider.destroy()
1✔
360

361
            globalScope = GetGlobalAA()
1✔
362
            globalScope.rotor_framework_helper = {
1✔
363
                frameworkInstance: invalid
364
            }
365

366
            m.taskNode.rootNode = invalid
1✔
367
            m.taskNode = invalid
1✔
368
        end sub
369

370
    end class
371

372
end namespace
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE TRIAL · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc