Last active
February 13, 2026 21:08
-
-
Save Mark-Marks/4404a813a6f80b6b087d9507509af59a to your computer and use it in GitHub Desktop.
Better performance for immutable updates in Lyra
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| diff --git a/src/Session.luau b/src/Session.luau | |
| index 256e1d1..1c6661e 100644 | |
| --- a/src/Session.luau | |
| +++ b/src/Session.luau | |
| @@ -61,8 +61,8 @@ local noYield = require(script.Parent.noYield) | |
| .isSaved (self: Session<T>) -> boolean | |
| -- Internal state mutation | |
| - .setData (self: Session<T>, data: T) -> () | |
| - .mutateKey (self: Session<T>, newData: T) -> () | |
| + .setData (self: Session<T>, data: T, immutableUpdate: boolean?) -> () | |
| + .mutateKey (self: Session<T>, newData: T, immutableUpdate: boolean?) -> () | |
| -- Autosave management | |
| .startAutosaving (self: Session<T>) -> () | |
| @@ -81,8 +81,8 @@ type SessionImpl<T> = { | |
| orphanFile: (self: Session<T>, file: Types.File) -> (), | |
| writeRecord: (self: Session<T>, txInfo: Types.TxInfo) -> Promise.TPromise<any>, | |
| isSaved: (self: Session<T>) -> boolean, | |
| - setData: (self: Session<T>, data: T) -> (), | |
| - mutateKey: (self: Session<T>, newData: T) -> (), | |
| + setData: (self: Session<T>, data: T, immutableUpdate: boolean?) -> (), | |
| + mutateKey: (self: Session<T>, newData: T, immutableUpdate: boolean?) -> (), | |
| startAutosaving: (self: Session<T>) -> (), | |
| stopAutosaving: (self: Session<T>) -> (), | |
| unload: (self: Session<T>) -> Promise.Promise, | |
| @@ -778,13 +778,19 @@ end | |
| @within Session | |
| @private | |
| @param data any -- The new data value. | |
| + @param immutableUpdate boolean? -- Whether this originates from an immutable data update. This triggers a faster freeze function. | |
| ]=] | |
| -function Session:setData(data: any): () | |
| +function Session:setData(data: any, immutableUpdate: boolean?): () | |
| -- Generate a unique ID for this mutation and add it to the changeSet. | |
| local mutationId = HttpService:GenerateGUID(false) | |
| self.changeSet[mutationId] = true | |
| - -- Freeze and update the data reference. | |
| - Tables.freezeDeep(data) | |
| + -- Make sure the data is frozen and update the data reference. | |
| + if immutableUpdate == true then | |
| + -- POTENTIALLY UNSAFE: See function doc comment | |
| + Tables.ensureFrozen(data) | |
| + else | |
| + Tables.freezeDeep(data) | |
| + end | |
| self.data = data | |
| end | |
| @@ -795,12 +801,13 @@ end | |
| @within Session | |
| @private | |
| @param newData any -- The new data value. | |
| + @param immutableUpdate boolean? -- Whether this originates from an immutable data update. This triggers a faster freeze function. | |
| ]=] | |
| -function Session:mutateKey(newData: any): () | |
| +function Session:mutateKey(newData: any, immutableUpdate: boolean?): () | |
| local oldData = self.data -- Store reference to previous data | |
| -- Update internal data (sets .data and marks changeSet) | |
| - self:setData(newData) | |
| + self:setData(newData, immutableUpdate) | |
| -- Trigger change callbacks asynchronously | |
| for _, callback in self.ctx.changedCallbacks do | |
| @@ -1008,12 +1015,22 @@ local function updateInternal( | |
| end | |
| -- Ensure the new data is frozen | |
| - Tables.freezeDeep(nextData :: any) | |
| + if immutable then | |
| + -- POTENTIALLY UNSAFE: See function doc comment | |
| + Tables.ensureFrozen(nextData :: any) | |
| + else | |
| + Tables.freezeDeep(nextData :: any) | |
| + end | |
| -- Check if the data actually changed | |
| - if Tables.equalsDeep(nextData :: any, currentData :: any) then | |
| - logger:log("trace", "transform resulted in no data change, resolving true") | |
| - return resolve(true) -- Considered successful, but data remains same | |
| + -- We probably don't want to use Tables.equalsDeep if we're not in an immutable context | |
| + -- POTENTIALLY UNSAFE: See function doc comment for Tables.immutableEqualsDeep | |
| + if | |
| + (immutable and Tables.immutableEqualsDeep(nextData :: any, currentData :: any)) | |
| + or (immutable == false and Tables.equalsDeep(nextData :: any, currentData :: any)) | |
| + then | |
| + logger:log("trace", "transform resulted in no data change, resolving true") | |
| + return resolve(true) -- Considered successful, but data remains same | |
| end | |
| if immutable == false then | |
| @@ -1021,7 +1038,7 @@ local function updateInternal( | |
| end | |
| -- Data changed and is valid, apply the mutation | |
| - self:mutateKey(nextData) | |
| + self:mutateKey(nextData, immutable) | |
| logger:log("trace", "update applied successfully") | |
| diff --git a/src/Tables.luau b/src/Tables.luau | |
| index e32c053..ae5fe4c 100644 | |
| --- a/src/Tables.luau | |
| +++ b/src/Tables.luau | |
| @@ -98,6 +98,31 @@ local function equalsDeep(a: { [any]: any }, b: { [any]: any }): boolean | |
| return true | |
| end | |
| +-- POTENTIALLY UNSAFE: If the original, unmodified data isn't ensured to be completely frozen, non-frozen tables within frozen tables won't be able to be detected as changed, be it user intention or error. | |
| +local function immutableEqualsDeep(a: { [any]: any }, b: { [any]: any }): boolean | |
| + if typeof(a) ~= "table" or typeof(b) ~= "table" then | |
| + return a == b | |
| + end | |
| + | |
| + if table.isfrozen(a) or table.isfrozen(b) then | |
| + return a == b | |
| + end | |
| + | |
| + for key, value in a do | |
| + if not equalsDeep(value, b[key]) then | |
| + return false | |
| + end | |
| + end | |
| + | |
| + for key, value in b do | |
| + if not equalsDeep(value, a[key]) then | |
| + return false | |
| + end | |
| + end | |
| + | |
| + return true | |
| +end | |
| + | |
| local function freezeDeep<T>(t: T): () | |
| if typeof(t) ~= "table" then | |
| return | |
| @@ -114,6 +139,24 @@ local function freezeDeep<T>(t: T): () | |
| end | |
| end | |
| +--- POTENTIALLY UNSAFE: Assume that if a table is frozen, all tables underneath it are also frozen. | |
| +--- This improves performance for immutable updates at the cost of potentially not freezing some tables, be it user intention or error. | |
| +local function ensureFrozen<T>(t: T): () | |
| + if typeof(t) ~= "table" then | |
| + return | |
| + end | |
| + | |
| + if table.isfrozen(t) == true then | |
| + return | |
| + end | |
| + | |
| + table.freeze(t) | |
| + | |
| + for _, value in t :: any do | |
| + ensureFrozen(value) | |
| + end | |
| +end | |
| + | |
| -- Performs a deep reconciliation of a target table with a source table, | |
| -- applying Copy-On-Write (COW) semantics for the target. | |
| -- The function assumes 'target' and 'source' are non-nil tables. | |
| @@ -223,7 +266,9 @@ return { | |
| mergeDeep = mergeDeep, | |
| mergeShallow = mergeShallow, | |
| equalsDeep = equalsDeep, | |
| + immutableEqualsDeep = immutableEqualsDeep, | |
| freezeDeep = freezeDeep, | |
| + ensureFrozen = ensureFrozen, | |
| map = map, | |
| reconcileDeep = reconcileDeep, | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment