-- -- https://github.com/ayongm2/TableMonitor -- -- https://github.com/jojo59516/traceable -- local next = next -- local pairs = pairs -- local ipairs = ipairs -- local unpack = table.unpack -- local load = load -- local setmetatable = setmetatable -- local assert = assert -- local getmetatable = getmetatable -- local type = type -- local tonumber = tonumber -- ----------------------------------------------------- -- local traceable = {} -- local _M = traceable -- local function merge(dst, src) -- for k, v in next, src do -- dst[k] = v -- end -- return dst -- end -- local function normalize_field_path(field_path) -- return field_path:gsub("^([-?%d]+)", "[%1]"):gsub("%.([-?%d]+)", "[%1]"):gsub("^([^.%[%]]+)", "['%1']"):gsub( -- "%.([^.%[%]]+)", -- "['%1']" -- ) -- end -- local getter_template = -- [[ -- return function(t) -- local success, value = pcall(function (t) -- return t%s -- end, t) -- if success then -- return value -- end -- end -- ]] -- local setter_template = -- [[ -- return function(t, v) -- local success, err = pcall(function (t, v) -- t%s = v -- end, t, v) -- end -- ]] -- local getter_cache = {} -- local setter_cache = {} -- local function compile_normalized_property(field_path) -- local getter, setter = getter_cache[field_path], setter_cache[field_path] -- if not getter then -- getter = assert(load(getter_template:format(field_path)))() -- getter_cache[field_path] = getter -- setter = assert(load(setter_template:format(field_path)))() -- setter_cache[field_path] = setter -- end -- return getter, setter -- end -- function _M.compile_property(field_path) -- return compile_normalized_property(normalize_field_path(field_path)) -- end -- local set -- local function create() -- local o = { -- dirty = false, -- ignored = false, -- opaque = false, -- _stage = {}, -- _traced = {}, -- _lastversion = {}, -- _parent = false -- } -- return setmetatable( -- o, -- { -- __index = o._stage, -- __newindex = set, -- __len = _M.len, -- __pairs = _M.pairs, -- __ipairs = _M.ipairs -- } -- ) -- end -- local function mark_dirty(t) -- while t and not t.dirty do -- t.dirty = true -- t = t._parent -- end -- end -- set = function(t, k, v, force) -- local stage = t._stage -- local u = stage[k] -- if _M.is_traceable(v) then -- if _M.is(u) then -- for _ in next, u._stage do -- if v[k] == nil then -- u[k] = nil -- end -- end -- else -- u = create() -- u._parent = t -- stage[k] = u -- end -- merge(u, v) -- else -- local traced = t._traced -- local lastverison = t._lastversion -- if stage[k] ~= v or force then -- if not traced[k] then -- traced[k] = true -- lastverison[k] = u -- end -- stage[k] = v -- mark_dirty(t) -- end -- end -- end -- function _M.is(t) -- local mt = getmetatable(t) -- return type(mt) == "table" and mt.__newindex == set -- end -- function _M.is_traceable(t) -- if type(t) ~= "table" then -- return false -- end -- local mt = getmetatable(t) -- return mt == nil or mt.__newindex == set -- end -- function _M.new(t) -- local o = create() -- if t then -- merge(o, t) -- end -- return o -- end -- function _M.mark_changed(t, k) -- local v = t[k] -- if _M.is(v) then -- mark_dirty(v) -- else -- set(t, k, v, true) -- end -- end -- function _M.set_ignored(t, ignored) -- t.ignored = ignored -- if not ignored and t.dirty then -- mark_dirty(t._parent) -- end -- end -- function _M.set_opaque(t, opaque) -- t.opaque = opaque -- end -- local k_stub = -- setmetatable( -- {}, -- { -- __tostring = function() -- return "k_stub" -- end -- } -- ) -- function _M.compile_map(src) -- local o = {} -- for i = 1, #src do -- local v = src[i] -- local argc = #v -- -- v = {argc, action, arg1, arg2, ..., argn} -- v = {argc, unpack(v)} -- copy -- for j = 1, argc - 1 do -- local k = j + 2 -- local arg = normalize_field_path(v[k]) -- v[k] = compile_normalized_property(arg) -- local p, c = nil, o -- for field in arg:gmatch('([^%[%]\'"]+)') do -- field = tonumber(field) or field -- p, c = c, c[field] -- if not c then -- c = {} -- p[field] = c -- end -- end -- local stub = c[k_stub] -- if not stub then -- stub = {[1] = 1} -- c[k_stub] = stub -- end -- local n = stub[1] + 1 -- stub[1] = n -- stub[n] = v -- end -- end -- return o -- end -- local function get_map_stub(map, k) -- local map_k = map[k] -- return map_k and map_k[k_stub] -- end -- local argv = {} -- local function do_map(t, mappings, mapped) -- for i = 2, mappings[1] do -- local mapping = mappings[i] -- if not mapped[mapping] then -- mapped[mapping] = true -- local argc = mapping[1] -- argv[1] = t -- for i = 2, argc do -- argv[i] = mapping[i + 1](t) -- end -- local action = mapping[2] -- action(unpack(argv, 1, argc)) -- end -- end -- end -- local function next_traced(t, k) -- local k = next(t._traced, k) -- return k, t._stage[k], t._lastversion[k] -- end -- local function _commit(keep_dirty, t, sub, changed, newestversion, lastversion, map, mapped) -- if not sub.dirty then -- return false -- end -- local traced = sub._traced -- local trace = sub._lastversion -- local has_changed = next(traced) ~= nil -- for k, v in next, sub._stage do -- if _M.is(v) and not v.ignored then -- if v.opaque then -- has_changed = _commit(keep_dirty, t, v) -- if has_changed then -- if changed then -- changed[k] = true -- end -- end -- else -- local c, n, l = changed and {}, newestversion and {}, lastversion and {} -- has_changed = _commit(keep_dirty, t, v, c, n, l, map and map[k], mapped) -- if has_changed then -- if changed then -- changed[k] = c -- end -- if newestversion then -- newestversion[k] = n -- end -- if lastversion then -- lastversion[k] = l -- end -- end -- end -- if has_changed and map then -- local stub = get_map_stub(map, k) -- if stub then -- do_map(t, stub, mapped) -- end -- end -- end -- end -- for k, v, u in next_traced, sub do -- if not keep_dirty then -- traced[k] = nil -- trace[k] = nil -- end -- if changed then -- changed[k] = true -- end -- if newestversion then -- newestversion[k] = v -- end -- if lastversion then -- lastversion[k] = u -- end -- if map then -- local stub = get_map_stub(map, k) -- if stub then -- do_map(t, stub, mapped) -- end -- end -- end -- if not keep_dirty then -- sub.dirty = false -- end -- return has_changed -- end -- local flag_map = { -- [("k"):byte()] = "changed", -- [("v"):byte()] = "newestversion", -- [("u"):byte()] = "lastversion" -- } -- local function commit(t, map, diff_flags, keep_dirty) -- local has_changed, diff -- if diff_flags then -- diff = {} -- for i = 1, #diff_flags do -- local field_name = flag_map[diff_flags:byte(i)] -- if field_name then -- diff[field_name] = {} -- end -- end -- has_changed = _commit(keep_dirty, t, t, diff.changed, diff.newestversion, diff.lastversion, map, map and {}) -- else -- has_changed = _commit(keep_dirty, t, t, nil, nil, nil, map, map and {}) -- end -- return has_changed, diff -- end -- function _M.commit(t, map, diff_flags) -- if type(map) == "string" then -- map, diff_flags = nil, map -- end -- return commit(t, map, diff_flags, false) -- end -- function _M.diff(t, map, diff_flags) -- if type(map) == "string" then -- map, diff_flags = nil, map -- end -- return commit(t, map, diff_flags, true) -- end -- local function do_maps(t, sub, map, mapped) -- for k, map_k in next, map do -- local stub = map_k[k_stub] -- if stub then -- do_map(t, stub, mapped) -- end -- local v = sub[k] -- if type(v) == "table" then -- do_maps(t, v, map_k, mapped) -- end -- end -- end -- function _M.map(t, map) -- do_maps(t, t, map, map and {}) -- end -- local function forward_to(field, func) -- return function(t, ...) -- return func(t[field], ...) -- end -- end -- function _M.len(t) -- return #(t._stage) -- end -- _M.next = forward_to("_stage", next) -- _M.pairs = forward_to("_stage", pairs) -- _M.ipairs = forward_to("_stage", ipairs) -- _M.unpack = forward_to("_stage", unpack) -- return _M -- -- local traceable = require "traceable" -- -- local mt = { -- -- __tostring = function(t) -- -- return "object" -- -- end -- -- } -- -- local object = setmetatable({}, mt) -- -- local raw_data = { -- -- boolean_value = false, -- -- number_value = 1, -- -- string_value = "string", -- -- list_value = {1, 2, 3}, -- -- nested_table_value = { -- -- nested_table_value = { -- -- stub_value = true -- -- } -- -- }, -- -- object_value = object -- -- } -- -- local data = traceable.new(raw_data) -- -- data.list_value = {1, 2, 3} -- -- data.list_value = {1, 2, 3, 4} -- -- data.list_value = {1, 2, 3} -- -- traceable.commit(data) -- -- print("traceable data.dirty 00000 ", data.dirty) -- -- table.insert(data.list_value, 4) -- -- traceable.commit(data) -- -- print("traceable data.dirty aaaa", data.dirty) -- -- traceable.commit(data) -- -- print("traceable data.dirty bbbb", data.dirty) -- -- table.insert(data.list_value, 5) -- -- table.remove(data.list_value, #data.list_value) -- -- print("traceable data.dirty ccccc", data.dirty) local string_find = string.find local string_match = string.match local string_format = string.format local string_sub = string.sub local table_insert = table.insert local table_remove = table.remove local ipairs = ipairs local tonumber = tonumber local tostring = tostring local type = type local setmetatable = setmetatable local getmetatable = getmetatable local string_split = function(input, delimiter) input = tostring(input) delimiter = tostring(delimiter) if (delimiter == '') then return false end local pos, arr = 0, {} -- for each divider found for st, sp in function() return string_find(input, delimiter, pos, true) end do table_insert(arr, string_sub(input, pos, st - 1)) pos = sp + 1 end table_insert(arr, string_sub(input, pos)) return arr end local table_removebyvalue = function(array, value, removeall) local c, i, max = 0, 1, #array while i <= max do if array[i] == value then table_remove(array, i) c = c + 1 i = i - 1 max = max - 1 if not removeall then break end end i = i + 1 end return c end local function getByPath(t, path) if not path then return t elseif string_find(path, "%.") then local paths = string_split(path, ".") local cur = t local hasN = string_find(path, "%[") ~= nil for i, keyname in ipairs(paths) do local v if hasN then local n = tonumber(string_match(keyname, "^%[(.+)%]$")) if n then v = cur[n] end end if not v then v = cur[keyname] end if v then cur = v else return nil end end return cur else return t[path] end end local function setByPath(t, path, value) if not path then t = value elseif string_find(path, "%.") then local paths = string_split(path, ".") local cur = t local count = #paths local hasN = string_find(path, "%[") ~= nil for i = 1, count - 1 do local keyname = paths[i] local v if hasN then local n = tonumber(string_match(keyname, "^%[(.+)%]$")) if n then v = cur[n] keyname = n end end if not v then v = cur[keyname] end if v then cur = v else cur[keyname] = {} cur = cur[keyname] end end local lastKeyname = paths[count] if hasN then lastKeyname = tonumber(string_match(lastKeyname, "^%[(.+)%]$")) or lastKeyname end cur[lastKeyname] = value else t[path] = value end return t end local table_monitor_ function table_monitor_(tb, callback, path) local data_ = tb or {} local monitors_ = {} local spath = path local function createKey(subpath, key) if subpath then if "number" == type(key) then return string_format("%s.[%s]", subpath, key) else return string_format("%s.%s", subpath, key) end else if "number" == type(key) then return string_format("[%s]", key) else return key end end end local mt = { __index = function(_, k) if "__ismonitor__" == k then return true end if not data_ then return end local result = data_[k] if type(result) == "table" then if not monitors_[k] then monitors_[k] = table_monitor_(result, callback, createKey(spath, k)) end return monitors_[k] else return result end end, __newindex = function(_, k, v) local oldValue if data_ then oldValue = data_[k] end if oldValue ~= v then data_[k] = v if callback then local key = createKey(spath, k) if type(v) == "table" then monitors_[k] = table_monitor_(v, callback, key) callback(key, monitors_[k], oldValue) else if monitors_[k] then monitors_[k] = nil end callback(key, v, oldValue) end end end end, __call = function(t, k, v) if "[path]" == k then return spath elseif "string" == type(k) then if v then setByPath(t, k, v) else return getByPath(t, k) end else return data_ end end, } return setmetatable({}, mt) end local M = {} function M.new(tb, callback) if tb.__ismonitor__ then tb = tb() end local callbacks = {} if callback then callbacks[#callbacks + 1] = callback end local addCallback = function(self, cb) for _, cc in ipairs(callbacks) do if cc == cb then return end end callbacks[#callbacks + 1] = cb end local removeCallback = function(self, cb) table_removebyvalue(callbacks, cb, true) end local removeAllCallbacks = function(self) callbacks = {} end local cb = function(path, value, oldValue) for _, listener in ipairs(callbacks) do listener(path, value, oldValue) end end local result = table_monitor_(tb, cb) local mt = getmetatable(result) local index_fn = mt.__index mt.__index = function(t, k) if k == "addCallback" then return addCallback elseif k == "removeCallback" then return removeCallback elseif k == "removeAllCallbacks" then return removeAllCallbacks else return index_fn(t, k) end end setmetatable(result, mt) return result end return M -- local traceable = require("traceable") -- local a = { -- a1 = 1, -- a2 = { 1, 2, 3}, -- a3 = { -- a31 = "a", -- a32 = { -- "b" -- }, -- }, -- } -- local ta = traceable.new(a, function ( path, value, oldValue ) -- print(string.format("table change at %s from ", path), oldValue, "to ", value) -- end) -- print("start~~~") -- print(ta.a3.a32[1]) -- ta.a3.a32[2] = 2 -- ta.a3.a32 = {"c"} -- ta.a3.a32.a = 3 -- ta.a3.a32 = 33 -- print("~~~", ta.a3.a32) -- print(ta.a2[3]) -- ta.a3.a32 = {33} -- print(ta.a3.a32[1]) -- ta.a3.a32 = nil -- ta.a3.a32 = {5} -- print(ta.a3.a32) -- print("over~~~")