diff --git a/FreshShit/_ReactiveValue/export b/FreshShit/_ReactiveValue/export new file mode 100644 index 0000000..3d19cdf --- /dev/null +++ b/FreshShit/_ReactiveValue/export @@ -0,0 +1 @@ +!T3rdxTnoYFlB6RVnPnKLa829oETSLsPT0Jf6HZ2(UhVgSirjXho2(SSdLTl53(PzKKT8NXoeAHslT0eBPzgnFPrJgz3VB)E9nM23yi8x(hDddSTCO9no(p7D4bhTFFJrUobgw)fT)rD3OVrG7GzuFMLRt)J(T13C917B85JhnIrd6Fe)ZdTyE2KR6r)CqFJh7134kTBsCgmX1)9Uwo8BU3(h1B)t6BmW121NJ5J6I)Dd83BI)ElOx8M6thZXxVR84Kvac5bHSa3Paw(tVHKa(1PZOaujH8RtcSg8rRHbt6BSl)78RoiGdagm(ybe)a(h4TDKLJfBI4Z8pcx1yO7zcy3)LkS03yT1w7fdTiJDCzCip3KpijNBt3EU5ulgZYz8A(0GqFN5VZ2DaXEU5OqheJ8Mgo1Rh04MbWVBZVc1lysR5VBUP4hRrZnBIxCU5oZn3y91Bn3myc1jUjQF885CUMn23331NJ8xP3joGsGUwnALfakYu9DQZW4VmY1FU5fCkCMawEelFMGS5u0q3SGdj9aUyP5mElE(ZNB2aBEJchbjhjSa()nUJp1Rzd4QnIzpMD6WjgX)1y78glQFIhYZuDFU5tNB2nN(qTz0vgrjU1S8qJoBf)I4kk1JP0acxRLW5zZn)sCtb9mpIpz6Ctg1MZCpHcAUZOFGyhsZTDUC(SFPnukZt2K)2XYoUzNDgz4qKyukUnb83wc(CgHYbcEBWUe7mQjuupaDf9MhRSm3KW5oIB25Sa82zVc0bGOYRfZWr9cu5u8bbqKD5PPGr5IYYglc1g1GrNsHBR16LGkb9nHGE5PpNWPNt9VDOVNwpYtbJe6G8w2(UGLW0q7hKwcpzLzjCBQP9KVJ00yHN)GutBT7fAAR9DKM2qRzpi10(L7fAA)Y3tZE6(WmoYhFVqt7XFhPP55E5dsnT5xFVqvlImVNORDURRnL4ORHr)FpivWIK91tbRCyDxrMAh8GuM(SvNi9z31KO0hMs0vOv6ZURzLo(HPv6oRor6o31KOpmTs3zfALUZQ0knkClmeWK(tDYkQkGHNoKXioE1yvpsF8v9WyZG1OuVxrjKydE4d0MAeqRAkBwFvkgeKKUyiWvETQiluGr1Pcgx3yxaxqVklTQDFPMkX5Qc02sUeoh6LwodPFoxxdCC1wcXIDqW4a2Bp3PE20aHtcFYLJPbkG04SeTOrH6XjbZZfKQjSHO5CVreyBexK2g5swmHu(OPOTNTwMcZ)7AybUYjUmxwtf8uoc(ekDkWuxkn3BcXzmD4)c0YWngvOWDDEGFT1MBEWiCCgP3DofnyimM1yhk3)TfJ)TcvS1bvpGDzABXcOOD0CZbiXWbaSl(wbayNBEj12oFaSlmFHNVRhzmjavoCbDKiOe6DjXFiRmXOpDqOpJtNYzF46XWgfhi2BBH0b1)sCdHlHfiVHgTTWw4qCms9znlxSxIyD7ZCCdSgDvZwLF)xBrThkfQnJfUlOx76uHoE75kt5RI7elDkNk1zvXoPuRfoTZP87eOpOYHYFRCevTj2Q7ePP9vc(rGs3OwEsYZ1EPywYvSC4kHyDBKRFBCSuK)A9(wxMKerXGOqjrTYtLUsusDLu6IO3S1GgzqgAZvcpG7LWn0MBWZ1)rJ75MVymyIcLAe4kckOkaJd4d3lTGYBjyc4ABGn3vNeyhGx05cWx1pZKWYNsgY75v0G2i7XhCTa)YIXcv(2gZHiStKdPSb(wNJUqzwtTSj(OhnolAkt6sc8dcyEkX)cWbl4t8Jh8(oD6ijJ3Wbl)Ah8ZC)KMtiGVmyeyJE1aA3nmqagSAJCGK4kY(OhDa34xeLpNWgj8C)brPi9i8Qcg3JEe3BRQDmYOy))btiCapGalbHQ8MdKjqci(uUJfWHdON8KDTVKCf4GgCxFYh2gMxSLS58phG8dpFodjaheiUyp5jkHyONNRFalN2ihzODe7zN7VJSlOwhRAekkHG4Pmhb(efHKquSlqbbVEe0Xb1(FMaHRaJjcSWl5KhBRrd8)yhsKFrArgqzbz1y7WJsRjulvytH2STRJWNCZi3GQjrW2KQA1eGvmGKdXgIsAjrV42qjqcih2iZvea7PZn3Sv8iPKrUO61UbJ8V0LB6Sb)FBEDkEqY5PI5fOaPDQrxULygxspLi5qDEmlft6XmO8YWbqNbUod4Tub6gDA0kcdfWapTRiKRTeEKqC0nfk2sV9BjAV2awRNBLQNpdPRDs2Ffk7kX5rGLV74M)0p1cu0hqqBSiKjJolq4hhTvqVwzJylRGUhh0CM44XuH9axmAFozWfsZhnkYu4bbJt0ZNoZYnKJxQsrzO7Qr5aev(XY7FOJurDerpZG1UBvKuxpg9SocJM5qQvD7kE1LTn6cIIk7rmZiUc(exzIRx5svHiezefzb9oD2(leXyKJb5lK87ZSvlOqY0FMIh0oktG7KSh4)Fy6Uju4BxcusbgI04kdKej1QgqY1H2ZAk9WAps0t4H(1LbNixEH(D0wKNeK)(83LK7MTOGFeQ9)giYq0pwip4sF7RWqtKKGkGPfUWNkNtQezXbJknBMOuRdlfXYdlQ20QsteBOsNkwTKDJwTh3A(d5KBkgn)nAq18IgG8an(W(h1l7W(e6yGYfZjfpiebMcZ3jhKG16L8LRqNjUD(8POOv1qdGdq(iM7m(CgKk2xGTPCjWHSRejXg53aMP2CTHshDBJm22IOY3wP53kFflOxT2o1sOuXuMR8qHXVsYJfpTYDujJKwljdTjLDy7JexN(jv8cRcXP2CJ5ktvXP81w2QVwQ7dY0vOetcr5rKrml5Vp38TWIWhsPEQ1QlcWk5cMRHKpzqVLAqRD0AU9e(hF0E7)d3TPLsdQQR2SZSh6fZZzsffcKWhu7KL2WkkKh7RsXB7HPXYsK9fj7J6Wc9JsMJgAWexHPi2cABsOwBgSiz47ABhf03jAaSKqDQi7yHtHChMROfhC5BmxBnewsWGhH51pbzlghfzjoHOTq3B2(BRR0hfj9(Wzffs7QtaXYHTDkbWgDs4vfpla9asLhxyIGyfg5QLFjeZjb1MDszKgdS4eDguByl3QKAfsEHsHI0vRT0Psm3UDKiz7Kl8BhKZSRG(eeaxtlIi5RusAkSyM(DF5xLThZUrAfhm1sj7tfo0pK5322SP2EZkioVooby756WjPWbb85yENMseC61Ti2Fiv6iK3n3CouyH2NkDeDWbPorQJSKZa8UmLNbOBJvwL2ow)fiBAQJaDl9vJd7cUDIjF(Y1PVF6KhLBJYj1q52USj(j3MfVdUjy0PAvqIcvtVLzgKAsKNNq(aXoikVK3vHSYGaRGuZuPIkRODWCXjoHdJfN9Ksk2bTcepghvAhCR((BJYbTC5GBvSUSQC8rHhZcnBS)N5togawQYu0Qdb8jbaFmpgYLQ8(jr7QQCFsRjgX1QCjAKaSi4QysxkM5QTAbrrdOasJAZC1Y1AgMzeAwmZmEy0QagzjC3iFpNQWOib6qQ)l2GsdJLwaofd)fx8e5j7QFgwEaiiJ9WKCIItXV(jTI7BXvmvXWi5KevqZkxaDlPLvfCTCAClvMDYGgz(NEE0NGj9(NWFEyPGMjiLtr(rD1rlbm1wnTiyDlPPwr0TCkRd(X0AfeL7TK0CbOPQsXuHkx380TARt66eHM45NvC2X0FkALOG8Z9zPvvlv34krO6weRGs4VSLsMJaBjtHyoAHvyHsFRe4aIYTiTrmw)Q0UwihXv6vsMVPCujfVeNvhrovkUsM4IYnkXZG6NClT)Osxk0XJ7MqclZRxm4wUQm)gx155iyQ9dzofdnnJuSEULLv(Daxl)ZBawN6X64vrzT0PmA2QEtcvj8R5ZUcOn3P5koH(5ppJAPKCBOZYort6PIxWCoybJryx0H5rU0PPgS1M3QTUx1seVQE3Cj6BbbsOazvdKOgCk77lSOYMDFUzVJF1XBp38pixbBhTfuBRtXuYZCNcvIMRVm)6(0XW5sqCwaWACN3a4KdmglIFDyAeoyIOe)f0lMjCiB8pPdUMYUPaqYE)YWa15uO5eYSOoB766X7iUt867Jju(VuGtGhFkbzpW1zOfykWfoxkoJebe4mo4r9XcJfcYpbwHdQhXxTRPe8KmG15lhTxe6LAa6cNuHPQdEHBencBGppciXOYKCEMdTqBytscGvODaKNLX4EZITIafeqawzcxsUQSQ0jvvpgTJiRGTdj)T)u3rtv3cIfFMMk9m8I8RZAlNIcS9SegFQD4OmtUyCODkcR68gQDGiWxYUFEj1jDnsJKcGvFv61FneL7XQicAPDGfTSc8b57IsADKVRQ1TkTOKAwFBFtTCQX(3DFX4jFQK3JxfLPUhLIvLR(k282jCRNw5ntgFwWWaSdtaEXZbHyARCBVkzwv1qak4CdJhZoMOOWWJ0InZvz1ZsyEikQhMBu5JzBXNJMby15Nd0pVCd4tJfgqt27AryhGNd6WPrZLk3F6PWZS8XtcGPNG4dut3zfiOmbMX59GljsJMftCsa52Dxfp)gFq4Gen2AO26UsnM0gbrtBciNRemJYQ3ijFNwjobnv25vHoZUzGlxZOsVzfZ7vIhTbjxCV(25g98f)S3CAJeUrB8P8odQIZ4HHG)EoDSLdEkyuxn6e6oJ)b8bNpcK1t0Td49bp4iQJsvQEVWtti24fEEcXwf5niffL47WJ89m9OgNbrSFQtCtmXfFoeXRXnQO(WXhwNsEU4PnVOjQwwe7twsjPzBLWVqYVX9nMg2)nASmSo0ZA057n4QcvY8PS2ys3XrMhKWz8iIc1MqBbT0whbkAWNY0EuEK52CaU9ilNH6jYpkCdmn(YhDITYN0fv4zAAUy7PAC(t1h9dSp)cHNHVrkenYcCT3gdsxPRVifL4Hi)FBD9YQWSxuDs7tN6odyqPLaadRzLnuvpypIRqUbY99mp4QliQ(X(C1jy6O2zPcxKJOyvY3Io6jhK25fnFrwN3t9DfhHZcUog68ceHAEuxgHYRlxEK3XZeiVcoHMRqHGGjSm(mJ5FBScNSrWUo2bcL0Akv)SsuatBW9qhiBkDG096wRCEN0zUxiBsTM0(lx)TFc7Bo79(OVSfzPD6JIEkseDGQRU)QB2S5zg1oKPIO7B4jCqLUbdCTKyO0aPtPT1b72xjDUosACPC7ji0vEu2FRed6pOaUFBgvJqcYvgI3b4MBurpKIg3PIC609OQQqBDNufkl4YMiVszhQFU(h6ICUChXob)v2Rs(m8svO)Y1ft(vo7jpOKNBwv5PH4x5llRJKCzy01Ln)1IjBipurr8ibtgtvtDygIEG)EZs1tRMykkABoXjsfP4QfCaL6haVYoLhXBM4DhAVOlGVgpPNhoAK4ng6B3)W3)6)8q(9XVsc9jB03G5rTTpyit86)KfEo(ce1G3lRp334S921O3zg929eoWcH3nOgWtHpoWna)8YEjFNJ(wkXgExJQaY79PiqmE)(hEi0UqNysdjDXOPN4I)H7qA)JwRl(QoLlKhznw9ojnG67qS)G69T6g)AFJ)Bid2gQ(ghU)R7bVsvTMImv93TPgd5Jr4INjgYmkSt6mfhWX1HcWFkXYP6To6TLA1Apq)d7BKqYHVEwN46FSN61XkaxFodLl7ibeqokFjX(A4Qc5NXENS)(hHSgr1aO6OlW8FTV1Fn38Fhsg6JVrn7XjWlD9h(rFIxFJpg9jUW5e81iRsItThjFt0(YJ7174)agHFMou8EJ9OnGxST2Ue8DIlC8yXxtUH2bwkzi(KrkZvzWlk3uxe0aaEXXV)n)2Et(pV6VE7)aBAFJn609x7SzFJz8r(wB0LF1())) \ No newline at end of file diff --git a/FreshShit/_ReactiveValue/init.lua b/FreshShit/_ReactiveValue/init.lua new file mode 100644 index 0000000..7534a90 --- /dev/null +++ b/FreshShit/_ReactiveValue/init.lua @@ -0,0 +1,676 @@ +---@diagnostic disable: missing-return +local function dumpTable(table, depth) + if (depth > 200) then + print("Error: Depth > 200 in dumpTable()") + return + end + for k, v in pairs(table) do + if (type(v) == "table") then + print(string.rep(" ", depth) .. k .. ":") + dumpTable(v, depth + 1) + else + print(string.rep(" ", depth) .. k .. ": ", v) + end + end +end + +local metadata = { + ---@param self ReactiveValue + ---@param other ReactiveValue + ---@return ReactiveValue|nil + __add = function(self, other) + local otherType = type(other) + if otherType == "table" and other._type and other._type == self._type and other._value then + return self._value + other._value + end + if otherType == "string" and self._type == otherType then + return self._value .. other + end + if otherType == "number" and self._type == otherType then + return self._value + other + end + return nil + end, + ---@param self ReactiveValue + ---@param other ReactiveValue + ---@return ReactiveValue|nil + __mul = function(self, other) + local otherType = type(other) + if otherType == "table" and other._type and other._type == self._type and other._value then + return self._value * other._value + end + if otherType == "number" and self._type == otherType then + return self._value * other + end + return nil + end, + ---@param self ReactiveValue + ---@param other ReactiveValue + ---@return ReactiveValue|nil + __sub = function(self, other) + local otherType = type(other) + if otherType == "table" and other._type and other._type == self._type and other._value then + return self._value - other._value + end + if otherType == "number" and self._type == otherType then + return self._value - other + end + return nil + end, + ---@param self ReactiveValue + ---@param other ReactiveValue + ---@return ReactiveValue|nil + __div = function(self, other) + local otherType = type(other) + if otherType == "table" and other._type and other._type == self._type and other._value then + return self._value / other._value + end + if otherType == "number" and self._type == otherType then + return self._value / other + end + return nil + end, + ---@param self ReactiveValue + ---@param other ReactiveValue + ---@return ReactiveValue|nil + __mod = function(self, other) + local otherType = type(other) + if otherType == "table" and other._type and other._type == self._type and other._value then + return self._value % other._value + end + if otherType == "number" and self._type == otherType then + return self._value % other + end + return nil + end, + ---@param self ReactiveValue + ---@param other ReactiveValue + ---@return ReactiveValue|nil + __pow = function(self, other) + local otherType = type(other) + if otherType == "table" and other._type and other._type == self._type and other._value then + return self._value ^ other._value + end + if otherType == "number" and self._type == otherType then + return self._value ^ other + end + return nil + end, + ---@param self ReactiveValue + ---@param other ReactiveValue + ---@return boolean + __eq = function(self, other) + local otherType = type(other) + if otherType == "table" and other._type and other._type == self._type and other._value then + return self._value == other._value + end + return self._value == other + end, + ---@param self ReactiveValue + ---@param other ReactiveValue + ---@return boolean + __lt = function(self, other) + local otherType = type(other) + if otherType == "table" and other._type and other._type == self._type and other._value then + return self._value < other._value + end + return self._value < other + end, + ---@param self ReactiveValue + ---@param other ReactiveValue + ---@return boolean + __le = function(self, other) + local otherType = type(other) + if otherType == "table" and other._type and other._type == self._type and other._value then + return self._value <= other._value + end + return self._value <= other + end, + ---@param self ReactiveValue + ---@param other ReactiveValue + ---@return boolean + __gt = function(self, other) + local otherType = type(other) + if otherType == "table" and other._type and other._type == self._type and other._value then + return self._value > other._value + end + return self._value > other + end, + ---@param self ReactiveValue + ---@param other ReactiveValue + ---@return boolean + __ge = function(self, other) + local otherType = type(other) + if otherType == "table" and other._type and other._type == self._type and other._value then + return self._value >= other._value + end + return self._value >= other + end, + ---@param self ReactiveValue + ---@return number + __len = function(self) + if self._type == "table" then + return #self._value + end + if self._type == "string" then + return string.len(self._value) + end + return 0 + end, + ---@param self ReactiveValue + ---@return string + __tostring = function(self) + return tostring(self._value) + end, + ---@param self ReactiveValue + ---@param key string + ---@param value any + ---@return nil + __newindex = function(self, key, value) + local setupComplete = rawget(self, "_setupComplete") + if setupComplete == nil or setupComplete == false then + rawset(self, key, value) + return + end + if self._type ~= "table" then + rawset(self, key, value) + return + end + + self._value[key] = value + local ChangedKey = { key } + + -- If the value being assigned is a ReactiveValue + -- Then listen to changes on it as well + -- And propagate those changes upwards + if self._recursive and getmetatable(value) == getmetatable(self) then + self:_setupListeners(key, value) + end + + self:_notify() + self:_notifyFieldChanged(ChangedKey) + self:_notifyAnyFieldChanged(ChangedKey) + end, + ---@param self ReactiveValue + ---@param key string + ---@return any|nil + __index = function(self, key) + local value = rawget(self, key) + if value ~= nil then + return value + end + if rawget(self, "_type") ~= "table" then + return nil + end + local innerTable = rawget(self, "_value") + if innerTable ~= nil then + return rawget(innerTable, key) + end + return nil + end + -- __index = ReactiveValue +} + +--- Sadly I could not get @generic to play nice with this class +--- I think it's not ready yet, there are issues on github describing similar problems and it is marked as WIP... +--- Guess I'll have to live without it for now and specify type of a RV in #type + +---## A type safe value that can be listened to for changes +---### **Always use RV:set() for setting primitive values** +--- Supports primitive values and tables
+--- Tables can be listened to for changes on any field or a specific field
+---### Example usage (value):
+--- ```lua +--- local test = ReactiveValue.new(1) +--- test:onChange(function(value) +--- print("test changed to " .. value) +--- end) +--- test:set(2) +--- test:set(test + 3) +--- ``` +---### Example usage (table):
+--- ```lua +--- local test = ReactiveValue.new({1, 2, 3}) +--- test:onAnyFieldChange(function(field, value) +--- print(string.format("test.%s changed to %s", table.concat(field, "."), value)) +--- end) +--- test[1] = 4 -- test.1 changed to 4 +--- test[4] = {1, 2, 3} -- test.4 changed to +--- test[4][1] = 14 -- No log(!!) because test[4] is a table and not a ReactiveValue +--- ``` +---### To trigger a callback for `test[4][1]` in the previous example do:
+--- ```lua +--- local test = ReactiveValue.new({1, 2, 3}, true) +--- test:onAnyFieldChange(function(field, value) +--- print(string.format("test.%s changed to %s", table.concat(field, "."), value)) +--- end) +--- test[1] = 4 -- test.1 changed to 4 +--- test[4] = {1, 2, 3} -- test.4 changed to
+--- test[4][1] = 14 -- test.4.1 changed to 14 +--- ``` +---### To listen to a specific field of a table do:
+--- ```lua +--- local test = ReactiveValue.new({1, 2, 3}, true) +--- test:onFieldChange("1", function(value) +--- print("test.1 changed to " .. value) +--- end) +--- test[1] = 4 -- test.1 changed to 4 +--- test[4] = {1, 2, 3} -- Does not trigger callback +-- ``` +---@class ReactiveValue +---@field _listeners table +---@field _fieldListeners table> +---@field _anyFieldListeners table> +---@field _oneTimeListeners table +---@field _value any +---@field _type string +---@field _recursive boolean? +ReactiveValue = { + ---#### Get the underlying value of a ReactiveValue + ---@param self ReactiveValue + ---@return any + get = function(self) end, + + ---### Set the underlying value of a ReactiveValue triggering listener callbacks + ---@param self ReactiveValue + ---@param newValue any + set = function(self, newValue) end, + + ---## EVENT + ---### Register a listener that is triggered whenever the underlying value changes + --- Returns a function that can be called to undo the callback + ---@param self ReactiveValue + ---@param callback fun(value: any, type: string) + ---@return fun(): nil + onChange = function(self, callback) end, + + ---## EVENT + ---### Register a listener that is triggered whenever a specific field of a table changes + --- Returns a function that can be called to undo the callback + ---@param self ReactiveValue + ---@param field string + ---@param callback fun(field: string[], value: any, type: string) + ---@return fun(): nil + onFieldChange = function(self, field, callback) end, + + ---## EVENT + ---### Register a listener that is triggered whenever any field of a table changes + --- Returns a function that can be called to undo the callback + ---@param self ReactiveValue + ---@param callback fun(field: string[], value: any, type: string) + ---@param depth number? How deep to listen for changes + ---@return fun(): nil + onAnyFieldChange = function(self, callback, depth) end, + + ---## EVENT + ---### Register a listener that is triggered ONCE whenever the underlying value changes + --- Returns a function that can be called to undo the callback + ---@param self ReactiveValue + ---@param callback fun(value: any, type: string) + ---@return fun(): nil + once = function(self, callback) end, + ---### Setup listeners for all fields of a table recursively + --- This is used to ensure that listeners are notified recursively + + ---@param self ReactiveValue + _setupAllListenersRecursively = function(self) end, + ---### Setup listeners for a specific field of a table recursively + --- This is used to ensure that listeners are notified recursively + + ---@param self ReactiveValue + _setupListeners = function(self, key, value, recursive) end, + + ---### Notify listeners that the underlying value has changed + ---@param self ReactiveValue + ---@return nil + ---#### Event contains: + --- 2. value: any - The new value of the changed field + --- 3. type: string - The type of the new value of the changed field + _notify = function(self) end, + + ---### Notify listeners that a specific field of the underlying value has changed + ---#### Event contains: + --- 1. field: table - A list of keys that lead to the changed field + --- 2. value: any - The new value of the changed field + --- 3. type: string - The type of the new value of the changed field + ---@param self ReactiveValue + _notifyFieldChanged = function(self, field) end, + + ---### Notify listeners that any field of the underlying value has changed + ---#### Event contains: + --- 1. field: table - A list of keys that lead to the changed field + --- 2. value: any - The new value of the changed field + --- 3. type: string - The type of the new value of the changed field + _notifyAnyFieldChanged = function(self, field) end, +} +---### Constructor +---@param initialValue any +---@param recursive boolean? +---@return ReactiveValue +ReactiveValue.new = function(initialValue, recursive) + local self = setmetatable({}, metadata) + self._listeners = {} + self._fieldListeners = {} + self._anyFieldListeners = {} + self._oneTimeListeners = {} + self._value = initialValue + self._type = type(initialValue) + self._recursive = recursive or false + + ---@return any + self.get = function(self) + return self._value + end + ---@param newValue any + self.set = function(self, newValue) + if self._value == newValue then + return + end + if type(newValue) ~= self._type then + error("Expected " .. self._type .. ", got " .. type(newValue)) + return + end + self._value = newValue + self:_notify() + end + self.onChange = function(self, callback) + if type(callback) ~= "function" then + error("Expected function, got " .. type(callback)) + return function() end + end + self._listeners[callback] = true + return function() + self._listeners[callback] = nil + end + end + self.onFieldChange = function(self, field, callback) + if type(callback) ~= "function" then + error("Expected function, got " .. type(callback)) + return function() end + end + if self._fieldListeners[field] == nil then + self._fieldListeners[field] = {} + end + self._fieldListeners[field][callback] = true + return function() + self._fieldListeners[field][callback] = nil + end + end + self.onAnyFieldChange = function(self, callback, depth) + depth = depth or 99999 + if type(callback) ~= "function" then + error("Expected function, got " .. type(callback)) + return function() end + end + if self._anyFieldListeners[depth] == nil then + self._anyFieldListeners[depth] = {} + end + self._anyFieldListeners[depth][callback] = true + return function() + self._anyFieldListeners[depth][callback] = nil + end + end + self.once = function(self, callback) + if type(callback) ~= "function" then + error("Expected function, got " .. type(callback)) + return function() end + end + self._oneTimeListeners[callback] = true + return function() + self._oneTimeListeners[callback] = nil + end + end + + self._setupAllListenersRecursively = function(self) + if self._type ~= "table" then + return + end + for key, value in pairs(self._value) do + self:_setupListeners(key, value, true) + end + end + ---@param key string + ---@param value any + ---@param recursive boolean? + self._setupListeners = function(self, key, value, recursive) + recursive = recursive or false + if self._type ~= "table" then + return + end + if getmetatable(value) ~= getmetatable(self) then + return + end + value._recursive = true + if value._type == "table" then + value:onAnyFieldChange(function(key2) + ChangedKey = { key, table.unpack(key2) } + self:_notifyFieldChanged(ChangedKey) + self:_notifyAnyFieldChanged(ChangedKey) + end) + else + value:onChange(function(newVal) + ChangedKey = { key } + self:_notifyFieldChanged(ChangedKey) + self:_notifyAnyFieldChanged(ChangedKey) + end) + end + + if recursive then + value:_setupAllListenersRecursively() + end + end + + if recursive then + self:_setupAllListenersRecursively() + end + + self._notify = function(self) + for listener, _ in pairs(self._oneTimeListeners) do + -- task.spawn(listener, self._value, self._type) + listener(self._value, self._type) + self._oneTimeListeners[listener] = nil + end + for listener, _ in pairs(self._listeners) do + -- task.spawn(listener, self._value, self._type) + listener(self._value, self._type) + end + end + -- TODO: Maybe implement some sort of regex here or something... + -- Such as listening to *.field1 or something + -- But this (having to loop over listeners and evaluate some condition) would tank performance + -- Compared to a simple lookup + -- So I'm not going to do anything about it for now, until I figure out a better way + ---@param field table A list of keys that lead to the changed field + ---@return nil + self._notifyFieldChanged = function(self, field) + local value = self._value + for _, key in ipairs(field) do + value = value[key] + end + + local strfield = table.concat(field, ".") + if self._fieldListeners[strfield] == nil then + return + end + for listener, _ in pairs(self._fieldListeners[strfield]) do + -- task.spawn(listener, value, type(value)) + listener(value, type(value)) + end + end + ---@param self ReactiveValue + ---@param field table A list of keys that lead to the changed field + ---@return nil + self._notifyAnyFieldChanged = function(self, field) + local value = self._value + for _, key in ipairs(field) do + value = value[key] + end + local keyDepth = #field + for listenerDepth, listeners in pairs(self._anyFieldListeners) do + if listenerDepth >= keyDepth then + for listener, _ in pairs(listeners) do + -- The reason this also returns type(value) is so that clients don't have to compute type(value) + -- I assume some of them might want to do it so computing it once is probably better than having every client compute it for themselves + -- task.spawn(listener, field, value, type(value)) + listener(field, value, type(value)) + end + end + end + end + + self._setupComplete = true + return self +end + +_G["ReactiveValue"] = ReactiveValue + +-- S -- begintest +-- S local invocations = 0 +-- S -- Integer example +-- S local test = ReactiveValue.new(1) +-- S test:onChange(function(value) +-- S invocations = invocations + 1 +-- S print("test changed to " .. value) +-- S end) +-- S test:set(2) +-- S assert(invocations == 1) +-- S +-- S invocations = 0 +-- String example +-- S test = ReactiveValue.new("test") +-- S test:onChange(function(value) +-- S invocations = invocations + 1 +-- S print("test changed to " .. value) +-- S end) +-- S test:set("test2") +-- S assert(invocations == 1) +-- S +-- S -- Type safety example +-- S local res, err = pcall(test.set, test, 1) +-- S assert(res == false) +-- S assert(err:find("Expected string, got number")) +-- S +-- S -- Table example +-- S invocations = 0 +-- S test = ReactiveValue.new({1, 2, 3}) +-- S local clbk = test:onChange(function(value) +-- S invocations = invocations + 1 +-- S print("test changed to") +-- S dumpTable(value, 0) +-- S end) +-- S test:set({1, 2, 3, 4}) +-- S assert(invocations == 1) +-- S +-- S -- Callback removal example +-- S clbk() +-- S +-- S invocations = 0 +-- S -- Any field change example +-- S clbk = test:onAnyFieldChange(function(field, value) +-- S invocations = invocations + 1 +-- S print("test." .. table.concat(field, ".") .. " changed to " .. tostring(value)) +-- S end) +-- S test.Pero = 1 +-- S test.Pero = nil +-- S assert(invocations == 2) +-- S clbk() +-- S +-- S invocations = 0 +-- S -- Field change example +-- S test:onFieldChange("Pero", function(value) +-- S invocations = invocations + 1 +-- S print("test.Pero changed to " .. value) +-- S end) +-- S test.Pero = 2 +-- S assert(invocations == 1) +-- S +-- S invocations = 0 +-- S -- One time listener example +-- S test:once(function(value) +-- S invocations = invocations + 1 +-- S print("test changed to") +-- S dumpTable(value, 0) +-- S end) +-- S test:set({3, 2, 1}) +-- S assert(invocations == 1) +-- S +-- S invocations = 0 +-- S -- Table push example +-- S test = ReactiveValue.new({}) +-- S test:onChange(function(value) +-- S invocations = invocations + 1 +-- S print("test changed to") +-- S dumpTable(value, 0) +-- S end) +-- S test:onAnyFieldChange(function(field, value) +-- S invocations = invocations + 1 +-- S print("test." .. table.concat(field, ".") .. " changed to " .. value) +-- S end) +-- S test[#test + 1] = 4 +-- S assert(invocations == 2) +-- S +-- S invocations = 0 +-- S test = ReactiveValue.new({ +-- S name = "pero", +-- S coins = ReactiveValue.new(1) +-- S }) +-- S test.coins:onChange(function(value) +-- S invocations = invocations + 1 +-- S print("test.coins changed to " .. value) +-- S end) +-- S test.coins:set(2) +-- S assert(invocations == 1) +-- S +-- S invocations = 0 +-- S test = ReactiveValue.new({ +-- S name = "pero", +-- S coins = ReactiveValue.new(1) +-- S }, true) +-- S test:onAnyFieldChange(function(field, value) +-- S invocations = invocations + 1 +-- S print("test." .. table.concat(field, ".") .. " changed to " .. tostring(value)) +-- S end) +-- S test.coins:set(2) +-- S test.pero2 = ReactiveValue.new({}) +-- S test.pero2.coins = ReactiveValue.new(1) +-- S test.pero2.coins:set(2) +-- S assert(invocations == 4) +-- S +-- S invocations = 0 +-- S test = ReactiveValue.new({ +-- S name = "pero", +-- S coins = ReactiveValue.new({ +-- S value = ReactiveValue.new(1) +-- S }) +-- S }, true) +-- S test:onAnyFieldChange(function(field, value) +-- S invocations = invocations + 1 +-- S print("test." .. table.concat(field, ".") .. " changed to " .. tostring(value)) +-- S end) +-- S test.coins.value:set(2) +-- S assert(invocations == 1) +-- S +-- S invocations = 0 +-- S test = ReactiveValue.new({}, true) +-- S test.coins = ReactiveValue.new({}) +-- S test.coins.value = ReactiveValue.new(1) +-- S test:onAnyFieldChange(function(field, value) +-- S invocations = invocations + 1 +-- S print("test." .. table.concat(field, ".") .. " changed to " .. tostring(value)) +-- S end) +-- S test.coins.value:set(3) +-- S assert(invocations == 1) +--S +--S invocations = 0 +--S test = ReactiveValue.new({}, true) +--S test:onAnyFieldChange(function(field, value) +--S invocations = invocations + 1 +--S print("test." .. table.concat(field, ".") .. " changed to " .. tostring(value)) +--S end, 1) +--S test.test2 = ReactiveValue.new({}, true) +--S test.test2.test3 = ReactiveValue.new(1) +--S assert(invocations == 1) +--S +-- S -- endtest + +-- return ReactiveValue