diff --git a/.vscode/launch.json b/.vscode/launch.json
index c5e69ae..7c7488d 100644
--- a/.vscode/launch.json
+++ b/.vscode/launch.json
@@ -24,7 +24,7 @@
]
},
{
- "name": "Debug test-project",
+ "name": "Debug test app",
"type": "brightscript",
"request": "launch",
"rootDir": "${workspaceFolder}/out/dist",
diff --git a/.vscode/settings.json b/.vscode/settings.json
index 3a12e80..c2886cd 100644
--- a/.vscode/settings.json
+++ b/.vscode/settings.json
@@ -20,5 +20,5 @@
"typescript.format.insertSpaceAfterOpeningAndBeforeClosingNonemptyBrackets": false,
"files.trimTrailingWhitespace": true,
"typescript.tsdk": "node_modules\\typescript\\lib",
- "brightscript.bsdk": "embedded"
+ "brightscript.bsdk": "1.0.0-alpha.39"
}
diff --git a/.vscode/tasks.json b/.vscode/tasks.json
index cb8641a..6100116 100644
--- a/.vscode/tasks.json
+++ b/.vscode/tasks.json
@@ -13,7 +13,7 @@
{
"label": "build-test-app",
"type": "shell",
- "command": "cd test-project && npx bsc",
+ "command": "cd test-app && npx bsc",
"group": {
"kind": "test",
"isDefault": true
diff --git a/lib/components/Reftracker.bs b/lib/components/Reftracker.bs
index 59dafd0..752e4c9 100644
--- a/lib/components/Reftracker.bs
+++ b/lib/components/Reftracker.bs
@@ -32,11 +32,15 @@ function discover(_ = invalid)
end for
'process the nodes one-by-one
- return promises.onThen(processNextNode(), function(result)
- print "done processing nodes"
+ return promises.onThen(processNodes(), function(result)
+ printNodes()
end function)
end function
+function printNodes()
+ print FormatJson(m.keypathsByNodeReftrackerId)
+end function
+
'Register a reference to a node so we can process it later. This
function registerNodeRef(keypath as string, node as roSGNode)
reftrackerId = reftracker.internal.getReftrackerId(node)
@@ -61,34 +65,50 @@ function registerNodeRef(keypath as string, node as roSGNode)
m.keypathsByNodeReftrackerId[reftrackerId].push(keypath)
end function
-function processNextNode()
+function processNodes()
'if we have no more nodes, we are done!
if m.nodeQueue.count() = 0
+ reftracker.internal.writeLog("All nodes have been processed. Exiting node process loop")
return promises.resolve(invalid)
end if
- nodeQueueItem = m.nodeQueue.pop()
-
- print `reftracker: processing node ${nodeQueueItem.keypath}<${nodeQueueItem.node.subtype()}>`
-
- return promises.chain(promises.resolve(true), nodeQueueItem).then(function(result, queueItem as NodeQueueItem)
- 'if this node supports reftracker functionality, process the node's internal `m`
- if (nodeQueueItem.node as dynamic).reftrackerEnabled then
- return nodeQueueItem.node@.reftracker_internal_execute({ command: "discover", reftracker: m.top })
+ queueItem = m.nodeQueue.pop() as NodeQueueItem
+
+ reftracker.internal.writeLog("processing node", queueItem.keypath, queueItem.node.subtype())
+
+ 'if this node supports reftracker functionality, process the node's internal `m`
+ return promises.chain(promises.resolve(true), queueItem).then(function(result, queueItem as NodeQueueItem)
+ if (queueItem.node as dynamic).reftrackerEnabled then
+ reftracker.internal.writeLog("processing node's internal m properties", queueItem.keypath, queueItem.node.subtype())
+ return queueItem.node@.reftracker_internal_execute({
+ command: "discover",
+ reftracker: m.top,
+ keypath: queueItem.keypath
+ })
+ else
+ reftracker.internal.writeLog("node does not support reftracker intraspection", queueItem.keypath, queueItem.node.subtype())
end if
- end function).then(function(result, nodeQueueItem as NodeQueueItem)
+
'add all public fields to a list of stuff to work on
- for each field in nodeQueueItem.node.getFields()
- value = nodeQueueItem.node[field]
- reftracker.internal.registerWorkItem(m.top, `${nodeQueueItem.keypath}.${field}`, value)
+ end function).then(function(result, queueItem as NodeQueueItem)
+ reftracker.internal.writeLog("processing node fields", queueItem.keypath, queueItem.node.subtype())
+ for each fieldName in queueItem.node.getFields()
+ value = queueItem.node[fieldName]
+ reftracker.internal.registerWorkItem(queueItem.keypath, fieldName, value)
end for
'now process this data (it will run async and process in chunks until all are finished)
- return reftracker.internal.processWorkItems(m.top)
-
- end function).then(function(result, nodeQueueItem as NodeQueueItem)
+ return reftracker.internal.processWorkItems({
+ reftracker: m.top,
+ keypath: queueItem.keypath
+ })
- return processNextNode()
+ 'now process the next node
+ end function).then(function(result, queueItem as NodeQueueItem)
+ reftracker.internal.writeLog("Processing next node")
+ return processNodes()
+ end function).catch(function(error, _)
+ print FormatJson(error)
end function).toPromise()
end function
diff --git a/lib/components/Reftracker.xml b/lib/components/Reftracker.xml
index 6f4a387..fb02dde 100644
--- a/lib/components/Reftracker.xml
+++ b/lib/components/Reftracker.xml
@@ -4,5 +4,5 @@
-
+
diff --git a/lib/source/reftrackerLib.bs b/lib/source/reftrackerLib.bs
index 48e5d0c..968b3b4 100644
--- a/lib/source/reftrackerLib.bs
+++ b/lib/source/reftrackerLib.bs
@@ -1,11 +1,12 @@
import "pkg:/source/roku_modules/promises/promises.brs"
-'typecast m as mm
+' typecast m as mm
interface mm
- reftrackers as roSGNodeReftracker[]
- processedItems as roAssociativeArray
- __deviceInfo as roDeviceInfo
+ ___reftracker_reftrackers as roSGNodeReftracker[]
+ ___reftracker_processedItems as roAssociativeArray
+ ___reftracker_deviceInfo as roDeviceInfo
+ ___reftracker_workQueue as roArray
end interface
@@ -29,123 +30,186 @@ namespace reftracker.internal
'@param {ExecuteOptions} options
function execute(options)
if options.command = "discover"
- registerWorkItem(options.reftracker, options.keypath + ".m", m)
- return processWorkItems(options.reftracker)
+ registerWorkItem(options.keypath, "m", m)
+ return processWorkItems(options)
+ else
+ return promises.reject({
+ message: "Unknown command"
+ })
end if
end function
- function getWorkQueue(tracker as roSGNodeReftracker) as WorkQueueItem[]
- key = `__workQueue_${tracker.id}`
- if m[key] = invalid
- m[key] = []
+ function getWorkQueue() as WorkQueueItem[]
+ if m.___reftracker_workQueue = invalid
+ m.___reftracker_workQueue = []
end if
- return m[key]
+ return m.___reftracker_workQueue
end function
' register some item to be processed later
- function registerWorkItem(tracker as roSGNodeReftracker, keypath as string, item as dynamic)
+ function registerWorkItem(parentKeypath as string, key as string, item as dynamic) as void
+ if isReftrackerProperty(key)
+ writeLog("skipping reftracker property", `${parentKeypath}.${key}`, type(item))
+ return
+ end if
+
if item <> invalid
- workQueue = getWorkQueue(tracker)
+ workQueue = getWorkQueue()
workQueue.push({
- keypath: keypath
+ keypath: `${parentKeypath}.${key}`,
+ key: key,
item: item
})
end if
end function
- function processWorkItems(tracker as roSGNodeReftracker) as roSGNodepromises_Promise
+ function processWorkItems(options as reftracker.internal.ProcessOptions) as roSGNodepromises_Promise
startTime = createObject("roTimeSpan")
- workQueue = getWorkQueue(tracker)
+ workQueue = getWorkQueue()
'if we have work, and we've been running for less than 500ms, process another item
while workQueue[0] <> invalid and startTime.TotalMilliseconds() < 500
- item = workQueue.Shift()
+ workItem = workQueue.Shift()
+
+ item = workItem.item
+ itemKeypath = workItem.keypath
itemType = type(item)
- print `reftracker: processing item ${item.keypath} (${itemType}`
+ writeLog("processing item", itemKeypath, itemType)
'if this is a node, push register it with the reftracker to be processed later
if itemType = "roSGNode"
- tracker@.registerNodeRef(item)
- else if itemType = "roArray" and not isProcessed(item)
- 'if this is an array, register each item in the array
- for i = 0 to (item as ifArray).Count() - 1
- registerWorkItem(tracker, `${item.keypath}.${i}`, item[i])
- end for
- else if itemType = "roAssociativeArray" and not isProcessed(item)
- 'if this is an associative array, register each item in the array
- for each key in item
- registerWorkItem(tracker, `${item.keypath}.${key}`, item[key])
- end for
+ writeLog("item is a node. pushing to node queue", itemKeypath, itemType)
+ options.reftracker@.registerNodeRef(item)
+ else if itemType = "roArray"
+ if isProcessed(item)
+ writeLog("item is an array and already processed. Skipping", itemKeypath, itemType)
+ else
+ markProcessed(item)
+ writeLog("item is an array. registering all entries", itemKeypath, itemType)
+ 'if this is an array, register each item in the array
+ for i = 0 to (item as ifArray).Count() - 1
+ value = item[i]
+ 'if we encountered the reftracker property, we can stop processing the rest of this array (because it ends with [___reftracker_id, ""])
+ if isReftrackerProperty(value)
+ exit for
+ end if
+ registerWorkItem(itemKeypath, i.ToStr(), item[i])
+ end for
+ end if
+ else if itemType = "roAssociativeArray"
+ if isProcessed(item)
+ writeLog("item is an AA and already processed. skipping", itemKeypath, itemType)
+ else
+
+ markProcessed(item)
+ writeLog("item is an AA. registering all properties", itemKeypath, itemType)
+ 'if this is an associative array, register each item in the array
+ for each key in item
+ value = item[key]
+ registerWorkItem(itemKeypath, key, item[key])
+ end for
+ end if
else
'all other data types can be ignored, since they don't include references to other nodes
+ writeLog("skipping item because it does not support storing node references", itemKeypath, itemType)
end if
end while
'we have no more work, return a resolved promise
if workQueue[0] = invalid
+ writeLog("work queue is complete", options.keypath)
+ 'TODO clean up
return promises.resolve(true)
end if
'small delay, then process work again
- return promises.chain(reftracker.internal.delay(.01)).then(function(result, tracker)
+ return promises.chain(reftracker.internal.delay(.01), options).then(function(result, options)
'process work items again
- return processWorkItems(tracker)
- end function, tracker).toPromise()
+ return processWorkItems(options)
+ end function).toPromise()
end function
+ 'check if an item has already been processed
function isProcessed(item as dynamic)
+ return false
+ if m.___reftracker_processedItems = invalid
+ m.___reftracker_processedItems = {}
+ end if
+
id = getReftrackerId(item)
'if we've processed this item, it'll have its id in the processedItems array
- return m.processedItems[id] = invalid
+ return m.___reftracker_processedItems[id] = true
+ end function
+
+ 'mark an item as processed so we know to skip it in the future
+ function markProcessed(item as dynamic) as void
+ return
+ if m.___reftracker_processedItems = invalid
+ m.___reftracker_processedItems = {}
+ end if
+
+ id = getReftrackerId(item)
+ 'if we've processed this item, it'll have its id in the processedItems array
+ m.___reftracker_processedItems[id] = true
end function
'Ensure this node has a unique reftracker id so we can use it for lookups and equality checks
- function getReftrackerId(item as dynamic)
+ function getReftrackerId(item as dynamic) as string
reftrackerId = reftracker.internal.getRandomUUID()
- key = "reftracker_id"
+ key = "___reftracker_id"
itemType = type(item)
- if itemType = "roSGNode" and item[key] = invalid then
- item.addField(key, "string", false)
- item[key] = reftrackerId
- else if itemType = "roAssociativeArray" and item[key] = invalid
- item[key] = reftrackerId
- else if itemType = "roArray" and item[(item as roArray).Count() - 2] <> "reftracker_id"
- item.push("reftracker_id")
- item.push(reftrackerId)
- else
- return invalid
+ if itemType = "roSGNode"
+ if item[key] = invalid then
+ item.addField(key, "string", false)
+ item[key] = reftrackerId
+ end if
+ return item[key]
+ else if itemType = "roAssociativeArray"
+ if item[key] = invalid then
+ item[key] = reftrackerId
+ end if
+ return item[key]
+ else if itemType = "roArray"
+ nextToLastArrayValue = item[(item as roArray).Count() - 2]
+ if not isString(nextToLastArrayValue) or (isString(nextToLastArrayValue) and nextToLastArrayValue <> key)
+ item.push(key)
+ item.push(reftrackerId)
+ end if
+ return item[(item as roArray).Count() - 1]
end if
-
- return reftrackerId
+ 'return invalid, which will probably crash outside (that's desired to identify issues)
+ return invalid
end function
function registerReftracker(finder as roSGNodeRefTracker)
- if m.reftrackers = invalid
- m.reftrackers = []
+ if m.___reftracker_reftrackers = invalid
+ m.___reftracker_reftrackers = []
end if
- m.reftrackers.push(finder)
+ m.___reftracker_reftrackers.push(finder)
end function
function getDeviceObject() as object
- if m.__deviceInfo <> invalid then return m.__deviceInfo
- m.__deviceInfo = createObject("roDeviceInfo")
- return m.__deviceInfo
+ if m.___reftracker_deviceInfo <> invalid then return m.___reftracker_deviceInfo
+ m.___reftracker_deviceInfo = createObject("roDeviceInfo")
+ return m.___reftracker_deviceInfo
end function
' Get a promise that resolves after a given duration
- sub delay(duration = 0.0001 as float)
+ sub delay(duration = 0.0001 as float) as roSGNodepromises_Promise
timer = createObject("roSGNode", "Timer")
timer.duration = duration
timer.repeat = false
- timer.id = "__delay_" + getRandomUUID()
+ timer.id = "___reftracker_delay_" + getRandomUUID()
+
+ promise = promises.create()
m[timer.id] = {
timer: timer
- promise: promises.create()
+ promise: promise
}
timer.observeFieldScoped("fire", (sub(event as object)
@@ -153,11 +217,12 @@ namespace reftracker.internal
options = m[delayId]
promise = options.promise
promises.resolve(promise, invalid)
- m[delayId].unobserveFieldScoped("fire")
+ m[delayId].timer.unobserveFieldScoped("fire")
m.delete(delayId)
end sub).toStr().mid(10))
timer.control = "start"
+ return promise
end sub
function getRandomUUID()
@@ -174,8 +239,34 @@ namespace reftracker.internal
reftracker as roSGNodeReftracker
end interface
+ interface ProcessOptions
+ reftracker as roSGNodeReftracker
+ keypath as string
+ end interface
+
interface WorkQueueItem
keypath as string
item as dynamic
end interface
+
+ 'All reftracker properties are prefixed with ___reftracker_ , so this method can be used to filter them out
+ function isReftrackerProperty(key as dynamic)
+ return isString(key) and key.left(14) = "___reftracker_"
+ end function
+
+ function writeLog(message as string, keypath = "" as string, typeInfo = "" as string)
+ text = `[reftracker]`
+ if keypath <> ""
+ text += `[${keypath}]`
+ end if
+ text += ` ${message}`
+ if typeInfo <> ""
+ text += ` (${typeInfo})`
+ end if
+ print text
+ end function
+
+ function isString(value as dynamic) as boolean
+ return type(value) = "String" or type(value) = "roString"
+ end function
end namespace
diff --git a/package.json b/package.json
index 9e34500..dc443c8 100644
--- a/package.json
+++ b/package.json
@@ -9,7 +9,7 @@
"typings": "dist/index.d.ts",
"scripts": {
"build": "rimraf out && tsc",
- "build-test-project": "cd test-project && npx bsc",
+ "build-test-app": "cd test-app && npx bsc",
"lint": "eslint \"src/**\"",
"preversion": "npm run build && npm run lint && npm run test",
"test": "nyc mocha",
diff --git a/src/Plugin.ts b/src/Plugin.ts
index 5ff104c..91d0d05 100644
--- a/src/Plugin.ts
+++ b/src/Plugin.ts
@@ -23,7 +23,7 @@ export class Plugin implements CompilerPlugin {
program.options.files.push(...files);
for (const file of files) {
- program.setFile(file, fsExtra.readFileSync(s`${cwd}/lib/${file}`).toString());
+ program.setFile(file, fsExtra.readFileSync(file.src).toString());
}
}
diff --git a/test-project/bsconfig.json b/test-app/bsconfig.json
similarity index 100%
rename from test-project/bsconfig.json
rename to test-app/bsconfig.json
diff --git a/test-project/components/MainScene.bs b/test-app/components/MainScene.bs
similarity index 100%
rename from test-project/components/MainScene.bs
rename to test-app/components/MainScene.bs
diff --git a/test-project/components/MainScene.xml b/test-app/components/MainScene.xml
similarity index 100%
rename from test-project/components/MainScene.xml
rename to test-app/components/MainScene.xml
diff --git a/test-project/manifest b/test-app/manifest
similarity index 100%
rename from test-project/manifest
rename to test-app/manifest
diff --git a/test-project/source/main.bs b/test-app/source/main.bs
similarity index 100%
rename from test-project/source/main.bs
rename to test-app/source/main.bs