diff --git a/LibEventSourcing.xml b/LibEventSourcing.xml
index 112aa9f..23ad292 100644
--- a/LibEventSourcing.xml
+++ b/LibEventSourcing.xml
@@ -4,6 +4,7 @@
+
diff --git a/readme.MD b/readme.MD
index 1b3bd09..dfe899d 100644
--- a/readme.MD
+++ b/readme.MD
@@ -51,10 +51,10 @@ Then there is the issue of responding to requests on broadcast channels. We don'
with data...
- When announcing that you are able to send a hash include a timestamp.
-- If multiple announcements are sent simultaneously, only the sender with the lowest timestamp (and player name in case of same timestamp)
- will start sending data after at 5 seconds.
+- If multiple announcements are sent simultaneously, only the sender with the lowest timestamp (and player name in case of same timestamp)
+ will start sending data after at 5 seconds.
+
-
Denial of service
-----------------
The consuming addon is responsible for providing secure communication channels.
@@ -62,6 +62,15 @@ Specifically if we can only authorize entries but not sync messages someone can
block sync by rebroadcasting every advertisement with a lower timestamp and then never sending actual data.
+# V2
+
+Work has started on V2 for WOTLK.
+Some improvements are on the list:
+- simplify auth
+- allow zero or more handlers for log entries
+- allow zero or more handlers for reset
+- more efficient serialization
+
# Contributions
As much as possible is automated when it comes to chores in this library.
diff --git a/source/SortedList.lua b/source/SortedList.lua
index dcb8504..11a9e92 100644
--- a/source/SortedList.lua
+++ b/source/SortedList.lua
@@ -2,7 +2,7 @@
Sorted lists with an insert API
]]--
-local SortedList, _ = LibStub:NewLibrary("EventSourcing/SortedList", 1)
+local SortedList, _ = LibStub:NewLibrary("EventSourcing/SortedList", 2)
if not SortedList then
return end
@@ -40,6 +40,10 @@ end
function SortedList:state()
return self._state
end
+
+--[[
+ Inserts an element into the list, returns the position of the inserted element
+]]
function SortedList:insert(element)
if (self._unique) then
error("This list only supports uniqueInsert")
@@ -47,16 +51,17 @@ function SortedList:insert(element)
self._state = self._state + 1
-- since we expect elements to be mostly appended, we do a shortcut check.
if (#self._entries == 0 or self._compare(self._entries[#self._entries], element) == -1) then
- table.insert(self._entries, element)
- return
+ self._entries[#self._entries + 1] = element
+ return #self._entries
end
local position = Util.BinarySearch(self._entries, element, self._compare)
- if position == nil then
- table.insert(self._entries, element)
- else
+ if position ~= nil then
table.insert(self._entries, position, element)
+ return position
end
+ self._entries[#self._entries + 1] = element
+ return #self._entries
end
--[[
@@ -68,13 +73,13 @@ end
function SortedList:uniqueInsert(element)
self._state = self._state + 1
if (#self._entries == 0 or self._compare(self._entries[#self._entries], element) == -1) then
- table.insert(self._entries, element)
+ self._entries[#self._entries + 1] = element
return true
end
local position = Util.BinarySearch(self._entries, element, self._compare)
if position == nil then
- table.insert(self._entries, element)
+ self._entries[#self._entries + 1] = element
elseif self._compare(self._entries[position], element) ~= 0 then
table.insert(self._entries, position, element)
else
@@ -95,17 +100,19 @@ function SortedList:wipe()
end
self._state = self._state + 1
end
--- We don't return a value since we are change the table, this makes it clear for consuming code
---function SortedList:cast(table, compare)
--- if (table._entries == nil) then
--- error("This is not a sorted list table")
--- end
--- setmetatable(table, SortedList)
--- table._compare = compare
---end
-
-
function SortedList:searchGreaterThanOrEqual(entry)
return Util.BinarySearch(self._entries, entry, self._compare)
end
+
+--[[
+ Remove an element from the list
+]]
+function SortedList:remove(entry)
+ local position = self:searchGreaterThanOrEqual(entry)
+ if position == nil or entry ~= self._entries[position] then
+ error("Element to remove not found")
+ end
+ table.remove(self._entries, position)
+ return position
+end
\ No newline at end of file
diff --git a/source/StateManager.lua b/source/StateManager.lua
index 3ecdc25..abe8f26 100644
--- a/source/StateManager.lua
+++ b/source/StateManager.lua
@@ -27,6 +27,7 @@ local function hydrateEntryFromList(entry, data)
end
local function trigger(stateManager, event, ...)
+ local start = GetTimePreciseSec()
for _, callback in ipairs(stateManager.listeners[event] or {}) do
-- trigger callback, pass state manager
local success, result = pcall(callback, stateManager, ...)
@@ -35,6 +36,10 @@ local function trigger(stateManager, event, ...)
end
end
+ local duration = GetTimePreciseSec() - start
+ if (duration > 0.01) then
+ stateManager.logger:Warning("Event handlers for %s took %f seconds, keep your event handlers below 10ms", event, duration)
+ end
end
local function entryToList(entry)
diff --git a/source/Table.lua b/source/Table.lua
new file mode 100644
index 0000000..0a0c270
--- /dev/null
+++ b/source/Table.lua
@@ -0,0 +1,175 @@
+local Table, _ = LibStub:NewLibrary("EventSourcing/Table", 1)
+if not Table then
+ return
+end
+
+local Util = LibStub("EventSourcing/Util")
+local SortedList = LibStub("EventSourcing/SortedList")
+
+
+local function curryOne(func, param)
+ return function(...)
+ return func(param, ...)
+ end
+end
+
+
+local function addRow(table, row)
+ -- Add the row to each index
+ local triggers = {}
+ table.rowCount = table.rowCount + 1
+ for indexName, index in pairs(table.indices) do
+ local position = index:insert(row)
+ -- Check watches
+ for _, watch in ipairs(table.watches[indexName]) do
+ local callback, offset, length = unpack(watch)
+ if position >= offset and position <= offset + length then
+ triggers[#triggers + 1] = callback
+ end
+ end
+ end
+ for indexName, uniqueIndex in pairs(table.uniqueIndices) do
+ uniqueIndex[indexName] = row
+ end
+
+ -- Execute triggers
+ for _, callback in ipairs(triggers) do
+ pcall(callback, 'addRow')
+ end
+end
+
+local function updateRow(table, row, mutator)
+ -- Get the current location in each index
+ local oldPositions = {}
+ for indexName, index in pairs(table.indices) do
+ oldPositions[indexName] = index:remove(row)
+ end
+ mutator()
+
+ -- After mutating we insert the row into all indices again
+ local triggers = {}
+ for indexName, index in pairs(table.indices) do
+ local newPosition = index:insert(row)
+ -- Check watches
+ for _, watch in ipairs(table.watches[indexName]) do
+ local callback, offset, length = unpack(watch)
+ -- trigger for same spot
+ if (true or oldPositions[indexName] ~= newPosition) and (
+ (oldPositions[indexName] >= offset and oldPositions[indexName] <= offset + length)
+ or (newPosition >= offset and newPosition <= offset + length)
+ ) then
+ triggers[#triggers + 1] = callback
+ end
+ end
+ end
+
+ -- Execute triggers
+ for _, callback in ipairs(triggers) do
+ pcall(callback, 'updateRow')
+ end
+end
+
+local function iterateByIndex(table, name, start, length)
+ local i = (start or 1) - 1
+ local data = table.indices[name]:entries()
+ local n = math.min(#data, i + (length or 0))
+ return function()
+ i = i + 1
+ if i <=n then return i, data[i] end
+ end
+end
+
+local function retrieveByUniqueIndex(table, indexName, key)
+ return table.uniqueIndices[indexName][key]
+end
+
+--[[
+ Register a callback to be called when a specific part of the index changes
+ Returns a function that can be used to remove the watch and a function to update the offset / length.
+]]
+local function watchIndexRange(table, indexName, callback, offset, length)
+ if table.indices[indexName] == nil then
+ error(string.format("Attempt to watch unknown sorted index %s"))
+ end
+ local watches = table.watches[indexName]
+ local watch = {nil, offset, length}
+ local paused = false
+ -- We want to pass an iterator to the callback, so we curry it.
+ watch[1] = function(reason)
+ return paused or callback(iterateByIndex(table, indexName, watch[2], watch[3]), reason, table.rowCount)
+ end
+ watches[#watches+1] = watch
+
+ local updateOffset = function(newOffset)
+ watch[2] = newOffset
+ watch[1]('updateOffset')
+ end
+
+ local update = function(newOffset, newLength)
+ watch[2] = newOffset
+ watch[3] = newLength
+ watch[1]('updateWatch')
+ end
+
+ local cancel = function()
+ for i, v in ipairs(watches) do
+ if v == watch then
+ watches[i] = nil
+ break;
+ end
+ end
+ end
+
+ return {
+ update = update,
+ updateOffset = updateOffset,
+ cancel = cancel,
+ pause = function() paused = true end,
+ resume = function()
+ paused = false
+ watch[1]('resume')
+
+ end,
+ trigger = function() watch[1]('trigger') end
+ }
+end
+
+
+-- unique indices are implemented as dictionaries
+Table.new = function(uniqueIndices, indices)
+ local private = {
+ -- A list of index data tables
+ indices = {},
+ watches = {},
+ -- store data
+ uniqueIndices = {},
+ -- store retriever function
+ uniqueIndexers = {},
+ rowCount = 0
+ }
+
+ for name, compare in pairs(indices or {}) do
+ Util.assertFunction(compare)
+ private.indices[name] = SortedList:new({}, compare, false)
+ private.watches[name] = {}
+ end
+ for name, indexer in pairs(uniqueIndices or {}) do
+ Util.assertFunction(indexer)
+ private.uniqueIndices[name] = {}
+ private.uniqueIndexers[name] = indexer
+ private.watches[name] = {}
+ end
+ local public = {
+ addRow = curryOne(addRow, private),
+ updateRow = curryOne(updateRow, private),
+ iterateByIndex = curryOne(iterateByIndex, private),
+ watchIndexRange = curryOne(watchIndexRange, private),
+ retrieveByUniqueIndex = curryOne(retrieveByUniqueIndex, private)
+
+ }
+
+ return public
+end
+
+
+
diff --git a/source/Util.lua b/source/Util.lua
index 24131e4..5329db8 100644
--- a/source/Util.lua
+++ b/source/Util.lua
@@ -4,10 +4,7 @@ if not Util then
return end
-- Searches for the first index that has value >= to the given value
function Util.BinarySearch(list, value, comparator, min, max)
- if type(list) ~= 'table' then
- error("Argument 1 must be a table")
- end
-
+ Util.assertTable(list, 'list')
if min == nil then
min = 1
max = #list
@@ -21,6 +18,31 @@ function Util.BinarySearch(list, value, comparator, min, max)
local result = comparator(list[test], value)
+ if result == 0 then
+ if (list[test] == value) then
+ return test
+ end
+ -- Linear search left side
+ local linearTest = test
+ while linearTest > 1 and result == 0 do
+ linearTest = linearTest - 1
+ result = comparator(list[linearTest], value)
+ if list[linearTest] == value then
+ return linearTest
+ end
+ end
+
+ linearTest = test
+ result = 0
+ while linearTest < #list and result == 0 do
+ linearTest = linearTest + 1
+ result = comparator(list[linearTest], value)
+ if list[linearTest] == value then
+ return linearTest
+ end
+ end
+ end
+
if result == -1 then
diff --git a/source/docs/Table.md b/source/docs/Table.md
new file mode 100644
index 0000000..f1bd763
--- /dev/null
+++ b/source/docs/Table.md
@@ -0,0 +1,111 @@
+# Table library
+
+The table library allows you to manage a collection of rows (represented as tables).
+The table supports multiple indices as well as change events for specific parts of an index.
+
+When inserting rows into the table you should make sure that the rows are not edited externally.
+
+## Index
+An index is defined by a comparison function, the comparison can be single or multiple columns.
+See `Util.CreateMultiFieldSorter` and `Util.CreateFieldSorter`.
+
+```lua
+local table = Table.new({
+ primary = Util.createFieldSorter('a')
+})
+
+
+table.addRow({a = 5})
+table.addRow({a = 3})
+
+for i, v in ipairs(table.iterateByIndex('primary') do
+ print(i, v.a)
+done
+```
+Will output
+```
+1 3
+2 5
+```
+
+Note the use of `iterateByIndex` this creates an iterator allowing you to go over the elements in any order as long as there is an index for it. The iteration itself is really cheap since the index is already in memory.
+
+
+## Updating rows
+To update a row you should wrap the code in a closure and pass it to the table:
+```lua
+local table = Table.new({
+ primary = Util.createFieldSorter('a')
+})
+
+local row = {a = 5, t = 'row1'}
+table.addRow(row)
+table.addRow({a = 3, t = 'row2'})
+
+for i, v in ipairs(table.iterateByIndex('primary') do
+ print(i, v.a, v.t)
+done
+
+table.updateRow(row, function()
+ row.a = 1
+))
+
+for i, v in ipairs(table.iterateByIndex('primary') do
+ print(i, v.a, v.t)
+done
+
+```
+Will output
+```
+1 3 row2
+2 5 row1
+1 1 row1
+2 3 row2
+```
+
+Internally the library will do some bookkeeping to make sure indices remain sorted.
+
+## Watching index ranges
+Imagine you have a very large table, 10.000 records for example.
+When displaying this table in a UI, most records won't be visible. Suppose there are 50 visible lines, each line showing data from 1 row.
+
+Now if any of the rows change, normally what you do is redraw the whole table, regardless if the change is relevant.
+
+To aid with the library supports watching indexes for changes. The reasoning is that you will watch the index that has the same sort order used in your UI.
+
+```lua
+local table = Table.new({
+ name = Util.createFieldSorter('name'),
+ age = Util.createFieldSorter('age')
+})
+
+-- build the initial table.
+table.addRow({name = "Bob1", age = 20})
+table.addRow({name = "Bob2", age = 30})
+table.addRow({name = "Bob3", age = 40})
+table.addRow({name = "Bob4", age = 25})
+table.addRow({name = "Bob5", age = 35})
+table.addRow({name = "Bob6", age = 45})
+local UI = ImaginaryScrollTable()
+-- Let's assume our UI is very small, so it can only show 2 rows.
+-- We pass an offset, 1 (our UI scrollbar is at the top) and a length.
+local watch = table.watchIndexRange('age', function(iterator, reason)
+ -- This function is called every time something in our visible area has changed.
+ -- Reason contains the cause (addRow, updateRow, trigger, updateWatch, updateOffset)
+
+end, 1, 2)
+
+-- The watch object allows us to cancel our subscription as well as update our offset & length.
+
+UI.onScroll = function(offset) {
+ -- Updating the offset or length will always trigger the watch.
+ watch.updateOffset(offset)
+}
+
+-- Initial setup of UI is done, load data.
+watch.trigger()
+
+-- When the UI closes you can cancel the watch.
+watch.cancel()
+```
+
diff --git a/tests/TableTest.lua b/tests/TableTest.lua
new file mode 100644
index 0000000..190a76b
--- /dev/null
+++ b/tests/TableTest.lua
@@ -0,0 +1,155 @@
+beginTests()
+
+local Util = LibStub("EventSourcing/Util")
+local Table = LibStub("EventSourcing/Table")
+
+
+local table = Table.new({
+
+
+}, {
+ a = Util.CreateFieldSorter('a'),
+ inverse_a = Util.InvertSorter(Util.CreateFieldSorter('a'))
+
+})
+
+table.addRow({
+ a = 5
+})
+
+table.addRow({
+ a = 10
+})
+
+table.addRow({
+ a = 11
+})
+
+table.addRow({
+ a = 8
+})
+table.addRow({
+ a = 8
+})
+table.addRow({
+ a = 8
+})
+table.addRow({
+ a = 8
+})
+
+local someRow = {
+ a = 8
+}
+
+table.addRow(someRow)
+
+local updateCounter = 0
+local watch = table.watchIndexRange('a', function(iterator, reason)
+ updateCounter = updateCounter + 1
+end, 1, 5)
+
+assertSame(0, updateCounter)
+table.addRow({
+ a = 1
+})
+
+assertSame(1, updateCounter)
+watch.update(1, 50)
+assertSame(2, updateCounter)
+
+table.updateRow(someRow, function()
+ someRow.a = 400
+end)
+assertSame(3, updateCounter)
+table.updateRow(someRow, function()
+ someRow.a = 400
+end)
+assertSame(4, updateCounter)
+
+watch.pause()
+for i = 0, 10000 do
+ table.addRow({a = math.random(10000)})
+end
+assertSame(4, updateCounter)
+watch.resume()
+assertSame(5, updateCounter)
+
+
+-- test nonexistent index
+assertError(function()
+table.watchIndexRange('test', function() end)
+end)
+
+
+
+
+-- unique indexes are faster but dont support index range watches
+-- test this with a model type table that CLM uses:
+
+local Raid = {} -- Raid information
+function Raid:New(uid, name, roster, config, creator, entry)
+ local o = {}
+
+ setmetatable(o, self)
+ self.__index = self
+
+ o.entry = entry
+
+ -- Raid Management
+ o.uid = uid
+ o.roster = roster
+
+ o.config = config
+ o.name = name
+ o.status = "CREATED"
+ -- o.owner = creator
+
+ o.startTime = 0
+ o.endTime = 0
+
+ -- GUID dict
+ -- Dynamic status of tje raid
+ o.players = { [creator] = true } -- for raid mangement we check sometimes if creator is part of raid
+ o.standby = { }
+
+ -- Historical storage of the raid
+ o.participated = {
+ inRaid = {},
+ standby = {}
+ }
+
+ return o
+end
+
+function Raid:UID()
+ return self.uid
+end
+
+function Raid:Name()
+ return self.name
+end
+
+local raids = Table.new({
+ name = function(raid) return raid:Name() end,
+ uid = test
+})
+
+local A = {}
+A.__index = A
+function A:new()
+ local o = {}
+ setmetatable(o, self)
+
+ return o
+end
+
+function A:test()
+
+print("test")
+end
+
+local a = A:new()
+print(a:test())
+
+printResultsAndExit()
\ No newline at end of file
diff --git a/tests/UtilTest.lua b/tests/UtilTest.lua
index d3348d6..cf4f564 100644
--- a/tests/UtilTest.lua
+++ b/tests/UtilTest.lua
@@ -37,5 +37,32 @@ local function TestBinarySearch()
end
+local function TestBinarySearchDuplicates()
+ local comparator = function(a, b)
+ if a.val < b.val then
+ return -1
+ elseif a.val > b.val then
+ return 1
+ else
+ return 0
+ end
+ end
+ local search = { val = 2 }
+ local cases = {
+ { list = {{ val = 1 }, { val = 2 }, search, { val = 2 }}, search = search, expected = 3 },
+ { list = {{ val = 1 }, { val = 1 }, { val = 1 }, { val = 2, a = 4 }, { val = 2, b = 6 }, search}, search = search, expected = 6 },
+ { list = {{ val = 1 }, search, { val = 2 }, { val = 2 }}, search = search, expected = 2 },
+ { list = {{ val = 1 }, search, { val = 2 }, { val = 2 }, { val = 2 }, { val = 2 }, { val = 2 }, { val = 2 }}, search = search, expected = 2 },
+ { list = {{ val = 1 }, { val = 2 }, { val = 2 }, { val = 2 }, { val = 2 }, { val = 2 }, { val = 2 }, search}, search = search, expected = 8 }
+ }
+
+ for _, v in ipairs(cases) do
+ local result = Util.BinarySearch(v.list, v.search, comparator)
+ assertSame(v.expected, result)
+ end
+
+end
+
TestBinarySearch()
+TestBinarySearchDuplicates()
printResultsAndExit()