rotor-framework 0.7.7 β†’ 0.8.0

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/README.md CHANGED
@@ -96,7 +96,7 @@ You can find [🌱](./docs/ai/readme.opt.yaml) symbols in all documentation page
96
96
 
97
97
  ## πŸ“š Learn More
98
98
 
99
- ![Version](https://img.shields.io/badge/version-v0.7.7-blue?label=Documents%20TAG)
99
+ ![Version](https://img.shields.io/badge/version-v0.8.0-blue?label=Documents%20TAG)
100
100
 
101
101
  ### Framework Core
102
102
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "rotor-framework",
3
- "version": "0.7.7",
3
+ "version": "0.8.0",
4
4
  "description": "Roku toolkit library providing a ViewBuilder, full UI lifecycle with focus handling and many core features, plus MVI-based state management.",
5
5
  "author": "BalΓ‘zs MolnΓ‘r",
6
6
  "license": "Apache-2.0",
@@ -52,8 +52,9 @@
52
52
  },
53
53
  "homepage": "https://github.com/mobalazs/rotor-framework#readme",
54
54
  "devDependencies": {
55
- "@rokucommunity/bslint": "^1.0.0-alpha.48",
56
- "brighterscript": "^1.0.0-alpha.48",
57
- "rooibos-roku": "file:./vendor/rooibos-roku-6.0.0-alpha.48-fixed354.tgz"
55
+ "@rokucommunity/bslint": "1.0.0-alpha.50",
56
+ "brighterscript": "1.0.0-alpha.50",
57
+ "roku-deploy": "^3.16.1",
58
+ "rooibos-roku": "6.0.0-alpha.50"
58
59
  }
59
60
  }
@@ -4,7 +4,7 @@
4
4
  ' β–β–›β–€β–šβ––β–β–Œ β–β–Œ β–ˆ β–β–Œ β–β–Œβ–β–›β–€β–šβ–– β–β–›β–€β–€β–˜β–β–›β–€β–šβ––β–β–›β–€β–œβ–Œβ–β–Œ β–β–Œβ–β–›β–€β–€β–˜β–β–Œ β–β–Œβ–β–Œ β–β–Œβ–β–›β–€β–šβ––β–β–›β–šβ––
5
5
  ' β–β–Œ β–β–Œβ–β–šβ–„β–žβ–˜ β–ˆ β–β–šβ–„β–žβ–˜β–β–Œ β–β–Œ β–β–Œ β–β–Œ β–β–Œβ–β–Œ β–β–Œβ–β–Œ β–β–Œβ–β–™β–„β–„β––β–β–™β–ˆβ–Ÿβ–Œβ–β–šβ–„β–žβ–˜β–β–Œ β–β–Œβ–β–Œ β–β–Œ
6
6
  ' Rotor Frameworkβ„’
7
- ' Version 0.7.7
7
+ ' Version 0.8.0
8
8
  ' Β© 2025-2026 BalΓ‘zs MolnΓ‘r β€” Apache License 2.0
9
9
  ' =========================================================================
10
10
 
@@ -86,7 +86,7 @@ namespace Rotor
86
86
  class Framework
87
87
 
88
88
  name = "Rotor Framework"
89
- version = "0.7.7"
89
+ version = "0.8.0"
90
90
 
91
91
  config = {
92
92
  tasks: invalid, ' @array (optional)
@@ -333,6 +333,20 @@ namespace Rotor
333
333
  return m.builder.nodePool.presetNodePool(config)
334
334
  end function
335
335
 
336
+ ' ---------------------------------------------------------------------
337
+ ' registerSourceObject - Not available on render thread
338
+ '
339
+ public sub registerSourceObject(objectId as string, dispatcherId as string, sourceObject as object, eventFilter = invalid as dynamic)
340
+ throw "registerSourceObject() is only available on the task thread. Source objects require a task thread message port for event routing."
341
+ end sub
342
+
343
+ ' ---------------------------------------------------------------------
344
+ ' unregisterSourceObject - Not available on render thread
345
+ '
346
+ public sub unregisterSourceObject(objectId as string)
347
+ throw "unregisterSourceObject() is only available on the task thread."
348
+ end sub
349
+
336
350
  ' =====================================================================
337
351
  ' INTERNAL METHODS - INITIALIZATION AND TASK SYNCING
338
352
  ' =====================================================================
@@ -4,7 +4,7 @@
4
4
  ' β–β–›β–€β–šβ––β–β–Œ β–β–Œ β–ˆ β–β–Œ β–β–Œβ–β–›β–€β–šβ–– β–β–›β–€β–€β–˜β–β–›β–€β–šβ––β–β–›β–€β–œβ–Œβ–β–Œ β–β–Œβ–β–›β–€β–€β–˜β–β–Œ β–β–Œβ–β–Œ β–β–Œβ–β–›β–€β–šβ––β–β–›β–šβ––
5
5
  ' β–β–Œ β–β–Œβ–β–šβ–„β–žβ–˜ β–ˆ β–β–šβ–„β–žβ–˜β–β–Œ β–β–Œ β–β–Œ β–β–Œ β–β–Œβ–β–Œ β–β–Œβ–β–Œ β–β–Œβ–β–™β–„β–„β––β–β–™β–ˆβ–Ÿβ–Œβ–β–šβ–„β–žβ–˜β–β–Œ β–β–Œβ–β–Œ β–β–Œ
6
6
  ' Rotor Frameworkβ„’
7
- ' Version 0.7.7
7
+ ' Version 0.8.0
8
8
  ' Β© 2025-2026 BalΓ‘zs MolnΓ‘r β€” Apache License 2.0
9
9
  ' =========================================================================
10
10
 
@@ -70,7 +70,7 @@ namespace Rotor
70
70
  class FrameworkTask
71
71
 
72
72
  name = "Rotor Framework"
73
- version = "0.7.7"
73
+ version = "0.8.0"
74
74
 
75
75
  config = {
76
76
  tasks: invalid, ' optional
@@ -86,8 +86,10 @@ namespace Rotor
86
86
  taskNode as object
87
87
  dispatcherProvider as object
88
88
  port as object
89
- asyncTransferRegistry = {} ' transferId -> { dispatcherId, context }
90
- deviceInfoRegistry = {} ' deviceInfoId -> { dispatcherId, context, deviceInfo }
89
+ sourceObjectRegistry = {} ' identity -> { dispatcherId, objectId, eventFilter }
90
+ sourceObjectIdIndex = {} ' objectId -> identity (reverse index for unregistration)
91
+ sourceObjectTypeRegistry = [] ' [{ dispatcherId, objectId, eventFilter }] - broadcast routing
92
+ private _eventFilterFn = invalid as dynamic
91
93
  onTick as function
92
94
 
93
95
  ' ---------------------------------------------------------------------
@@ -132,42 +134,59 @@ namespace Rotor
132
134
  end function
133
135
 
134
136
  ' ---------------------------------------------------------------------
135
- ' registerAsyncTransfer - Registers an async transfer with dispatcher context
137
+ ' registerSourceObject - Registers a source object for event routing
136
138
  '
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.
139
+ ' Generic registry for any Roku source object. Auto-detects routing mode:
140
+ ' - Identity-based: If sourceObject implements ifSourceIdentity, routes via
141
+ ' GetSourceIdentity() on the event (roUrlTransfer, roChannelStore).
142
+ ' - Broadcast: If sourceObject does NOT implement ifSourceIdentity, broadcasts
143
+ ' to all registered dispatchers (roDeviceInfo, roInput, roAppManager).
140
144
  '
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
145
+ ' @param {string} objectId - Unique identifier for this registration
146
+ ' @param {string} dispatcherId - Dispatcher ID that will handle events
147
+ ' @param {object} sourceObject - The source object (SetMessagePort will be called)
148
+ ' @param {function} eventFilter - Optional filter function. Receives msg, returns boolean.
144
149
  '
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
+ public sub registerSourceObject(objectId as string, dispatcherId as string, sourceObject as object, eventFilter = invalid as dynamic)
151
+ sourceObject.SetMessagePort(m.port)
152
+ if FindMemberFunction(sourceObject, "GetIdentity") <> invalid
153
+ ' Identity-based routing
154
+ identity = sourceObject.GetIdentity().ToStr()
155
+ m.sourceObjectRegistry[identity] = {
156
+ dispatcherId: dispatcherId,
157
+ objectId: objectId,
158
+ eventFilter: eventFilter
159
+ }
160
+ m.sourceObjectIdIndex[objectId] = identity
161
+ else
162
+ ' Broadcast routing
163
+ m.sourceObjectTypeRegistry.push({
164
+ dispatcherId: dispatcherId,
165
+ objectId: objectId,
166
+ eventFilter: eventFilter
167
+ })
168
+ end if
150
169
  end sub
151
170
 
152
171
  ' ---------------------------------------------------------------------
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.
172
+ ' unregisterSourceObject - Unregisters a port-based object by its objectId
158
173
  '
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
174
+ ' @param {string} objectId - The unique identifier used during registration
163
175
  '
164
- public sub registerDeviceInfo(deviceInfoId as string, dispatcherId as string, deviceInfo as object, context = invalid as dynamic)
165
- deviceInfo.SetMessagePort(m.port)
166
- m.deviceInfoRegistry[deviceInfoId] = {
167
- dispatcherId: dispatcherId,
168
- deviceInfo: deviceInfo,
169
- context: context
170
- }
176
+ public sub unregisterSourceObject(objectId as string)
177
+ ' Try identity-based registry
178
+ if m.sourceObjectIdIndex.DoesExist(objectId)
179
+ identity = m.sourceObjectIdIndex[objectId]
180
+ m.sourceObjectRegistry.Delete(identity)
181
+ m.sourceObjectIdIndex.Delete(objectId)
182
+ return
183
+ end if
184
+ ' Try broadcast registry
185
+ for i = m.sourceObjectTypeRegistry.count() - 1 to 0 step -1
186
+ if m.sourceObjectTypeRegistry[i].objectId = objectId
187
+ m.sourceObjectTypeRegistry.Delete(i)
188
+ end if
189
+ end for
171
190
  end sub
172
191
 
173
192
  ' ---------------------------------------------------------------------
@@ -241,53 +260,63 @@ namespace Rotor
241
260
 
242
261
  end if
243
262
  else
244
-
263
+ ' Cross-thread state change: notify task-side listeners
245
264
  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)
265
+ dispatcherId = fieldId
266
+ dispatcherInstance = m.dispatcherProvider.get(dispatcherId)
267
+ if dispatcherInstance <> invalid
254
268
  dispatcherInstance.notifyListeners(data)
255
269
  end if
256
270
 
257
271
  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
272
+ else
273
+ ' Generic source object routing
274
+ routed = false
275
+
276
+ ' Try identity-based routing
277
+ if m.sourceObjectRegistry.count() > 0
278
+ try
279
+ sourceIdentity = msg.GetSourceIdentity().ToStr()
280
+ if m.sourceObjectRegistry.DoesExist(sourceIdentity)
281
+ entry = m.sourceObjectRegistry[sourceIdentity]
282
+
283
+ ' Apply event filter if provided
284
+ allowed = true
285
+ if entry.eventFilter <> invalid
286
+ m._eventFilterFn = entry.eventFilter
287
+ allowed = m._eventFilterFn(msg)
288
+ end if
274
289
 
290
+ if allowed
291
+ dispatcherInstance = m.dispatcherProvider.get(entry.dispatcherId)
292
+ if dispatcherInstance <> invalid
293
+ dispatcherInstance.onSourceEvent(msg)
294
+ end if
295
+ end if
296
+ routed = true
297
+ end if
298
+ catch e
299
+ ' Event doesn't support GetSourceIdentity - fall through to broadcast
300
+ end try
275
301
  end if
276
- else if msgType = "roDeviceInfoEvent"
277
- ' Handle device info events (ignore appFocused, etc.)
278
- if m.deviceInfoRegistry.count() > 0
279
- eventInfo = msg.GetInfo()
280
- ' Only process linkStatus events, ignore appFocused and other events
281
- if eventInfo?.linkStatus <> invalid
282
- ' Route to all registered device info dispatchers
283
- for each deviceInfoId in m.deviceInfoRegistry
284
- registryEntry = m.deviceInfoRegistry[deviceInfoId]
285
- dispatcherInstance = m.dispatcherProvider.get(registryEntry.dispatcherId)
302
+
303
+ ' Broadcast to all non-identity registered dispatchers
304
+ if not routed
305
+ for each entry in m.sourceObjectTypeRegistry
306
+ ' Apply event filter if provided
307
+ allowed = true
308
+ if entry.eventFilter <> invalid
309
+ m._eventFilterFn = entry.eventFilter
310
+ allowed = m._eventFilterFn(msg)
311
+ end if
312
+
313
+ if allowed
314
+ dispatcherInstance = m.dispatcherProvider.get(entry.dispatcherId)
286
315
  if dispatcherInstance <> invalid
287
- dispatcherInstance.asyncReducerCallback(eventInfo, registryEntry?.context as dynamic)
316
+ dispatcherInstance.onSourceEvent(msg)
288
317
  end if
289
- end for
290
- end if
318
+ end if
319
+ end for
291
320
  end if
292
321
  end if
293
322
  end if
@@ -354,8 +383,9 @@ namespace Rotor
354
383
  ' Destroys dispatcher provider and clears global framework helper.
355
384
  '
356
385
  public sub destroy()
357
- m.asyncTransferRegistry.clear()
358
- m.deviceInfoRegistry.clear()
386
+ m.sourceObjectRegistry.clear()
387
+ m.sourceObjectIdIndex.clear()
388
+ m.sourceObjectTypeRegistry.clear()
359
389
  m.dispatcherProvider.destroy()
360
390
 
361
391
  globalScope = GetGlobalAA()
@@ -32,12 +32,12 @@ namespace Rotor
32
32
  dispatch as function ' Dispatch intent to owner dispatcher
33
33
  dispatchTo as function ' Dispatch intent to dispatcher by ID
34
34
  getStateFrom as function ' Get state from dispatcher by ID
35
- ownerDispatcher as object ' Owning dispatcher instance
36
- ownerDispatcherId as string ' Owning dispatcher ID
35
+ dispatcher as object ' Owning dispatcher instance
36
+ dispatcherId as string ' Owning dispatcher ID
37
37
 
38
38
  ' Internal
39
39
  middlewareFnScoped as dynamic ' Currently executing middleware
40
-
40
+
41
41
  ' =============================================================
42
42
  ' CONSTRUCTOR
43
43
  ' =============================================================
@@ -56,12 +56,12 @@ namespace Rotor
56
56
 
57
57
  ' Inject dispatch method for dispatching intents
58
58
  m.dispatch = sub(intent as object)
59
- m.ownerDispatcher.dispatch(intent)
59
+ m.dispatcher.dispatch(intent)
60
60
  end sub
61
61
 
62
62
  ' Inject getState method for accessing current state
63
63
  m.getState = function() as Rotor.Model
64
- return m.ownerDispatcher.getState()
64
+ return m.dispatcher.getState()
65
65
  end function
66
66
 
67
67
  ' Inject dispatchTo method for dispatching intents to other dispatchers
@@ -105,6 +105,19 @@ namespace Rotor
105
105
  return state
106
106
  end function
107
107
 
108
+ ' ---------------------------------------------------------------------
109
+ ' onCreateDispatcher - Lifecycle hook called after dispatcher is fully registered
110
+ '
111
+ ' Called by the framework after the dispatcher is created and registered
112
+ ' with the DispatcherProvider. At this point, dispatcherId and all
113
+ ' framework integrations are available.
114
+ '
115
+ ' Override this method to perform initialization that requires
116
+ ' the dispatcher to be fully set up (e.g., source object registration).
117
+ '
118
+ sub onCreateDispatcher()
119
+ end sub
120
+
108
121
  ' =============================================================
109
122
  ' MIDDLEWARE
110
123
  ' =============================================================
@@ -174,34 +187,37 @@ namespace Rotor
174
187
  end function
175
188
 
176
189
  ' =============================================================
177
- ' ASYNC TRANSFER SUPPORT
190
+ ' SOURCE OBJECT SUPPORT
178
191
  ' =============================================================
179
192
 
180
193
  ' ---------------------------------------------------------------------
181
- ' registerAsyncTransfer - Registers a roUrlTransfer with the framework
182
- '
183
- ' Helper method to simplify async HTTP request tracking. Call this after
184
- ' creating a roUrlTransfer but BEFORE calling AsyncGetToString() or other
185
- ' async methods.
194
+ ' registerSourceObject - Registers a source object with the framework
186
195
  '
187
- ' @param {object} transfer - The roUrlTransfer object
188
- ' @param {object} context - Optional data to receive in asyncReducerCallback
196
+ ' Helper method for registering any Roku source object (roUrlTransfer,
197
+ ' roDeviceInfo, roChannelStore, etc.). The framework will call
198
+ ' SetMessagePort and route events to this reducer's onSourceEvent.
199
+ ' Routing mode is auto-detected: identity-based if sourceObject has GetIdentity(),
200
+ ' broadcast otherwise.
189
201
  '
190
- ' Example:
191
- ' transfer = CreateObject("roUrlTransfer")
192
- ' transfer.SetUrl("https://api.example.com/data")
193
- ' transfer.SetMessagePort(m.port)
194
- ' m.registerAsyncTransfer(transfer, { userId: 123 })
195
- ' transfer.AsyncGetToString()
202
+ ' @param {object} sourceObject - The source object
203
+ ' @param {function} eventFilter - Optional filter function. Receives msg, returns boolean.
196
204
  '
197
- sub registerAsyncTransfer(transfer as object, context = invalid as dynamic)
205
+ sub registerSourceObject(sourceObject as object, eventFilter = invalid as dynamic)
198
206
  frameworkInstance = GetGlobalAA().rotor_framework_helper.frameworkInstance
199
- transferId = transfer.GetIdentity().ToStr()
200
207
 
201
- ' Only call if the method exists (task thread only)
202
- if Rotor.Utils.isFunction(frameworkInstance?.registerAsyncTransfer)
203
- frameworkInstance.registerAsyncTransfer(transferId, m.ownerDispatcherId, context)
204
- end if
208
+ objectId = m.getSourceObjectId(sourceObject)
209
+ frameworkInstance.registerSourceObject(objectId, m.dispatcherId, sourceObject, eventFilter)
210
+ end sub
211
+
212
+ ' ---------------------------------------------------------------------
213
+ ' unregisterSourceObject - Unregisters a source object from the framework
214
+ '
215
+ ' @param {object} sourceObject - The source object to unregister
216
+ '
217
+ sub unregisterSourceObject(sourceObject as object)
218
+ frameworkInstance = GetGlobalAA().rotor_framework_helper.frameworkInstance
219
+ objectId = m.getSourceObjectId(sourceObject)
220
+ frameworkInstance.unregisterSourceObject(objectId)
205
221
  end sub
206
222
 
207
223
  ' =============================================================
@@ -209,11 +225,28 @@ namespace Rotor
209
225
  ' =============================================================
210
226
 
211
227
  ' ---------------------------------------------------------------------
212
- ' asyncReducerCallback - Callback for async middleware operations
228
+ ' getSourceObjectId - Generates unique objectId for a source object
229
+ '
230
+ ' Identity-based objects (roUrlTransfer, etc.): dispatcherId + GetIdentity()
231
+ ' Broadcast objects (roDeviceInfo, etc.): dispatcherId + type name
232
+ '
233
+ ' @param {object} sourceObject - The source object
234
+ ' @returns {string} Unique objectId
235
+ '
236
+ private function getSourceObjectId(sourceObject as object) as string
237
+ if FindMemberFunction(sourceObject, "GetIdentity") <> invalid
238
+ return `${m.dispatcherId}_${sourceObject.GetIdentity()}`
239
+ else
240
+ return `${m.dispatcherId}_${type(sourceObject)}`
241
+ end if
242
+ end function
243
+
244
+ ' ---------------------------------------------------------------------
245
+ ' onSourceEvent - Callback for source object events
213
246
  '
214
- ' @param {object} msg - Message from async operation
247
+ ' @param {object} msg - Raw message from source object (roUrlEvent, roDeviceInfoEvent, etc.)
215
248
  '
216
- sub asyncReducerCallback(msg as roUrlEvent, context as dynamic)
249
+ sub onSourceEvent(msg as object)
217
250
  ' Override in subclass to handle async responses
218
251
  end sub
219
252
 
@@ -225,7 +258,7 @@ namespace Rotor
225
258
  ' destroy - Cleans up reducer references
226
259
  '
227
260
  sub destroy()
228
- m.ownerDispatcher = invalid
261
+ m.dispatcher = invalid
229
262
  m.port = invalid
230
263
  end sub
231
264
 
@@ -62,8 +62,8 @@ namespace Rotor
62
62
 
63
63
  ' Link reducer to this dispatcher
64
64
  m.reducerInstance = reducerInstance
65
- m.reducerInstance.ownerDispatcher = m
66
- m.reducerInstance.ownerDispatcherId = dispatcherId
65
+ m.reducerInstance.dispatcher = m
66
+ m.reducerInstance.dispatcherId = dispatcherId
67
67
  m.modelInstance = modelInstance
68
68
 
69
69
  ' Create field on task node for render thread communication
@@ -75,6 +75,10 @@ namespace Rotor
75
75
  ' Register with dispatcher provider
76
76
  dispatcherProvider = globalScope.rotor_framework_helper.frameworkInstance.dispatcherProvider
77
77
  dispatcherProvider.registerDispatcher(m, dispatcherId)
78
+
79
+ ' Lifecycle hook - called after dispatcher is fully registered
80
+ m.reducerInstance.onCreateDispatcher()
81
+
78
82
  end sub
79
83
 
80
84
  ' =============================================================
@@ -155,12 +159,12 @@ namespace Rotor
155
159
  ' =============================================================
156
160
 
157
161
  ' ---------------------------------------------------------------------
158
- ' asyncReducerCallback - Routes async callback to reducer
162
+ ' onSourceEvent - Routes async callback to reducer
159
163
  '
160
164
  ' @param {object} msg - Message from async operation
161
165
  '
162
- sub asyncReducerCallback(msg as roUrlEvent, context as dynamic)
163
- m.reducerInstance.asyncReducerCallback(msg as roUrlEvent, context as dynamic)
166
+ sub onSourceEvent(msg as object)
167
+ m.reducerInstance.onSourceEvent(msg)
164
168
  end sub
165
169
 
166
170
  ' =============================================================
@@ -97,7 +97,7 @@ namespace Rotor.ViewBuilder
97
97
  end function
98
98
 
99
99
  ' render - Renders widget updates (self, descendants, or children) *'
100
- widget.render = sub(payloads as dynamic, params = {} as object)
100
+ widget.render = sub(payloads = invalid as dynamic, params = {} as object)
101
101
  if payloads = invalid and m.isViewModel = true
102
102
  ' Self update (only for viewModels)
103
103
  payloads = m.template()
@@ -264,15 +264,6 @@ namespace Rotor.ViewBuilder
264
264
  ' Apply threshold to ALL speech (both flush=true and flush=false)
265
265
  ' This prevents rapid interruptions from focus changes (e.g., "Home Home Home")
266
266
  ' If new request comes within 300ms, replace pending (filter rapid changes)
267
- #if debug
268
- if m.pendingSpeech <> invalid
269
- ' ? "[TTS_SERVICE][THRESHOLD] Replacing pending: '"; m.pendingSpeech.textToSpeak; "' with: '"; textToSpeak; "'"
270
- else
271
- flushLabel = ""
272
- if shouldFlush then flushLabel = " (will flush)"
273
- ' ? "[TTS_SERVICE][THRESHOLD] Pending: '"; textToSpeak; "' ("; m.debounceDelay; "ms)"; flushLabel
274
- end if
275
- #end if
276
267
  m.pendingSpeech = {
277
268
  textToSpeak: textToSpeak,
278
269
  shouldFlush: shouldFlush,
@@ -1154,8 +1154,6 @@ namespace Rotor
1154
1154
  if not isGroup and candidate.isEnabled = false then continue for
1155
1155
 
1156
1156
  candidateMetrics = candidate.refreshBounding()
1157
- candidateLeft = candidateMetrics.segments[Rotor.Const.Segment.LEFT]
1158
- candidateTop = candidateMetrics.segments[Rotor.Const.Segment.TOP]
1159
1157
  ' Pass appropriate reference segments based on direction
1160
1158
  if direction = "left" or direction = "right"
1161
1159
  result = validator(candidateMetrics.segments, refSegmentLeft, refSegmentRight)
@@ -1539,33 +1537,28 @@ namespace Rotor
1539
1537
  if defaultFocusId <> ""
1540
1538
  focusItemsHIDlist = m.getGroupMembersHIDs()
1541
1539
  if focusItemsHIDlist.Count() > 0
1542
-
1543
1540
  ' Try find valid HID in focusItems by node id
1544
1541
  focusItemHID = m.findHIDinFocusItemsByNodeId(defaultFocusId, focusItemsHIDlist)
1545
1542
  if focusItemHID <> ""
1546
- HID = focusItemHID
1543
+ return focusItemHID
1547
1544
  end if
1548
-
1549
- else
1550
-
1551
- return defaultFocusId
1552
-
1553
1545
  end if
1546
+ ' If not found as focusItem, return defaultFocusId string
1547
+ ' so capturingFocus_recursively can try to resolve it as a group
1548
+ return defaultFocusId
1554
1549
  end if
1555
1550
 
1556
1551
  return HID
1557
1552
  end function
1558
1553
 
1559
1554
  function findHIDinFocusItemsByNodeId(nodeId as string, focusItemsHIDlist as object) as string
1560
- HID = ""
1561
- for each HID in focusItemsHIDlist
1562
- focusItem = m.focusItemsRef.get(HID)
1555
+ for each itemHID in focusItemsHIDlist
1556
+ focusItem = m.focusItemsRef.get(itemHID)
1563
1557
  if focusItem <> invalid and focusItem.id = nodeId
1564
- HID = focusItem.HID
1565
- exit for
1558
+ return focusItem.HID
1566
1559
  end if
1567
1560
  end for
1568
- return HID
1561
+ return ""
1569
1562
  end function
1570
1563
 
1571
1564
  sub applyFocus(isFocused as boolean)
@@ -34,56 +34,36 @@ namespace Rotor
34
34
  sub new(pluginKey = "observer" as string)
35
35
  super()
36
36
  m.pluginKey = pluginKey
37
- end sub
38
-
39
- ' =============================================================
40
- ' LIFECYCLE HOOKS
41
- ' =============================================================
42
-
43
- hooks = {
44
- ' ---------------------------------------------------------------------
45
- ' beforeMount - Attaches observers when widget is mounted
46
- '
47
- ' Called during widget creation to register all observers defined in the widget config.
48
- '
49
- ' @param {object} scope - Plugin instance (m)
50
- ' @param {object} widget - Widget being mounted
51
- '
52
- beforeMount: sub(scope as object, widget as object)
53
- config = widget[scope.pluginKey]
54
- scope.attach(widget.node, config, widget)
55
- end sub,
56
37
 
57
- ' ---------------------------------------------------------------------
58
- ' beforeUpdate - Updates observers when widget config changes
38
+ ' WORKAROUND: Hooks must be assigned here in the constructor body
39
+ ' (after super()), NOT as a class-level field initializer.
59
40
  '
60
- ' Detaches old observers and attaches new ones based on updated configuration.
41
+ ' Rooibos code coverage instrumentation (tested up to v6.0.0-alpha.50)
42
+ ' reorders the compiled constructor so that class-level field initializers
43
+ ' are placed BEFORE the super() call. Since BasePlugin's constructor sets
44
+ ' m.hooks = invalid, this causes the parent to overwrite the hooks AA that
45
+ ' was already set by the field initializer.
61
46
  '
62
- ' @param {object} scope - Plugin instance (m)
63
- ' @param {object} widget - Widget being updated
64
- ' @param {object} newValue - New observer configuration
65
- ' @param {object} oldValue - Previous observer configuration
66
- '
67
- beforeUpdate: sub(scope as object, widget as object, newValue, oldValue = {})
68
- if oldValue <> invalid
47
+ ' Only affects classes with an explicit sub new() + super() AND
48
+ ' class-level field initializers. Other plugins (FieldsPlugin, FocusPlugin,
49
+ ' etc.) are not affected because they have no explicit constructor.
50
+ m.hooks = {
51
+ beforeMount: sub(scope as object, widget as object)
52
+ config = widget[scope.pluginKey]
53
+ scope.attach(widget.node, config, widget)
54
+ end sub,
55
+ beforeUpdate: sub(scope as object, widget as object, newValue, oldValue = {})
56
+ if oldValue <> invalid
57
+ scope.detach(widget.node)
58
+ end if
59
+ widget[scope.pluginKey] = newValue
60
+ scope.attach(widget.node, newValue, widget)
61
+ end sub,
62
+ beforeDestroy: sub(scope as object, widget as object)
69
63
  scope.detach(widget.node)
70
- end if
71
- widget[scope.pluginKey] = newValue
72
- scope.attach(widget.node, newValue, widget)
73
- end sub,
74
-
75
- ' ---------------------------------------------------------------------
76
- ' beforeDestroy - Detaches all observers before widget destruction
77
- '
78
- ' Ensures cleanup of all observers when widget is removed from scene graph.
79
- '
80
- ' @param {object} scope - Plugin instance (m)
81
- ' @param {object} widget - Widget being destroyed
82
- '
83
- beforeDestroy: sub(scope as object, widget as object)
84
- scope.detach(widget.node)
85
- end sub
86
- }
64
+ end sub
65
+ }
66
+ end sub
87
67
 
88
68
  ' =============================================================
89
69
  ' INITIALIZATION