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

mobalazs / rotor-framework / 29758339845

20 Jul 2026 04:08PM UTC coverage: 90.677% (+0.01%) from 90.667%
29758339845

push

github

web-flow
feat: implement registerNodeObserver and unregisterNodeObserver for SceneGraph node field events (#27)

24 of 25 new or added lines in 2 files covered. (96.0%)

1 existing line in 1 file now uncovered.

2237 of 2467 relevant lines covered (90.68%)

1.26 hits per line

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

87.42
/src/source/RotorFrameworkTask.bs
1
' =========================================================================
2
' ▗▄▄▖  ▗▄▖▗▄▄▄▖▗▄▖ ▗▄▄▖     ▗▄▄▄▖▗▄▄▖  ▗▄▖ ▗▖  ▗▖▗▄▄▄▖▗▖ ▗▖ ▗▄▖ ▗▄▄▖ ▗▖ ▗▖
3
' ▐▌ ▐▌▐▌ ▐▌ █ ▐▌ ▐▌▐▌ ▐▌    ▐▌   ▐▌ ▐▌▐▌ ▐▌▐▛▚▞▜▌▐▌   ▐▌ ▐▌▐▌ ▐▌▐▌ ▐▌▐▌▗▞▘
4
' ▐▛▀▚▖▐▌ ▐▌ █ ▐▌ ▐▌▐▛▀▚▖    ▐▛▀▀▘▐▛▀▚▖▐▛▀▜▌▐▌  ▐▌▐▛▀▀▘▐▌ ▐▌▐▌ ▐▌▐▛▀▚▖▐▛▚▖
5
' ▐▌ ▐▌▝▚▄▞▘ █ ▝▚▄▞▘▐▌ ▐▌    ▐▌   ▐▌ ▐▌▐▌ ▐▌▐▌  ▐▌▐▙▄▄▖▐▙█▟▌▝▚▄▞▘▐▌ ▐▌▐▌ ▐▌
6
' Rotor Framework™
7
' Version 0.9.2
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/CombinedReducer.bs"
23
import "base/BaseModel.bs"
24
import "base/BaseStack.bs"
25

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

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

73
        name = "Rotor Framework"
74
        version = "0.9.2"
75

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

82
        threadType = Rotor.Const.ThreadType.TASK
83

84
        keepAlive = true
85

86
        ' helper vars
87
        taskNode as object
88
        renderQueue as object
89
        dispatcherProvider as object
90
        port as object
91
        sourceObjectRegistry = {} ' identity -> { dispatcherId, objectId, eventFilter }
92
        sourceObjectIdIndex = {} ' objectId -> identity (reverse index for unregistration)
93
        sharedSourceObjects = {} ' typeName -> { sourceObject, subscribers: [{ dispatcherId, eventFilter }] }
94
        nodeObserverRegistry = {} ' fieldId -> { dispatcherId, node } (SG-node field events routed to onSourceEvent)
95
        private _eventFilterFn = invalid as dynamic
96
        onTick as function
97

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

108
            Rotor.Utils.deepExtendAA(m.config, config)
1✔
109

110
            globalScope = GetGlobalAA()
1✔
111
            globalScope.rotor_framework_helper = { ' this give to dispatcher instance the possibility to self-register
1✔
112
                threadType: m.threadType,
113
                frameworkInstance: m
114
            }
115
            m.taskNode = globalScope.top
1✔
116
            m.renderQueue = CreateObject("roRenderThreadQueue")
1✔
117

118
            m.dispatcherProvider = new Rotor.DispatcherProvider(m.threadType)
1✔
119

120
            m.taskNode.addField("rotorSync", "assocarray", true)
1✔
121
            m.port = CreateObject("roMessagePort")
1✔
122
            m.taskNode.observeFieldScopedEx("rotorSync", m.port)
1✔
123

124
        end sub
125

126
        ' =====================================================================
127
        ' PUBLIC API
128
        ' =====================================================================
129

130
        ' =====================================================================
131
        ' ROUTER — Task → Render
132
        ' =====================================================================
133

134
        ' ---------------------------------------------------------------------
135
        ' sendStateToRender - Routes dispatcher state update to render thread
136
        '
137
        ' Called by DispatcherOriginal.exposeState(). Sends via the single
138
        ' __rotor__ channel on roRenderThreadQueue — zero rendezvous on task thread.
139
        '
140
        ' @param {string} dispatcherId - Dispatcher identifier
141
        ' @param {object} state - Current state AA
142
        '
143
        public sub sendStateToRender(dispatcherId as string, state as object)
144
            m.renderQueue.CopyMessage(Rotor.Const.ROTOR_QUEUE_ID, {
1✔
145
                type: Rotor.Const.RouterMsgType.STATE,
146
                dispatcherId: dispatcherId,
147
                state: state
148
            })
149
        end sub
150

151
        ' ---------------------------------------------------------------------
152
        ' sendIntent - Routes a cross-thread intent dispatch (task → render thread)
153
        '
154
        ' Called by DispatcherCrossThread.dispatch() on the task thread. A task cannot
155
        ' write a foreign (render-owned) node's rotorSync field without a blocking
156
        ' rendezvous, so the intent is relayed to the render thread via the
157
        ' rendezvous-free __rotor__ channel (CopyMessage), symmetric with
158
        ' sendStateToRender. The render thread resolves the target dispatcher by id and
159
        ' re-dispatches locally (see RotorFramework.handleIntentMessage). Enables
160
        ' task → render and task → task intent dispatch.
161
        '
162
        ' Thread-neutral counterpart of Framework.sendIntent (render → task). Same
163
        ' signature so DispatcherCrossThread.dispatch() works unchanged from either
164
        ' thread. The node parameter is unused on the task side — the render thread
165
        ' resolves the target from its own registry by dispatcherId.
166
        '
167
        ' @param {object} node - Target dispatcher state node (unused on task side)
168
        ' @param {string} dispatcherId - Target dispatcher identifier
169
        ' @param {object} intent - Intent to dispatch
170
        '
171
        public sub sendIntent(node as object, dispatcherId as string, intent as object)
172
            m.renderQueue.CopyMessage(Rotor.Const.ROTOR_QUEUE_ID, {
×
173
                type: Rotor.Const.RouterMsgType.INTENT,
174
                dispatcherId: dispatcherId,
175
                intent: intent
176
            })
177
        end sub
178

179
        ' ---------------------------------------------------------------------
180
        ' connectDispatcher - Creates a new dispatcher facade connection by ID
181
        '
182
        ' Each call creates a unique facade with its own listenerId and scope binding.
183
        ' Use facade.release() when done to clean up listeners.
184
        '
185
        ' @param {string} dispatcherId - Dispatcher identifier
186
        ' @returns {object} Dispatcher facade instance
187
        '
188
        public function connectDispatcher(dispatcherId as string) as object
189
            return m.dispatcherProvider.getFacade(dispatcherId, GetGlobalAA())
×
190
        end function
191

192
        ' ---------------------------------------------------------------------
193
        ' dispatchTo - Dispatches an intent to a specific dispatcher by ID
194
        '
195
        ' @param {string} dispatcherId - Target dispatcher identifier
196
        ' @param {object} dispatchObject - Intent object to dispatch
197
        '
198
        public sub dispatchTo(dispatcherId as string, dispatchObject as object)
199
            dispatcherFacade = m.connectDispatcher(dispatcherId)
×
200
            dispatcherFacade.dispatch(dispatchObject)
×
201
        end sub
202

203
        ' ---------------------------------------------------------------------
204
        ' getStateFrom - Gets current state from a specific dispatcher by ID
205
        '
206
        ' @param {string} dispatcherId - Target dispatcher identifier
207
        ' @param {dynamic} [mapStateToProps=invalid] - Optional state mapping function
208
        ' @returns {object} Current state from the target dispatcher
209
        '
210
        public function getStateFrom(dispatcherId as string, mapStateToProps = invalid as dynamic) as object
211
            dispatcherFacade = m.connectDispatcher(dispatcherId)
×
212
            return dispatcherFacade.getState(mapStateToProps)
×
213
        end function
214

215
        ' ---------------------------------------------------------------------
216
        ' registerSourceObject - Creates and registers a source object for event routing
217
        '
218
        ' Creates a Roku source object by type name and auto-detects routing mode:
219
        '   - Identity-based: If sourceObject implements GetIdentity, each call creates
220
        '     a new instance with unique routing (roUrlTransfer, roChannelStore).
221
        '   - Broadcast (singleton): If sourceObject does NOT implement GetIdentity,
222
        '     first call creates the instance, subsequent calls return the shared instance
223
        '     and add the dispatcher as a subscriber (roDeviceInfo, roInput, roAppManager).
224
        '
225
        ' @param {string} typeName - Roku object type name (e.g. "roUrlTransfer", "roDeviceInfo")
226
        ' @param {string} dispatcherId - Dispatcher ID that will handle events
227
        ' @param {function} eventFilter - Optional filter function. Receives msg, returns boolean.
228
        ' @returns {object} The created (or shared) source object
229
        '
230
        public function registerSourceObject(typeName as string, dispatcherId as string, eventFilter = invalid as dynamic) as object
231
            sourceObject = CreateObject(typeName)
1✔
232
            if FindMemberFunction(sourceObject, "GetIdentity") <> invalid
3✔
233
                ' Identity-based: unique per call
234
                sourceObject.SetMessagePort(m.port)
1✔
235
                identity = sourceObject.GetIdentity().ToStr()
1✔
236
                objectId = `${dispatcherId}_${identity}`
1✔
237
                m.sourceObjectRegistry[identity] = {
1✔
238
                    dispatcherId: dispatcherId,
239
                    objectId: objectId,
240
                    eventFilter: eventFilter
241
                }
242
                m.sourceObjectIdIndex[objectId] = identity
1✔
243
            else
244
                ' Broadcast: singleton per type
3✔
245
                if m.sharedSourceObjects.DoesExist(typeName)
2✔
246
                    ' Existing — add subscriber, discard new object
247
                    m.sharedSourceObjects[typeName].subscribers.push({
×
248
                        dispatcherId: dispatcherId,
249
                        eventFilter: eventFilter
250
                    })
251
                    return m.sharedSourceObjects[typeName].sourceObject
×
252
                else
253
                    ' First — store as shared
3✔
254
                    sourceObject.SetMessagePort(m.port)
1✔
255
                    m.sharedSourceObjects[typeName] = {
1✔
256
                        sourceObject: sourceObject,
257
                        subscribers: [{
258
                            dispatcherId: dispatcherId,
259
                            eventFilter: eventFilter
260
                        }]
261
                    }
262
                end if
263
            end if
264
            return sourceObject
1✔
265
        end function
266

267
        ' ---------------------------------------------------------------------
268
        ' unregisterSourceObject - Unregisters a source object
269
        '
270
        ' Identity-based objects: removes by identity from registry.
271
        ' Broadcast objects: removes dispatcher subscriber, cleans up if no subscribers remain.
272
        '
273
        ' @param {object} sourceObject - The source object to unregister
274
        ' @param {string} dispatcherId - Dispatcher ID that owns this registration
275
        '
276
        public sub unregisterSourceObject(sourceObject as object, dispatcherId as string)
277
            if FindMemberFunction(sourceObject, "GetIdentity") <> invalid
3✔
278
                ' Identity-based: remove by identity
279
                identity = sourceObject.GetIdentity().ToStr()
1✔
280
                if m.sourceObjectRegistry.DoesExist(identity)
3✔
281
                    m.sourceObjectRegistry.Delete(identity)
1✔
282
                end if
283
                objectId = `${dispatcherId}_${identity}`
1✔
284
                if m.sourceObjectIdIndex.DoesExist(objectId)
3✔
285
                    m.sourceObjectIdIndex.Delete(objectId)
1✔
286
                end if
287
            else
288
                ' Broadcast: remove subscriber, cleanup if empty
3✔
289
                typeName = type(sourceObject)
1✔
290
                if m.sharedSourceObjects.DoesExist(typeName)
3✔
291
                    shared = m.sharedSourceObjects[typeName]
1✔
292
                    for i = shared.subscribers.count() - 1 to 0 step -1
1✔
293
                        if shared.subscribers[i].dispatcherId = dispatcherId
3✔
294
                            shared.subscribers.Delete(i)
1✔
295
                        end if
296
                    end for
297
                    if shared.subscribers.count() = 0
3✔
298
                        m.sharedSourceObjects.Delete(typeName)
1✔
299
                    end if
300
                end if
301
            end if
302
        end sub
303

304
        ' ---------------------------------------------------------------------
305
        ' registerNodeObserver - Routes a task-created SG node's field events to a dispatcher
306
        '
307
        ' SG nodes (e.g. ChannelStore) deliver results via observed fields (roSGNodeEvent).
308
        ' The task loop reserves SG-node field events for the cross-thread state protocol
309
        ' (fieldId = dispatcherId), so an unregistered field would be silently dropped.
310
        ' This registry claims a fieldId and routes its events to the owning dispatcher's
311
        ' onSourceEvent instead.
312
        '
313
        ' NOTE: the fieldId must not collide with a dispatcher id (the registry takes
314
        ' precedence over the cross-thread state routing for that fieldId).
315
        '
316
        ' @param {object} node - SG node created on this task thread (e.g. roSGNode "ChannelStore")
317
        ' @param {string} fieldId - The node field to observe (e.g. "requestStatus")
318
        ' @param {string} dispatcherId - Dispatcher whose onSourceEvent receives the events
319
        '
320
        public sub registerNodeObserver(node as object, fieldId as string, dispatcherId as string)
321
            #if debug
4✔
322
                ' A fieldId equal to a dispatcher id would shadow that dispatcher's
323
                ' cross-thread state routing (the registry is checked first). Surface it.
324
                if m.dispatcherProvider.get(fieldId) <> invalid
2✔
NEW
325
                    print "[ROTOR][NODE_OBSERVER][WARNING] fieldId '" ; fieldId ; "' collides with a dispatcher id — its cross-thread state routing will be shadowed."
×
326
                end if
327
            #end if
328

329
            ' Re-registering the same fieldId onto a different node: drop the stale
330
            ' observer on the previous node so it does not keep firing after rebind.
331
            existing = m.nodeObserverRegistry[fieldId]
1✔
332
            if existing <> invalid and existing.node <> invalid and not existing.node.isSameNode(node)
3✔
333
                existing.node.unobserveFieldScoped(fieldId)
1✔
334
            end if
335

336
            m.nodeObserverRegistry[fieldId] = {
1✔
337
                dispatcherId: dispatcherId,
338
                node: node
339
            }
340
            node.observeFieldScopedEx(fieldId, m.port)
1✔
341
        end sub
342

343
        ' ---------------------------------------------------------------------
344
        ' unregisterNodeObserver - Removes an SG-node field observer registration
345
        '
346
        ' @param {object} node - The observed SG node
347
        ' @param {string} fieldId - The observed field
348
        '
349
        public sub unregisterNodeObserver(node as object, fieldId as string)
350
            node.unobserveFieldScoped(fieldId)
1✔
351
            if m.nodeObserverRegistry.DoesExist(fieldId)
3✔
352
                m.nodeObserverRegistry.Delete(fieldId)
1✔
353
            end if
354
        end sub
355

356
        ' ---------------------------------------------------------------------
357
        ' sync - Starts the message loop for cross-thread communication
358
        '
359
        ' IMPORTANT: This method creates an infinite loop that handles:
360
        '   - Intent dispatching from render thread
361
        '   - External dispatcher registration
362
        '   - State change notifications
363
        '   - Async reducer callbacks
364
        '
365
        ' This method MUST be the last call in your task function, as it
366
        ' blocks execution until the framework is destroyed.
367
        '
368
        sub sync(waitMs = 0 as integer, onTick = invalid as dynamic)
369
            m.notifySyncStatus(Rotor.Const.ThreadSyncType.TASK_SYNCING)
1✔
370

371
            keepAlive = true
1✔
372

373
            ' Initialize tick timer if waitMs > 0
374
            lastTickTime = invalid
1✔
375
            if waitMs > 0
3✔
376
                lastTickTime = CreateObject("roTimespan")
1✔
377
                lastTickTime.Mark()
1✔
378
            end if
379

380
            while true and keepAlive = true
1✔
381
                msg = wait(waitMs, m.port)
1✔
382

383
                if msg = invalid
2✔
384
                    ' Timeout - check if tick interval elapsed
385
                    if waitMs > 0 and onTick <> invalid and lastTickTime <> invalid
3✔
386
                        elapsedMs = lastTickTime.TotalMilliseconds()
1✔
387
                        if elapsedMs >= waitMs
3✔
388
                            ' Tick interval elapsed - call callback
389
                            Rotor.Utils.callbackScoped(onTick, GetGlobalAA())
1✔
390
                            ' Reset tick timer
391
                            lastTickTime.Mark()
1✔
392
                        end if
393
                    end if
394
                else if msg <> invalid
3✔
395
                    msgType = type(msg)
1✔
396
                    if msgType = "roSGNodeEvent"
3✔
397
                        fieldId = msg.getField()
1✔
398

399
                        if fieldId = "rotorSync"
3✔
400

401
                            sync = msg.getData() ' @type:AA
1✔
402

403
                            if sync.type = Rotor.Const.ThreadSyncType.DISPATCH
2✔
404

405

406
                                dispatcherId = sync.payload.dispatcherId
1✔
407
                                intent = sync.payload.intent
1✔
408
                                dispatcherInstance = m.dispatcherProvider.stack.LookupCI(dispatcherId)
1✔
409

410
                                ' taskIntent = Rotor.Utils.deepCopy(intent)
411
                                dispatcherInstance.dispatch(intent)
1✔
412

413
                            else if sync.type = Rotor.Const.ThreadSyncType.REGISTER_CROSS_THREAD_DISPATCHER
2✔
414

415
                                for each item in sync.crossThreadDispatcherList
1✔
416
                                    m.dispatcherProvider.registerCrossThreadDispatchers(item.dispatcherId, item.stateNode)
1✔
417
                                end for
418

419
                                m.notifySyncStatus(Rotor.Const.ThreadSyncType.TASK_SYNCED)
1✔
420

421
                            else if sync.type = Rotor.Const.ThreadSyncType.DESTROY
3✔
422

423
                                keepAlive = false
1✔
424

425
                            end if
426
                        else if m.nodeObserverRegistry.DoesExist(fieldId)
3✔
427
                            ' Registered SG-node field observer (see registerNodeObserver):
428
                            ' route the raw event to the owning dispatcher, same contract as
429
                            ' source objects (onSourceEvent receives the event).
430
                            entry = m.nodeObserverRegistry[fieldId]
1✔
431
                            dispatcherInstance = m.dispatcherProvider.get(entry.dispatcherId)
1✔
432
                            if dispatcherInstance <> invalid
3✔
433
                                dispatcherInstance.onSourceEvent(msg)
1✔
434
                            end if
435
                        else
436
                            ' Cross-thread state change: notify task-side listeners
×
437
                            data = msg.getData()
×
438
                            dispatcherId = fieldId
×
439
                            dispatcherInstance = m.dispatcherProvider.get(dispatcherId)
×
440
                            if dispatcherInstance <> invalid
×
441
                                dispatcherInstance.notifyListeners(data)
×
442
                            end if
443

444
                        end if
445
                    else
446
                        ' Generic source object routing
3✔
447
                        routed = false
1✔
448

449
                        ' Try identity-based routing
450
                        if m.sourceObjectRegistry.count() > 0
451
                            try
452
                                sourceIdentity = msg.GetSourceIdentity().ToStr()
1✔
453
                                if m.sourceObjectRegistry.DoesExist(sourceIdentity)
2✔
454
                                    entry = m.sourceObjectRegistry[sourceIdentity]
1✔
455

456
                                    ' Apply event filter if provided
457
                                    allowed = true
1✔
458
                                    if entry.eventFilter <> invalid
2✔
459
                                        m._eventFilterFn = entry.eventFilter
×
460
                                        allowed = m._eventFilterFn(msg)
×
461
                                    end if
462

463
                                    if allowed
3✔
464
                                        dispatcherInstance = m.dispatcherProvider.get(entry.dispatcherId)
1✔
465
                                        if dispatcherInstance <> invalid
3✔
466
                                            dispatcherInstance.onSourceEvent(msg)
1✔
467
                                        end if
468
                                    end if
469
                                    routed = true
1✔
470
                                end if
471
                            catch e
472
                                ' Event doesn't support GetSourceIdentity - fall through to broadcast
473
                            end try
474
                        end if
475

476
                        ' Broadcast to shared source object subscribers
477
                        if not routed
3✔
478
                            for each typeName in m.sharedSourceObjects
1✔
479
                                shared = m.sharedSourceObjects[typeName]
1✔
480
                                for each subscriber in shared.subscribers
1✔
481
                                    allowed = true
1✔
482
                                    if subscriber.eventFilter <> invalid
3✔
483
                                        m._eventFilterFn = subscriber.eventFilter
1✔
484
                                        allowed = m._eventFilterFn(msg)
1✔
485
                                    end if
486

487
                                    if allowed
3✔
488
                                        dispatcherInstance = m.dispatcherProvider.get(subscriber.dispatcherId)
1✔
489
                                        if dispatcherInstance <> invalid
3✔
490
                                            dispatcherInstance.onSourceEvent(msg)
1✔
491
                                        end if
492
                                    end if
493
                                end for
494
                            end for
495
                        end if
496
                    end if
497
                end if
498
            end while
499
            m.destroy()
1✔
500
        end sub
501

502
        ' =====================================================================
503
        ' INTERNAL METHODS
504
        ' =====================================================================
505

506
        ' ---------------------------------------------------------------------
507
        ' notifySyncStatus - Notifies render thread of sync status
508
        '
509
        ' Sends sync status message to render thread via rotorSync field.
510
        '
511
        ' @param {string} status - Sync status type (TASK_SYNCING or TASK_SYNCED)
512
        '
513
        sub notifySyncStatus(status as string)
514

515
            payload = {
1✔
516
                type: Rotor.Const.RouterMsgType.SYNC,
517
                syncType: status,
518
                taskNode: m.taskNode
519
            }
520

521
            if status = Rotor.Const.ThreadSyncType.TASK_SYNCING
2✔
522
                dispatcherIds = m.dispatcherProvider.stack.Keys()
1✔
523
                initialStates = {}
1✔
524
                for each id in dispatcherIds
1✔
525
                    initialStates[id] = m.dispatcherProvider.get(id).getState()
1✔
526
                end for
527
                payload.append({
1✔
528
                    dispatcherIds: dispatcherIds,
529
                    initialStates: initialStates,
530
                    tasks: m.config.tasks
531
                })
532
            end if
533

534
            m.renderQueue.CopyMessage(Rotor.Const.ROTOR_QUEUE_ID, payload)
1✔
535

536
        end sub
537

538
        ' ---------------------------------------------------------------------
539
        ' addObserver - Adds field observer to task thread message port
540
        '
541
        ' @param {string} fieldId - Field name to observe
542
        ' @param {object} node - SceneGraph node to observe
543
        '
544
        sub addObserver(fieldId as string, node)
545
            node.observeFieldScopedEx(fieldId, m.port)
×
546
        end sub
547

548
        ' ---------------------------------------------------------------------
549
        ' removeObserver - Removes field observer from node
550
        '
551
        ' @param {string} fieldId - Field name to stop observing
552
        ' @param {object} node - SceneGraph node to unobserve
553
        '
554
        sub removeObserver(fieldId as string, node)
555
            node.unobserveFieldScoped(fieldId)
×
556
        end sub
557

558
        ' =====================================================================
559
        ' CLEANUP
560
        ' =====================================================================
561

562
        ' ---------------------------------------------------------------------
563
        ' destroy - Cleans up task thread resources
564
        '
565
        ' Destroys dispatcher provider and clears global framework helper.
566
        '
567
        public sub destroy()
568
            m.sourceObjectRegistry.clear()
1✔
569
            m.sourceObjectIdIndex.clear()
1✔
570
            m.sharedSourceObjects.clear()
1✔
571
            for each fieldId in m.nodeObserverRegistry
1✔
572
                entry = m.nodeObserverRegistry[fieldId]
1✔
573
                if entry.node <> invalid
3✔
574
                    entry.node.unobserveFieldScoped(fieldId)
1✔
575
                end if
576
            end for
577
            m.nodeObserverRegistry.clear()
1✔
578
            m.dispatcherProvider.destroy()
1✔
579

580
            globalScope = GetGlobalAA()
1✔
581
            globalScope.rotor_framework_helper = {
1✔
582
                frameworkInstance: invalid
583
            }
584

585
            m.renderQueue = invalid
1✔
586
            m.taskNode.rootNode = invalid
1✔
587
            m.taskNode = invalid
1✔
588
        end sub
589

590
    end class
591

592
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