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

mobalazs / rotor-framework / 22205737759

20 Feb 2026 12:15AM UTC coverage: 89.705% (-0.06%) from 89.769%
22205737759

push

github

mobalazs
refactor: update registerSourceObject and unregisterSourceObject methods for improved clarity and functionality

39 of 42 new or added lines in 3 files covered. (92.86%)

1 existing line in 1 file now uncovered.

2187 of 2438 relevant lines covered (89.7%)

1.25 hits per line

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

89.08
/src/source/RotorFrameworkTask.bs
1
' =========================================================================
2
' ▗▄▄▖  ▗▄▖▗▄▄▄▖▗▄▖ ▗▄▄▖     ▗▄▄▄▖▗▄▄▖  ▗▄▖ ▗▖  ▗▖▗▄▄▄▖▗▖ ▗▖ ▗▄▖ ▗▄▄▖ ▗▖ ▗▖
3
' ▐▌ ▐▌▐▌ ▐▌ █ ▐▌ ▐▌▐▌ ▐▌    ▐▌   ▐▌ ▐▌▐▌ ▐▌▐▛▚▞▜▌▐▌   ▐▌ ▐▌▐▌ ▐▌▐▌ ▐▌▐▌▗▞▘
4
' ▐▛▀▚▖▐▌ ▐▌ █ ▐▌ ▐▌▐▛▀▚▖    ▐▛▀▀▘▐▛▀▚▖▐▛▀▜▌▐▌  ▐▌▐▛▀▀▘▐▌ ▐▌▐▌ ▐▌▐▛▀▚▖▐▛▚▖
5
' ▐▌ ▐▌▝▚▄▞▘ █ ▝▚▄▞▘▐▌ ▐▌    ▐▌   ▐▌ ▐▌▐▌ ▐▌▐▌  ▐▌▐▙▄▄▖▐▙█▟▌▝▚▄▞▘▐▌ ▐▌▐▌ ▐▌
6
' Rotor Framework™
7
' Version 0.8.1
8
' © 2025-2026 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/DispatcherOriginal.bs"
20
import "base/DispatcherCrossThread.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.8.1"
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
        sourceObjectRegistry = {} ' identity -> { dispatcherId, objectId, eventFilter }
90
        sourceObjectIdIndex = {} ' objectId -> identity (reverse index for unregistration)
91
        sharedSourceObjects = {} ' typeName -> { sourceObject, subscribers: [{ dispatcherId, eventFilter }] }
92
        private _eventFilterFn = invalid as dynamic
93
        onTick as function
94

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

105
            Rotor.Utils.deepExtendAA(m.config, config)
1✔
106

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

114
            m.dispatcherProvider = new Rotor.DispatcherProvider(m.threadType)
1✔
115

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

120
        end sub
121

122
        ' =====================================================================
123
        ' PUBLIC API
124
        ' =====================================================================
125

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

136
        ' ---------------------------------------------------------------------
137
        ' registerSourceObject - Creates and registers a source object for event routing
138
        '
139
        ' Creates a Roku source object by type name and auto-detects routing mode:
140
        '   - Identity-based: If sourceObject implements GetIdentity, each call creates
141
        '     a new instance with unique routing (roUrlTransfer, roChannelStore).
142
        '   - Broadcast (singleton): If sourceObject does NOT implement GetIdentity,
143
        '     first call creates the instance, subsequent calls return the shared instance
144
        '     and add the dispatcher as a subscriber (roDeviceInfo, roInput, roAppManager).
145
        '
146
        ' @param {string} typeName - Roku object type name (e.g. "roUrlTransfer", "roDeviceInfo")
147
        ' @param {string} dispatcherId - Dispatcher ID that will handle events
148
        ' @param {function} eventFilter - Optional filter function. Receives msg, returns boolean.
149
        ' @returns {object} The created (or shared) source object
150
        '
151
        public function registerSourceObject(typeName as string, dispatcherId as string, eventFilter = invalid as dynamic) as object
152
            sourceObject = CreateObject(typeName)
1✔
153
            if FindMemberFunction(sourceObject, "GetIdentity") <> invalid
3✔
154
                ' Identity-based: unique per call
155
                sourceObject.SetMessagePort(m.port)
1✔
156
                identity = sourceObject.GetIdentity().ToStr()
1✔
157
                objectId = `${dispatcherId}_${identity}`
1✔
158
                m.sourceObjectRegistry[identity] = {
1✔
159
                    dispatcherId: dispatcherId,
160
                    objectId: objectId,
161
                    eventFilter: eventFilter
162
                }
163
                m.sourceObjectIdIndex[objectId] = identity
1✔
164
            else
165
                ' Broadcast: singleton per type
3✔
166
                if m.sharedSourceObjects.DoesExist(typeName)
2✔
167
                    ' Existing — add subscriber, discard new object
NEW
168
                    m.sharedSourceObjects[typeName].subscribers.push({
×
169
                        dispatcherId: dispatcherId,
170
                        eventFilter: eventFilter
171
                    })
NEW
172
                    return m.sharedSourceObjects[typeName].sourceObject
×
173
                else
174
                    ' First — store as shared
3✔
175
                    sourceObject.SetMessagePort(m.port)
1✔
176
                    m.sharedSourceObjects[typeName] = {
1✔
177
                        sourceObject: sourceObject,
178
                        subscribers: [{
179
                            dispatcherId: dispatcherId,
180
                            eventFilter: eventFilter
181
                        }]
182
                    }
183
                end if
184
            end if
185
            return sourceObject
1✔
186
        end function
187

188
        ' ---------------------------------------------------------------------
189
        ' unregisterSourceObject - Unregisters a source object
190
        '
191
        ' Identity-based objects: removes by identity from registry.
192
        ' Broadcast objects: removes dispatcher subscriber, cleans up if no subscribers remain.
193
        '
194
        ' @param {object} sourceObject - The source object to unregister
195
        ' @param {string} dispatcherId - Dispatcher ID that owns this registration
196
        '
197
        public sub unregisterSourceObject(sourceObject as object, dispatcherId as string)
198
            if FindMemberFunction(sourceObject, "GetIdentity") <> invalid
3✔
199
                ' Identity-based: remove by identity
200
                identity = sourceObject.GetIdentity().ToStr()
1✔
201
                if m.sourceObjectRegistry.DoesExist(identity)
3✔
202
                    m.sourceObjectRegistry.Delete(identity)
1✔
203
                end if
204
                objectId = `${dispatcherId}_${identity}`
1✔
205
                if m.sourceObjectIdIndex.DoesExist(objectId)
3✔
206
                    m.sourceObjectIdIndex.Delete(objectId)
1✔
207
                end if
208
            else
209
                ' Broadcast: remove subscriber, cleanup if empty
3✔
210
                typeName = type(sourceObject)
1✔
211
                if m.sharedSourceObjects.DoesExist(typeName)
3✔
212
                    shared = m.sharedSourceObjects[typeName]
1✔
213
                    for i = shared.subscribers.count() - 1 to 0 step -1
1✔
214
                        if shared.subscribers[i].dispatcherId = dispatcherId
3✔
215
                            shared.subscribers.Delete(i)
1✔
216
                        end if
217
                    end for
218
                    if shared.subscribers.count() = 0
3✔
219
                        m.sharedSourceObjects.Delete(typeName)
1✔
220
                    end if
221
                end if
222
            end if
223
        end sub
224

225
        ' ---------------------------------------------------------------------
226
        ' sync - Starts the message loop for cross-thread communication
227
        '
228
        ' IMPORTANT: This method creates an infinite loop that handles:
229
        '   - Intent dispatching from render thread
230
        '   - External dispatcher registration
231
        '   - State change notifications
232
        '   - Async reducer callbacks
233
        '
234
        ' This method MUST be the last call in your task function, as it
235
        ' blocks execution until the framework is destroyed.
236
        '
237
        sub sync(waitMs = 0 as integer, onTick = invalid as dynamic)
238
            m.notifySyncStatus(Rotor.Const.ThreadSyncType.TASK_SYNCING)
1✔
239

240
            keepAlive = true
1✔
241

242
            ' Initialize tick timer if waitMs > 0
243
            lastTickTime = invalid
1✔
244
            if waitMs > 0
3✔
245
                lastTickTime = CreateObject("roTimespan")
1✔
246
                lastTickTime.Mark()
1✔
247
            end if
248

249
            while true and keepAlive = true
1✔
250
                msg = wait(waitMs, m.port)
1✔
251

252
                if msg = invalid
2✔
253
                    ' Timeout - check if tick interval elapsed
254
                    if waitMs > 0 and onTick <> invalid and lastTickTime <> invalid
3✔
255
                        elapsedMs = lastTickTime.TotalMilliseconds()
1✔
256
                        if elapsedMs >= waitMs
3✔
257
                            ' Tick interval elapsed - call callback
258
                            Rotor.Utils.callbackScoped(onTick, GetGlobalAA())
1✔
259
                            ' Reset tick timer
260
                            lastTickTime.Mark()
1✔
261
                        end if
262
                    end if
263
                else if msg <> invalid
3✔
264
                    msgType = type(msg)
1✔
265
                    if msgType = "roSGNodeEvent"
3✔
266
                        fieldId = msg.getField()
1✔
267

268
                        if fieldId = "rotorSync"
3✔
269

270
                            sync = msg.getData() ' @type:AA
1✔
271

272
                            if sync.type = Rotor.Const.ThreadSyncType.DISPATCH
2✔
273

274

275
                                dispatcherId = sync.payload.dispatcherId
1✔
276
                                intent = sync.payload.intent
1✔
277
                                dispatcherInstance = m.dispatcherProvider.stack.LookupCI(dispatcherId)
1✔
278

279
                                ' taskIntent = Rotor.Utils.deepCopy(intent)
280
                                dispatcherInstance.dispatch(intent)
1✔
281

282
                            else if sync.type = Rotor.Const.ThreadSyncType.REGISTER_CROSS_THREAD_DISPATCHER
2✔
283

284
                                for each item in sync.crossThreadDispatcherList
1✔
285
                                    m.dispatcherProvider.registerCrossThreadDispatchers(item.dispatcherId, item.stateNode)
1✔
286
                                end for
287

288
                                m.notifySyncStatus(Rotor.Const.ThreadSyncType.TASK_SYNCED)
1✔
289

290
                            else if sync.type = Rotor.Const.ThreadSyncType.DESTROY
3✔
291

292
                                keepAlive = false
1✔
293

294
                            end if
295
                        else
296
                            ' Cross-thread state change: notify task-side listeners
×
297
                            data = msg.getData()
×
298
                            dispatcherId = fieldId
×
299
                            dispatcherInstance = m.dispatcherProvider.get(dispatcherId)
×
300
                            if dispatcherInstance <> invalid
×
301
                                dispatcherInstance.notifyListeners(data)
×
302
                            end if
303

304
                        end if
305
                    else
306
                        ' Generic source object routing
3✔
307
                        routed = false
1✔
308

309
                        ' Try identity-based routing
310
                        if m.sourceObjectRegistry.count() > 0
2✔
311
                            try
312
                                sourceIdentity = msg.GetSourceIdentity().ToStr()
1✔
313
                                if m.sourceObjectRegistry.DoesExist(sourceIdentity)
2✔
314
                                    entry = m.sourceObjectRegistry[sourceIdentity]
1✔
315

316
                                    ' Apply event filter if provided
317
                                    allowed = true
1✔
318
                                    if entry.eventFilter <> invalid
2✔
319
                                        m._eventFilterFn = entry.eventFilter
×
320
                                        allowed = m._eventFilterFn(msg)
×
321
                                    end if
322

323
                                    if allowed
3✔
324
                                        dispatcherInstance = m.dispatcherProvider.get(entry.dispatcherId)
1✔
325
                                        if dispatcherInstance <> invalid
3✔
326
                                            dispatcherInstance.onSourceEvent(msg)
1✔
327
                                        end if
328
                                    end if
329
                                    routed = true
1✔
330
                                end if
331
                            catch e
332
                                ' Event doesn't support GetSourceIdentity - fall through to broadcast
333
                            end try
334
                        end if
335

336
                        ' Broadcast to shared source object subscribers
337
                        if not routed
3✔
338
                            for each typeName in m.sharedSourceObjects
1✔
339
                                shared = m.sharedSourceObjects[typeName]
1✔
340
                                for each subscriber in shared.subscribers
1✔
341
                                    allowed = true
1✔
342
                                    if subscriber.eventFilter <> invalid
3✔
343
                                        m._eventFilterFn = subscriber.eventFilter
1✔
344
                                        allowed = m._eventFilterFn(msg)
1✔
345
                                    end if
346

347
                                    if allowed
3✔
348
                                        dispatcherInstance = m.dispatcherProvider.get(subscriber.dispatcherId)
1✔
349
                                        if dispatcherInstance <> invalid
3✔
350
                                            dispatcherInstance.onSourceEvent(msg)
1✔
351
                                        end if
352
                                    end if
353
                                end for
354
                            end for
355
                        end if
356
                    end if
357
                end if
358
            end while
359
            m.destroy()
1✔
360
        end sub
361

362
        ' =====================================================================
363
        ' INTERNAL METHODS
364
        ' =====================================================================
365

366
        ' ---------------------------------------------------------------------
367
        ' notifySyncStatus - Notifies render thread of sync status
368
        '
369
        ' Sends sync status message to render thread via rotorSync field.
370
        '
371
        ' @param {string} status - Sync status type (TASK_SYNCING or TASK_SYNCED)
372
        '
373
        sub notifySyncStatus(status as string)
374

375
            payload = {
1✔
376
                type: status,
377
                taskNode: m.taskNode
378
            }
379

380
            if status = Rotor.Const.ThreadSyncType.TASK_SYNCING
2✔
381
                payload.append({
1✔
382
                    dispatcherIds: m.dispatcherProvider.stack.Keys(),
383
                    tasks: m.config.tasks
384
                })
385
            end if
386

387
            m.taskNode.rootNode.setField("rotorSync", payload)
1✔
388

389
        end sub
390

391
        ' ---------------------------------------------------------------------
392
        ' addObserver - Adds field observer to task thread message port
393
        '
394
        ' @param {string} fieldId - Field name to observe
395
        ' @param {object} node - SceneGraph node to observe
396
        '
397
        sub addObserver(fieldId as string, node)
398
            node.observeFieldScoped(fieldId, m.port)
×
399
        end sub
400

401
        ' ---------------------------------------------------------------------
402
        ' removeObserver - Removes field observer from node
403
        '
404
        ' @param {string} fieldId - Field name to stop observing
405
        ' @param {object} node - SceneGraph node to unobserve
406
        '
407
        sub removeObserver(fieldId as string, node)
408
            node.unobserveFieldScoped(fieldId)
×
409
        end sub
410

411
        ' =====================================================================
412
        ' CLEANUP
413
        ' =====================================================================
414

415
        ' ---------------------------------------------------------------------
416
        ' destroy - Cleans up task thread resources
417
        '
418
        ' Destroys dispatcher provider and clears global framework helper.
419
        '
420
        public sub destroy()
421
            m.sourceObjectRegistry.clear()
1✔
422
            m.sourceObjectIdIndex.clear()
1✔
423
            m.sharedSourceObjects.clear()
1✔
424
            m.dispatcherProvider.destroy()
1✔
425

426
            globalScope = GetGlobalAA()
1✔
427
            globalScope.rotor_framework_helper = {
1✔
428
                frameworkInstance: invalid
429
            }
430

431
            m.taskNode.rootNode = invalid
1✔
432
            m.taskNode = invalid
1✔
433
        end sub
434

435
    end class
436

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

© 2026 Coveralls, Inc