From 734788f2755c187d7bb182004a89304c087bfc3d Mon Sep 17 00:00:00 2001 From: Daniel P H Fox Date: Sat, 2 Sep 2023 01:34:24 +0100 Subject: [PATCH 001/287] Add scoped function --- src/Memory/scoped.lua | 15 +++++++++++++++ 1 file changed, 15 insertions(+) create mode 100644 src/Memory/scoped.lua diff --git a/src/Memory/scoped.lua b/src/Memory/scoped.lua new file mode 100644 index 000000000..fafd303df --- /dev/null +++ b/src/Memory/scoped.lua @@ -0,0 +1,15 @@ +--!strict + +--[[ + Creates cleanup tables with access to constructors as methods. +]] + +local function scoped(...) + local merged = {} + for name, func in {...} do + merged[name] = func + end + return setmetatable({}, {__index = merged}) :: any +end + +return scoped \ No newline at end of file From b40505ae9c41cfeb6eb11a3328ec1f76e7b1bf3b Mon Sep 17 00:00:00 2001 From: Elttob Date: Sat, 2 Sep 2023 03:34:25 +0100 Subject: [PATCH 002/287] Refactoring --- src/Instances/applyInstanceProps.lua | 2 +- src/{Utility => Memory}/cleanup.lua | 0 src/{Utility => Memory}/doNothing.lua | 0 src/{Utility => Memory}/needsDestruction.lua | 0 src/Memory/scoped.lua | 10 ++++------ src/State/Computed.lua | 2 +- src/State/ForKeys.lua | 4 ++-- src/State/ForPairs.lua | 4 ++-- src/State/ForValues.lua | 4 ++-- src/init.lua | 4 ++-- test/{Utility => Memory}/cleanup.spec.lua | 2 +- test/{Utility => Memory}/doNothing.spec.lua | 2 +- 12 files changed, 16 insertions(+), 18 deletions(-) rename src/{Utility => Memory}/cleanup.lua (100%) rename src/{Utility => Memory}/doNothing.lua (100%) rename src/{Utility => Memory}/needsDestruction.lua (100%) rename test/{Utility => Memory}/cleanup.spec.lua (97%) rename test/{Utility => Memory}/doNothing.spec.lua (96%) diff --git a/src/Instances/applyInstanceProps.lua b/src/Instances/applyInstanceProps.lua index a030649b9..206c74a9a 100644 --- a/src/Instances/applyInstanceProps.lua +++ b/src/Instances/applyInstanceProps.lua @@ -15,7 +15,7 @@ local Package = script.Parent.Parent local PubTypes = require(Package.PubTypes) -local cleanup = require(Package.Utility.cleanup) +local cleanup = require(Package.Memory.cleanup) local xtypeof = require(Package.Utility.xtypeof) local logError = require(Package.Logging.logError) local Observer = require(Package.State.Observer) diff --git a/src/Utility/cleanup.lua b/src/Memory/cleanup.lua similarity index 100% rename from src/Utility/cleanup.lua rename to src/Memory/cleanup.lua diff --git a/src/Utility/doNothing.lua b/src/Memory/doNothing.lua similarity index 100% rename from src/Utility/doNothing.lua rename to src/Memory/doNothing.lua diff --git a/src/Utility/needsDestruction.lua b/src/Memory/needsDestruction.lua similarity index 100% rename from src/Utility/needsDestruction.lua rename to src/Memory/needsDestruction.lua diff --git a/src/Memory/scoped.lua b/src/Memory/scoped.lua index fafd303df..7e175e532 100644 --- a/src/Memory/scoped.lua +++ b/src/Memory/scoped.lua @@ -4,12 +4,10 @@ Creates cleanup tables with access to constructors as methods. ]] -local function scoped(...) - local merged = {} - for name, func in {...} do - merged[name] = func - end - return setmetatable({}, {__index = merged}) :: any +-- This return type is technically a lie, but it's required for useful type +-- checking behaviour. +local function scoped(constructors: T): T + return setmetatable({}, {__index = constructors}) :: any end return scoped \ No newline at end of file diff --git a/src/State/Computed.lua b/src/State/Computed.lua index 7a02892ff..89379edba 100644 --- a/src/State/Computed.lua +++ b/src/State/Computed.lua @@ -14,7 +14,7 @@ local logWarn = require(Package.Logging.logWarn) local parseError = require(Package.Logging.parseError) -- Utility local isSimilar = require(Package.Utility.isSimilar) -local needsDestruction = require(Package.Utility.needsDestruction) +local needsDestruction = require(Package.Memory.needsDestruction) -- State local makeUseCallback = require(Package.State.makeUseCallback) diff --git a/src/State/ForKeys.lua b/src/State/ForKeys.lua index 8db00c6a7..125064ad7 100644 --- a/src/State/ForKeys.lua +++ b/src/State/ForKeys.lua @@ -20,8 +20,8 @@ local logErrorNonFatal = require(Package.Logging.logErrorNonFatal) local logError = require(Package.Logging.logError) local logWarn = require(Package.Logging.logWarn) -- Utility -local cleanup = require(Package.Utility.cleanup) -local needsDestruction = require(Package.Utility.needsDestruction) +local cleanup = require(Package.Memory.cleanup) +local needsDestruction = require(Package.Memory.needsDestruction) -- State local peek = require(Package.State.peek) local makeUseCallback = require(Package.State.makeUseCallback) diff --git a/src/State/ForPairs.lua b/src/State/ForPairs.lua index a2236ba0d..31cb53426 100644 --- a/src/State/ForPairs.lua +++ b/src/State/ForPairs.lua @@ -20,8 +20,8 @@ local logErrorNonFatal = require(Package.Logging.logErrorNonFatal) local logError = require(Package.Logging.logError) local logWarn = require(Package.Logging.logWarn) -- Utility -local cleanup = require(Package.Utility.cleanup) -local needsDestruction = require(Package.Utility.needsDestruction) +local cleanup = require(Package.Memory.cleanup) +local needsDestruction = require(Package.Memory.needsDestruction) -- State local peek = require(Package.State.peek) local makeUseCallback = require(Package.State.makeUseCallback) diff --git a/src/State/ForValues.lua b/src/State/ForValues.lua index 774b52a25..63e756240 100644 --- a/src/State/ForValues.lua +++ b/src/State/ForValues.lua @@ -19,8 +19,8 @@ local logError = require(Package.Logging.logError) local logErrorNonFatal = require(Package.Logging.logErrorNonFatal) local logWarn = require(Package.Logging.logWarn) -- Utility -local cleanup = require(Package.Utility.cleanup) -local needsDestruction = require(Package.Utility.needsDestruction) +local cleanup = require(Package.Memory.cleanup) +local needsDestruction = require(Package.Memory.needsDestruction) -- State local peek = require(Package.State.peek) local makeUseCallback = require(Package.State.makeUseCallback) diff --git a/src/init.lua b/src/init.lua index 79fe026d1..ccf9bbf14 100644 --- a/src/init.lua +++ b/src/init.lua @@ -33,8 +33,8 @@ local Fusion = restrictRead("Fusion", { Tween = require(script.Animation.Tween), Spring = require(script.Animation.Spring), - cleanup = require(script.Utility.cleanup), - doNothing = require(script.Utility.doNothing), + cleanup = require(script.Memory.cleanup), + doNothing = require(script.Memory.doNothing), peek = require(script.State.peek) }) :: Fusion diff --git a/test/Utility/cleanup.spec.lua b/test/Memory/cleanup.spec.lua similarity index 97% rename from test/Utility/cleanup.spec.lua rename to test/Memory/cleanup.spec.lua index b7ea6249b..261479b57 100644 --- a/test/Utility/cleanup.spec.lua +++ b/test/Memory/cleanup.spec.lua @@ -1,6 +1,6 @@ local Package = game:GetService("ReplicatedStorage").Fusion local New = require(Package.Instances.New) -local cleanup = require(Package.Utility.cleanup) +local cleanup = require(Package.Memory.cleanup) return function() it("should destroy instances", function() diff --git a/test/Utility/doNothing.spec.lua b/test/Memory/doNothing.spec.lua similarity index 96% rename from test/Utility/doNothing.spec.lua rename to test/Memory/doNothing.spec.lua index 393ae9b66..3e810ef3e 100644 --- a/test/Utility/doNothing.spec.lua +++ b/test/Memory/doNothing.spec.lua @@ -1,6 +1,6 @@ local Package = game:GetService("ReplicatedStorage").Fusion local New = require(Package.Instances.New) -local doNothing = require(Package.Utility.doNothing) +local doNothing = require(Package.Memory.doNothing) return function() it("should not destroy instances", function() From 0cf4dc0f49d558e37fee9f8fdfd89d573519f726 Mon Sep 17 00:00:00 2001 From: Elttob Date: Sat, 2 Sep 2023 03:37:57 +0100 Subject: [PATCH 003/287] Remove GC pauses from unit tests --- test/Instances/New.spec.lua | 2 -- test/State/Computed.spec.lua | 4 ---- test/State/ForKeys.spec.lua | 5 ----- test/State/ForPairs.spec.lua | 4 ---- test/State/ForValues.spec.lua | 7 +------ test/State/Value.spec.lua | 6 ------ test/Utility/waitForGC.lua | 17 ----------------- 7 files changed, 1 insertion(+), 44 deletions(-) delete mode 100644 test/Utility/waitForGC.lua diff --git a/test/Instances/New.spec.lua b/test/Instances/New.spec.lua index 166b8e4f1..fb5ef78d1 100644 --- a/test/Instances/New.spec.lua +++ b/test/Instances/New.spec.lua @@ -2,8 +2,6 @@ local Package = game:GetService("ReplicatedStorage").Fusion local New = require(Package.Instances.New) local defaultProps = require(Package.Instances.defaultProps) -local waitForGC = require(script.Parent.Parent.Utility.waitForGC) - return function() it("should create a new instance", function() local ins = New "Frame" {} diff --git a/test/State/Computed.spec.lua b/test/State/Computed.spec.lua index 9c0213086..193baccaa 100644 --- a/test/State/Computed.spec.lua +++ b/test/State/Computed.spec.lua @@ -5,8 +5,6 @@ local Computed = require(Package.State.Computed) local Value = require(Package.State.Value) local peek = require(Package.State.peek) -local waitForGC = require(script.Parent.Parent.Utility.waitForGC) - return function() it("should construct a Computed object", function() local computed = Computed(function(use) end) @@ -90,7 +88,6 @@ return function() end) end - waitForGC() state:set(5) expect(counter).to.equal(1) @@ -113,7 +110,6 @@ return function() end) end - waitForGC() state:set(5) expect(counter).to.equal(2) diff --git a/test/State/ForKeys.spec.lua b/test/State/ForKeys.spec.lua index 0e449160b..47b6f5dc5 100644 --- a/test/State/ForKeys.spec.lua +++ b/test/State/ForKeys.spec.lua @@ -5,8 +5,6 @@ local ForKeys = require(Package.State.ForKeys) local Value = require(Package.State.Value) local peek = require(Package.State.peek) -local waitForGC = require(script.Parent.Parent.Utility.waitForGC) - return function() it("should construct a ForKeys object", function() local forKeys = ForKeys({}, function(use) end) @@ -243,8 +241,6 @@ return function() end) end - waitForGC() - state:set({ ["bar"] = "baz", }) @@ -271,7 +267,6 @@ return function() end) end - waitForGC() state:set({ ["bar"] = "baz", }) diff --git a/test/State/ForPairs.spec.lua b/test/State/ForPairs.spec.lua index b4fc5886a..ec1b3fb54 100644 --- a/test/State/ForPairs.spec.lua +++ b/test/State/ForPairs.spec.lua @@ -5,8 +5,6 @@ local ForPairs = require(Package.State.ForPairs) local Value = require(Package.State.Value) local peek = require(Package.State.peek) -local waitForGC = require(script.Parent.Parent.Utility.waitForGC) - return function() it("should construct a ForPairs object", function() local forPairs = ForPairs({}, function(use) end) @@ -240,7 +238,6 @@ return function() end) end - waitForGC() state:set({ 5 }) expect(counter).to.equal(1) @@ -263,7 +260,6 @@ return function() end) end - waitForGC() state:set({ 5 }) expect(counter).to.equal(2) diff --git a/test/State/ForValues.spec.lua b/test/State/ForValues.spec.lua index 27ce26638..6323a32d4 100644 --- a/test/State/ForValues.spec.lua +++ b/test/State/ForValues.spec.lua @@ -5,8 +5,6 @@ local ForValues = require(Package.State.ForValues) local Value = require(Package.State.Value) local peek = require(Package.State.peek) -local waitForGC = require(script.Parent.Parent.Utility.waitForGC) - return function() it("should construct a ForValues object", function() local forKeys = ForValues({}, function(use) end) @@ -320,8 +318,6 @@ return function() end) end - waitForGC() - state:set({ [1] = "biz", }) @@ -347,8 +343,7 @@ return function() return value end) end - - waitForGC() + state:set({ [1] = 2, }) diff --git a/test/State/Value.spec.lua b/test/State/Value.spec.lua index 5fcd6a25e..0ce387c84 100644 --- a/test/State/Value.spec.lua +++ b/test/State/Value.spec.lua @@ -3,8 +3,6 @@ local Value = require(Package.State.Value) local ForValues = require(Package.State.ForValues) local peek = require(Package.State.peek) -local waitForGC = require(script.Parent.Parent.Utility.waitForGC) - return function() it("should construct a Value object", function() local value = Value() @@ -29,8 +27,6 @@ return function() local value = setmetatable({ { 2 } }, { __mode = "kv" }) Value(value[1]) - waitForGC() - expect(value[1]).to.equal(nil) end) @@ -40,8 +36,6 @@ return function() return innerValue[1] + 1 end) - waitForGC() - expect(value[1]).never.to.equal(nil) expect(peek(transformed)[1]).to.equal(3) end) diff --git a/test/Utility/waitForGC.lua b/test/Utility/waitForGC.lua deleted file mode 100644 index 25d0d29e6..000000000 --- a/test/Utility/waitForGC.lua +++ /dev/null @@ -1,17 +0,0 @@ -return function() - local ref = setmetatable({ {} }, { __mode = "kv" }) - - local lastLen = 1 - repeat - table.insert(ref, {}) - lastLen = #ref - print("waitForGC", lastLen) - - if #ref > 60 then - error("Timed out waiting for garbage collection cycle") - end - - task.wait(1) - until #ref < lastLen - print("waitForGC Done") -end From b9a6dcb73252daec28f4aba65c563436315021a9 Mon Sep 17 00:00:00 2001 From: Elttob Date: Sat, 2 Sep 2023 03:50:37 +0100 Subject: [PATCH 004/287] Rename doCleanup --- src/Instances/applyInstanceProps.lua | 4 ++-- src/Memory/{cleanup.lua => doCleanup.lua} | 10 +++++----- src/State/ForKeys.lua | 6 +++--- src/State/ForPairs.lua | 6 +++--- src/State/ForValues.lua | 6 +++--- src/init.lua | 2 +- test/Memory/cleanup.spec.lua | 20 ++++++++++---------- test/init.spec.lua | 2 +- 8 files changed, 28 insertions(+), 28 deletions(-) rename src/Memory/{cleanup.lua => doCleanup.lua} (86%) diff --git a/src/Instances/applyInstanceProps.lua b/src/Instances/applyInstanceProps.lua index 206c74a9a..3504129b5 100644 --- a/src/Instances/applyInstanceProps.lua +++ b/src/Instances/applyInstanceProps.lua @@ -15,7 +15,7 @@ local Package = script.Parent.Parent local PubTypes = require(Package.PubTypes) -local cleanup = require(Package.Memory.cleanup) +local doCleanup = require(Package.Memory.doCleanup) local xtypeof = require(Package.Utility.xtypeof) local logError = require(Package.Logging.logError) local Observer = require(Package.State.Observer) @@ -120,7 +120,7 @@ local function applyInstanceProps(props: PubTypes.PropertyTable, applyTo: Instan end applyTo.Destroying:Connect(function() - cleanup(cleanupTasks) + doCleanup(cleanupTasks) end) end diff --git a/src/Memory/cleanup.lua b/src/Memory/doCleanup.lua similarity index 86% rename from src/Memory/cleanup.lua rename to src/Memory/doCleanup.lua index 6a20519a5..c514a0d2e 100644 --- a/src/Memory/cleanup.lua +++ b/src/Memory/doCleanup.lua @@ -11,7 +11,7 @@ - an array - `cleanup` will be called on each item ]] -local function cleanupOne(task: any) +local function doCleanupOne(task: any) local taskType = typeof(task) -- case 1: Instance @@ -38,16 +38,16 @@ local function cleanupOne(task: any) -- case 6: array of tasks elseif task[1] ~= nil then for _, subtask in ipairs(task) do - cleanupOne(subtask) + doCleanupOne(subtask) end end end end -local function cleanup(...: any) +local function doCleanup(...: any) for index = 1, select("#", ...) do - cleanupOne(select(index, ...)) + doCleanupOne(select(index, ...)) end end -return cleanup \ No newline at end of file +return doCleanup \ No newline at end of file diff --git a/src/State/ForKeys.lua b/src/State/ForKeys.lua index 125064ad7..6935a0b70 100644 --- a/src/State/ForKeys.lua +++ b/src/State/ForKeys.lua @@ -20,7 +20,7 @@ local logErrorNonFatal = require(Package.Logging.logErrorNonFatal) local logError = require(Package.Logging.logError) local logWarn = require(Package.Logging.logWarn) -- Utility -local cleanup = require(Package.Memory.cleanup) +local doCleanup = require(Package.Memory.doCleanup) local needsDestruction = require(Package.Memory.needsDestruction) -- State local peek = require(Package.State.peek) @@ -133,7 +133,7 @@ function class:update(): boolean -- clean up the old calculated value local oldMetaValue = meta[oldOutKey] - local destructOK, err = xpcall(self._destructor or cleanup, parseError, oldOutKey, oldMetaValue) + local destructOK, err = xpcall(self._destructor or doCleanup, parseError, oldOutKey, oldMetaValue) if not destructOK then logErrorNonFatal("forKeysDestructorError", err) end @@ -177,7 +177,7 @@ function class:update(): boolean -- clean up the old calculated value local oldMetaValue = meta[outputKey] - local destructOK, err = xpcall(self._destructor or cleanup, parseError, outputKey, oldMetaValue) + local destructOK, err = xpcall(self._destructor or doCleanup, parseError, outputKey, oldMetaValue) if not destructOK then logErrorNonFatal("forKeysDestructorError", err) end diff --git a/src/State/ForPairs.lua b/src/State/ForPairs.lua index 31cb53426..72c63d138 100644 --- a/src/State/ForPairs.lua +++ b/src/State/ForPairs.lua @@ -20,7 +20,7 @@ local logErrorNonFatal = require(Package.Logging.logErrorNonFatal) local logError = require(Package.Logging.logError) local logWarn = require(Package.Logging.logWarn) -- Utility -local cleanup = require(Package.Memory.cleanup) +local doCleanup = require(Package.Memory.doCleanup) local needsDestruction = require(Package.Memory.needsDestruction) -- State local peek = require(Package.State.peek) @@ -159,7 +159,7 @@ function class:update(): boolean if oldOutValue ~= newOutValue then local oldMetaValue = meta[newOutKey] if oldOutValue ~= nil then - local destructOK, err = xpcall(self._destructor or cleanup, parseError, newOutKey, oldOutValue, oldMetaValue) + local destructOK, err = xpcall(self._destructor or doCleanup, parseError, newOutKey, oldOutValue, oldMetaValue) if not destructOK then logErrorNonFatal("forPairsDestructorError", err) end @@ -234,7 +234,7 @@ function class:update(): boolean -- clean up the old output pair local oldMetaValue = meta[oldOutKey] if oldOutValue ~= nil then - local destructOK, err = xpcall(self._destructor or cleanup, parseError, oldOutKey, oldOutValue, oldMetaValue) + local destructOK, err = xpcall(self._destructor or doCleanup, parseError, oldOutKey, oldOutValue, oldMetaValue) if not destructOK then logErrorNonFatal("forPairsDestructorError", err) end diff --git a/src/State/ForValues.lua b/src/State/ForValues.lua index 63e756240..53e34376e 100644 --- a/src/State/ForValues.lua +++ b/src/State/ForValues.lua @@ -19,7 +19,7 @@ local logError = require(Package.Logging.logError) local logErrorNonFatal = require(Package.Logging.logErrorNonFatal) local logWarn = require(Package.Logging.logWarn) -- Utility -local cleanup = require(Package.Memory.cleanup) +local doCleanup = require(Package.Memory.doCleanup) local needsDestruction = require(Package.Memory.needsDestruction) -- State local peek = require(Package.State.peek) @@ -132,7 +132,7 @@ function class:update(): boolean -- pass the old value to the destructor if it exists if value ~= nil then - local destructOK, err = xpcall(self._destructor or cleanup, parseError, value, meta) + local destructOK, err = xpcall(self._destructor or doCleanup, parseError, value, meta) if not destructOK then logErrorNonFatal("forValuesDestructorError", err) end @@ -183,7 +183,7 @@ function class:update(): boolean local oldValue = valueInfo.value local oldMetaValue = valueInfo.meta - local destructOK, err = xpcall(self._destructor or cleanup, parseError, oldValue, oldMetaValue) + local destructOK, err = xpcall(self._destructor or doCleanup, parseError, oldValue, oldMetaValue) if not destructOK then logErrorNonFatal("forValuesDestructorError", err) end diff --git a/src/init.lua b/src/init.lua index ccf9bbf14..84e124241 100644 --- a/src/init.lua +++ b/src/init.lua @@ -33,7 +33,7 @@ local Fusion = restrictRead("Fusion", { Tween = require(script.Animation.Tween), Spring = require(script.Animation.Spring), - cleanup = require(script.Memory.cleanup), + doCleanup = require(script.Memory.doCleanup), doNothing = require(script.Memory.doNothing), peek = require(script.State.peek) }) :: Fusion diff --git a/test/Memory/cleanup.spec.lua b/test/Memory/cleanup.spec.lua index 261479b57..dce92d16e 100644 --- a/test/Memory/cleanup.spec.lua +++ b/test/Memory/cleanup.spec.lua @@ -1,6 +1,6 @@ local Package = game:GetService("ReplicatedStorage").Fusion local New = require(Package.Instances.New) -local cleanup = require(Package.Memory.cleanup) +local doCleanup = require(Package.Memory.doCleanup) return function() it("should destroy instances", function() @@ -8,7 +8,7 @@ return function() -- one of the only reliable ways to test for proper destruction local conn = instance.AncestryChanged:Connect(function() end) - cleanup(instance) + doCleanup(instance) expect(conn.Connected).to.equal(false) end) @@ -17,7 +17,7 @@ return function() local instance = New "Folder" {} local conn = instance.AncestryChanged:Connect(function() end) - cleanup(conn) + doCleanup(conn) expect(conn.Connected).to.equal(false) end) @@ -25,7 +25,7 @@ return function() it("should invoke callbacks", function() local didRun = false - cleanup(function() + doCleanup(function() didRun = true end) @@ -35,7 +35,7 @@ return function() it("should invoke :destroy() methods", function() local didRun = false - cleanup({ + doCleanup({ destroy = function() didRun = true end @@ -47,7 +47,7 @@ return function() it("should invoke :Destroy() methods", function() local didRun = false - cleanup({ + doCleanup({ Destroy = function() didRun = true end @@ -63,7 +63,7 @@ return function() numRuns += 1 end - cleanup({doRun, doRun, doRun}) + doCleanup({doRun, doRun, doRun}) expect(numRuns).to.equal(3) end) @@ -75,7 +75,7 @@ return function() numRuns += 1 end - cleanup({{doRun, {doRun, {doRun}}}}) + doCleanup({{doRun, {doRun, {doRun}}}}) expect(numRuns).to.equal(3) end) @@ -97,7 +97,7 @@ return function() table.insert(runs, 2) end - cleanup(tasks) + doCleanup(tasks) expect(runs[1]).to.equal(1) expect(runs[2]).to.equal(2) @@ -111,7 +111,7 @@ return function() numRuns += 1 end - cleanup(doRun, doRun, doRun) + doCleanup(doRun, doRun, doRun) expect(numRuns).to.equal(3) end) diff --git a/test/init.spec.lua b/test/init.spec.lua index 0fe7a9f92..a4719ae43 100644 --- a/test/init.spec.lua +++ b/test/init.spec.lua @@ -30,7 +30,7 @@ return function() Tween = "function", Spring = "function", - cleanup = "function", + doCleanup = "function", doNothing = "function", peek = "function" } From 95c422aeb142ecc3ac10a5c8f99e38a0f89e207c Mon Sep 17 00:00:00 2001 From: Elttob Date: Sat, 2 Sep 2023 04:12:38 +0100 Subject: [PATCH 005/287] Computed, Value, Observer destructors --- src/State/Computed.lua | 18 ++++++++++++------ src/State/Observer.lua | 30 ++++++++---------------------- src/State/Value.lua | 7 +++++++ 3 files changed, 27 insertions(+), 28 deletions(-) diff --git a/src/State/Computed.lua b/src/State/Computed.lua index 89379edba..2e6f37ca7 100644 --- a/src/State/Computed.lua +++ b/src/State/Computed.lua @@ -21,7 +21,6 @@ local makeUseCallback = require(Package.State.makeUseCallback) local class = {} local CLASS_METATABLE = {__index = class} -local WEAK_KEYS_METATABLE = {__mode = "k"} --[[ Recalculates this Computed's cached value and dependencies. @@ -93,15 +92,22 @@ function class:get() logError("stateGetWasRemoved") end +function class:destroy() + for dependency in pairs(self.dependencySet) do + dependency.dependentSet[self] = nil + end + if self._destructor ~= nil then + self._destructor(self._value) + end + table.clear(self) +end + local function Computed(processor: () -> T, destructor: ((T) -> ())?): Types.Computed - local dependencySet = {} local self = setmetatable({ type = "State", kind = "Computed", - dependencySet = dependencySet, - -- if we held strong references to the dependents, then they wouldn't be - -- able to get garbage collected when they fall out of scope - dependentSet = setmetatable({}, WEAK_KEYS_METATABLE), + dependencySet = {}, + dependentSet = {}, _oldDependencySet = {}, _processor = processor, _destructor = destructor, diff --git a/src/State/Observer.lua b/src/State/Observer.lua index 7f07210dc..aa0d66cd6 100644 --- a/src/State/Observer.lua +++ b/src/State/Observer.lua @@ -16,9 +16,6 @@ type Set = {[T]: any} local class = {} local CLASS_METATABLE = {__index = class} --- Table used to hold Observer objects in memory. -local strongRefs: Set = {} - --[[ Called when the watched state changes value. ]] @@ -39,26 +36,9 @@ end ]] function class:onChange(callback: () -> ()): () -> () local uniqueIdentifier = {} - - self._numChangeListeners += 1 self._changeListeners[uniqueIdentifier] = callback - - -- disallow gc (this is important to make sure changes are received) - strongRefs[self] = true - - local disconnected = false return function() - if disconnected then - return - end - disconnected = true self._changeListeners[uniqueIdentifier] = nil - self._numChangeListeners -= 1 - - if self._numChangeListeners == 0 then - -- allow gc if all listeners are disconnected - strongRefs[self] = nil - end end end @@ -71,14 +51,20 @@ function class:onBind(callback: () -> ()): () -> () return self:onChange(callback) end +function class:destroy() + for dependency in pairs(self.dependencySet) do + dependency.dependentSet[self] = nil + end + table.clear(self) +end + local function Observer(watchedState: PubTypes.Value): Types.Observer local self = setmetatable({ type = "State", kind = "Observer", dependencySet = {[watchedState] = true}, dependentSet = {}, - _changeListeners = {}, - _numChangeListeners = 0, + _changeListeners = {} }, CLASS_METATABLE) -- add this object to the watched state's dependent set diff --git a/src/State/Value.lua b/src/State/Value.lua index 13b5f63fc..4583a6cbd 100644 --- a/src/State/Value.lua +++ b/src/State/Value.lua @@ -45,6 +45,13 @@ function class:get() logError("stateGetWasRemoved") end +function class:destroy() + for dependency in pairs(self.dependencySet) do + dependency.dependentSet[self] = nil + end + table.clear(self) +end + local function Value(initialValue: T): Types.State local self = setmetatable({ type = "State", From 0b8d7a2cd1096993e7af17a25aced5ccaf899ae2 Mon Sep 17 00:00:00 2001 From: Elttob Date: Sat, 2 Sep 2023 04:27:57 +0100 Subject: [PATCH 006/287] Adjust return type of scoped() --- src/Memory/scoped.lua | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/Memory/scoped.lua b/src/Memory/scoped.lua index 7e175e532..a0a3a3b4c 100644 --- a/src/Memory/scoped.lua +++ b/src/Memory/scoped.lua @@ -4,9 +4,12 @@ Creates cleanup tables with access to constructors as methods. ]] +local Package = script.Parent.Parent +local PubTypes = require(Package.PubTypes) + -- This return type is technically a lie, but it's required for useful type -- checking behaviour. -local function scoped(constructors: T): T +local function scoped(constructors: T): {PubTypes.Task} & T return setmetatable({}, {__index = constructors}) :: any end From ceec221cdb20390606a09ab83ea1b727b169352d Mon Sep 17 00:00:00 2001 From: Elttob Date: Sun, 3 Sep 2023 00:13:28 +0100 Subject: [PATCH 007/287] Cleanup table parameters + tween/spring stuff --- src/Animation/Spring.lua | 9 +++++++++ src/Animation/Tween.lua | 9 +++++++++ src/State/Computed.lua | 8 +++++++- src/State/ForKeys.lua | 6 +++--- src/State/ForPairs.lua | 6 +++--- src/State/ForValues.lua | 6 +++--- src/State/Observer.lua | 6 +++++- src/State/Value.lua | 8 +++++++- 8 files changed, 46 insertions(+), 12 deletions(-) diff --git a/src/Animation/Spring.lua b/src/Animation/Spring.lua index 295d5e392..79dd2fe36 100644 --- a/src/Animation/Spring.lua +++ b/src/Animation/Spring.lua @@ -160,7 +160,15 @@ function class:get() logError("stateGetWasRemoved") end +function class:destroy() + for dependency in pairs(self.dependencySet) do + dependency.dependentSet[self] = nil + end + table.clear(self) +end + local function Spring( + cleanupTable: {PubTypes.Task}, goalState: PubTypes.Value, speed: PubTypes.CanBeState?, damping: PubTypes.CanBeState? @@ -207,6 +215,7 @@ local function Spring( -- add this object to the goal state's dependent set goalState.dependentSet[self] = true self:update() + table.insert(self, cleanupTable) return self end diff --git a/src/Animation/Tween.lua b/src/Animation/Tween.lua index 9430e1ba8..76c87b0f1 100644 --- a/src/Animation/Tween.lua +++ b/src/Animation/Tween.lua @@ -70,7 +70,15 @@ function class:get() logError("stateGetWasRemoved") end +function class:destroy() + for dependency in pairs(self.dependencySet) do + dependency.dependentSet[self] = nil + end + table.clear(self) +end + local function Tween( + cleanupTable: {PubTypes.Task}, goalState: PubTypes.StateObject, tweenInfo: PubTypes.CanBeState? ): Types.Tween @@ -118,6 +126,7 @@ local function Tween( -- add this object to the goal state's dependent set goalState.dependentSet[self] = true + table.insert(cleanupTable, self) return self end diff --git a/src/State/Computed.lua b/src/State/Computed.lua index 2e6f37ca7..8bf75008d 100644 --- a/src/State/Computed.lua +++ b/src/State/Computed.lua @@ -6,6 +6,7 @@ ]] local Package = script.Parent.Parent +local PubTypes = require(Package.PubTypes) local Types = require(Package.Types) -- Logging local logError = require(Package.Logging.logError) @@ -102,7 +103,11 @@ function class:destroy() table.clear(self) end -local function Computed(processor: () -> T, destructor: ((T) -> ())?): Types.Computed +local function Computed( + cleanupTable: {PubTypes.Task}, + processor: () -> T, + destructor: ((T) -> ())? +): Types.Computed local self = setmetatable({ type = "State", kind = "Computed", @@ -115,6 +120,7 @@ local function Computed(processor: () -> T, destructor: ((T) -> ())?): Types. }, CLASS_METATABLE) self:update() + table.insert(cleanupTable, self) return self end diff --git a/src/State/ForKeys.lua b/src/State/ForKeys.lua index 6935a0b70..001c03f45 100644 --- a/src/State/ForKeys.lua +++ b/src/State/ForKeys.lua @@ -210,13 +210,12 @@ function class:get() end local function ForKeys( + cleanupTable: {PubTypes.Task}, inputTable: PubTypes.CanBeState<{ [KI]: any }>, processor: (KI) -> (KO, M?), destructor: (KO, M?) -> ()? ): Types.ForKeys - local inputIsState = isState(inputTable) - local self = setmetatable({ type = "State", kind = "ForKeys", @@ -228,7 +227,7 @@ local function ForKeys( _processor = processor, _destructor = destructor, - _inputIsState = inputIsState, + _inputIsState = isState(inputTable), _inputTable = inputTable, _oldInputTable = {}, @@ -240,6 +239,7 @@ local function ForKeys( }, CLASS_METATABLE) self:update() + table.insert(cleanupTable, self) return self end diff --git a/src/State/ForPairs.lua b/src/State/ForPairs.lua index 72c63d138..74090747a 100644 --- a/src/State/ForPairs.lua +++ b/src/State/ForPairs.lua @@ -272,13 +272,12 @@ function class:get() end local function ForPairs( + cleanupTable: {PubTypes.Task}, inputTable: PubTypes.CanBeState<{ [KI]: VI }>, processor: (KI, VI) -> (KO, VO, M?), destructor: (KO, VO, M?) -> ()? ): Types.ForPairs - local inputIsState = isState(inputTable) - local self = setmetatable({ type = "State", kind = "ForPairs", @@ -290,7 +289,7 @@ local function ForPairs( _processor = processor, _destructor = destructor, - _inputIsState = inputIsState, + _inputIsState = isState(inputTable), _inputTable = inputTable, _oldInputTable = {}, @@ -302,6 +301,7 @@ local function ForPairs( }, CLASS_METATABLE) self:update() + table.insert(cleanupTable, self) return self end diff --git a/src/State/ForValues.lua b/src/State/ForValues.lua index 53e34376e..5494e17d8 100644 --- a/src/State/ForValues.lua +++ b/src/State/ForValues.lua @@ -211,13 +211,12 @@ function class:get() end local function ForValues( + cleanupTable: {PubTypes.Task}, inputTable: PubTypes.CanBeState<{ [any]: VI }>, processor: (VI) -> (VO, M?), destructor: (VO, M?) -> ()? ): Types.ForValues - local inputIsState = isState(inputTable) - local self = setmetatable({ type = "State", kind = "ForValues", @@ -229,7 +228,7 @@ local function ForValues( _processor = processor, _destructor = destructor, - _inputIsState = inputIsState, + _inputIsState = isState(inputTable), _inputTable = inputTable, _outputTable = {}, @@ -238,6 +237,7 @@ local function ForValues( }, CLASS_METATABLE) self:update() + table.insert(cleanupTable, self) return self end diff --git a/src/State/Observer.lua b/src/State/Observer.lua index aa0d66cd6..10d5a61c3 100644 --- a/src/State/Observer.lua +++ b/src/State/Observer.lua @@ -58,7 +58,10 @@ function class:destroy() table.clear(self) end -local function Observer(watchedState: PubTypes.Value): Types.Observer +local function Observer( + cleanupTable: {PubTypes.Task}, + watchedState: PubTypes.Value +): Types.Observer local self = setmetatable({ type = "State", kind = "Observer", @@ -69,6 +72,7 @@ local function Observer(watchedState: PubTypes.Value): Types.Observer -- add this object to the watched state's dependent set watchedState.dependentSet[self] = true + table.insert(cleanupTable, self) return self end diff --git a/src/State/Value.lua b/src/State/Value.lua index 4583a6cbd..f161d2c4f 100644 --- a/src/State/Value.lua +++ b/src/State/Value.lua @@ -6,6 +6,7 @@ ]] local Package = script.Parent.Parent +local PubTypes = require(Package.PubTypes) local Types = require(Package.Types) -- Logging local logError = require(Package.Logging.logError) @@ -52,7 +53,10 @@ function class:destroy() table.clear(self) end -local function Value(initialValue: T): Types.State +local function Value( + cleanupTable: {PubTypes.Task}, + initialValue: T +): Types.State local self = setmetatable({ type = "State", kind = "Value", @@ -62,6 +66,8 @@ local function Value(initialValue: T): Types.State _value = initialValue }, CLASS_METATABLE) + table.insert(cleanupTable, self) + return self end From 4699a9401a6cfcf5eaef7132309a046b453f5d18 Mon Sep 17 00:00:00 2001 From: Elttob Date: Sun, 3 Sep 2023 00:59:45 +0100 Subject: [PATCH 008/287] Value spec updates --- src/State/Value.lua | 9 +-------- test/State/Value.spec.lua | 39 +++++++++++++++++++-------------------- 2 files changed, 20 insertions(+), 28 deletions(-) diff --git a/src/State/Value.lua b/src/State/Value.lua index f161d2c4f..5b74d4bc9 100644 --- a/src/State/Value.lua +++ b/src/State/Value.lua @@ -18,7 +18,6 @@ local isSimilar = require(Package.Utility.isSimilar) local class = {} local CLASS_METATABLE = {__index = class} -local WEAK_KEYS_METATABLE = {__mode = "k"} --[[ Updates the value stored in this State object. @@ -47,10 +46,6 @@ function class:get() end function class:destroy() - for dependency in pairs(self.dependencySet) do - dependency.dependentSet[self] = nil - end - table.clear(self) end local function Value( @@ -60,9 +55,7 @@ local function Value( local self = setmetatable({ type = "State", kind = "Value", - -- if we held strong references to the dependents, then they wouldn't be - -- able to get garbage collected when they fall out of scope - dependentSet = setmetatable({}, WEAK_KEYS_METATABLE), + dependentSet = {}, _value = initialValue }, CLASS_METATABLE) diff --git a/test/State/Value.spec.lua b/test/State/Value.spec.lua index 0ce387c84..834d8f000 100644 --- a/test/State/Value.spec.lua +++ b/test/State/Value.spec.lua @@ -2,41 +2,40 @@ local Package = game:GetService("ReplicatedStorage").Fusion local Value = require(Package.State.Value) local ForValues = require(Package.State.ForValues) local peek = require(Package.State.peek) +local doCleanup = require(Package.Memory.doCleanup) return function() - it("should construct a Value object", function() - local value = Value() + it("should construct in scopes", function() + local scope = {} + local value = Value(scope) expect(value).to.be.a("table") expect(value.type).to.equal("State") expect(value.kind).to.equal("Value") + expect(scope[1]).to.equal(value) + + doCleanup(scope) end) - it("should be able to store arbitrary values", function() - local value = Value(0) + it("should be settable", function() + local scope = {} + local value = Value(scope, 0) expect(peek(value)).to.equal(0) value:set(10) expect(peek(value)).to.equal(10) - value:set(Value) - expect(peek(value)).to.equal(Value) - end) - - it("should garbage-collect unused objects", function() - local value = setmetatable({ { 2 } }, { __mode = "kv" }) - Value(value[1]) + value:set("foo") + expect(peek(value)).to.equal("foo") - expect(value[1]).to.equal(nil) + doCleanup(scope) end) - it("should not garbage-collect objects in use", function() - local value = setmetatable({ { 2 } }, { __mode = "kv" }) - local transformed = ForValues(Value(value[1]), function(innerValue) - return innerValue[1] + 1 - end) - - expect(value[1]).never.to.equal(nil) - expect(peek(transformed)[1]).to.equal(3) + it("should be destroyable", function() + local value = Value({}) + expect(value.destroy).to.be.a("function") + expect(function() + value:destroy() + end).to.never.throw() end) end From 092587b664ae9a13091366a88c47c1ebce6025c5 Mon Sep 17 00:00:00 2001 From: Elttob Date: Sun, 3 Sep 2023 01:06:02 +0100 Subject: [PATCH 009/287] Value spec refactors --- test/State/Value.spec.lua | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/test/State/Value.spec.lua b/test/State/Value.spec.lua index 834d8f000..a3848539a 100644 --- a/test/State/Value.spec.lua +++ b/test/State/Value.spec.lua @@ -1,6 +1,5 @@ local Package = game:GetService("ReplicatedStorage").Fusion local Value = require(Package.State.Value) -local ForValues = require(Package.State.ForValues) local peek = require(Package.State.peek) local doCleanup = require(Package.Memory.doCleanup) @@ -17,6 +16,14 @@ return function() doCleanup(scope) end) + it("should be destroyable", function() + local value = Value({}) + expect(value.destroy).to.be.a("function") + expect(function() + value:destroy() + end).to.never.throw() + end) + it("should be settable", function() local scope = {} local value = Value(scope, 0) @@ -30,12 +37,4 @@ return function() doCleanup(scope) end) - - it("should be destroyable", function() - local value = Value({}) - expect(value.destroy).to.be.a("function") - expect(function() - value:destroy() - end).to.never.throw() - end) end From 187755194c8f4d9651434dd935a3637a8cd6b569 Mon Sep 17 00:00:00 2001 From: Elttob Date: Sun, 3 Sep 2023 01:09:20 +0100 Subject: [PATCH 010/287] Concise spec messages --- test/State/Value.spec.lua | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/test/State/Value.spec.lua b/test/State/Value.spec.lua index a3848539a..e737d5589 100644 --- a/test/State/Value.spec.lua +++ b/test/State/Value.spec.lua @@ -4,7 +4,7 @@ local peek = require(Package.State.peek) local doCleanup = require(Package.Memory.doCleanup) return function() - it("should construct in scopes", function() + it("constructs in scopes", function() local scope = {} local value = Value(scope) @@ -16,7 +16,7 @@ return function() doCleanup(scope) end) - it("should be destroyable", function() + it("is destroyable", function() local value = Value({}) expect(value.destroy).to.be.a("function") expect(function() @@ -24,7 +24,7 @@ return function() end).to.never.throw() end) - it("should be settable", function() + it("is settable", function() local scope = {} local value = Value(scope, 0) expect(peek(value)).to.equal(0) From c23d6fa5554dd78a385e838c2e85cfd3da34b74b Mon Sep 17 00:00:00 2001 From: Elttob Date: Sun, 3 Sep 2023 01:23:12 +0100 Subject: [PATCH 011/287] Default value test --- test/State/Value.spec.lua | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/test/State/Value.spec.lua b/test/State/Value.spec.lua index e737d5589..f191fe8af 100644 --- a/test/State/Value.spec.lua +++ b/test/State/Value.spec.lua @@ -24,6 +24,13 @@ return function() end).to.never.throw() end) + it("accepts a default value", function() + local scope = {} + local value = Value(scope, 5) + expect(peek(value)).to.equal(5) + doCleanup(scope) + end) + it("is settable", function() local scope = {} local value = Value(scope, 0) From c45b17f6b73aeed9131bdd56dfb22099d2a2a776 Mon Sep 17 00:00:00 2001 From: Elttob Date: Sun, 3 Sep 2023 01:43:56 +0100 Subject: [PATCH 012/287] Computed spec --- test/State/Computed.spec.lua | 176 ++++++++++++++--------------------- 1 file changed, 70 insertions(+), 106 deletions(-) diff --git a/test/State/Computed.spec.lua b/test/State/Computed.spec.lua index 193baccaa..deab7f850 100644 --- a/test/State/Computed.spec.lua +++ b/test/State/Computed.spec.lua @@ -1,134 +1,98 @@ -local RunService = game:GetService("RunService") - local Package = game:GetService("ReplicatedStorage").Fusion local Computed = require(Package.State.Computed) local Value = require(Package.State.Value) local peek = require(Package.State.peek) +local doCleanup = require(Package.Memory.doCleanup) return function() - it("should construct a Computed object", function() - local computed = Computed(function(use) end) + it("constructs in scopes", function() + local scope = {} + local computed = Computed(scope, function() + -- intentionally blank + end) expect(computed).to.be.a("table") expect(computed.type).to.equal("State") expect(computed.kind).to.equal("Computed") - end) + expect(scope[1]).to.equal(computed) - it("should calculate and retrieve its value", function() - local computed = Computed(function(use) - return "foo" - end) + doCleanup(scope) + end) - expect(peek(computed)).to.equal("foo") + it("is destroyable", function() + local scope = {} + local computed = Computed(scope) + expect(function() + computed:destroy() + end).to.never.throw() end) - it("should recalculate its value in response to State objects", function() - local currentNumber = Value(2) - local doubled = Computed(function(use) - return use(currentNumber) * 2 + it("uses constants", function() + local scope = {} + local computed = Computed(scope, function(use) + return use(5) end) - - expect(peek(doubled)).to.equal(4) - - currentNumber:set(4) - expect(peek(doubled)).to.equal(8) + expect(peek(computed)).to.equal(5) + doCleanup(scope) end) - it("should recalculate its value in response to Computed objects", function() - local currentNumber = Value(2) - local doubled = Computed(function(use) - return use(currentNumber) * 2 + it("uses state objects", function() + local scope = {} + local dependency = Value(scope, 5) + local computed = Computed(scope, function(use) + return use(dependency) end) - local tripled = Computed(function(use) - return use(doubled) * 1.5 - end) - - expect(peek(tripled)).to.equal(6) - - currentNumber:set(4) - expect(peek(tripled)).to.equal(12) + expect(peek(computed)).to.equal(5) + dependency:set("foo") + expect(peek(computed)).to.equal("foo") + doCleanup(scope) end) - it("should not corrupt dependencies after an error", function() - local state = Value(1) - local simulateError = false + itFOCUS("preserves value on error", function() + local scope = {} + local dependency = Value(scope, 5) local computed = Computed(function(use) - if simulateError then - -- in a naive implementation, this would corrupt dependencies as - -- use(state) hasn't been captured yet, preventing future - -- reactive updates from taking place - -- to avoid this, dependencies captured when a callback errors - -- have to be discarded - error("This is an intentional error from a unit test") - end - - return use(state) + assert(use(dependency) ~= 13, "This is an intentional error from a unit test") + return use(dependency) end) - - expect(peek(computed)).to.equal(1) - - simulateError = true - state:set(5) -- update the computed to invoke the error - - simulateError = false - state:set(10) -- if dependencies are corrupt, the computed won't update - - expect(peek(computed)).to.equal(10) + expect(peek(computed)).to.equal(5) + dependency:set(13) -- this will invoke the error + expect(peek(computed)).to.equal(5) + dependency:set(2) + expect(peek(computed)).to.equal(2) + doCleanup(scope) end) - it("should garbage-collect unused objects", function() - local state = Value(2) - - local counter = 0 - - do - local computed = Computed(function(use) - counter += 1 - return use(state) - end) - end - - state:set(5) - - expect(counter).to.equal(1) - end) - - it("should not garbage-collect objects in use", function() - local state = Value(2) - local computed2 - - local counter = 0 - - do - local computed = Computed(function(use) - counter += 1 - return use(state) - end) - - computed2 = Computed(function(use) - return use(computed) - end) - end - - state:set(5) - - expect(counter).to.equal(2) + itFOCUS("calls destructor on update", function() + local scope = {} + local destructed = {} + local dependency = Value(scope, 1) + local _ = Computed(scope, function(use) + return use(dependency) + end, function(value) + destructed[value] = true + end) + expect(destructed[1]).to.equal(nil) + dependency:set(2) + expect(destructed[1]).to.equal(true) + expect(destructed[2]).to.equal(nil) + dependency:set(3) + expect(destructed[2]).to.equal(true) + + doCleanup(scope) end) - it("should call destructors when old values are replaced", function() - local didRun = false - local function destructor(x) - if x == "old" then - didRun = true - end - end - - local value = Value("old") - local computed = Computed(function(use) - return use(value) - end, destructor) - value:set("new") - - expect(didRun).to.equal(true) + itFOCUS("calls destructor on destroy", function() + local scope = {} + local destructed = {} + local dependency = Value(scope, 1) + local _ = Computed(scope, function(use) + return use(dependency) + end, function(value) + destructed[value] = true + end) + expect(destructed[1]).to.equal(nil) + doCleanup(scope) + expect(destructed[1]).to.equal(true) end) end From 179fcae200b937cc30cb904801d5a2f693511082 Mon Sep 17 00:00:00 2001 From: Elttob Date: Sun, 3 Sep 2023 01:44:47 +0100 Subject: [PATCH 013/287] Remove table.clear calls to self --- src/Animation/Spring.lua | 1 - src/Animation/Tween.lua | 1 - src/State/Computed.lua | 1 - src/State/Observer.lua | 1 - 4 files changed, 4 deletions(-) diff --git a/src/Animation/Spring.lua b/src/Animation/Spring.lua index 79dd2fe36..d707f9d12 100644 --- a/src/Animation/Spring.lua +++ b/src/Animation/Spring.lua @@ -164,7 +164,6 @@ function class:destroy() for dependency in pairs(self.dependencySet) do dependency.dependentSet[self] = nil end - table.clear(self) end local function Spring( diff --git a/src/Animation/Tween.lua b/src/Animation/Tween.lua index 76c87b0f1..04c9dd535 100644 --- a/src/Animation/Tween.lua +++ b/src/Animation/Tween.lua @@ -74,7 +74,6 @@ function class:destroy() for dependency in pairs(self.dependencySet) do dependency.dependentSet[self] = nil end - table.clear(self) end local function Tween( diff --git a/src/State/Computed.lua b/src/State/Computed.lua index 8bf75008d..4c2b71c9a 100644 --- a/src/State/Computed.lua +++ b/src/State/Computed.lua @@ -100,7 +100,6 @@ function class:destroy() if self._destructor ~= nil then self._destructor(self._value) end - table.clear(self) end local function Computed( diff --git a/src/State/Observer.lua b/src/State/Observer.lua index 10d5a61c3..0ca821526 100644 --- a/src/State/Observer.lua +++ b/src/State/Observer.lua @@ -55,7 +55,6 @@ function class:destroy() for dependency in pairs(self.dependencySet) do dependency.dependentSet[self] = nil end - table.clear(self) end local function Observer( From 5ef7275164998fa50731f0342e8cbbbde768240a Mon Sep 17 00:00:00 2001 From: Elttob Date: Sun, 3 Sep 2023 02:03:25 +0100 Subject: [PATCH 014/287] Don't call destructor on creation --- src/State/Computed.lua | 16 +++++++++++++--- test/State/Computed.spec.lua | 21 +++++++++++++++++---- 2 files changed, 30 insertions(+), 7 deletions(-) diff --git a/src/State/Computed.lua b/src/State/Computed.lua index 4c2b71c9a..4e4988535 100644 --- a/src/State/Computed.lua +++ b/src/State/Computed.lua @@ -23,11 +23,21 @@ local class = {} local CLASS_METATABLE = {__index = class} +--[[ + Called when a dependency changes value. + Returns true if this object changed value. +]] +function class:update(): boolean + return self:_recalculate(false) +end + --[[ Recalculates this Computed's cached value and dependencies. Returns true if it changed, or false if it's identical. ]] -function class:update(): boolean +function class:_recalculate( + firstTime: boolean +): boolean -- remove this object from its dependencies' dependent sets for dependency in pairs(self.dependencySet) do dependency.dependentSet[self] = nil @@ -54,7 +64,7 @@ function class:update(): boolean local oldValue = self._value local similar = isSimilar(oldValue, newValue) - if self._destructor ~= nil then + if self._destructor ~= nil and not firstTime then self._destructor(oldValue) end self._value = newValue @@ -118,7 +128,7 @@ local function Computed( _value = nil }, CLASS_METATABLE) - self:update() + self:_recalculate(true) table.insert(cleanupTable, self) return self diff --git a/test/State/Computed.spec.lua b/test/State/Computed.spec.lua index deab7f850..3759ee952 100644 --- a/test/State/Computed.spec.lua +++ b/test/State/Computed.spec.lua @@ -48,10 +48,10 @@ return function() doCleanup(scope) end) - itFOCUS("preserves value on error", function() + it("preserves value on error", function() local scope = {} local dependency = Value(scope, 5) - local computed = Computed(function(use) + local computed = Computed(scope, function(use) assert(use(dependency) ~= 13, "This is an intentional error from a unit test") return use(dependency) end) @@ -63,7 +63,20 @@ return function() doCleanup(scope) end) - itFOCUS("calls destructor on update", function() + it("doesn't call destructor on creation", function() + local scope = {} + local destructed = false + local _ = Computed(scope, function() + -- intentionally blank + end, function() + destructed = true + end) + expect(destructed).to.equal(false) + + doCleanup(scope) + end) + + it("calls destructor on update", function() local scope = {} local destructed = {} local dependency = Value(scope, 1) @@ -82,7 +95,7 @@ return function() doCleanup(scope) end) - itFOCUS("calls destructor on destroy", function() + it("calls destructor on destroy", function() local scope = {} local destructed = {} local dependency = Value(scope, 1) From 7242de7e934553318d8a226cebaf49e6299bd3c1 Mon Sep 17 00:00:00 2001 From: Elttob Date: Sun, 3 Sep 2023 07:30:50 +0100 Subject: [PATCH 015/287] Observer unit tests --- test/State/Observer.spec.lua | 130 ++++++++++++++++++++++++----------- 1 file changed, 88 insertions(+), 42 deletions(-) diff --git a/test/State/Observer.spec.lua b/test/State/Observer.spec.lua index f3d74838e..89e1ba0fc 100644 --- a/test/State/Observer.spec.lua +++ b/test/State/Observer.spec.lua @@ -1,66 +1,112 @@ -local RunService = game:GetService("RunService") - local Package = game:GetService("ReplicatedStorage").Fusion local Observer = require(Package.State.Observer) local Value = require(Package.State.Value) local peek = require(Package.State.peek) +local doCleanup = require(Package.Memory.doCleanup) return function() - it("should fire connections on change", function() - local state = Value(false) - local observer = Observer(state) - - local changed = false - observer:onChange(function() - changed = true - end) + it("constructs in scopes", function() + local scope = {} + local dependency = Value(scope, 5) + local observer = Observer(scope, dependency) - state:set(true) + expect(observer).to.be.a("table") + expect(observer.type).to.equal("State") + expect(observer.kind).to.equal("Observer") + expect(scope[2]).to.equal(observer) - -- Wait twice in case it gets deferred - RunService.RenderStepped:Wait() - RunService.RenderStepped:Wait() - - expect(changed).to.equal(true) + doCleanup(scope) end) - it("should fire connections only after the value changes", function() - local state = Value(false) - local observer = Observer(state) + it("is destroyable", function() + local scope = {} + local dependency = Value(scope, 5) + local observer = Observer(scope, dependency) + expect(function() + observer:destroy() + end).to.never.throw() + end) - local changedValue - local completed = false - observer:onChange(function(value) - changedValue = peek(state) - completed = true + it("fires once after change", function() + local scope = {} + local dependency = Value(scope, 5) + local observer = Observer(scope, dependency) + local numFires = 0 + local disconnect = observer:onChange(function() + numFires += 1 end) + dependency:set(15) + disconnect() - state:set(true) + expect(numFires).to.equal(1) + + doCleanup(scope) + end) + + it("fires asynchronously", function() + local scope = {} + local dependency = Value(scope, 5) + local observer = Observer(scope, dependency) + local numFires = 0 + local disconnect = observer:onChange(function() + task.wait(1) + numFires += 1 + end) + dependency:set(15) + disconnect() - -- Wait twice in case it gets deferred - RunService.RenderStepped:Wait() - RunService.RenderStepped:Wait() + expect(numFires).to.equal(0) - expect(changedValue).to.equal(true) + doCleanup(scope) end) - it("should fire connections only once after the value changes", function() - local state = Value(false) - local observer = Observer(state) + it("fires onBind at bind time", function() + local scope = {} + local dependency = Value(scope, 5) + local observer = Observer(scope, dependency) + local numFires = 0 + local disconnect = observer:onBind(function() + numFires += 1 + end) + disconnect() + + expect(numFires).to.equal(1) - local timesFired = 0 - local completed = false - observer:onChange(function(value) - timesFired += 1 - completed = true + doCleanup(scope) + end) + + it("disconnects properly", function() + local scope = {} + local dependency = Value(scope, 5) + local observer = Observer(scope, dependency) + local numFires = 0 + local disconnect = observer:onChange(function() + numFires += 1 end) + dependency:set(15) + disconnect() + dependency:set(2) - state:set(true) + expect(numFires).to.equal(1) - -- Wait twice in case it gets deferred - RunService.RenderStepped:Wait() - RunService.RenderStepped:Wait() + doCleanup(scope) + end) - expect(timesFired).to.equal(1) + it("disconnects on destroy", function() + local scope = {} + local dependency = Value(scope, 5) + local observer = Observer({}, dependency) + local numFires = 0 + local _ = observer:onChange(function() + numFires += 1 + end) + dependency:set(15) + observer:destroy() + dependency:set(2) + + expect(numFires).to.equal(1) + + doCleanup(scope) end) + end From a6b3c7c9478126bdaa73a1ff79bdca3cd4536f84 Mon Sep 17 00:00:00 2001 From: Elttob Date: Sun, 3 Sep 2023 07:44:16 +0100 Subject: [PATCH 016/287] Update language in observer spec --- test/State/Observer.spec.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/State/Observer.spec.lua b/test/State/Observer.spec.lua index 89e1ba0fc..f170e91f0 100644 --- a/test/State/Observer.spec.lua +++ b/test/State/Observer.spec.lua @@ -75,7 +75,7 @@ return function() doCleanup(scope) end) - it("disconnects properly", function() + it("disconnects manually", function() local scope = {} local dependency = Value(scope, 5) local observer = Observer(scope, dependency) From 1bd72c31ce1d578b23c32c0e35a257cb5493b2ee Mon Sep 17 00:00:00 2001 From: Elttob Date: Sun, 3 Sep 2023 21:57:44 +0100 Subject: [PATCH 017/287] ForKeys destroy method --- src/State/ForKeys.lua | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/src/State/ForKeys.lua b/src/State/ForKeys.lua index 001c03f45..5f74f541b 100644 --- a/src/State/ForKeys.lua +++ b/src/State/ForKeys.lua @@ -209,6 +209,23 @@ function class:get() logError("stateGetWasRemoved") end +function class:destroy() + for dependency in pairs(self.dependencySet) do + dependency.dependentSet[self] = nil + end + + local keyOIMap = self._keyOIMap + local meta = self._meta + for outputKey, _ in pairs(keyOIMap) do + -- clean up the old calculated value + local oldMetaValue = meta[outputKey] + local destructOK, err = xpcall(self._destructor or doCleanup, parseError, outputKey, oldMetaValue) + if not destructOK then + logErrorNonFatal("forKeysDestructorError", err) + end + end +end + local function ForKeys( cleanupTable: {PubTypes.Task}, inputTable: PubTypes.CanBeState<{ [KI]: any }>, From eea2cbcaf8baf134a355748e92042c5d6b81d06f Mon Sep 17 00:00:00 2001 From: Elttob Date: Sun, 3 Sep 2023 22:02:38 +0100 Subject: [PATCH 018/287] Language change for COmputed spec --- test/State/Computed.spec.lua | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/State/Computed.spec.lua b/test/State/Computed.spec.lua index 3759ee952..ce979625a 100644 --- a/test/State/Computed.spec.lua +++ b/test/State/Computed.spec.lua @@ -27,7 +27,7 @@ return function() end).to.never.throw() end) - it("uses constants", function() + it("computes with constants", function() local scope = {} local computed = Computed(scope, function(use) return use(5) @@ -36,7 +36,7 @@ return function() doCleanup(scope) end) - it("uses state objects", function() + it("computes with state objects", function() local scope = {} local dependency = Value(scope, 5) local computed = Computed(scope, function(use) From df6fc395549e632babb8970d417499050aec7588 Mon Sep 17 00:00:00 2001 From: Elttob Date: Sun, 3 Sep 2023 22:04:48 +0100 Subject: [PATCH 019/287] Fix type issue in Computed spec --- test/State/Computed.spec.lua | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/test/State/Computed.spec.lua b/test/State/Computed.spec.lua index ce979625a..f62cf03c0 100644 --- a/test/State/Computed.spec.lua +++ b/test/State/Computed.spec.lua @@ -21,7 +21,9 @@ return function() it("is destroyable", function() local scope = {} - local computed = Computed(scope) + local computed = Computed(scope, function() + -- intentionally blank + end) expect(function() computed:destroy() end).to.never.throw() From dc53563951d3a58c01f40e8aa46c3bec0672c684 Mon Sep 17 00:00:00 2001 From: Elttob Date: Sun, 3 Sep 2023 22:31:06 +0100 Subject: [PATCH 020/287] Silence non fatal errors during unit test --- src/External.lua | 4 + src/Logging/logErrorNonFatal.lua | 5 + test-runner/Run.client.lua | 3 + test/State/Computed.spec.lua | 2 +- test/State/ForKeys.spec.lua | 377 +++++++++++-------------------- 5 files changed, 151 insertions(+), 240 deletions(-) diff --git a/src/External.lua b/src/External.lua index 0b274b1d8..fd7c65590 100644 --- a/src/External.lua +++ b/src/External.lua @@ -9,6 +9,10 @@ local logError = require(Package.Logging.logError) local External = {} +-- Silences non-fatal error logging, used in unit tests for cleaning up output +-- and removing false negatives when checking how systems respond to errors. +External.unitTestSilenceNonFatal = false + export type Scheduler = { doTaskImmediate: ( resume: () -> () diff --git a/src/Logging/logErrorNonFatal.lua b/src/Logging/logErrorNonFatal.lua index e5b740d86..e4edc8f54 100644 --- a/src/Logging/logErrorNonFatal.lua +++ b/src/Logging/logErrorNonFatal.lua @@ -6,9 +6,14 @@ local Package = script.Parent.Parent local Types = require(Package.Types) +local External = require(Package.External) local messages = require(Package.Logging.messages) local function logErrorNonFatal(messageID: string, errObj: Types.Error?, ...) + if External.unitTestSilenceNonFatal then + return + end + local formatString: string if messages[messageID] ~= nil then diff --git a/test-runner/Run.client.lua b/test-runner/Run.client.lua index f9b292dc4..f8282d15f 100644 --- a/test-runner/Run.client.lua +++ b/test-runner/Run.client.lua @@ -9,9 +9,12 @@ local RUN_BENCHMARKS = false -- run unit tests if RUN_TESTS then print("Running unit tests...") + local External = require(ReplicatedStorage.Fusion.External) + External.unitTestSilenceNonFatal = true local data = TestEZ.TestBootstrap:run({ ReplicatedStorage.FusionTest }) + External.unitTestSilenceNonFatal = false if data.failureCount > 0 then return diff --git a/test/State/Computed.spec.lua b/test/State/Computed.spec.lua index f62cf03c0..30d9f2533 100644 --- a/test/State/Computed.spec.lua +++ b/test/State/Computed.spec.lua @@ -50,7 +50,7 @@ return function() doCleanup(scope) end) - it("preserves value on error", function() + itFOCUS("preserves value on error", function() local scope = {} local dependency = Value(scope, 5) local computed = Computed(scope, function(use) diff --git a/test/State/ForKeys.spec.lua b/test/State/ForKeys.spec.lua index 47b6f5dc5..5be715195 100644 --- a/test/State/ForKeys.spec.lua +++ b/test/State/ForKeys.spec.lua @@ -1,276 +1,175 @@ -local RunService = game:GetService("RunService") - local Package = game:GetService("ReplicatedStorage").Fusion local ForKeys = require(Package.State.ForKeys) local Value = require(Package.State.Value) local peek = require(Package.State.peek) +local doCleanup = require(Package.Memory.doCleanup) return function() - it("should construct a ForKeys object", function() - local forKeys = ForKeys({}, function(use) end) - - expect(forKeys).to.be.a("table") - expect(forKeys.type).to.equal("State") - expect(forKeys.kind).to.equal("ForKeys") - end) - - it("should calculate and retrieve its value", function() - local computedPair = ForKeys({ ["foo"] = true }, function(use, key) - return key .. "baz" + it("constructs in scopes", function() + local scope = {} + local forkeys = ForKeys(scope, {}, function() + -- intentionally blank end) - local state = peek(computedPair) + expect(forkeys).to.be.a("table") + expect(forkeys.type).to.equal("State") + expect(forkeys.kind).to.equal("ForKeys") + expect(scope[1]).to.equal(forkeys) - expect(state["foobaz"]).to.be.ok() - expect(state["foobaz"]).to.equal(true) + doCleanup(scope) end) - it("should not recalculate its KO in response to an unchanged KI", function() - local state = Value({ - ["foo"] = "bar", - }) - - local calculations = 0 - - local computedPair = ForKeys(state, function(use, key) - calculations += 1 - return key + it("is destroyable", function() + local scope = {} + local forkeys = ForKeys(scope, {}, function() + -- intentionally blank end) - - expect(calculations).to.equal(1) - - state:set({ - ["foo"] = "biz", - ["baz"] = "bar", - }) - - expect(calculations).to.equal(2) + expect(function() + forkeys:destroy() + end).to.never.throw() end) - it("should call the destructor when a key gets removed", function() - local state = Value({ - ["foo"] = "bar", - ["baz"] = "bar", - }) - - local destructions = 0 - - local computedPair = ForKeys(state, function(use, key) - return key .. "biz" - end, function(key) - destructions += 1 + it("iterates on constants", function() + local scope = {} + local data = {foo = 1, bar = 2} + local forkeys = ForKeys(scope, data, function(_, key) + return key:upper() end) - - state:set({ - ["foo"] = "bar", - }) - - expect(destructions).to.equal(1) - - state:set({ - ["baz"] = "bar", - }) - - expect(destructions).to.equal(2) - - state:set({ - ["foo"] = "bar", - ["baz"] = "bar", - }) - - expect(destructions).to.equal(2) - - state:set({}) - - expect(destructions).to.equal(4) + expect(peek(forkeys)).to.be.a("table") + expect(peek(forkeys).FOO).to.equal(1) + expect(peek(forkeys).BAR).to.equal(2) + doCleanup(scope) end) - it("should throw if there is a key collision", function() - expect(function() - local state = Value({ - ["foo"] = "bar", - ["baz"] = "bar", - }) - - local computed = ForKeys(state, function(use) - return "foo" - end) - end).to.throw("forKeysKeyCollision") - - local state = Value({ - ["foo"] = "bar", - }) - - local computed = ForKeys(state, function(use) - return "foo" + it("iterates on state objects", function() + local scope = {} + local data = Value(scope, {foo = 1, bar = 2}) + local forkeys = ForKeys(scope, data, function(_, key) + return key:upper() end) - - expect(function() - state:set({ - ["foo"] = "bar", - ["baz"] = "bar", - }) - end).to.throw("forKeysKeyCollision") + expect(peek(forkeys)).to.be.a("table") + expect(peek(forkeys).FOO).to.equal(1) + expect(peek(forkeys).BAR).to.equal(2) + doCleanup(scope) end) - it("should call the destructor with meta data", function() - local state = Value({ - ["foo"] = "bar", - }) - - local destructions = 0 - - local computedKey = ForKeys(state, function(use, key) - local newKey = key .. "biz" - return newKey, newKey - end, function(key, meta) - expect(meta).to.equal(key) - destructions += 1 + it("computes with constants", function() + local scope = {} + local data = {foo = 1, bar = 2} + local forkeys = ForKeys(scope, data, function(use, key) + return key .. use("baz") end) - - state:set({ - ["baz"] = "bar", - }) - - -- this verifies that the meta expectation passed - expect(destructions).to.equal(1) - - state:set({}) - - -- this verifies that the meta expectation passed - expect(destructions).to.equal(2) + expect(peek(forkeys).foobaz).to.equal(1) + expect(peek(forkeys).barbaz).to.equal(2) + doCleanup(scope) end) - it("should only destruct an output key when it has no associated input key", function() - local map = { - ["foo"] = "fiz", - ["bar"] = "fiz", - ["baz"] = "fuzz", - } - - local state = Value({ - ["foo"] = true, - }) - - local destructions = 0 - - local computedKey = ForKeys(state, function(use, key) - return map[key] - end, function() - destructions += 1 + it("computes with state objects", function() + local scope = {} + local data = {foo = 1, bar = 2} + local suffix = Value(scope, "first") + local forkeys = ForKeys(scope, data, function(use, key) + return key .. use(suffix) end) - - state:set({ - ["bar"] = true, - }) - - expect(destructions).to.equal(0) - - state:set({ - ["foo"] = true, - ["bar"] = true, - ["baz"] = true, - }) - - expect(destructions).to.equal(0) - - state:set({ - ["baz"] = true, - }) - - expect(destructions).to.equal(1) + expect(peek(forkeys).foofirst).to.equal(1) + expect(peek(forkeys).barfirst).to.equal(2) + suffix:set("second") + expect(peek(forkeys).foofirst).to.equal(nil) + expect(peek(forkeys).barfirst).to.equal(nil) + expect(peek(forkeys).foosecond).to.equal(1) + expect(peek(forkeys).barsecond).to.equal(2) + doCleanup(scope) end) - it("should recalculate its value in response to State objects", function() - local baseMap = Value({ - ["foo"] = "baz", - }) - local barMap = ForKeys(baseMap, function(use, key) - return key .. "bar" - end) - - expect(peek(barMap)["foobar"]).to.be.ok() - - baseMap:set({ - ["baz"] = "foo", - }) - - expect(peek(barMap)["bazbar"]).to.be.ok() + it("rejects key collisions", function() + expect(function() + local scope = {} + local data = {foo = 1, bar = 2} + local _ = ForKeys(scope, data, function(use, key) + return "samuel" + end) + doCleanup(scope) + end).to.throw("forKeysKeyCollision") end) - it("should recalculate its value in response to ForKeys objects", function() - local baseMap = Value({ - ["foo"] = "baz", - }) - local barMap = ForKeys(baseMap, function(use, key) - return key .. "bar" + it("preserves value on error", function() + local scope = {} + local data = {foo = 1, bar = 2} + local suffix = Value(scope, "first") + local forkeys = ForKeys(scope, data, function(use, key) + assert(use(suffix) ~= "second", "This is an intentional error from a unit test") + return key .. use(suffix) end) - local bizMap = ForKeys(barMap, function(use, key) - return key .. "biz" - end) - - expect(peek(barMap)["foobar"]).to.be.ok() - expect(peek(bizMap)["foobarbiz"]).to.be.ok() - - baseMap:set({ - ["fiz"] = "foo", - ["baz"] = "foo", - }) - - expect(peek(barMap)["fizbar"]).to.be.ok() - expect(peek(bizMap)["fizbarbiz"]).to.be.ok() - expect(peek(barMap)["bazbar"]).to.be.ok() - expect(peek(bizMap)["bazbarbiz"]).to.be.ok() + expect(peek(forkeys).foofirst).to.equal(1) + expect(peek(forkeys).barfirst).to.equal(2) + suffix:set("second") -- will invoke the error + expect(peek(forkeys).foofirst).to.equal(1) + expect(peek(forkeys).barfirst).to.equal(2) + expect(peek(forkeys).foosecond).to.equal(nil) + expect(peek(forkeys).barsecond).to.equal(nil) + suffix:set("third") + expect(peek(forkeys).foofirst).to.equal(nil) + expect(peek(forkeys).barfirst).to.equal(nil) + expect(peek(forkeys).foosecond).to.equal(nil) + expect(peek(forkeys).barsecond).to.equal(nil) + expect(peek(forkeys).foothird).to.equal(1) + expect(peek(forkeys).barthird).to.equal(2) + doCleanup(scope) end) - itSKIP("should not corrupt dependencies after an error", function() - -- needs rewrite + it("doesn't call destructor on creation", function() + local scope = {} + local destructed = {} + local data = Value(scope, {foo = 1, bar = 2}) + local _ = ForKeys(scope, data, function(use, key) + return key, "meta" .. key + end, function(key, meta) + destructed[key] = true + destructed[meta] = true + end) + expect(destructed.foo).to.equal(nil) + expect(destructed.foometa).to.equal(nil) + expect(destructed.bar).to.equal(nil) + expect(destructed.barmeta).to.equal(nil) + doCleanup(scope) end) - it("should garbage-collect unused objects", function() - local state = Value({ - ["foo"] = "bar", - }) - - local counter = 0 - - do - local computedKeys = ForKeys(state, function(use, key) - counter += 1 - return key - end) - end - - state:set({ - ["bar"] = "baz", - }) - - expect(counter).to.equal(1) + it("calls destructor on update", function() + local scope = {} + local destructed = {} + local data = Value(scope, {foo = 1, bar = 2}) + local _ = ForKeys(scope, data, function(use, key) + return key, "meta" .. key + end, function(key, meta) + destructed[key] = true + destructed[meta] = true + end) + data:set({foo = 100, baz = 3}) + expect(destructed.foo).to.equal(nil) + expect(destructed.foometa).to.equal(nil) + expect(destructed.bar).to.equal(true) + expect(destructed.barmeta).to.equal(true) + doCleanup(scope) end) - it("should not garbage-collect objects in use", function() - local state = Value({ - ["foo"] = "bar", - }) - local computed2 - - local counter = 0 - - do - local computed = ForKeys(state, function(use, key) - counter += 1 - return key - end) - - computed2 = ForKeys(computed, function(use, key) - return key - end) - end - - state:set({ - ["bar"] = "baz", - }) - - expect(counter).to.equal(2) + it("calls destructor on destroy", function() + local scope = {} + local destructed = {} + local data = Value(scope, {foo = 1, bar = 2}) + local _ = ForKeys(scope, data, function(use, key) + return key, "meta" .. key + end, function(key, meta) + destructed[key] = true + destructed[meta] = true + end) + expect(destructed.foo).to.equal(nil) + expect(destructed.foometa).to.equal(nil) + expect(destructed.bar).to.equal(nil) + expect(destructed.barmeta).to.equal(nil) + doCleanup(scope) + expect(destructed.foo).to.equal(true) + expect(destructed.foometa).to.equal(true) + expect(destructed.bar).to.equal(true) + expect(destructed.barmeta).to.equal(true) end) end From ae593417b06e68ae4e4f6c55568ac2012fb6bc3b Mon Sep 17 00:00:00 2001 From: Elttob Date: Sun, 3 Sep 2023 22:43:35 +0100 Subject: [PATCH 021/287] ForKeys updated spec --- test/State/Computed.spec.lua | 2 +- test/State/ForKeys.spec.lua | 16 ++++++++-------- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/test/State/Computed.spec.lua b/test/State/Computed.spec.lua index 30d9f2533..f62cf03c0 100644 --- a/test/State/Computed.spec.lua +++ b/test/State/Computed.spec.lua @@ -50,7 +50,7 @@ return function() doCleanup(scope) end) - itFOCUS("preserves value on error", function() + it("preserves value on error", function() local scope = {} local dependency = Value(scope, 5) local computed = Computed(scope, function(use) diff --git a/test/State/ForKeys.spec.lua b/test/State/ForKeys.spec.lua index 5be715195..a8a84b766 100644 --- a/test/State/ForKeys.spec.lua +++ b/test/State/ForKeys.spec.lua @@ -128,9 +128,9 @@ return function() destructed[meta] = true end) expect(destructed.foo).to.equal(nil) - expect(destructed.foometa).to.equal(nil) + expect(destructed.metafoo).to.equal(nil) expect(destructed.bar).to.equal(nil) - expect(destructed.barmeta).to.equal(nil) + expect(destructed.metabar).to.equal(nil) doCleanup(scope) end) @@ -146,9 +146,9 @@ return function() end) data:set({foo = 100, baz = 3}) expect(destructed.foo).to.equal(nil) - expect(destructed.foometa).to.equal(nil) + expect(destructed.metafoo).to.equal(nil) expect(destructed.bar).to.equal(true) - expect(destructed.barmeta).to.equal(true) + expect(destructed.metabar).to.equal(true) doCleanup(scope) end) @@ -163,13 +163,13 @@ return function() destructed[meta] = true end) expect(destructed.foo).to.equal(nil) - expect(destructed.foometa).to.equal(nil) + expect(destructed.metafoo).to.equal(nil) expect(destructed.bar).to.equal(nil) - expect(destructed.barmeta).to.equal(nil) + expect(destructed.metabar).to.equal(nil) doCleanup(scope) expect(destructed.foo).to.equal(true) - expect(destructed.foometa).to.equal(true) + expect(destructed.metafoo).to.equal(true) expect(destructed.bar).to.equal(true) - expect(destructed.barmeta).to.equal(true) + expect(destructed.metabar).to.equal(true) end) end From c5043015452f0d8d78f56b7542edcf39a6b1a0cc Mon Sep 17 00:00:00 2001 From: Elttob Date: Sun, 3 Sep 2023 23:24:44 +0100 Subject: [PATCH 022/287] API reference updates --- docs/api-reference/index.md | 27 ++++++----- docs/api-reference/memory/docleanup.md | 40 ++++++++++++++++ .../{state => memory}/donothing.md | 4 +- docs/api-reference/memory/index.md | 48 +++++++++++++++++++ docs/api-reference/memory/scoped.md | 47 ++++++++++++++++++ .../{state/cleanup.md => memory/task.md} | 26 +++++----- docs/assets/theme/api-reference.css | 8 ++++ mkdocs.yml | 8 +++- 8 files changed, 177 insertions(+), 31 deletions(-) create mode 100644 docs/api-reference/memory/docleanup.md rename docs/api-reference/{state => memory}/donothing.md (88%) create mode 100644 docs/api-reference/memory/index.md create mode 100644 docs/api-reference/memory/scoped.md rename docs/api-reference/{state/cleanup.md => memory/task.md} (61%) diff --git a/docs/api-reference/index.md b/docs/api-reference/index.md index d70204fe9..fd05c58bc 100644 --- a/docs/api-reference/index.md +++ b/docs/api-reference/index.md @@ -30,28 +30,31 @@ page. Errors :octicons-chevron-right-24: + - - :octicons-list-unordered-24: - State +
-### Animation +### Animation :octicons-package-24: Spring diff --git a/docs/api-reference/memory/docleanup.md b/docs/api-reference/memory/docleanup.md new file mode 100644 index 000000000..cdb1b3be4 --- /dev/null +++ b/docs/api-reference/memory/docleanup.md @@ -0,0 +1,40 @@ + + +

+ :octicons-code-24: + doCleanup + + function + since v0.3 + +

+ +Attempts to clean up all [tasks](../task) passed to it. Values which are not tasks +are ignored. + +```Lua +(...any) -> () +``` + +----- + +## Parameters + +- `...` - Any objects that need to be destroyed. + +----- + +## Example Usage + +```Lua +doCleanup( + workspace.Part1, + RunService.RenderStepped:Connect(print), + function() + print("I will be run!") + end +) +``` \ No newline at end of file diff --git a/docs/api-reference/state/donothing.md b/docs/api-reference/memory/donothing.md similarity index 88% rename from docs/api-reference/state/donothing.md rename to docs/api-reference/memory/donothing.md index 0c11a522f..41df1a10f 100644 --- a/docs/api-reference/state/donothing.md +++ b/docs/api-reference/memory/donothing.md @@ -1,6 +1,6 @@

@@ -8,7 +8,7 @@ doNothing function - since v0.2 + since v0.3

diff --git a/docs/api-reference/memory/index.md b/docs/api-reference/memory/index.md new file mode 100644 index 000000000..c304c84ce --- /dev/null +++ b/docs/api-reference/memory/index.md @@ -0,0 +1,48 @@ + + +

+ :octicons-list-unordered-24: + Memory +

+ +Functions and utilities for managing memory and object destruction. + +----- + + \ No newline at end of file diff --git a/docs/api-reference/memory/scoped.md b/docs/api-reference/memory/scoped.md new file mode 100644 index 000000000..f02ddb3fc --- /dev/null +++ b/docs/api-reference/memory/scoped.md @@ -0,0 +1,47 @@ + + +

+ :octicons-code-24: + scoped + + function + since v0.3 + +

+ +Creates and returns a blank cleanup table, with the `__index` metatable pointing +at the given list of constructors for syntax convenience. + +```Lua +(constructors: T) -> {Task} & T +``` + +!!! info "Approximated type" + The return type of this function is approximate. Luau does not offer a way + of annotating metatable types as of v0.3, so the type signature is + intentionally incorrect to try and usefully annotate for common usage. + +----- + +## Parameters + +- `constructors: T` - A table including constructors with cleanup tables as the +first parameter. + +----- + +## Example Usage + +```Lua +local scope = scoped(Fusion) +local value = scope:Value(5) +doCleanup(scope) + +-- equivalent to +local scope = {} +local value = Value(scope, 5) +doCleanup(scope) +``` \ No newline at end of file diff --git a/docs/api-reference/state/cleanup.md b/docs/api-reference/memory/task.md similarity index 61% rename from docs/api-reference/state/cleanup.md rename to docs/api-reference/memory/task.md index ccde7d676..2d0662908 100644 --- a/docs/api-reference/state/cleanup.md +++ b/docs/api-reference/memory/task.md @@ -1,41 +1,37 @@

- :octicons-code-24: - cleanup + :octicons-checklist-24: + Task - function - since v0.2 + type + since v0.3

-Attempts to destroy all destructible objects passed to it. +Represents types which have default cleanup behaviour defined by Fusion. ```Lua -(...any) -> () +Instance | RBXScriptConnection | () -> () | {destroy: (self) -> ()} | {Destroy: (self) -> ()} | {Task} ``` ----- -## Parameters - -- `...` - Any objects that need to be destroyed. - ------ - ## Example Usage ```Lua -Fusion.cleanup( +local stuff: {Task} = { workspace.Part1, RunService.RenderStepped:Connect(print), function() print("I will be run!") end -) +} + +doCleanup(stuff) ``` ----- diff --git a/docs/assets/theme/api-reference.css b/docs/assets/theme/api-reference.css index a1667c190..3485f6a58 100644 --- a/docs/assets/theme/api-reference.css +++ b/docs/assets/theme/api-reference.css @@ -85,6 +85,14 @@ color: var(--fusiondoc-fg-3); } +.fusiondoc-api-index-header { + color: var(--fusiondoc-fg-1) !important; +} + +.fusiondoc-api-index-header:hover { + color: var(--fusiondoc-accent-hover) !important; +} + .fusiondoc-api-index-link { display: flex; align-items: center; diff --git a/mkdocs.yml b/mkdocs.yml index 685d4b9f9..81da6fdb0 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -96,14 +96,18 @@ nav: - Home: api-reference/index.md - Errors: - api-reference/errors/index.md + - Memory: + - api-reference/memory/index.md + - doCleanup: api-reference/memory/docleanup.md + - doNothing: api-reference/memory/donothing.md + - Task: api-reference/memory/task.md + - scoped: api-reference/memory/scoped.md - State: - api-reference/state/index.md - CanBeState: api-reference/state/canbestate.md - Computed: api-reference/state/computed.md - - cleanup: api-reference/state/cleanup.md - Dependency: api-reference/state/dependency.md - Dependent: api-reference/state/dependent.md - - doNothing: api-reference/state/donothing.md - ForKeys: api-reference/state/forkeys.md - ForPairs: api-reference/state/forpairs.md - ForValues: api-reference/state/forvalues.md From 04e4593157e56e062742ca37312a40024a10becb Mon Sep 17 00:00:00 2001 From: Elttob Date: Sun, 3 Sep 2023 23:32:21 +0100 Subject: [PATCH 023/287] Remove dead State links --- docs/api-reference/state/index.md | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/docs/api-reference/state/index.md b/docs/api-reference/state/index.md index 3c898f685..d83aba1df 100644 --- a/docs/api-reference/state/index.md +++ b/docs/api-reference/state/index.md @@ -50,18 +50,6 @@ Fundamental state objects and utilities for working with reactive graphs.
### Functions - - :octicons-code-24: - cleanup - :octicons-chevron-right-24: - - - - :octicons-code-24: - doNothing - :octicons-chevron-right-24: - - :octicons-code-24: peek From c351e675265d9adb9202d19a5202b1900b237348 Mon Sep 17 00:00:00 2001 From: Elttob Date: Sun, 26 Nov 2023 20:33:10 +0000 Subject: [PATCH 024/287] Start working on unified For backend --- src/State/For.lua | 163 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 163 insertions(+) create mode 100644 src/State/For.lua diff --git a/src/State/For.lua b/src/State/For.lua new file mode 100644 index 000000000..633a4aa67 --- /dev/null +++ b/src/State/For.lua @@ -0,0 +1,163 @@ +--!nonstrict + +--[[ + The private generic implementation for all public `For` objects. +]] + +local Package = script.Parent.Parent +local PubTypes = require(Package.PubTypes) +local Types = require(Package.Types) +-- Logging +local logError = require(Package.Logging.logError) +-- State +local peek = require(Package.State.peek) +local isState = require(Package.State.isState) + +local class = {} + +local CLASS_METATABLE = { __index = class } +local WEAK_KEYS_METATABLE = { __mode = "k" } + + +--[[ + Called when the original table is changed. +]] + +function class:update(): boolean + local inputIsState = self._inputIsState + local newInputTable = peek(self._inputTable) + local existingProcessors = error("TODO") + + -- clean out main dependency set + for dependency in pairs(self.dependencySet) do + dependency.dependentSet[self] = nil + end + + self._oldDependencySet, self.dependencySet = self.dependencySet, self._oldDependencySet + table.clear(self.dependencySet) + + -- if the input table is a state object, add it as a dependency + if inputIsState then + self._inputTable.dependentSet[self] = true + self.dependencySet[self._inputTable] = true + end + + local remainingPairs = {} + local numPairs = 0 + for key, value in newInputTable do + numPairs += 1 + if remainingPairs[key] == nil then + remainingPairs[key] = {[value] = true} + else + remainingPairs[key][value] = true + end + end + + local newProcessors = {} + -- First, try and reuse processors who match both the key and value of a + -- remaining pair. This can be done with no recomputation. + for tryReuseProcessor in existingProcessors do + if numPairs <= 0 then + break + end + local key = peek(tryReuseProcessor.key) + local value = peek(tryReuseProcessor.value) + if remainingPairs[key] ~= nil and remainingPairs[key][value] ~= nil then + remainingPairs[key][value] = nil + numPairs -= 1 + error("TODO: bring forward key from old table") + newProcessors[tryReuseProcessor] = true + end + end + -- Next, try and reuse processors who match the key of a remaining pair. + -- The value will change but the key will stay stable. + for tryReuseProcessor in existingProcessors do + if numPairs <= 0 then + break + elseif newProcessors[tryReuseProcessor] == nil then + local key = peek(tryReuseProcessor.key) + if remainingPairs[key] ~= nil then + local value = next(remainingPairs[key]) + if value ~= nil then + remainingPairs[key][value] = nil + numPairs -= 1 + tryReuseProcessor.value:set(value) + error("TODO: bring forward key from old table") + newProcessors[tryReuseProcessor] = true + end + end + end + end + -- Next, try and reuse processors who match the value of a remaining pair. + -- The key will change but the value will stay stable. + for tryReuseProcessor in existingProcessors do + error("TODO") + end + -- Finally, try and reuse any remaining processors, even if they do not + -- match a pair. Both key and value will be changed. + for tryReuseProcessor in existingProcessors do + error("TODO") + end + -- By this point, we can be in one of three cases: + -- 1) some existing processors are left over; no remaining pairs (shrunk) + -- 2) no existing processors are left over; no remaining pairs (same size) + -- 3) no existing processors are left over; some remaining pairs (grew) + -- So, existing processors should be destroyed, and remaining pairs should + -- be created. This accomodates for table growth and shrinking. + + + return error("TODO") +end + +--[[ + Returns the interior value of this state object. +]] +function class:_peek(): any + return self._outputTable +end + +function class:get() + logError("stateGetWasRemoved") +end + +function class:destroy() + for dependency in pairs(self.dependencySet) do + dependency.dependentSet[self] = nil + end + + error("TODO") +end + +local function For( + cleanupTable: {PubTypes.Task}, + inputTable: PubTypes.CanBeState<{ [KI]: VI }>, + processor: ( + PubTypes.StateObject, + PubTypes.StateObject + ) -> (PubTypes.StateObject, PubTypes.StateObject) +): Types.For + + local self = setmetatable({ + type = "State", + kind = "For", + dependencySet = {}, + -- if we held strong references to the dependents, then they wouldn't be + -- able to get garbage collected when they fall out of scope + dependentSet = setmetatable({}, WEAK_KEYS_METATABLE), + _oldDependencySet = {}, + + _processor = processor, + _inputIsState = isState(inputTable), + + _inputTable = inputTable, + _oldInputTable = {}, + _outputTable = {} + }, CLASS_METATABLE) + + self:update() + table.insert(cleanupTable, self) + + return self +end + +return For \ No newline at end of file From ceae0fab2569b710d2e59362cf81e3b0955814ef Mon Sep 17 00:00:00 2001 From: Elttob Date: Sun, 26 Nov 2023 20:47:05 +0000 Subject: [PATCH 025/287] Cleanup in reverse order --- src/Memory/doCleanup.lua | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/Memory/doCleanup.lua b/src/Memory/doCleanup.lua index c514a0d2e..69870cff5 100644 --- a/src/Memory/doCleanup.lua +++ b/src/Memory/doCleanup.lua @@ -37,8 +37,10 @@ local function doCleanupOne(task: any) -- case 6: array of tasks elseif task[1] ~= nil then - for _, subtask in ipairs(task) do - doCleanupOne(subtask) + -- It is important to iterate backwards through the table, since + -- objects are added in order of construction. + for index = #task, 1, -1 do + doCleanupOne(task[index]) end end end From 36479f110326f3a5efbbe173771501570ae8580b Mon Sep 17 00:00:00 2001 From: Elttob Date: Sun, 26 Nov 2023 20:54:24 +0000 Subject: [PATCH 026/287] Add warn when using cleanup() --- src/Logging/messages.lua | 1 + src/Memory/legacyCleanup.lua | 12 ++++++++++++ src/init.lua | 3 ++- 3 files changed, 15 insertions(+), 1 deletion(-) create mode 100644 src/Memory/legacyCleanup.lua diff --git a/src/Logging/messages.lua b/src/Logging/messages.lua index 17b3a3c72..97930f975 100644 --- a/src/Logging/messages.lua +++ b/src/Logging/messages.lua @@ -11,6 +11,7 @@ return { cannotConnectAttributeChange = "The %s class doesn't have an attribute called '%s'.", cannotConnectEvent = "The %s class doesn't have an event called '%s'.", cannotCreateClass = "Can't create a new instance of class '%s'.", + cleanupWasRenamed = "`Fusion.cleanup` was renamed to `Fusion.doCleanup`. This will be an error in future versions of Fusion.", computedCallbackError = "Computed callback error: ERROR_MESSAGE", destructorNeededValue = "To save instances into Values, provide a destructor function. This will be an error soon - see discussion #183 on GitHub.", destructorNeededComputed = "To return instances from Computeds, provide a destructor function. This will be an error soon - see discussion #183 on GitHub.", diff --git a/src/Memory/legacyCleanup.lua b/src/Memory/legacyCleanup.lua new file mode 100644 index 000000000..39a75a0b7 --- /dev/null +++ b/src/Memory/legacyCleanup.lua @@ -0,0 +1,12 @@ +--!strict + +local Package = script.Parent.Parent +local logWarn = require(Package.Logging.logWarn) +local doCleanup = require(Package.Memory.doCleanup) + +local function legacyCleanup(...: any) + logWarn("cleanupWasRenamed") + return doCleanup(...) +end + +return legacyCleanup \ No newline at end of file diff --git a/src/init.lua b/src/init.lua index 03c5dd13d..e8cc6aa8e 100644 --- a/src/init.lua +++ b/src/init.lua @@ -40,6 +40,7 @@ local Fusion = restrictRead("Fusion", { Tween = require(script.Animation.Tween), Spring = require(script.Animation.Spring), + cleanup = require(script.Memory.legacyCleanup), doCleanup = require(script.Memory.doCleanup), doNothing = require(script.Memory.doNothing), peek = require(script.State.peek) @@ -83,7 +84,7 @@ type Fusion = { Tween: (goalState: StateObject, tweenInfo: TweenInfo?) -> Tween, Spring: (goalState: StateObject, speed: CanBeState?, damping: CanBeState?) -> Spring, - cleanup: (...any) -> (), + doCleanup: (...any) -> (), doNothing: (...any) -> (), peek: Use } From e0f920b75ba219b6f69f6383901aa9e29fbc2633 Mon Sep 17 00:00:00 2001 From: Elttob Date: Sun, 26 Nov 2023 20:58:38 +0000 Subject: [PATCH 027/287] add destroy() to typedefs --- src/PubTypes.lua | 24 ++++++++++++++++-------- 1 file changed, 16 insertions(+), 8 deletions(-) diff --git a/src/PubTypes.lua b/src/PubTypes.lua index ea61673aa..012eb9c2c 100644 --- a/src/PubTypes.lua +++ b/src/PubTypes.lua @@ -86,30 +86,36 @@ export type Use = (target: CanBeState) -> T -- A state object whose value can be set at any time by the user. export type Value = StateObject & { kind: "State", - set: (Value, newValue: any, force: boolean?) -> () + set: (Value, newValue: any, force: boolean?) -> (), + destroy: () -> () } -- A state object whose value is derived from other objects using a callback. export type Computed = StateObject & Dependent & { - kind: "Computed" + kind: "Computed", + destroy: () -> () } -- A state object whose value is derived from other objects using a callback. export type ForPairs = StateObject<{ [KO]: VO }> & Dependent & { - kind: "ForPairs" + kind: "ForPairs", + destroy: () -> () } -- A state object whose value is derived from other objects using a callback. export type ForKeys = StateObject<{ [KO]: V }> & Dependent & { - kind: "ForKeys" + kind: "ForKeys", + destroy: () -> () } -- A state object whose value is derived from other objects using a callback. export type ForValues = StateObject<{ [K]: VO }> & Dependent & { - kind: "ForKeys" + kind: "ForKeys", + destroy: () -> () } -- A state object which follows another state object using tweens. export type Tween = StateObject & Dependent & { - kind: "Tween" + kind: "Tween", + destroy: () -> () } -- A state object which follows another state object using spring simulation. @@ -117,13 +123,15 @@ export type Spring = StateObject & Dependent & { kind: "Spring", setPosition: (Spring, newPosition: Animatable) -> (), setVelocity: (Spring, newVelocity: Animatable) -> (), - addVelocity: (Spring, deltaVelocity: Animatable) -> () + addVelocity: (Spring, deltaVelocity: Animatable) -> (), + destroy: () -> () } -- An object which can listen for updates on another state object. export type Observer = Dependent & { kind: "Observer", - onChange: (Observer, callback: () -> ()) -> (() -> ()) + onChange: (Observer, callback: () -> ()) -> (() -> ()), + destroy: () -> () } --[[ From 5073cd4cdc1f628c7931cd944595c4c961ecc2cb Mon Sep 17 00:00:00 2001 From: Elttob Date: Mon, 27 Nov 2023 13:11:10 +0000 Subject: [PATCH 028/287] Complete skeleton of For logic --- src/State/For.lua | 65 ++++++++++++++++++++++++++++++----------------- 1 file changed, 42 insertions(+), 23 deletions(-) diff --git a/src/State/For.lua b/src/State/For.lua index 633a4aa67..707856bc8 100644 --- a/src/State/For.lua +++ b/src/State/For.lua @@ -43,9 +43,7 @@ function class:update(): boolean end local remainingPairs = {} - local numPairs = 0 for key, value in newInputTable do - numPairs += 1 if remainingPairs[key] == nil then remainingPairs[key] = {[value] = true} else @@ -57,46 +55,59 @@ function class:update(): boolean -- First, try and reuse processors who match both the key and value of a -- remaining pair. This can be done with no recomputation. for tryReuseProcessor in existingProcessors do - if numPairs <= 0 then - break - end local key = peek(tryReuseProcessor.key) local value = peek(tryReuseProcessor.value) - if remainingPairs[key] ~= nil and remainingPairs[key][value] ~= nil then - remainingPairs[key][value] = nil - numPairs -= 1 + local remainingValues = remainingPairs[key] + if remainingValues ~= nil and remainingValues[value] ~= nil then + remainingValues[value] = nil error("TODO: bring forward key from old table") newProcessors[tryReuseProcessor] = true + existingProcessors[tryReuseProcessor] = nil end end -- Next, try and reuse processors who match the key of a remaining pair. -- The value will change but the key will stay stable. for tryReuseProcessor in existingProcessors do - if numPairs <= 0 then - break - elseif newProcessors[tryReuseProcessor] == nil then - local key = peek(tryReuseProcessor.key) - if remainingPairs[key] ~= nil then - local value = next(remainingPairs[key]) - if value ~= nil then - remainingPairs[key][value] = nil - numPairs -= 1 - tryReuseProcessor.value:set(value) - error("TODO: bring forward key from old table") - newProcessors[tryReuseProcessor] = true - end + local key = peek(tryReuseProcessor.key) + local remainingValues = remainingPairs[key] + if remainingValues ~= nil then + local value = next(remainingValues) + if value ~= nil then + remainingValues[value] = nil + tryReuseProcessor.value:set(value) + error("TODO: bring forward key from old table") + newProcessors[tryReuseProcessor] = true + existingProcessors[tryReuseProcessor] = nil end end end -- Next, try and reuse processors who match the value of a remaining pair. -- The key will change but the value will stay stable. for tryReuseProcessor in existingProcessors do - error("TODO") + local value = peek(tryReuseProcessor.value) + for key, remainingValues in remainingPairs do + if remainingValues[value] ~= nil then + remainingValues[value] = nil + tryReuseProcessor.key:set(key) + error("TODO: bring forward key from old table") + newProcessors[tryReuseProcessor] = true + existingProcessors[tryReuseProcessor] = nil + end + end end -- Finally, try and reuse any remaining processors, even if they do not -- match a pair. Both key and value will be changed. for tryReuseProcessor in existingProcessors do - error("TODO") + for key, remainingValues in remainingPairs do + local value = next(remainingValues) + if value ~= nil then + remainingValues[value] = nil + tryReuseProcessor.value:set(value) + error("TODO: bring forward key from old table") + newProcessors[tryReuseProcessor] = true + existingProcessors[tryReuseProcessor] = nil + end + end end -- By this point, we can be in one of three cases: -- 1) some existing processors are left over; no remaining pairs (shrunk) @@ -104,7 +115,15 @@ function class:update(): boolean -- 3) no existing processors are left over; some remaining pairs (grew) -- So, existing processors should be destroyed, and remaining pairs should -- be created. This accomodates for table growth and shrinking. + for unusedProcessor in existingProcessors do + error("TOOD: cleanup unused processor") + end + for key, remainingValues in remainingPairs do + for value in remainingValues do + error("TOOD: create new processor") + end + end return error("TODO") end From 1f139566180c4e3bc96a1c86b8dbb803b2e249c9 Mon Sep 17 00:00:00 2001 From: Elttob Date: Mon, 27 Nov 2023 13:29:20 +0000 Subject: [PATCH 029/287] Processor mechanics for For --- src/Logging/messages.lua | 1 + src/State/For.lua | 90 +++++++++++++++++++++++++--------------- 2 files changed, 57 insertions(+), 34 deletions(-) diff --git a/src/Logging/messages.lua b/src/Logging/messages.lua index 97930f975..7476c899c 100644 --- a/src/Logging/messages.lua +++ b/src/Logging/messages.lua @@ -27,6 +27,7 @@ return { forPairsProcessorError = "ForPairs callback error: ERROR_MESSAGE", forValuesProcessorError = "ForValues callback error: ERROR_MESSAGE", forValuesDestructorError = "ForValues destructor error: ERROR_MESSAGE", + forProcessorError = "For (internal) callback error: ERROR_MESSAGE", invalidChangeHandler = "The change handler for the '%s' property must be a function.", invalidAttributeChangeHandler = "The change handler for the '%s' attribute must be a function.", invalidEventHandler = "The handler for the '%s' event must be a function.", diff --git a/src/State/For.lua b/src/State/For.lua index 707856bc8..f28defa44 100644 --- a/src/State/For.lua +++ b/src/State/For.lua @@ -9,40 +9,38 @@ local PubTypes = require(Package.PubTypes) local Types = require(Package.Types) -- Logging local logError = require(Package.Logging.logError) +local logErrorNonFatal = require(Package.Logging.logErrorNonFatal) +local parseError = require(Package.Logging.parseError) -- State local peek = require(Package.State.peek) local isState = require(Package.State.isState) +local Value = require(Package.State.Value) +-- Memory +local doCleanup = require(Package.Memory.doCleanup) local class = {} local CLASS_METATABLE = { __index = class } local WEAK_KEYS_METATABLE = { __mode = "k" } +type Processor = { + inputKey: PubTypes.Value, + inputValue: PubTypes.Value, + outputKey: PubTypes.StateObject, + outputValue: PubTypes.StateObject, + cleanupTask: any +} --[[ Called when the original table is changed. ]] function class:update(): boolean - local inputIsState = self._inputIsState local newInputTable = peek(self._inputTable) - local existingProcessors = error("TODO") + local existingProcessors = self._existingProcessors :: {[Processor]: true} - -- clean out main dependency set - for dependency in pairs(self.dependencySet) do - dependency.dependentSet[self] = nil - end - - self._oldDependencySet, self.dependencySet = self.dependencySet, self._oldDependencySet - table.clear(self.dependencySet) - - -- if the input table is a state object, add it as a dependency - if inputIsState then - self._inputTable.dependentSet[self] = true - self.dependencySet[self._inputTable] = true - end - - local remainingPairs = {} + local remainingPairs = self._remainingPairs + table.clear(remainingPairs) for key, value in newInputTable do if remainingPairs[key] == nil then remainingPairs[key] = {[value] = true} @@ -51,12 +49,12 @@ function class:update(): boolean end end - local newProcessors = {} + local newProcessors: {[Processor]: true} = self._newProcessors -- First, try and reuse processors who match both the key and value of a -- remaining pair. This can be done with no recomputation. for tryReuseProcessor in existingProcessors do - local key = peek(tryReuseProcessor.key) - local value = peek(tryReuseProcessor.value) + local key = peek(tryReuseProcessor.inputKey) + local value = peek(tryReuseProcessor.inputValue) local remainingValues = remainingPairs[key] if remainingValues ~= nil and remainingValues[value] ~= nil then remainingValues[value] = nil @@ -68,13 +66,13 @@ function class:update(): boolean -- Next, try and reuse processors who match the key of a remaining pair. -- The value will change but the key will stay stable. for tryReuseProcessor in existingProcessors do - local key = peek(tryReuseProcessor.key) + local key = peek(tryReuseProcessor.inputKey) local remainingValues = remainingPairs[key] if remainingValues ~= nil then local value = next(remainingValues) if value ~= nil then remainingValues[value] = nil - tryReuseProcessor.value:set(value) + tryReuseProcessor.inputValue:set(value) error("TODO: bring forward key from old table") newProcessors[tryReuseProcessor] = true existingProcessors[tryReuseProcessor] = nil @@ -84,11 +82,11 @@ function class:update(): boolean -- Next, try and reuse processors who match the value of a remaining pair. -- The key will change but the value will stay stable. for tryReuseProcessor in existingProcessors do - local value = peek(tryReuseProcessor.value) + local value = peek(tryReuseProcessor.inputValue) for key, remainingValues in remainingPairs do if remainingValues[value] ~= nil then remainingValues[value] = nil - tryReuseProcessor.key:set(key) + tryReuseProcessor.inputKey:set(key) error("TODO: bring forward key from old table") newProcessors[tryReuseProcessor] = true existingProcessors[tryReuseProcessor] = nil @@ -98,11 +96,11 @@ function class:update(): boolean -- Finally, try and reuse any remaining processors, even if they do not -- match a pair. Both key and value will be changed. for tryReuseProcessor in existingProcessors do - for key, remainingValues in remainingPairs do + for _, remainingValues in remainingPairs do local value = next(remainingValues) if value ~= nil then remainingValues[value] = nil - tryReuseProcessor.value:set(value) + tryReuseProcessor.inputValue:set(value) error("TODO: bring forward key from old table") newProcessors[tryReuseProcessor] = true existingProcessors[tryReuseProcessor] = nil @@ -116,15 +114,36 @@ function class:update(): boolean -- So, existing processors should be destroyed, and remaining pairs should -- be created. This accomodates for table growth and shrinking. for unusedProcessor in existingProcessors do - error("TOOD: cleanup unused processor") + doCleanup(unusedProcessor.cleanupTask) end + table.clear(existingProcessors) for key, remainingValues in remainingPairs do for value in remainingValues do - error("TOOD: create new processor") + local scope = {} + local inputKey = Value(scope, key) + local inputValue = Value(scope, value) + local processOK, outputKey, outputValue = xpcall(self._processor, parseError, inputKey, inputValue) + if processOK then + local processor = { + inputKey = inputKey, + inputValue = inputValue, + outputKey = outputKey, + outputValue = outputValue + } + newProcessors[processor] = true + else + logErrorNonFatal("forProcessorError", outputKey) + end + end end + self._existingProcessors = newProcessors + self._newProcessors = existingProcessors + + error("TODO: reconstruct table") + return error("TODO") end @@ -163,17 +182,20 @@ local function For( -- if we held strong references to the dependents, then they wouldn't be -- able to get garbage collected when they fall out of scope dependentSet = setmetatable({}, WEAK_KEYS_METATABLE), - _oldDependencySet = {}, - _processor = processor, - _inputIsState = isState(inputTable), - _inputTable = inputTable, - _oldInputTable = {}, - _outputTable = {} + _outputTable = {}, + _existingProcessors = {}, + _newProcessors = {}, + _remainingPairs = {} }, CLASS_METATABLE) self:update() + if isState(self._inputTable) then + self._inputTable.dependentSet[self] = true + self.dependencySet[self._inputTable] = true + end + table.insert(cleanupTable, self) return self From 6fd937d20881308bccd32e46f72d1b98fc2c9773 Mon Sep 17 00:00:00 2001 From: Elttob Date: Mon, 27 Nov 2023 13:29:53 +0000 Subject: [PATCH 030/287] Whoops clean up that scope! --- src/State/For.lua | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/State/For.lua b/src/State/For.lua index f28defa44..6c1b250d0 100644 --- a/src/State/For.lua +++ b/src/State/For.lua @@ -129,7 +129,8 @@ function class:update(): boolean inputKey = inputKey, inputValue = inputValue, outputKey = outputKey, - outputValue = outputValue + outputValue = outputValue, + cleanupTask = scope } newProcessors[processor] = true else From fa4ea0494c66a8152682fcb022f5a16031fd2fe6 Mon Sep 17 00:00:00 2001 From: Elttob Date: Mon, 27 Nov 2023 13:51:00 +0000 Subject: [PATCH 031/287] Output table construction --- src/State/For.lua | 209 +++++++++++++++++++++++++--------------------- 1 file changed, 113 insertions(+), 96 deletions(-) diff --git a/src/State/For.lua b/src/State/For.lua index 6c1b250d0..d67486e44 100644 --- a/src/State/For.lua +++ b/src/State/For.lua @@ -36,123 +36,142 @@ type Processor = { ]] function class:update(): boolean - local newInputTable = peek(self._inputTable) - local existingProcessors = self._existingProcessors :: {[Processor]: true} - + local existingInputTable = self._existingInputTable + local existingOutputTable = self._existingOutputTable + local existingProcessors = self._existingProcessors + local newInputTable = peek(self._newInputTable) + local newOutputTable = self._newOutputTable + local newProcessors = self._newProcessors local remainingPairs = self._remainingPairs - table.clear(remainingPairs) - for key, value in newInputTable do - if remainingPairs[key] == nil then - remainingPairs[key] = {[value] = true} - else - remainingPairs[key][value] = true - end + + -- clean out main dependency set + for dependency in pairs(self.dependencySet) do + dependency.dependentSet[self] = nil end + table.clear(self.dependencySet) - local newProcessors: {[Processor]: true} = self._newProcessors - -- First, try and reuse processors who match both the key and value of a - -- remaining pair. This can be done with no recomputation. - for tryReuseProcessor in existingProcessors do - local key = peek(tryReuseProcessor.inputKey) - local value = peek(tryReuseProcessor.inputValue) - local remainingValues = remainingPairs[key] - if remainingValues ~= nil and remainingValues[value] ~= nil then - remainingValues[value] = nil - error("TODO: bring forward key from old table") - newProcessors[tryReuseProcessor] = true - existingProcessors[tryReuseProcessor] = nil - end + if isState(self._inputTable) then + self._inputTable.dependentSet[self], self.dependencySet[self._inputTable] = true, true end - -- Next, try and reuse processors who match the key of a remaining pair. - -- The value will change but the key will stay stable. - for tryReuseProcessor in existingProcessors do - local key = peek(tryReuseProcessor.inputKey) - local remainingValues = remainingPairs[key] - if remainingValues ~= nil then - local value = next(remainingValues) - if value ~= nil then - remainingValues[value] = nil - tryReuseProcessor.inputValue:set(value) - error("TODO: bring forward key from old table") - newProcessors[tryReuseProcessor] = true - existingProcessors[tryReuseProcessor] = nil + + if newInputTable ~= existingInputTable then + for key, value in newInputTable do + if remainingPairs[key] == nil then + remainingPairs[key] = {[value] = true} + else + remainingPairs[key][value] = true end end - end - -- Next, try and reuse processors who match the value of a remaining pair. - -- The key will change but the value will stay stable. - for tryReuseProcessor in existingProcessors do - local value = peek(tryReuseProcessor.inputValue) - for key, remainingValues in remainingPairs do - if remainingValues[value] ~= nil then + + -- First, try and reuse processors who match both the key and value of a + -- remaining pair. This can be done with no recomputation. + for tryReuseProcessor in existingProcessors do + local key = peek(tryReuseProcessor.inputKey) + local value = peek(tryReuseProcessor.inputValue) + local remainingValues = remainingPairs[key] + if remainingValues ~= nil and remainingValues[value] ~= nil then remainingValues[value] = nil - tryReuseProcessor.inputKey:set(key) - error("TODO: bring forward key from old table") newProcessors[tryReuseProcessor] = true existingProcessors[tryReuseProcessor] = nil end end - end - -- Finally, try and reuse any remaining processors, even if they do not - -- match a pair. Both key and value will be changed. - for tryReuseProcessor in existingProcessors do - for _, remainingValues in remainingPairs do - local value = next(remainingValues) - if value ~= nil then - remainingValues[value] = nil - tryReuseProcessor.inputValue:set(value) - error("TODO: bring forward key from old table") - newProcessors[tryReuseProcessor] = true - existingProcessors[tryReuseProcessor] = nil + -- Next, try and reuse processors who match the key of a remaining pair. + -- The value will change but the key will stay stable. + for tryReuseProcessor in existingProcessors do + local key = peek(tryReuseProcessor.inputKey) + local remainingValues = remainingPairs[key] + if remainingValues ~= nil then + local value = next(remainingValues) + if value ~= nil then + remainingValues[value] = nil + tryReuseProcessor.inputValue:set(value) + newProcessors[tryReuseProcessor] = true + existingProcessors[tryReuseProcessor] = nil + end end end - end - -- By this point, we can be in one of three cases: - -- 1) some existing processors are left over; no remaining pairs (shrunk) - -- 2) no existing processors are left over; no remaining pairs (same size) - -- 3) no existing processors are left over; some remaining pairs (grew) - -- So, existing processors should be destroyed, and remaining pairs should - -- be created. This accomodates for table growth and shrinking. - for unusedProcessor in existingProcessors do - doCleanup(unusedProcessor.cleanupTask) - end - table.clear(existingProcessors) - - for key, remainingValues in remainingPairs do - for value in remainingValues do - local scope = {} - local inputKey = Value(scope, key) - local inputValue = Value(scope, value) - local processOK, outputKey, outputValue = xpcall(self._processor, parseError, inputKey, inputValue) - if processOK then - local processor = { - inputKey = inputKey, - inputValue = inputValue, - outputKey = outputKey, - outputValue = outputValue, - cleanupTask = scope - } - newProcessors[processor] = true - else - logErrorNonFatal("forProcessorError", outputKey) + -- Next, try and reuse processors who match the value of a remaining pair. + -- The key will change but the value will stay stable. + for tryReuseProcessor in existingProcessors do + local value = peek(tryReuseProcessor.inputValue) + for key, remainingValues in remainingPairs do + if remainingValues[value] ~= nil then + remainingValues[value] = nil + tryReuseProcessor.inputKey:set(key) + newProcessors[tryReuseProcessor] = true + existingProcessors[tryReuseProcessor] = nil + end + end + end + -- Finally, try and reuse any remaining processors, even if they do not + -- match a pair. Both key and value will be changed. + for tryReuseProcessor in existingProcessors do + for _, remainingValues in remainingPairs do + local value = next(remainingValues) + if value ~= nil then + remainingValues[value] = nil + tryReuseProcessor.inputValue:set(value) + newProcessors[tryReuseProcessor] = true + existingProcessors[tryReuseProcessor] = nil + end + end + end + -- By this point, we can be in one of three cases: + -- 1) some existing processors are left over; no remaining pairs (shrunk) + -- 2) no existing processors are left over; no remaining pairs (same size) + -- 3) no existing processors are left over; some remaining pairs (grew) + -- So, existing processors should be destroyed, and remaining pairs should + -- be created. This accomodates for table growth and shrinking. + for unusedProcessor in existingProcessors do + doCleanup(unusedProcessor.cleanupTask) + end + + for key, remainingValues in remainingPairs do + for value in remainingValues do + local scope = {} + local inputKey = Value(scope, key) + local inputValue = Value(scope, value) + local processOK, outputKey, outputValue = xpcall(self._processor, parseError, inputKey, inputValue) + if processOK then + local processor = { + inputKey = inputKey, + inputValue = inputValue, + outputKey = outputKey, + outputValue = outputValue, + cleanupTask = scope + } + newProcessors[processor] = true + else + logErrorNonFatal("forProcessorError", outputKey) + end end - end end + for processor in newProcessors do + local key = peek(processor.outputKey) + local value = peek(processor.outputValue) + key.dependentSet[self], self.dependencySet[key] = true, true + value.dependentSet[self], self.dependencySet[value] = true, true + newOutputTable[processor.outputKey] = processor.outputValue + end + self._existingProcessors = newProcessors + self._existingOutputTable = newOutputTable + table.clear(existingOutputTable) + table.clear(existingProcessors) + table.clear(remainingPairs) self._newProcessors = existingProcessors + self._newOutputTable = existingOutputTable - error("TODO: reconstruct table") - - return error("TODO") + return true end --[[ Returns the interior value of this state object. ]] function class:_peek(): any - return self._outputTable + return self._existingOutputTable end function class:get() @@ -185,17 +204,15 @@ local function For( dependentSet = setmetatable({}, WEAK_KEYS_METATABLE), _processor = processor, _inputTable = inputTable, - _outputTable = {}, + _existingInputTable = nil, + _existingOutputTable = {}, _existingProcessors = {}, + _newOutputTable = {}, _newProcessors = {}, _remainingPairs = {} }, CLASS_METATABLE) self:update() - if isState(self._inputTable) then - self._inputTable.dependentSet[self] = true - self.dependencySet[self._inputTable] = true - end table.insert(cleanupTable, self) From 32ea6867328540ad61a70faeb638c21bb6f5c237 Mon Sep 17 00:00:00 2001 From: Elttob Date: Mon, 27 Nov 2023 13:51:45 +0000 Subject: [PATCH 032/287] Generic For destruction --- src/State/For.lua | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/State/For.lua b/src/State/For.lua index d67486e44..ee5828a71 100644 --- a/src/State/For.lua +++ b/src/State/For.lua @@ -182,8 +182,9 @@ function class:destroy() for dependency in pairs(self.dependencySet) do dependency.dependentSet[self] = nil end - - error("TODO") + for unusedProcessor in self._existingProcessors do + doCleanup(unusedProcessor.cleanupTask) + end end local function For( From 0a97249e7580367011cfa6c17a1a295adefd9628 Mon Sep 17 00:00:00 2001 From: Elttob Date: Mon, 27 Nov 2023 14:11:39 +0000 Subject: [PATCH 033/287] Start work on generic For unit tests --- .../{cleanup.spec.lua => doCleanup.spec.lua} | 6 +- test/State/For.spec.lua | 222 +++++++ test/State/ForKeys.spec.lua | 310 +++++----- test/State/ForPairs.spec.lua | 398 ++++++------ test/State/ForValues.spec.lua | 570 +++++++++--------- 5 files changed, 864 insertions(+), 642 deletions(-) rename test/Memory/{cleanup.spec.lua => doCleanup.spec.lua} (94%) create mode 100644 test/State/For.spec.lua diff --git a/test/Memory/cleanup.spec.lua b/test/Memory/doCleanup.spec.lua similarity index 94% rename from test/Memory/cleanup.spec.lua rename to test/Memory/doCleanup.spec.lua index dce92d16e..72a9ee290 100644 --- a/test/Memory/cleanup.spec.lua +++ b/test/Memory/doCleanup.spec.lua @@ -80,7 +80,7 @@ return function() expect(numRuns).to.equal(3) end) - it("should clean up contents of arrays in order", function() + it("should clean up contents of arrays in reverse order", function() local runs = {} local tasks = {} @@ -99,9 +99,9 @@ return function() doCleanup(tasks) - expect(runs[1]).to.equal(1) + expect(runs[1]).to.equal(3) expect(runs[2]).to.equal(2) - expect(runs[3]).to.equal(3) + expect(runs[3]).to.equal(1) end) it("should clean up variadic arguments", function() diff --git a/test/State/For.spec.lua b/test/State/For.spec.lua new file mode 100644 index 000000000..0a3995d89 --- /dev/null +++ b/test/State/For.spec.lua @@ -0,0 +1,222 @@ +local Package = game:GetService("ReplicatedStorage").Fusion +local For = require(Package.State.For) +local Value = require(Package.State.Value) +local Computed = require(Package.State.Computed) +local peek = require(Package.State.peek) +local doCleanup = require(Package.Memory.doCleanup) + +return function() + it("constructs in scopes", function() + local scope = {} + local genericFor = For(scope, {}, function() + -- intentionally blank + end) + + expect(genericFor).to.be.a("table") + expect(genericFor.type).to.equal("State") + expect(genericFor.kind).to.equal("For") + expect(scope[1]).to.equal(genericFor) + + doCleanup(scope) + end) + + it("is destroyable", function() + local scope = {} + local genericFor = For(scope, {}, function() + -- intentionally blank + end) + + expect(function() + genericFor:destroy() + end).to.never.throw() + end) + + it("processes pairs for constant tables", function() + local scope = {} + local data = {foo = 1, bar = 2} + local sawFoo, sawBar, sawOther = false, false, false + local numCalls = 0 + local genericFor = For(scope, data, function(inputKey, inputValue) + numCalls += 1 + local k, v = peek(inputKey), peek(inputValue) + if k == "foo" and v == "1" then + sawFoo = true + elseif k == "bar" and v == "2" then + sawBar = true + else + sawOther = true + end + local outputKey = Computed(function(use) + return string.upper(use(inputKey)) + end) + local outputValue = Computed(function(use) + return use(inputValue) * 20 + end) + return outputKey, outputValue + end) + expect(sawFoo).to.equal(true) + expect(sawBar).to.equal(true) + expect(sawOther).to.equal(false) + expect(numCalls).to.equal(2) + + expect(peek(genericFor)).to.be.a("table") + expect(peek(genericFor).FOO).to.equal(10) + expect(peek(genericFor).BAR).to.equal(20) + doCleanup(scope) + end) + + it("processes pairs for state tables", function() + local scope = {} + local data = Value({foo = 1, bar = 2}) + local sawFoo, sawBar, sawOther = false, false, false + local numCalls = 0 + local genericFor = For(scope, data, function(inputKey, inputValue) + numCalls += 1 + local k, v = peek(inputKey), peek(inputValue) + if k == "foo" and v == "1" then + sawFoo = true + elseif k == "bar" and v == "2" then + sawBar = true + else + sawOther = true + end + local outputKey = Computed(function(use) + return string.upper(use(inputKey)) + end) + local outputValue = Computed(function(use) + return use(inputValue) * 20 + end) + return outputKey, outputValue + end) + expect(sawFoo).to.equal(true) + expect(sawBar).to.equal(true) + expect(sawOther).to.equal(false) + expect(numCalls).to.equal(2) + + expect(peek(genericFor)).to.be.a("table") + expect(peek(genericFor).FOO).to.equal(10) + expect(peek(genericFor).BAR).to.equal(20) + + data:set({frob = 3, garb = 4}) + + expect(numCalls).to.equal(2) + expect(peek(genericFor).FOO).to.equal(nil) + expect(peek(genericFor).BAR).to.equal(nil) + expect(peek(genericFor).FROB).to.equal(30) + expect(peek(genericFor).GARB).to.equal(40) + + data:set({frob = 5, garb = 6, baz = 7}) + + expect(numCalls).to.equal(3) + expect(peek(genericFor).FROB).to.equal(50) + expect(peek(genericFor).GARB).to.equal(60) + expect(peek(genericFor).BAZ).to.equal(70) + + data:set({garb = 6, baz = 7}) + + expect(numCalls).to.equal(3) + expect(peek(genericFor).FROB).to.equal(nil) + expect(peek(genericFor).GARB).to.equal(60) + expect(peek(genericFor).BAZ).to.equal(70) + + data:set({}) + + expect(numCalls).to.equal(3) + expect(peek(genericFor).GARB).to.equal(nil) + expect(peek(genericFor).BAZ).to.equal(nil) + + doCleanup(scope) + end) + + -- itSKIP("rejects key collisions", function() + -- expect(function() + -- local scope = {} + -- local data = {foo = 1, bar = 2} + -- local _ = ForKeys(scope, data, function(use, key) + -- return "samuel" + -- end) + -- doCleanup(scope) + -- end).to.throw("forKeysKeyCollision") + -- end) + + -- itSKIP("preserves value on error", function() + -- local scope = {} + -- local data = {foo = 1, bar = 2} + -- local suffix = Value(scope, "first") + -- local forkeys = ForKeys(scope, data, function(use, key) + -- assert(use(suffix) ~= "second", "This is an intentional error from a unit test") + -- return key .. use(suffix) + -- end) + -- expect(peek(forkeys).foofirst).to.equal(1) + -- expect(peek(forkeys).barfirst).to.equal(2) + -- suffix:set("second") -- will invoke the error + -- expect(peek(forkeys).foofirst).to.equal(1) + -- expect(peek(forkeys).barfirst).to.equal(2) + -- expect(peek(forkeys).foosecond).to.equal(nil) + -- expect(peek(forkeys).barsecond).to.equal(nil) + -- suffix:set("third") + -- expect(peek(forkeys).foofirst).to.equal(nil) + -- expect(peek(forkeys).barfirst).to.equal(nil) + -- expect(peek(forkeys).foosecond).to.equal(nil) + -- expect(peek(forkeys).barsecond).to.equal(nil) + -- expect(peek(forkeys).foothird).to.equal(1) + -- expect(peek(forkeys).barthird).to.equal(2) + -- doCleanup(scope) + -- end) + + -- itSKIP("doesn't call destructor on creation", function() + -- local scope = {} + -- local destructed = {} + -- local data = Value(scope, {foo = 1, bar = 2}) + -- local _ = ForKeys(scope, data, function(use, key) + -- return key, "meta" .. key + -- end, function(key, meta) + -- destructed[key] = true + -- destructed[meta] = true + -- end) + -- expect(destructed.foo).to.equal(nil) + -- expect(destructed.metafoo).to.equal(nil) + -- expect(destructed.bar).to.equal(nil) + -- expect(destructed.metabar).to.equal(nil) + -- doCleanup(scope) + -- end) + + -- itSKIP("calls destructor on update", function() + -- local scope = {} + -- local destructed = {} + -- local data = Value(scope, {foo = 1, bar = 2}) + -- local _ = ForKeys(scope, data, function(use, key) + -- return key, "meta" .. key + -- end, function(key, meta) + -- destructed[key] = true + -- destructed[meta] = true + -- end) + -- data:set({foo = 100, baz = 3}) + -- expect(destructed.foo).to.equal(nil) + -- expect(destructed.metafoo).to.equal(nil) + -- expect(destructed.bar).to.equal(true) + -- expect(destructed.metabar).to.equal(true) + -- doCleanup(scope) + -- end) + + -- itSKIP("calls destructor on destroy", function() + -- local scope = {} + -- local destructed = {} + -- local data = Value(scope, {foo = 1, bar = 2}) + -- local _ = ForKeys(scope, data, function(use, key) + -- return key, "meta" .. key + -- end, function(key, meta) + -- destructed[key] = true + -- destructed[meta] = true + -- end) + -- expect(destructed.foo).to.equal(nil) + -- expect(destructed.metafoo).to.equal(nil) + -- expect(destructed.bar).to.equal(nil) + -- expect(destructed.metabar).to.equal(nil) + -- doCleanup(scope) + -- expect(destructed.foo).to.equal(true) + -- expect(destructed.metafoo).to.equal(true) + -- expect(destructed.bar).to.equal(true) + -- expect(destructed.metabar).to.equal(true) + -- end) +end diff --git a/test/State/ForKeys.spec.lua b/test/State/ForKeys.spec.lua index a8a84b766..4e6d22ed5 100644 --- a/test/State/ForKeys.spec.lua +++ b/test/State/ForKeys.spec.lua @@ -5,171 +5,171 @@ local peek = require(Package.State.peek) local doCleanup = require(Package.Memory.doCleanup) return function() - it("constructs in scopes", function() - local scope = {} - local forkeys = ForKeys(scope, {}, function() - -- intentionally blank - end) + -- it("constructs in scopes", function() + -- local scope = {} + -- local forkeys = ForKeys(scope, {}, function() + -- -- intentionally blank + -- end) - expect(forkeys).to.be.a("table") - expect(forkeys.type).to.equal("State") - expect(forkeys.kind).to.equal("ForKeys") - expect(scope[1]).to.equal(forkeys) + -- expect(forkeys).to.be.a("table") + -- expect(forkeys.type).to.equal("State") + -- expect(forkeys.kind).to.equal("ForKeys") + -- expect(scope[1]).to.equal(forkeys) - doCleanup(scope) - end) + -- doCleanup(scope) + -- end) - it("is destroyable", function() - local scope = {} - local forkeys = ForKeys(scope, {}, function() - -- intentionally blank - end) - expect(function() - forkeys:destroy() - end).to.never.throw() - end) + -- it("is destroyable", function() + -- local scope = {} + -- local forkeys = ForKeys(scope, {}, function() + -- -- intentionally blank + -- end) + -- expect(function() + -- forkeys:destroy() + -- end).to.never.throw() + -- end) - it("iterates on constants", function() - local scope = {} - local data = {foo = 1, bar = 2} - local forkeys = ForKeys(scope, data, function(_, key) - return key:upper() - end) - expect(peek(forkeys)).to.be.a("table") - expect(peek(forkeys).FOO).to.equal(1) - expect(peek(forkeys).BAR).to.equal(2) - doCleanup(scope) - end) + -- it("iterates on constants", function() + -- local scope = {} + -- local data = {foo = 1, bar = 2} + -- local forkeys = ForKeys(scope, data, function(_, key) + -- return key:upper() + -- end) + -- expect(peek(forkeys)).to.be.a("table") + -- expect(peek(forkeys).FOO).to.equal(1) + -- expect(peek(forkeys).BAR).to.equal(2) + -- doCleanup(scope) + -- end) - it("iterates on state objects", function() - local scope = {} - local data = Value(scope, {foo = 1, bar = 2}) - local forkeys = ForKeys(scope, data, function(_, key) - return key:upper() - end) - expect(peek(forkeys)).to.be.a("table") - expect(peek(forkeys).FOO).to.equal(1) - expect(peek(forkeys).BAR).to.equal(2) - doCleanup(scope) - end) + -- it("iterates on state objects", function() + -- local scope = {} + -- local data = Value(scope, {foo = 1, bar = 2}) + -- local forkeys = ForKeys(scope, data, function(_, key) + -- return key:upper() + -- end) + -- expect(peek(forkeys)).to.be.a("table") + -- expect(peek(forkeys).FOO).to.equal(1) + -- expect(peek(forkeys).BAR).to.equal(2) + -- doCleanup(scope) + -- end) - it("computes with constants", function() - local scope = {} - local data = {foo = 1, bar = 2} - local forkeys = ForKeys(scope, data, function(use, key) - return key .. use("baz") - end) - expect(peek(forkeys).foobaz).to.equal(1) - expect(peek(forkeys).barbaz).to.equal(2) - doCleanup(scope) - end) + -- it("computes with constants", function() + -- local scope = {} + -- local data = {foo = 1, bar = 2} + -- local forkeys = ForKeys(scope, data, function(use, key) + -- return key .. use("baz") + -- end) + -- expect(peek(forkeys).foobaz).to.equal(1) + -- expect(peek(forkeys).barbaz).to.equal(2) + -- doCleanup(scope) + -- end) - it("computes with state objects", function() - local scope = {} - local data = {foo = 1, bar = 2} - local suffix = Value(scope, "first") - local forkeys = ForKeys(scope, data, function(use, key) - return key .. use(suffix) - end) - expect(peek(forkeys).foofirst).to.equal(1) - expect(peek(forkeys).barfirst).to.equal(2) - suffix:set("second") - expect(peek(forkeys).foofirst).to.equal(nil) - expect(peek(forkeys).barfirst).to.equal(nil) - expect(peek(forkeys).foosecond).to.equal(1) - expect(peek(forkeys).barsecond).to.equal(2) - doCleanup(scope) - end) + -- it("computes with state objects", function() + -- local scope = {} + -- local data = {foo = 1, bar = 2} + -- local suffix = Value(scope, "first") + -- local forkeys = ForKeys(scope, data, function(use, key) + -- return key .. use(suffix) + -- end) + -- expect(peek(forkeys).foofirst).to.equal(1) + -- expect(peek(forkeys).barfirst).to.equal(2) + -- suffix:set("second") + -- expect(peek(forkeys).foofirst).to.equal(nil) + -- expect(peek(forkeys).barfirst).to.equal(nil) + -- expect(peek(forkeys).foosecond).to.equal(1) + -- expect(peek(forkeys).barsecond).to.equal(2) + -- doCleanup(scope) + -- end) - it("rejects key collisions", function() - expect(function() - local scope = {} - local data = {foo = 1, bar = 2} - local _ = ForKeys(scope, data, function(use, key) - return "samuel" - end) - doCleanup(scope) - end).to.throw("forKeysKeyCollision") - end) + -- it("rejects key collisions", function() + -- expect(function() + -- local scope = {} + -- local data = {foo = 1, bar = 2} + -- local _ = ForKeys(scope, data, function(use, key) + -- return "samuel" + -- end) + -- doCleanup(scope) + -- end).to.throw("forKeysKeyCollision") + -- end) - it("preserves value on error", function() - local scope = {} - local data = {foo = 1, bar = 2} - local suffix = Value(scope, "first") - local forkeys = ForKeys(scope, data, function(use, key) - assert(use(suffix) ~= "second", "This is an intentional error from a unit test") - return key .. use(suffix) - end) - expect(peek(forkeys).foofirst).to.equal(1) - expect(peek(forkeys).barfirst).to.equal(2) - suffix:set("second") -- will invoke the error - expect(peek(forkeys).foofirst).to.equal(1) - expect(peek(forkeys).barfirst).to.equal(2) - expect(peek(forkeys).foosecond).to.equal(nil) - expect(peek(forkeys).barsecond).to.equal(nil) - suffix:set("third") - expect(peek(forkeys).foofirst).to.equal(nil) - expect(peek(forkeys).barfirst).to.equal(nil) - expect(peek(forkeys).foosecond).to.equal(nil) - expect(peek(forkeys).barsecond).to.equal(nil) - expect(peek(forkeys).foothird).to.equal(1) - expect(peek(forkeys).barthird).to.equal(2) - doCleanup(scope) - end) + -- it("preserves value on error", function() + -- local scope = {} + -- local data = {foo = 1, bar = 2} + -- local suffix = Value(scope, "first") + -- local forkeys = ForKeys(scope, data, function(use, key) + -- assert(use(suffix) ~= "second", "This is an intentional error from a unit test") + -- return key .. use(suffix) + -- end) + -- expect(peek(forkeys).foofirst).to.equal(1) + -- expect(peek(forkeys).barfirst).to.equal(2) + -- suffix:set("second") -- will invoke the error + -- expect(peek(forkeys).foofirst).to.equal(1) + -- expect(peek(forkeys).barfirst).to.equal(2) + -- expect(peek(forkeys).foosecond).to.equal(nil) + -- expect(peek(forkeys).barsecond).to.equal(nil) + -- suffix:set("third") + -- expect(peek(forkeys).foofirst).to.equal(nil) + -- expect(peek(forkeys).barfirst).to.equal(nil) + -- expect(peek(forkeys).foosecond).to.equal(nil) + -- expect(peek(forkeys).barsecond).to.equal(nil) + -- expect(peek(forkeys).foothird).to.equal(1) + -- expect(peek(forkeys).barthird).to.equal(2) + -- doCleanup(scope) + -- end) - it("doesn't call destructor on creation", function() - local scope = {} - local destructed = {} - local data = Value(scope, {foo = 1, bar = 2}) - local _ = ForKeys(scope, data, function(use, key) - return key, "meta" .. key - end, function(key, meta) - destructed[key] = true - destructed[meta] = true - end) - expect(destructed.foo).to.equal(nil) - expect(destructed.metafoo).to.equal(nil) - expect(destructed.bar).to.equal(nil) - expect(destructed.metabar).to.equal(nil) - doCleanup(scope) - end) + -- it("doesn't call destructor on creation", function() + -- local scope = {} + -- local destructed = {} + -- local data = Value(scope, {foo = 1, bar = 2}) + -- local _ = ForKeys(scope, data, function(use, key) + -- return key, "meta" .. key + -- end, function(key, meta) + -- destructed[key] = true + -- destructed[meta] = true + -- end) + -- expect(destructed.foo).to.equal(nil) + -- expect(destructed.metafoo).to.equal(nil) + -- expect(destructed.bar).to.equal(nil) + -- expect(destructed.metabar).to.equal(nil) + -- doCleanup(scope) + -- end) - it("calls destructor on update", function() - local scope = {} - local destructed = {} - local data = Value(scope, {foo = 1, bar = 2}) - local _ = ForKeys(scope, data, function(use, key) - return key, "meta" .. key - end, function(key, meta) - destructed[key] = true - destructed[meta] = true - end) - data:set({foo = 100, baz = 3}) - expect(destructed.foo).to.equal(nil) - expect(destructed.metafoo).to.equal(nil) - expect(destructed.bar).to.equal(true) - expect(destructed.metabar).to.equal(true) - doCleanup(scope) - end) + -- it("calls destructor on update", function() + -- local scope = {} + -- local destructed = {} + -- local data = Value(scope, {foo = 1, bar = 2}) + -- local _ = ForKeys(scope, data, function(use, key) + -- return key, "meta" .. key + -- end, function(key, meta) + -- destructed[key] = true + -- destructed[meta] = true + -- end) + -- data:set({foo = 100, baz = 3}) + -- expect(destructed.foo).to.equal(nil) + -- expect(destructed.metafoo).to.equal(nil) + -- expect(destructed.bar).to.equal(true) + -- expect(destructed.metabar).to.equal(true) + -- doCleanup(scope) + -- end) - it("calls destructor on destroy", function() - local scope = {} - local destructed = {} - local data = Value(scope, {foo = 1, bar = 2}) - local _ = ForKeys(scope, data, function(use, key) - return key, "meta" .. key - end, function(key, meta) - destructed[key] = true - destructed[meta] = true - end) - expect(destructed.foo).to.equal(nil) - expect(destructed.metafoo).to.equal(nil) - expect(destructed.bar).to.equal(nil) - expect(destructed.metabar).to.equal(nil) - doCleanup(scope) - expect(destructed.foo).to.equal(true) - expect(destructed.metafoo).to.equal(true) - expect(destructed.bar).to.equal(true) - expect(destructed.metabar).to.equal(true) - end) + -- it("calls destructor on destroy", function() + -- local scope = {} + -- local destructed = {} + -- local data = Value(scope, {foo = 1, bar = 2}) + -- local _ = ForKeys(scope, data, function(use, key) + -- return key, "meta" .. key + -- end, function(key, meta) + -- destructed[key] = true + -- destructed[meta] = true + -- end) + -- expect(destructed.foo).to.equal(nil) + -- expect(destructed.metafoo).to.equal(nil) + -- expect(destructed.bar).to.equal(nil) + -- expect(destructed.metabar).to.equal(nil) + -- doCleanup(scope) + -- expect(destructed.foo).to.equal(true) + -- expect(destructed.metafoo).to.equal(true) + -- expect(destructed.bar).to.equal(true) + -- expect(destructed.metabar).to.equal(true) + -- end) end diff --git a/test/State/ForPairs.spec.lua b/test/State/ForPairs.spec.lua index ec1b3fb54..b9cdf0ed3 100644 --- a/test/State/ForPairs.spec.lua +++ b/test/State/ForPairs.spec.lua @@ -6,262 +6,262 @@ local Value = require(Package.State.Value) local peek = require(Package.State.peek) return function() - it("should construct a ForPairs object", function() - local forPairs = ForPairs({}, function(use) end) + -- it("should construct a ForPairs object", function() + -- local forPairs = ForPairs({}, function(use) end) - expect(forPairs).to.be.a("table") - expect(forPairs.type).to.equal("State") - expect(forPairs.kind).to.equal("ForPairs") - end) + -- expect(forPairs).to.be.a("table") + -- expect(forPairs.type).to.equal("State") + -- expect(forPairs.kind).to.equal("ForPairs") + -- end) - it("should calculate and retrieve its value", function() - local computedPair = ForPairs({ ["foo"] = "bar" }, function(use, key, value) - return key .. "baz", value .. "biz" - end) + -- it("should calculate and retrieve its value", function() + -- local computedPair = ForPairs({ ["foo"] = "bar" }, function(use, key, value) + -- return key .. "baz", value .. "biz" + -- end) - local state = peek(computedPair) + -- local state = peek(computedPair) - expect(state["foobaz"]).to.be.ok() - expect(state["foobaz"]).to.equal("barbiz") - end) + -- expect(state["foobaz"]).to.be.ok() + -- expect(state["foobaz"]).to.equal("barbiz") + -- end) - it("should not recalculate its KO/VO in response to an unchanged KI/VI", function() - local state = Value({ - ["foo"] = "bar", - }) + -- it("should not recalculate its KO/VO in response to an unchanged KI/VI", function() + -- local state = Value({ + -- ["foo"] = "bar", + -- }) - local computedPair = ForPairs(state, function(use, key, value) - return key .. "biz", { value } - end) + -- local computedPair = ForPairs(state, function(use, key, value) + -- return key .. "biz", { value } + -- end) - local foobiz = peek(computedPair)["foobiz"] + -- local foobiz = peek(computedPair)["foobiz"] - state:set({ - ["foo"] = "bar", - ["baz"] = "bar", - }) + -- state:set({ + -- ["foo"] = "bar", + -- ["baz"] = "bar", + -- }) - expect(peek(computedPair)["foobiz"]).to.equal(foobiz) - end) + -- expect(peek(computedPair)["foobiz"]).to.equal(foobiz) + -- end) - it("should call the destructor when a key/value pair gets changed", function() - local state = Value({ - ["foo"] = "bar", - ["baz"] = "bar", - }) + -- it("should call the destructor when a key/value pair gets changed", function() + -- local state = Value({ + -- ["foo"] = "bar", + -- ["baz"] = "bar", + -- }) - local destructions = 0 + -- local destructions = 0 - local computedPair = ForPairs(state, function(use, key, value) - return key .. "biz", value .. "biz" - end, function(key, value) - destructions += 1 - end) + -- local computedPair = ForPairs(state, function(use, key, value) + -- return key .. "biz", value .. "biz" + -- end, function(key, value) + -- destructions += 1 + -- end) - state:set({ - ["foo"] = "bar", - }) + -- state:set({ + -- ["foo"] = "bar", + -- }) - expect(destructions).to.equal(1) + -- expect(destructions).to.equal(1) - state:set({ - ["baz"] = "bar", - }) + -- state:set({ + -- ["baz"] = "bar", + -- }) - expect(destructions).to.equal(2) + -- expect(destructions).to.equal(2) - state:set({ - ["foo"] = "bar", - ["baz"] = "bar", - }) + -- state:set({ + -- ["foo"] = "bar", + -- ["baz"] = "bar", + -- }) - expect(destructions).to.equal(2) + -- expect(destructions).to.equal(2) - state:set({}) + -- state:set({}) - expect(destructions).to.equal(4) - end) + -- expect(destructions).to.equal(4) + -- end) - it( - "should not call the destructor when an output key/value pair still remains with a new input key/value pair", - function() - local state = Value({ - ["foo"] = "bar", - }) + -- it( + -- "should not call the destructor when an output key/value pair still remains with a new input key/value pair", + -- function() + -- local state = Value({ + -- ["foo"] = "bar", + -- }) - local destructions = 0 + -- local destructions = 0 - local computedPair = ForPairs(state, function(use, key, value) - return value, value - end, function(key, value) - destructions += 1 - end) + -- local computedPair = ForPairs(state, function(use, key, value) + -- return value, value + -- end, function(key, value) + -- destructions += 1 + -- end) - state:set({ - ["baz"] = "bar", - }) + -- state:set({ + -- ["baz"] = "bar", + -- }) - expect(destructions).to.equal(0) + -- expect(destructions).to.equal(0) - state:set({ - ["biz"] = "baz", - }) + -- state:set({ + -- ["biz"] = "baz", + -- }) - expect(destructions).to.equal(1) + -- expect(destructions).to.equal(1) - state:set({ - ["foo"] = "bar", - ["biz"] = "baz", - }) + -- state:set({ + -- ["foo"] = "bar", + -- ["biz"] = "baz", + -- }) - expect(destructions).to.equal(1) + -- expect(destructions).to.equal(1) - state:set({ - ["foo"] = "baz", - ["biz"] = "bar", - }) + -- state:set({ + -- ["foo"] = "baz", + -- ["biz"] = "bar", + -- }) - expect(destructions).to.equal(1) + -- expect(destructions).to.equal(1) - state:set({ - ["biz"] = "bar", - }) + -- state:set({ + -- ["biz"] = "bar", + -- }) - expect(destructions).to.equal(2) + -- expect(destructions).to.equal(2) - state:set({}) + -- state:set({}) - expect(destructions).to.equal(3) - end - ) + -- expect(destructions).to.equal(3) + -- end + -- ) - it("should throw if there is a key collision", function() - expect(function() - local state = Value({ - ["foo"] = "bar", - ["baz"] = "bar", - }) + -- it("should throw if there is a key collision", function() + -- expect(function() + -- local state = Value({ + -- ["foo"] = "bar", + -- ["baz"] = "bar", + -- }) - local computed = ForPairs(state, function(use, key, value) - return value, key - end) - end).to.throw("forPairsKeyCollision") + -- local computed = ForPairs(state, function(use, key, value) + -- return value, key + -- end) + -- end).to.throw("forPairsKeyCollision") - local state = Value({ - ["foo"] = "bar", - }) + -- local state = Value({ + -- ["foo"] = "bar", + -- }) - local computed = ForPairs(state, function(use, key, value) - return value, key - end) + -- local computed = ForPairs(state, function(use, key, value) + -- return value, key + -- end) - expect(function() - state:set({ - ["foo"] = "bar", - ["baz"] = "bar", - }) - end).to.throw("forPairsKeyCollision") - end) + -- expect(function() + -- state:set({ + -- ["foo"] = "bar", + -- ["baz"] = "bar", + -- }) + -- end).to.throw("forPairsKeyCollision") + -- end) - it("should call the destructor with meta data", function() - local state = Value({ - ["foo"] = "bar", - }) + -- it("should call the destructor with meta data", function() + -- local state = Value({ + -- ["foo"] = "bar", + -- }) - local destructions = 0 + -- local destructions = 0 - local computedPair = ForPairs(state, function(use, key, value) - local newKey = key .. "biz" - local newValue = value .. "biz" + -- local computedPair = ForPairs(state, function(use, key, value) + -- local newKey = key .. "biz" + -- local newValue = value .. "biz" - return newKey, newValue, newKey .. newValue - end, function(key, value, meta) - expect(meta).to.equal(key .. value) - destructions += 1 - end) + -- return newKey, newValue, newKey .. newValue + -- end, function(key, value, meta) + -- expect(meta).to.equal(key .. value) + -- destructions += 1 + -- end) - state:set({ - ["foo"] = "baz", - }) + -- state:set({ + -- ["foo"] = "baz", + -- }) - -- this verifies that the meta expectation passed - expect(destructions).to.equal(1) + -- -- this verifies that the meta expectation passed + -- expect(destructions).to.equal(1) - state:set({}) + -- state:set({}) - -- this verifies that the meta expectation passed - expect(destructions).to.equal(2) - end) + -- -- this verifies that the meta expectation passed + -- expect(destructions).to.equal(2) + -- end) - it("should recalculate its value in response to State objects", function() - local currentNumber = Value({ ["foo"] = 2 }) - local doubled = ForPairs(currentNumber, function(use, key, value) - return key .. "bar", value * 2 - end) + -- it("should recalculate its value in response to State objects", function() + -- local currentNumber = Value({ ["foo"] = 2 }) + -- local doubled = ForPairs(currentNumber, function(use, key, value) + -- return key .. "bar", value * 2 + -- end) - expect(peek(doubled)["foobar"]).to.equal(4) + -- expect(peek(doubled)["foobar"]).to.equal(4) - currentNumber:set({ ["foo"] = 4 }) - expect(peek(doubled)["foobar"]).to.equal(8) - end) + -- currentNumber:set({ ["foo"] = 4 }) + -- expect(peek(doubled)["foobar"]).to.equal(8) + -- end) - it("should recalculate its value in response to ForPairs objects", function() - local currentNumbers = Value({ 1, 2 }) - local doubled = ForPairs(currentNumbers, function(use, key, value) - return key * 2, value * 2 - end) - local tripled = ForPairs(doubled, function(use, key, value) - return key * 2, value * 2 - end) + -- it("should recalculate its value in response to ForPairs objects", function() + -- local currentNumbers = Value({ 1, 2 }) + -- local doubled = ForPairs(currentNumbers, function(use, key, value) + -- return key * 2, value * 2 + -- end) + -- local tripled = ForPairs(doubled, function(use, key, value) + -- return key * 2, value * 2 + -- end) - expect(peek(tripled)[4]).to.equal(4) - expect(peek(tripled)[8]).to.equal(8) - - currentNumbers:set({ 2, 4 }) - expect(peek(tripled)[4]).to.equal(8) - expect(peek(tripled)[8]).to.equal(16) - end) + -- expect(peek(tripled)[4]).to.equal(4) + -- expect(peek(tripled)[8]).to.equal(8) + + -- currentNumbers:set({ 2, 4 }) + -- expect(peek(tripled)[4]).to.equal(8) + -- expect(peek(tripled)[8]).to.equal(16) + -- end) - itSKIP("should not corrupt dependencies after an error", function() - -- needs rewrite - end) + -- itSKIP("should not corrupt dependencies after an error", function() + -- -- needs rewrite + -- end) - it("should garbage-collect unused objects", function() - local state = Value({ 2 }) - - local counter = 0 - - do - local computedPairs = ForPairs(state, function(use, key, value) - counter += 1 - return key, value - end) - end - - state:set({ 5 }) - - expect(counter).to.equal(1) - end) - - it("should not garbage-collect objects in use", function() - local state = Value({ 2 }) - local computed2 + -- it("should garbage-collect unused objects", function() + -- local state = Value({ 2 }) + + -- local counter = 0 + + -- do + -- local computedPairs = ForPairs(state, function(use, key, value) + -- counter += 1 + -- return key, value + -- end) + -- end + + -- state:set({ 5 }) + + -- expect(counter).to.equal(1) + -- end) + + -- it("should not garbage-collect objects in use", function() + -- local state = Value({ 2 }) + -- local computed2 - local counter = 0 - - do - local computed = ForPairs(state, function(use, key, value) - counter += 1 - return key, value - end) + -- local counter = 0 + + -- do + -- local computed = ForPairs(state, function(use, key, value) + -- counter += 1 + -- return key, value + -- end) - computed2 = ForPairs(computed, function(use, key, value) - return key, value - end) - end + -- computed2 = ForPairs(computed, function(use, key, value) + -- return key, value + -- end) + -- end - state:set({ 5 }) + -- state:set({ 5 }) - expect(counter).to.equal(2) - end) + -- expect(counter).to.equal(2) + -- end) end diff --git a/test/State/ForValues.spec.lua b/test/State/ForValues.spec.lua index 6323a32d4..ac9f74884 100644 --- a/test/State/ForValues.spec.lua +++ b/test/State/ForValues.spec.lua @@ -6,348 +6,348 @@ local Value = require(Package.State.Value) local peek = require(Package.State.peek) return function() - it("should construct a ForValues object", function() - local forKeys = ForValues({}, function(use) end) + -- it("should construct a ForValues object", function() + -- local forKeys = ForValues({}, function(use) end) - expect(forKeys).to.be.a("table") - expect(forKeys.type).to.equal("State") - expect(forKeys.kind).to.equal("ForValues") - end) + -- expect(forKeys).to.be.a("table") + -- expect(forKeys.type).to.equal("State") + -- expect(forKeys.kind).to.equal("ForValues") + -- end) - it("should calculate and retrieve its value", function() - local computed = ForValues({ 1 }, function(use, value) - return value - end) + -- it("should calculate and retrieve its value", function() + -- local computed = ForValues({ 1 }, function(use, value) + -- return value + -- end) - local state = peek(computed) + -- local state = peek(computed) - expect(state[1]).to.be.ok() - expect(state[1]).to.equal(1) - end) + -- expect(state[1]).to.be.ok() + -- expect(state[1]).to.equal(1) + -- end) - it("should not recalculate its VO in response to a changed VI", function() - local state = Value({ - [1] = "foo", - }) + -- it("should not recalculate its VO in response to a changed VI", function() + -- local state = Value({ + -- [1] = "foo", + -- }) - local calculations = 0 + -- local calculations = 0 - local computed = ForValues(state, function(use, value) - calculations += 1 - return value - end) + -- local computed = ForValues(state, function(use, value) + -- calculations += 1 + -- return value + -- end) - expect(calculations).to.equal(1) + -- expect(calculations).to.equal(1) - state:set({ - [1] = "bar", - [2] = "foo", - }) + -- state:set({ + -- [1] = "bar", + -- [2] = "foo", + -- }) - expect(calculations).to.equal(2) - end) + -- expect(calculations).to.equal(2) + -- end) - it("should only call the processor the first time a constant output value is added", function() - local state = Value({ - [1] = "foo", - }) + -- it("should only call the processor the first time a constant output value is added", function() + -- local state = Value({ + -- [1] = "foo", + -- }) - local processorCalls = 0 + -- local processorCalls = 0 - local computed = ForValues(state, function(use, value) - processorCalls += 1 + -- local computed = ForValues(state, function(use, value) + -- processorCalls += 1 - return value .. "biz" - end) + -- return value .. "biz" + -- end) - expect(processorCalls).to.equal(1) + -- expect(processorCalls).to.equal(1) - state:set({ - [1] = "bar", - }) + -- state:set({ + -- [1] = "bar", + -- }) - expect(processorCalls).to.equal(2) + -- expect(processorCalls).to.equal(2) - state:set({ - [2] = "bar", - [3] = "bar", - }) + -- state:set({ + -- [2] = "bar", + -- [3] = "bar", + -- }) - expect(processorCalls).to.equal(2) + -- expect(processorCalls).to.equal(2) - state:set({ - [1] = "bar", - [2] = "bar", - }) + -- state:set({ + -- [1] = "bar", + -- [2] = "bar", + -- }) - expect(processorCalls).to.equal(2) + -- expect(processorCalls).to.equal(2) - state:set({}) + -- state:set({}) - expect(processorCalls).to.equal(2) + -- expect(processorCalls).to.equal(2) - state:set({ - [1] = "bar", - [2] = "foo", - }) + -- state:set({ + -- [1] = "bar", + -- [2] = "foo", + -- }) - expect(processorCalls).to.equal(4) - end) + -- expect(processorCalls).to.equal(4) + -- end) - it("should only call the destructor when a constant value gets removed from all indices", function() - local state = Value({ - [1] = "foo", - }) + -- it("should only call the destructor when a constant value gets removed from all indices", function() + -- local state = Value({ + -- [1] = "foo", + -- }) - local destructions = 0 + -- local destructions = 0 - local computed = ForValues(state, function(use, value) - return value .. "biz" - end, function(key) - destructions += 1 - end) + -- local computed = ForValues(state, function(use, value) + -- return value .. "biz" + -- end, function(key) + -- destructions += 1 + -- end) - state:set({ - [1] = "bar", - }) + -- state:set({ + -- [1] = "bar", + -- }) - expect(destructions).to.equal(1) + -- expect(destructions).to.equal(1) - state:set({ - [2] = "bar", - [3] = "bar", - }) + -- state:set({ + -- [2] = "bar", + -- [3] = "bar", + -- }) - expect(destructions).to.equal(1) + -- expect(destructions).to.equal(1) - state:set({ - [1] = "bar", - [2] = "bar", - }) + -- state:set({ + -- [1] = "bar", + -- [2] = "bar", + -- }) - expect(destructions).to.equal(1) + -- expect(destructions).to.equal(1) - state:set({}) + -- state:set({}) - expect(destructions).to.equal(2) - end) + -- expect(destructions).to.equal(2) + -- end) - it("should only call the destructor when a non-constant value gets removed from all indices", function() - local mem1 = Instance.new("Folder") - local mem2 = Instance.new("Folder") - local mem3 = Instance.new("Folder") + -- it("should only call the destructor when a non-constant value gets removed from all indices", function() + -- local mem1 = Instance.new("Folder") + -- local mem2 = Instance.new("Folder") + -- local mem3 = Instance.new("Folder") - local state = Value({ - [1] = mem1, - }) + -- local state = Value({ + -- [1] = mem1, + -- }) - local destructions = 0 + -- local destructions = 0 - local computed = ForValues(state, function(use, value) - local obj = Instance.new("Folder") - obj.Parent = value + -- local computed = ForValues(state, function(use, value) + -- local obj = Instance.new("Folder") + -- obj.Parent = value - return obj - end, function(value) - destructions += 1 - value:Destroy() - end) + -- return obj + -- end, function(value) + -- destructions += 1 + -- value:Destroy() + -- end) - state:set({ - [1] = mem2, - }) + -- state:set({ + -- [1] = mem2, + -- }) - expect(destructions).to.equal(1) + -- expect(destructions).to.equal(1) - state:set({ - [2] = mem3, - [3] = mem2, - }) + -- state:set({ + -- [2] = mem3, + -- [3] = mem2, + -- }) - expect(destructions).to.equal(1) + -- expect(destructions).to.equal(1) - state:set({ - [1] = mem2, - [2] = mem3, - }) + -- state:set({ + -- [1] = mem2, + -- [2] = mem3, + -- }) - expect(destructions).to.equal(1) + -- expect(destructions).to.equal(1) - state:set({}) + -- state:set({}) - expect(destructions).to.equal(3) - end) + -- expect(destructions).to.equal(3) + -- end) - it("should call the destructor with meta data", function() - local state = Value({ - [1] = "foo", - }) + -- it("should call the destructor with meta data", function() + -- local state = Value({ + -- [1] = "foo", + -- }) - local destructions = 0 + -- local destructions = 0 - local computed = ForValues(state, function(use, value) - local newValue = value .. "biz" - return newValue, newValue - end, function(value, meta) - expect(meta).to.equal(value) - destructions += 1 - end) + -- local computed = ForValues(state, function(use, value) + -- local newValue = value .. "biz" + -- return newValue, newValue + -- end, function(value, meta) + -- expect(meta).to.equal(value) + -- destructions += 1 + -- end) - state:set({ - ["baz"] = "bar", - }) + -- state:set({ + -- ["baz"] = "bar", + -- }) - -- this verifies that the meta expectation passed - expect(destructions).to.equal(1) + -- -- this verifies that the meta expectation passed + -- expect(destructions).to.equal(1) - state:set({}) - - -- this verifies that the meta expectation passed - expect(destructions).to.equal(2) - end) + -- state:set({}) + + -- -- this verifies that the meta expectation passed + -- expect(destructions).to.equal(2) + -- end) - it("should not make any value changes or processor/destructor calls when only the input key changes", function() - local state = Value({ - ["foo"] = "bar", - ["bar"] = "baz", - ["biz"] = "buzz", - }) + -- it("should not make any value changes or processor/destructor calls when only the input key changes", function() + -- local state = Value({ + -- ["foo"] = "bar", + -- ["bar"] = "baz", + -- ["biz"] = "buzz", + -- }) - local processorCalls = 0 - local destructorCalls = 0 - - local computed = ForValues(state, function(use, value) - processorCalls += 1 - return value - end, function(value) - destructorCalls += 1 - end) - - expect(processorCalls).to.equal(3) - expect(destructorCalls).to.equal(0) - - state:set({ - [1] = "buzz", - ["bar"] = "bar", - ["fiz"] = "baz", - }) - - expect(processorCalls).to.equal(3) - expect(destructorCalls).to.equal(0) - - state:set({ - [1] = "bar", - ["bar"] = "bar", - ["fiz"] = "bar", - }) - - expect(processorCalls).to.equal(3) - expect(destructorCalls).to.equal(2) - - state:set({ - [2] = "bar", - [3] = "baz", - }) - - expect(processorCalls).to.equal(4) - expect(destructorCalls).to.equal(2) - - state:set({}) - - expect(processorCalls).to.equal(4) - expect(destructorCalls).to.equal(4) - end) - - it("should recalculate its value in response to State objects", function() - local state = Value({ - [1] = "baz", - }) - local barMap = ForValues(state, function(use, value) - return value .. "bar" - end) - - expect(peek(barMap)[1]).to.equal("bazbar") - - state:set({ - [1] = "bar", - }) - - expect(peek(barMap)[1]).to.equal("barbar") - end) - - it("should recalculate its value in response to ForValues objects", function() - local state = Value({ - [1] = 1, - }) - local doubled = ForValues(state, function(use, value) - return value * 2 - end) - local tripled = ForValues(doubled, function(use, value) - return value * 2 - end) - - expect(peek(doubled)[1]).to.equal(2) - expect(peek(tripled)[1]).to.equal(4) - - state:set({ - [1] = 2, - [2] = 3, - }) - - expect(peek(doubled)[1]).to.equal(4) - expect(peek(tripled)[1]).to.equal(8) - expect(peek(doubled)[2]).to.equal(6) - expect(peek(tripled)[2]).to.equal(12) - end) - - itSKIP("should not corrupt dependencies after an error", function() - -- needs rewrite - end) - - it("should garbage-collect unused objects", function() - local state = Value({ - [1] = "bar", - }) - - local counter = 0 - - do - local computedKeys = ForValues(state, function(use, value) - counter += 1 - return value - end) - end - - state:set({ - [1] = "biz", - }) - - expect(counter).to.equal(1) - end) - - it("should not garbage-collect objects in use", function() - local state = Value({ - [1] = 1, - }) - local computed2 - - local counter = 0 - - do - local computed = ForValues(state, function(use, value) - counter += 1 - return value - end) - - computed2 = ForValues(computed, function(use, value) - return value - end) - end + -- local processorCalls = 0 + -- local destructorCalls = 0 + + -- local computed = ForValues(state, function(use, value) + -- processorCalls += 1 + -- return value + -- end, function(value) + -- destructorCalls += 1 + -- end) + + -- expect(processorCalls).to.equal(3) + -- expect(destructorCalls).to.equal(0) + + -- state:set({ + -- [1] = "buzz", + -- ["bar"] = "bar", + -- ["fiz"] = "baz", + -- }) + + -- expect(processorCalls).to.equal(3) + -- expect(destructorCalls).to.equal(0) + + -- state:set({ + -- [1] = "bar", + -- ["bar"] = "bar", + -- ["fiz"] = "bar", + -- }) + + -- expect(processorCalls).to.equal(3) + -- expect(destructorCalls).to.equal(2) + + -- state:set({ + -- [2] = "bar", + -- [3] = "baz", + -- }) + + -- expect(processorCalls).to.equal(4) + -- expect(destructorCalls).to.equal(2) + + -- state:set({}) + + -- expect(processorCalls).to.equal(4) + -- expect(destructorCalls).to.equal(4) + -- end) + + -- it("should recalculate its value in response to State objects", function() + -- local state = Value({ + -- [1] = "baz", + -- }) + -- local barMap = ForValues(state, function(use, value) + -- return value .. "bar" + -- end) + + -- expect(peek(barMap)[1]).to.equal("bazbar") + + -- state:set({ + -- [1] = "bar", + -- }) + + -- expect(peek(barMap)[1]).to.equal("barbar") + -- end) + + -- it("should recalculate its value in response to ForValues objects", function() + -- local state = Value({ + -- [1] = 1, + -- }) + -- local doubled = ForValues(state, function(use, value) + -- return value * 2 + -- end) + -- local tripled = ForValues(doubled, function(use, value) + -- return value * 2 + -- end) + + -- expect(peek(doubled)[1]).to.equal(2) + -- expect(peek(tripled)[1]).to.equal(4) + + -- state:set({ + -- [1] = 2, + -- [2] = 3, + -- }) + + -- expect(peek(doubled)[1]).to.equal(4) + -- expect(peek(tripled)[1]).to.equal(8) + -- expect(peek(doubled)[2]).to.equal(6) + -- expect(peek(tripled)[2]).to.equal(12) + -- end) + + -- itSKIP("should not corrupt dependencies after an error", function() + -- -- needs rewrite + -- end) + + -- it("should garbage-collect unused objects", function() + -- local state = Value({ + -- [1] = "bar", + -- }) + + -- local counter = 0 + + -- do + -- local computedKeys = ForValues(state, function(use, value) + -- counter += 1 + -- return value + -- end) + -- end + + -- state:set({ + -- [1] = "biz", + -- }) + + -- expect(counter).to.equal(1) + -- end) + + -- it("should not garbage-collect objects in use", function() + -- local state = Value({ + -- [1] = 1, + -- }) + -- local computed2 + + -- local counter = 0 + + -- do + -- local computed = ForValues(state, function(use, value) + -- counter += 1 + -- return value + -- end) + + -- computed2 = ForValues(computed, function(use, value) + -- return value + -- end) + -- end - state:set({ - [1] = 2, - }) + -- state:set({ + -- [1] = 2, + -- }) - expect(counter).to.equal(2) - end) + -- expect(counter).to.equal(2) + -- end) end From 443c1228166114e696ef7dda00c7143c74b79486 Mon Sep 17 00:00:00 2001 From: Elttob Date: Mon, 27 Nov 2023 14:26:26 +0000 Subject: [PATCH 034/287] Fix some unit tests --- src/State/For.lua | 10 ++++----- test/State/For.spec.lua | 45 ++++++++++++----------------------------- 2 files changed, 18 insertions(+), 37 deletions(-) diff --git a/src/State/For.lua b/src/State/For.lua index ee5828a71..2e9095d13 100644 --- a/src/State/For.lua +++ b/src/State/For.lua @@ -39,7 +39,7 @@ function class:update(): boolean local existingInputTable = self._existingInputTable local existingOutputTable = self._existingOutputTable local existingProcessors = self._existingProcessors - local newInputTable = peek(self._newInputTable) + local newInputTable = peek(self._inputTable) local newOutputTable = self._newOutputTable local newProcessors = self._newProcessors local remainingPairs = self._remainingPairs @@ -131,7 +131,7 @@ function class:update(): boolean local scope = {} local inputKey = Value(scope, key) local inputValue = Value(scope, value) - local processOK, outputKey, outputValue = xpcall(self._processor, parseError, inputKey, inputValue) + local processOK, outputKey, outputValue = xpcall(self._processor, parseError, scope, inputKey, inputValue) if processOK then local processor = { inputKey = inputKey, @@ -149,11 +149,10 @@ function class:update(): boolean end for processor in newProcessors do - local key = peek(processor.outputKey) - local value = peek(processor.outputValue) + local key, value = processor.outputKey, processor.outputValue key.dependentSet[self], self.dependencySet[key] = true, true value.dependentSet[self], self.dependencySet[value] = true, true - newOutputTable[processor.outputKey] = processor.outputValue + newOutputTable[peek(key)] = peek(value) end self._existingProcessors = newProcessors @@ -191,6 +190,7 @@ local function For( cleanupTable: {PubTypes.Task}, inputTable: PubTypes.CanBeState<{ [KI]: VI }>, processor: ( + {any}, PubTypes.StateObject, PubTypes.StateObject ) -> (PubTypes.StateObject, PubTypes.StateObject) diff --git a/test/State/For.spec.lua b/test/State/For.spec.lua index 0a3995d89..6d0b51af4 100644 --- a/test/State/For.spec.lua +++ b/test/State/For.spec.lua @@ -31,33 +31,26 @@ return function() end).to.never.throw() end) - it("processes pairs for constant tables", function() + itFOCUS("processes pairs for constant tables", function() local scope = {} local data = {foo = 1, bar = 2} - local sawFoo, sawBar, sawOther = false, false, false + local seen = {} local numCalls = 0 - local genericFor = For(scope, data, function(inputKey, inputValue) + local genericFor = For(scope, data, function(scope, inputKey, inputValue) numCalls += 1 local k, v = peek(inputKey), peek(inputValue) - if k == "foo" and v == "1" then - sawFoo = true - elseif k == "bar" and v == "2" then - sawBar = true - else - sawOther = true - end - local outputKey = Computed(function(use) + seen[k] = v + local outputKey = Computed(scope, function(use) return string.upper(use(inputKey)) end) - local outputValue = Computed(function(use) - return use(inputValue) * 20 + local outputValue = Computed(scope, function(use) + return use(inputValue) * 10 end) return outputKey, outputValue end) - expect(sawFoo).to.equal(true) - expect(sawBar).to.equal(true) - expect(sawOther).to.equal(false) expect(numCalls).to.equal(2) + expect(seen.foo).to.equal(1) + expect(seen.bar).to.equal(2) expect(peek(genericFor)).to.be.a("table") expect(peek(genericFor).FOO).to.equal(10) @@ -68,29 +61,17 @@ return function() it("processes pairs for state tables", function() local scope = {} local data = Value({foo = 1, bar = 2}) - local sawFoo, sawBar, sawOther = false, false, false local numCalls = 0 - local genericFor = For(scope, data, function(inputKey, inputValue) + local genericFor = For(scope, data, function(scope, inputKey, inputValue) numCalls += 1 - local k, v = peek(inputKey), peek(inputValue) - if k == "foo" and v == "1" then - sawFoo = true - elseif k == "bar" and v == "2" then - sawBar = true - else - sawOther = true - end - local outputKey = Computed(function(use) + local outputKey = Computed(scope, function(use) return string.upper(use(inputKey)) end) - local outputValue = Computed(function(use) - return use(inputValue) * 20 + local outputValue = Computed(scope, function(use) + return use(inputValue) * 10 end) return outputKey, outputValue end) - expect(sawFoo).to.equal(true) - expect(sawBar).to.equal(true) - expect(sawOther).to.equal(false) expect(numCalls).to.equal(2) expect(peek(genericFor)).to.be.a("table") From 24910a9cd8299194815f48ce243f87926f8e4e74 Mon Sep 17 00:00:00 2001 From: Elttob Date: Mon, 27 Nov 2023 15:51:18 +0000 Subject: [PATCH 035/287] Fixed up generic For object handling state changes --- src/State/For.lua | 9 ++++++++- test/State/For.spec.lua | 10 +++++++--- 2 files changed, 15 insertions(+), 4 deletions(-) diff --git a/src/State/For.lua b/src/State/For.lua index 2e9095d13..02e7c1359 100644 --- a/src/State/For.lua +++ b/src/State/For.lua @@ -23,7 +23,10 @@ local class = {} local CLASS_METATABLE = { __index = class } local WEAK_KEYS_METATABLE = { __mode = "k" } +local NAMES = {"Alice", "Bob", "Charlie", "Dennis", "Edward", "Fanta", "Graham", "Iliza", "James", "Katy", "Liam", "Mason", "Nora"} + type Processor = { + name: string, inputKey: PubTypes.Value, inputValue: PubTypes.Value, outputKey: PubTypes.StateObject, @@ -100,19 +103,22 @@ function class:update(): boolean tryReuseProcessor.inputKey:set(key) newProcessors[tryReuseProcessor] = true existingProcessors[tryReuseProcessor] = nil + break end end end -- Finally, try and reuse any remaining processors, even if they do not -- match a pair. Both key and value will be changed. for tryReuseProcessor in existingProcessors do - for _, remainingValues in remainingPairs do + for key, remainingValues in remainingPairs do local value = next(remainingValues) if value ~= nil then remainingValues[value] = nil + tryReuseProcessor.inputKey:set(key) tryReuseProcessor.inputValue:set(value) newProcessors[tryReuseProcessor] = true existingProcessors[tryReuseProcessor] = nil + break end end end @@ -134,6 +140,7 @@ function class:update(): boolean local processOK, outputKey, outputValue = xpcall(self._processor, parseError, scope, inputKey, inputValue) if processOK then local processor = { + name = table.remove(NAMES, 1), inputKey = inputKey, inputValue = inputValue, outputKey = outputKey, diff --git a/test/State/For.spec.lua b/test/State/For.spec.lua index 6d0b51af4..6b307ebee 100644 --- a/test/State/For.spec.lua +++ b/test/State/For.spec.lua @@ -31,7 +31,7 @@ return function() end).to.never.throw() end) - itFOCUS("processes pairs for constant tables", function() + it("processes pairs for constant tables", function() local scope = {} local data = {foo = 1, bar = 2} local seen = {} @@ -58,9 +58,9 @@ return function() doCleanup(scope) end) - it("processes pairs for state tables", function() + itFOCUS("processes pairs for state tables", function() local scope = {} - local data = Value({foo = 1, bar = 2}) + local data = Value(scope, {foo = 1, bar = 2}) local numCalls = 0 local genericFor = For(scope, data, function(scope, inputKey, inputValue) numCalls += 1 @@ -78,6 +78,7 @@ return function() expect(peek(genericFor).FOO).to.equal(10) expect(peek(genericFor).BAR).to.equal(20) + print("\ndata:set {frob = 3, garb = 4}") data:set({frob = 3, garb = 4}) expect(numCalls).to.equal(2) @@ -86,6 +87,7 @@ return function() expect(peek(genericFor).FROB).to.equal(30) expect(peek(genericFor).GARB).to.equal(40) + print("\ndata:set {frob = 5, garb = 6, baz = 7}") data:set({frob = 5, garb = 6, baz = 7}) expect(numCalls).to.equal(3) @@ -93,6 +95,7 @@ return function() expect(peek(genericFor).GARB).to.equal(60) expect(peek(genericFor).BAZ).to.equal(70) + print("\ndata:set {garb = 6, baz = 7}") data:set({garb = 6, baz = 7}) expect(numCalls).to.equal(3) @@ -100,6 +103,7 @@ return function() expect(peek(genericFor).GARB).to.equal(60) expect(peek(genericFor).BAZ).to.equal(70) + print("\ndata:set {}") data:set({}) expect(numCalls).to.equal(3) From 899a78c1d8db00871ae810621b4b2dc356433d90 Mon Sep 17 00:00:00 2001 From: Elttob Date: Mon, 27 Nov 2023 15:54:16 +0000 Subject: [PATCH 036/287] Remove debug prints --- test/State/For.spec.lua | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/test/State/For.spec.lua b/test/State/For.spec.lua index 6b307ebee..4aaf79c3a 100644 --- a/test/State/For.spec.lua +++ b/test/State/For.spec.lua @@ -6,6 +6,8 @@ local peek = require(Package.State.peek) local doCleanup = require(Package.Memory.doCleanup) return function() + FOCUS() + it("constructs in scopes", function() local scope = {} local genericFor = For(scope, {}, function() @@ -58,7 +60,7 @@ return function() doCleanup(scope) end) - itFOCUS("processes pairs for state tables", function() + it("processes pairs for state tables", function() local scope = {} local data = Value(scope, {foo = 1, bar = 2}) local numCalls = 0 @@ -78,7 +80,6 @@ return function() expect(peek(genericFor).FOO).to.equal(10) expect(peek(genericFor).BAR).to.equal(20) - print("\ndata:set {frob = 3, garb = 4}") data:set({frob = 3, garb = 4}) expect(numCalls).to.equal(2) @@ -87,7 +88,6 @@ return function() expect(peek(genericFor).FROB).to.equal(30) expect(peek(genericFor).GARB).to.equal(40) - print("\ndata:set {frob = 5, garb = 6, baz = 7}") data:set({frob = 5, garb = 6, baz = 7}) expect(numCalls).to.equal(3) @@ -95,7 +95,6 @@ return function() expect(peek(genericFor).GARB).to.equal(60) expect(peek(genericFor).BAZ).to.equal(70) - print("\ndata:set {garb = 6, baz = 7}") data:set({garb = 6, baz = 7}) expect(numCalls).to.equal(3) @@ -103,7 +102,6 @@ return function() expect(peek(genericFor).GARB).to.equal(60) expect(peek(genericFor).BAZ).to.equal(70) - print("\ndata:set {}") data:set({}) expect(numCalls).to.equal(3) From c3235a0480de95098102342af3392c3823cf4139 Mon Sep 17 00:00:00 2001 From: Elttob Date: Mon, 27 Nov 2023 17:46:11 +0000 Subject: [PATCH 037/287] Implement ForKeys/Values/Pairs as wrappers --- src/PubTypes.lua | 16 +- src/State/For.lua | 12 - src/State/ForKeys.lua | 271 ++-------------- src/State/ForPairs.lua | 328 +++---------------- src/State/ForValues.lua | 254 ++------------- src/Types.lua | 78 ++--- test/State/For.spec.lua | 2 - test/State/ForKeys.spec.lua | 310 +++++++++--------- test/State/ForPairs.spec.lua | 398 ++++++++++++------------ test/State/ForValues.spec.lua | 570 +++++++++++++++++----------------- 10 files changed, 744 insertions(+), 1495 deletions(-) diff --git a/src/PubTypes.lua b/src/PubTypes.lua index 012eb9c2c..7450dbf2c 100644 --- a/src/PubTypes.lua +++ b/src/PubTypes.lua @@ -96,19 +96,9 @@ export type Computed = StateObject & Dependent & { destroy: () -> () } --- A state object whose value is derived from other objects using a callback. -export type ForPairs = StateObject<{ [KO]: VO }> & Dependent & { - kind: "ForPairs", - destroy: () -> () -} --- A state object whose value is derived from other objects using a callback. -export type ForKeys = StateObject<{ [KO]: V }> & Dependent & { - kind: "ForKeys", - destroy: () -> () -} --- A state object whose value is derived from other objects using a callback. -export type ForValues = StateObject<{ [K]: VO }> & Dependent & { - kind: "ForKeys", +-- A state object which maps over keys and/or values in another table. +export type For = StateObject<{[KO]: VO}> & Dependent & { + kind: "For", destroy: () -> () } diff --git a/src/State/For.lua b/src/State/For.lua index 02e7c1359..67ccd3cad 100644 --- a/src/State/For.lua +++ b/src/State/For.lua @@ -23,17 +23,6 @@ local class = {} local CLASS_METATABLE = { __index = class } local WEAK_KEYS_METATABLE = { __mode = "k" } -local NAMES = {"Alice", "Bob", "Charlie", "Dennis", "Edward", "Fanta", "Graham", "Iliza", "James", "Katy", "Liam", "Mason", "Nora"} - -type Processor = { - name: string, - inputKey: PubTypes.Value, - inputValue: PubTypes.Value, - outputKey: PubTypes.StateObject, - outputValue: PubTypes.StateObject, - cleanupTask: any -} - --[[ Called when the original table is changed. ]] @@ -140,7 +129,6 @@ function class:update(): boolean local processOK, outputKey, outputValue = xpcall(self._processor, parseError, scope, inputKey, inputValue) if processOK then local processor = { - name = table.remove(NAMES, 1), inputKey = inputKey, inputValue = inputValue, outputKey = outputKey, diff --git a/src/State/ForKeys.lua b/src/State/ForKeys.lua index 5f74f541b..44779de11 100644 --- a/src/State/ForKeys.lua +++ b/src/State/ForKeys.lua @@ -1,264 +1,39 @@ ---!nonstrict +--!strict --[[ - Constructs a new ForKeys state object which maps keys of an array using - a `processor` function. + Constructs a new For object which maps keys of a table using a `processor` + function. - Optionally, a `destructor` function can be specified for cleaning up - calculated keys. If omitted, the default cleanup function will be used instead. + Optionally, a `destructor` function can be specified for cleaning up output. - Optionally, a `meta` value can be returned in the processor function as the - second value to pass data from the processor to the destructor. + Additionally, a `meta` table/value can optionally be returned to pass data + created when running the processor to the destructor when the created object + is cleaned up. ]] local Package = script.Parent.Parent local PubTypes = require(Package.PubTypes) local Types = require(Package.Types) --- Logging -local parseError = require(Package.Logging.parseError) -local logErrorNonFatal = require(Package.Logging.logErrorNonFatal) -local logError = require(Package.Logging.logError) -local logWarn = require(Package.Logging.logWarn) --- Utility -local doCleanup = require(Package.Memory.doCleanup) -local needsDestruction = require(Package.Memory.needsDestruction) -- State -local peek = require(Package.State.peek) -local makeUseCallback = require(Package.State.makeUseCallback) -local isState = require(Package.State.isState) +local For = require(Package.State.For) +local Computed = require(Package.State.Computed) -local class = {} - -local CLASS_METATABLE = { __index = class } -local WEAK_KEYS_METATABLE = { __mode = "k" } - - ---[[ - Called when the original table is changed. - - This will firstly find any keys meeting any of the following criteria: - - - they were not previously present - - a dependency used during generation of this value has changed - - It will recalculate those key pairs, storing information about any - dependencies used in the processor callback during output key generation, - and save the new key to the output array with the same value. If it is - overwriting an older value, that older value will be passed to the - destructor for cleanup. - - Finally, this function will find keys that are no longer present, and remove - their output keys from the output table and pass them to the destructor. -]] - -function class:update(): boolean - local inputIsState = self._inputIsState - local newInputTable = peek(self._inputTable) - local oldInputTable = self._oldInputTable - local outputTable = self._outputTable - - local keyOIMap = self._keyOIMap - local keyIOMap = self._keyIOMap - local meta = self._meta - - local didChange = false - - - -- clean out main dependency set - for dependency in pairs(self.dependencySet) do - dependency.dependentSet[self] = nil - end - - self._oldDependencySet, self.dependencySet = self.dependencySet, self._oldDependencySet - table.clear(self.dependencySet) - - -- if the input table is a state object, add it as a dependency - if inputIsState then - self._inputTable.dependentSet[self] = true - self.dependencySet[self._inputTable] = true - end - - - -- STEP 1: find keys that changed or were not previously present - for newInKey, value in pairs(newInputTable) do - -- get or create key data - local keyData = self._keyData[newInKey] - - if keyData == nil then - keyData = { - dependencySet = setmetatable({}, WEAK_KEYS_METATABLE), - oldDependencySet = setmetatable({}, WEAK_KEYS_METATABLE), - dependencyValues = setmetatable({}, WEAK_KEYS_METATABLE), - } - self._keyData[newInKey] = keyData - end - - -- check if the key is new - local shouldRecalculate = oldInputTable[newInKey] == nil - - -- check if the key's dependencies have changed - if shouldRecalculate == false then - for dependency, oldValue in pairs(keyData.dependencyValues) do - if oldValue ~= peek(dependency) then - shouldRecalculate = true - break - end - end - end - - - -- recalculate the output key if necessary - if shouldRecalculate then - keyData.oldDependencySet, keyData.dependencySet = keyData.dependencySet, keyData.oldDependencySet - table.clear(keyData.dependencySet) - - local use = makeUseCallback(keyData.dependencySet) - local processOK, newOutKey, newMetaValue = xpcall(self._processor, parseError, use, newInKey) - - if processOK then - if self._destructor == nil and (needsDestruction(newOutKey) or needsDestruction(newMetaValue)) then - logWarn("destructorNeededForKeys") - end - - local oldInKey = keyOIMap[newOutKey] - local oldOutKey = keyIOMap[newInKey] - - -- check for key collision - if oldInKey ~= newInKey and newInputTable[oldInKey] ~= nil then - logError("forKeysKeyCollision", nil, tostring(newOutKey), tostring(oldInKey), tostring(newOutKey)) - end - - -- check for a changed output key - if oldOutKey ~= newOutKey and keyOIMap[oldOutKey] == newInKey then - -- clean up the old calculated value - local oldMetaValue = meta[oldOutKey] - - local destructOK, err = xpcall(self._destructor or doCleanup, parseError, oldOutKey, oldMetaValue) - if not destructOK then - logErrorNonFatal("forKeysDestructorError", err) - end - - keyOIMap[oldOutKey] = nil - outputTable[oldOutKey] = nil - meta[oldOutKey] = nil - end - - -- update the stored data for this key - oldInputTable[newInKey] = value - meta[newOutKey] = newMetaValue - keyOIMap[newOutKey] = newInKey - keyIOMap[newInKey] = newOutKey - outputTable[newOutKey] = value - - -- if we had to recalculate the output, then we did change - didChange = true - else - -- restore old dependencies, because the new dependencies may be corrupt - keyData.oldDependencySet, keyData.dependencySet = keyData.dependencySet, keyData.oldDependencySet - - logErrorNonFatal("forKeysProcessorError", newOutKey) - end - end - - - -- save dependency values and add to main dependency set - for dependency in pairs(keyData.dependencySet) do - keyData.dependencyValues[dependency] = peek(dependency) - - self.dependencySet[dependency] = true - dependency.dependentSet[self] = true - end - end - - - -- STEP 2: find keys that were removed - for outputKey, inputKey in pairs(keyOIMap) do - if newInputTable[inputKey] == nil then - -- clean up the old calculated value - local oldMetaValue = meta[outputKey] - - local destructOK, err = xpcall(self._destructor or doCleanup, parseError, outputKey, oldMetaValue) - if not destructOK then - logErrorNonFatal("forKeysDestructorError", err) - end - - -- remove data - oldInputTable[inputKey] = nil - meta[outputKey] = nil - keyOIMap[outputKey] = nil - keyIOMap[inputKey] = nil - outputTable[outputKey] = nil - self._keyData[inputKey] = nil - - -- if we removed a key, then the table/state changed - didChange = true - end - end - - return didChange -end - ---[[ - Returns the interior value of this state object. -]] -function class:_peek(): any - return self._outputTable -end - -function class:get() - logError("stateGetWasRemoved") -end - -function class:destroy() - for dependency in pairs(self.dependencySet) do - dependency.dependentSet[self] = nil - end - - local keyOIMap = self._keyOIMap - local meta = self._meta - for outputKey, _ in pairs(keyOIMap) do - -- clean up the old calculated value - local oldMetaValue = meta[outputKey] - local destructOK, err = xpcall(self._destructor or doCleanup, parseError, outputKey, oldMetaValue) - if not destructOK then - logErrorNonFatal("forKeysDestructorError", err) - end - end -end - -local function ForKeys( +local function ForKeys( cleanupTable: {PubTypes.Task}, - inputTable: PubTypes.CanBeState<{ [KI]: any }>, - processor: (KI) -> (KO, M?), + inputTable: PubTypes.CanBeState<{[KI]: V}>, + processor: (PubTypes.Use, KI) -> (KO, M?), destructor: (KO, M?) -> ()? -): Types.ForKeys - - local self = setmetatable({ - type = "State", - kind = "ForKeys", - dependencySet = {}, - -- if we held strong references to the dependents, then they wouldn't be - -- able to get garbage collected when they fall out of scope - dependentSet = setmetatable({}, WEAK_KEYS_METATABLE), - _oldDependencySet = {}, - - _processor = processor, - _destructor = destructor, - _inputIsState = isState(inputTable), - - _inputTable = inputTable, - _oldInputTable = {}, - _outputTable = {}, - _keyOIMap = {}, - _keyIOMap = {}, - _keyData = {}, - _meta = {}, - }, CLASS_METATABLE) - - self:update() - table.insert(cleanupTable, self) - - return self +): Types.For + + return For( + cleanupTable, + inputTable, + function(scope, inputKey, inputValue) + return Computed(scope, function(use) + return processor(use, use(inputKey)) + end, destructor), inputValue + end + ) end return ForKeys \ No newline at end of file diff --git a/src/State/ForPairs.lua b/src/State/ForPairs.lua index 74090747a..4779837b9 100644 --- a/src/State/ForPairs.lua +++ b/src/State/ForPairs.lua @@ -1,309 +1,51 @@ ---!nonstrict +--!strict --[[ - Constructs a new ForPairs object which maps pairs of a table using - a `processor` function. + Constructs a new For object which maps pairs of a table using a `processor` + function. - Optionally, a `destructor` function can be specified for cleaning up values. - If omitted, the default cleanup function will be used instead. + Optionally, a `destructor` function can be specified for cleaning up output. - Additionally, a `meta` table/value can optionally be returned to pass data created - when running the processor to the destructor when the created object is cleaned up. + Additionally, a `meta` table/value can optionally be returned to pass data + created when running the processor to the destructor when the created object + is cleaned up. ]] local Package = script.Parent.Parent local PubTypes = require(Package.PubTypes) local Types = require(Package.Types) --- Logging -local parseError = require(Package.Logging.parseError) -local logErrorNonFatal = require(Package.Logging.logErrorNonFatal) -local logError = require(Package.Logging.logError) -local logWarn = require(Package.Logging.logWarn) --- Utility -local doCleanup = require(Package.Memory.doCleanup) -local needsDestruction = require(Package.Memory.needsDestruction) -- State -local peek = require(Package.State.peek) -local makeUseCallback = require(Package.State.makeUseCallback) -local isState = require(Package.State.isState) +local For = require(Package.State.For) +local Computed = require(Package.State.Computed) +-- Memory +local doNothing = require(Package.Memory.doNothing) -local class = {} - -local CLASS_METATABLE = { __index = class } -local WEAK_KEYS_METATABLE = { __mode = "k" } - ---[[ - Called when the original table is changed. - - This will firstly find any keys meeting any of the following criteria: - - - they were not previously present - - their associated value has changed - - a dependency used during generation of this value has changed - - It will recalculate those key/value pairs, storing information about any - dependencies used in the processor callback during value generation, and - save the new key/value pair to the output array. If it is overwriting an - older key/value pair, that older pair will be passed to the destructor - for cleanup. - - Finally, this function will find keys that are no longer present, and remove - their key/value pairs from the output table and pass them to the destructor. -]] -function class:update(): boolean - local inputIsState = self._inputIsState - local newInputTable = peek(self._inputTable) - local oldInputTable = self._oldInputTable - - local keyIOMap = self._keyIOMap - local meta = self._meta - - local didChange = false - - - -- clean out main dependency set - for dependency in pairs(self.dependencySet) do - dependency.dependentSet[self] = nil - end - - self._oldDependencySet, self.dependencySet = self.dependencySet, self._oldDependencySet - table.clear(self.dependencySet) - - -- if the input table is a state object, add it as a dependency - if inputIsState then - self._inputTable.dependentSet[self] = true - self.dependencySet[self._inputTable] = true - end - - -- clean out output table - self._oldOutputTable, self._outputTable = self._outputTable, self._oldOutputTable - - local oldOutputTable = self._oldOutputTable - local newOutputTable = self._outputTable - table.clear(newOutputTable) - - -- Step 1: find key/value pairs that changed or were not previously present - - for newInKey, newInValue in pairs(newInputTable) do - -- get or create key data - local keyData = self._keyData[newInKey] - - if keyData == nil then - keyData = { - dependencySet = setmetatable({}, WEAK_KEYS_METATABLE), - oldDependencySet = setmetatable({}, WEAK_KEYS_METATABLE), - dependencyValues = setmetatable({}, WEAK_KEYS_METATABLE), - } - self._keyData[newInKey] = keyData - end - - - -- check if the pair is new or changed - local shouldRecalculate = oldInputTable[newInKey] ~= newInValue - - -- check if the pair's dependencies have changed - if shouldRecalculate == false then - for dependency, oldValue in pairs(keyData.dependencyValues) do - if oldValue ~= peek(dependency) then - shouldRecalculate = true - break - end - end - end - - - -- recalculate the output pair if necessary - if shouldRecalculate then - keyData.oldDependencySet, keyData.dependencySet = keyData.dependencySet, keyData.oldDependencySet - table.clear(keyData.dependencySet) - - local use = makeUseCallback(keyData.dependencySet) - local processOK, newOutKey, newOutValue, newMetaValue = xpcall( - self._processor, parseError, use, newInKey, newInValue - ) - - if processOK then - if self._destructor == nil and (needsDestruction(newOutKey) or needsDestruction(newOutValue) or needsDestruction(newMetaValue)) then - logWarn("destructorNeededForPairs") - end - - -- if this key was already written to on this run-through, throw a fatal error. - if newOutputTable[newOutKey] ~= nil then - -- figure out which key/value pair previously wrote to this key - local previousNewKey, previousNewValue - for inKey, outKey in pairs(keyIOMap) do - if outKey == newOutKey then - previousNewValue = newInputTable[inKey] - if previousNewValue ~= nil then - previousNewKey = inKey - break - end - end - end - - if previousNewKey ~= nil then - logError( - "forPairsKeyCollision", - nil, - tostring(newOutKey), - tostring(previousNewKey), - tostring(previousNewValue), - tostring(newInKey), - tostring(newInValue) - ) - end - end - - local oldOutValue = oldOutputTable[newOutKey] - - if oldOutValue ~= newOutValue then - local oldMetaValue = meta[newOutKey] - if oldOutValue ~= nil then - local destructOK, err = xpcall(self._destructor or doCleanup, parseError, newOutKey, oldOutValue, oldMetaValue) - if not destructOK then - logErrorNonFatal("forPairsDestructorError", err) - end - end - - oldOutputTable[newOutKey] = nil - end - - -- update the stored data for this key/value pair - oldInputTable[newInKey] = newInValue - keyIOMap[newInKey] = newOutKey - meta[newOutKey] = newMetaValue - newOutputTable[newOutKey] = newOutValue - - -- if we had to recalculate the output, then we did change - didChange = true - else - -- restore old dependencies, because the new dependencies may be corrupt - keyData.oldDependencySet, keyData.dependencySet = keyData.dependencySet, keyData.oldDependencySet - - logErrorNonFatal("forPairsProcessorError", newOutKey) - end - else - local storedOutKey = keyIOMap[newInKey] - - -- check for key collision - if newOutputTable[storedOutKey] ~= nil then - -- figure out which key/value pair previously wrote to this key - local previousNewKey, previousNewValue - for inKey, outKey in pairs(keyIOMap) do - if storedOutKey == outKey then - previousNewValue = newInputTable[inKey] - - if previousNewValue ~= nil then - previousNewKey = inKey - break - end - end - end - - if previousNewKey ~= nil then - logError( - "forPairsKeyCollision", - nil, - tostring(storedOutKey), - tostring(previousNewKey), - tostring(previousNewValue), - tostring(newInKey), - tostring(newInValue) - ) - end - end - - -- copy the stored key/value pair into the new output table - newOutputTable[storedOutKey] = oldOutputTable[storedOutKey] - end - - - -- save dependency values and add to main dependency set - for dependency in pairs(keyData.dependencySet) do - keyData.dependencyValues[dependency] = peek(dependency) - - self.dependencySet[dependency] = true - dependency.dependentSet[self] = true - end - end - - -- STEP 2: find keys that were removed - for oldOutKey, oldOutValue in pairs(oldOutputTable) do - -- check if this key/value pair is in the new output table - if newOutputTable[oldOutKey] ~= oldOutValue then - -- clean up the old output pair - local oldMetaValue = meta[oldOutKey] - if oldOutValue ~= nil then - local destructOK, err = xpcall(self._destructor or doCleanup, parseError, oldOutKey, oldOutValue, oldMetaValue) - if not destructOK then - logErrorNonFatal("forPairsDestructorError", err) - end - end - - -- check if the key was completely removed from the output table - if newOutputTable[oldOutKey] == nil then - meta[oldOutKey] = nil - self._keyData[oldOutKey] = nil - end - - didChange = true - end - end - - for key in pairs(oldInputTable) do - if newInputTable[key] == nil then - oldInputTable[key] = nil - keyIOMap[key] = nil - end - end - - return didChange -end - ---[[ - Returns the interior value of this state object. -]] -function class:_peek(): any - return self._outputTable -end - -function class:get() - logError("stateGetWasRemoved") -end - -local function ForPairs( +local function ForPairs( cleanupTable: {PubTypes.Task}, - inputTable: PubTypes.CanBeState<{ [KI]: VI }>, - processor: (KI, VI) -> (KO, VO, M?), + inputTable: PubTypes.CanBeState<{[KI]: VI}>, + processor: (PubTypes.Use, KI, VI) -> (KO, VO, M?), destructor: (KO, VO, M?) -> ()? -): Types.ForPairs - - local self = setmetatable({ - type = "State", - kind = "ForPairs", - dependencySet = {}, - -- if we held strong references to the dependents, then they wouldn't be - -- able to get garbage collected when they fall out of scope - dependentSet = setmetatable({}, WEAK_KEYS_METATABLE), - _oldDependencySet = {}, - - _processor = processor, - _destructor = destructor, - _inputIsState = isState(inputTable), - - _inputTable = inputTable, - _oldInputTable = {}, - _outputTable = {}, - _oldOutputTable = {}, - _keyIOMap = {}, - _keyData = {}, - _meta = {}, - }, CLASS_METATABLE) - - self:update() - table.insert(cleanupTable, self) - - return self +): Types.For + + return For( + cleanupTable, + inputTable, + function(scope, inputKey, inputValue) + local pair = Computed(scope, function(use) + -- TODO: error checking + local key, value, meta = processor(use, use(inputKey), use(inputValue)) + return {key = key, value = value}, meta + end, function(data, meta) + -- TODO: error checking + destructor(data.key, meta) + end) + return Computed(function(use) + return use(pair).key + end, doNothing), Computed(function(use) + return use(pair).value + end, doNothing) + end + ) end return ForPairs \ No newline at end of file diff --git a/src/State/ForValues.lua b/src/State/ForValues.lua index 5494e17d8..71c1929f6 100644 --- a/src/State/ForValues.lua +++ b/src/State/ForValues.lua @@ -1,245 +1,39 @@ ---!nonstrict +--!strict --[[ - Constructs a new ForValues object which maps values of a table using - a `processor` function. + Constructs a new For object which maps values of a table using a `processor` + function. - Optionally, a `destructor` function can be specified for cleaning up values. - If omitted, the default cleanup function will be used instead. + Optionally, a `destructor` function can be specified for cleaning up output. - Additionally, a `meta` table/value can optionally be returned to pass data created - when running the processor to the destructor when the created object is cleaned up. + Additionally, a `meta` table/value can optionally be returned to pass data + created when running the processor to the destructor when the created object + is cleaned up. ]] + local Package = script.Parent.Parent local PubTypes = require(Package.PubTypes) local Types = require(Package.Types) --- Logging -local parseError = require(Package.Logging.parseError) -local logError = require(Package.Logging.logError) -local logErrorNonFatal = require(Package.Logging.logErrorNonFatal) -local logWarn = require(Package.Logging.logWarn) --- Utility -local doCleanup = require(Package.Memory.doCleanup) -local needsDestruction = require(Package.Memory.needsDestruction) -- State -local peek = require(Package.State.peek) -local makeUseCallback = require(Package.State.makeUseCallback) -local isState = require(Package.State.isState) - -local class = {} - -local CLASS_METATABLE = { __index = class } -local WEAK_KEYS_METATABLE = { __mode = "k" } - ---[[ - Called when the original table is changed. - - This will firstly find any values meeting any of the following criteria: - - - they were not previously present - - a dependency used during generation of this value has changed - - It will recalculate those values, storing information about any dependencies - used in the processor callback during value generation, and save the new value - to the output array with the same key. If it is overwriting an older value, - that older value will be passed to the destructor for cleanup. - - Finally, this function will find values that are no longer present, and remove - their values from the output table and pass them to the destructor. You can re-use - the same value multiple times and this will function will update them as little as - possible; reusing the same values where possible. -]] -function class:update(): boolean - local inputIsState = self._inputIsState - local inputTable = peek(self._inputTable) - local outputValues = {} - - local didChange = false - - -- clean out value cache - self._oldValueCache, self._valueCache = self._valueCache, self._oldValueCache - local newValueCache = self._valueCache - local oldValueCache = self._oldValueCache - table.clear(newValueCache) - - -- clean out main dependency set - for dependency in pairs(self.dependencySet) do - dependency.dependentSet[self] = nil - end - self._oldDependencySet, self.dependencySet = self.dependencySet, self._oldDependencySet - table.clear(self.dependencySet) - - -- if the input table is a state object, add it as a dependency - if inputIsState then - self._inputTable.dependentSet[self] = true - self.dependencySet[self._inputTable] = true - end - - - -- STEP 1: find values that changed or were not previously present - for inKey, inValue in pairs(inputTable) do - -- check if the value is new or changed - local oldCachedValues = oldValueCache[inValue] - local shouldRecalculate = oldCachedValues == nil - - -- get a cached value and its dependency/meta data if available - local value, valueData, meta - - if type(oldCachedValues) == "table" and #oldCachedValues > 0 then - local valueInfo = table.remove(oldCachedValues, #oldCachedValues) - value = valueInfo.value - valueData = valueInfo.valueData - meta = valueInfo.meta - - if #oldCachedValues <= 0 then - oldValueCache[inValue] = nil - end - elseif oldCachedValues ~= nil then - oldValueCache[inValue] = nil - shouldRecalculate = true - end - - if valueData == nil then - valueData = { - dependencySet = setmetatable({}, WEAK_KEYS_METATABLE), - oldDependencySet = setmetatable({}, WEAK_KEYS_METATABLE), - dependencyValues = setmetatable({}, WEAK_KEYS_METATABLE), - } - end - - -- check if the value's dependencies have changed - if shouldRecalculate == false then - for dependency, oldValue in pairs(valueData.dependencyValues) do - if oldValue ~= peek(dependency) then - shouldRecalculate = true - break - end - end - end - - -- recalculate the output value if necessary - if shouldRecalculate then - valueData.oldDependencySet, valueData.dependencySet = valueData.dependencySet, valueData.oldDependencySet - table.clear(valueData.dependencySet) - - local use = makeUseCallback(valueData.dependencySet) - local processOK, newOutValue, newMetaValue = xpcall(self._processor, parseError, use, inValue) - - if processOK then - if self._destructor == nil and (needsDestruction(newOutValue) or needsDestruction(newMetaValue)) then - logWarn("destructorNeededForValues") - end - - -- pass the old value to the destructor if it exists - if value ~= nil then - local destructOK, err = xpcall(self._destructor or doCleanup, parseError, value, meta) - if not destructOK then - logErrorNonFatal("forValuesDestructorError", err) - end - end - - -- store the new value and meta data - value = newOutValue - meta = newMetaValue - didChange = true - else - -- restore old dependencies, because the new dependencies may be corrupt - valueData.oldDependencySet, valueData.dependencySet = valueData.dependencySet, valueData.oldDependencySet - logErrorNonFatal("forValuesProcessorError", newOutValue) - end - end +local For = require(Package.State.For) +local Computed = require(Package.State.Computed) - - -- store the value and its dependency/meta data - local newCachedValues = newValueCache[inValue] - if newCachedValues == nil then - newCachedValues = {} - newValueCache[inValue] = newCachedValues - end - - table.insert(newCachedValues, { - value = value, - valueData = valueData, - meta = meta, - }) - - outputValues[inKey] = value - - - -- save dependency values and add to main dependency set - for dependency in pairs(valueData.dependencySet) do - valueData.dependencyValues[dependency] = peek(dependency) - - self.dependencySet[dependency] = true - dependency.dependentSet[self] = true - end - end - - - -- STEP 2: find values that were removed - -- for tables of data, we just need to check if it's still in the cache - for _oldInValue, oldCachedValueInfo in pairs(oldValueCache) do - for _, valueInfo in ipairs(oldCachedValueInfo) do - local oldValue = valueInfo.value - local oldMetaValue = valueInfo.meta - - local destructOK, err = xpcall(self._destructor or doCleanup, parseError, oldValue, oldMetaValue) - if not destructOK then - logErrorNonFatal("forValuesDestructorError", err) - end - - didChange = true - end - - table.clear(oldCachedValueInfo) - end - - self._outputTable = outputValues - - return didChange -end - ---[[ - Returns the interior value of this state object. -]] -function class:_peek(): any - return self._outputTable -end - -function class:get() - logError("stateGetWasRemoved") -end - -local function ForValues( +local function ForValues( cleanupTable: {PubTypes.Task}, - inputTable: PubTypes.CanBeState<{ [any]: VI }>, - processor: (VI) -> (VO, M?), + inputTable: PubTypes.CanBeState<{[K]: VI}>, + processor: (PubTypes.Use, VI) -> (VO, M?), destructor: (VO, M?) -> ()? -): Types.ForValues - - local self = setmetatable({ - type = "State", - kind = "ForValues", - dependencySet = {}, - -- if we held strong references to the dependents, then they wouldn't be - -- able to get garbage collected when they fall out of scope - dependentSet = setmetatable({}, WEAK_KEYS_METATABLE), - _oldDependencySet = {}, - - _processor = processor, - _destructor = destructor, - _inputIsState = isState(inputTable), - - _inputTable = inputTable, - _outputTable = {}, - _valueCache = {}, - _oldValueCache = {}, - }, CLASS_METATABLE) - - self:update() - table.insert(cleanupTable, self) - - return self +): Types.For + + return For( + cleanupTable, + inputTable, + function(scope, inputKey, inputValue) + return inputKey, Computed(scope, function(use) + return processor(use, use(inputValue)) + end, destructor) + end + ) end return ForValues \ No newline at end of file diff --git a/src/Types.lua b/src/Types.lua index 88f9d7b0e..b1510a2ba 100644 --- a/src/Types.lua +++ b/src/Types.lua @@ -54,65 +54,27 @@ export type Computed = PubTypes.Computed & { _value: T } --- A state object whose value is derived from other objects using a callback. -export type ForPairs = PubTypes.ForPairs & { - _oldDependencySet: Set, - _processor: (PubTypes.Use, KI, VI) -> (KO, VO), - _destructor: (VO, M?) -> (), - _inputIsState: boolean, - _inputTable: PubTypes.CanBeState<{ [KI]: VI }>, - _oldInputTable: { [KI]: VI }, - _outputTable: { [KO]: VO }, - _oldOutputTable: { [KO]: VO }, - _keyIOMap: { [KI]: KO }, - _meta: { [KO]: M? }, - _keyData: { - [KI]: { - dependencySet: Set, - oldDependencySet: Set, - dependencyValues: { [PubTypes.Dependency]: any }, - }, - }, +-- A state object which maps over keys and/or values in another table. +export type For = PubTypes.For & { + _processor: ( + {any}, + PubTypes.StateObject, + PubTypes.StateObject + ) -> (PubTypes.StateObject, PubTypes.StateObject), + _inputTable: PubTypes.CanBeState<{[KI]: VI}>, + _existingInputTable: {[KI]: VI}?, + _existingOutputTable: {[KO]: VO}, + _existingProcessors: {[For_Processor]: true}, + _newOutputTable: {[KO]: VO}, + _newProcessors: {[For_Processor]: true}, + _remainingPairs: {[KI]: {[VI]: true}} } - --- A state object whose value is derived from other objects using a callback. -export type ForKeys = PubTypes.ForKeys & { - _oldDependencySet: Set, - _processor: (PubTypes.Use, KI) -> (KO), - _destructor: (KO, M?) -> (), - _inputIsState: boolean, - _inputTable: PubTypes.CanBeState<{ [KI]: KO }>, - _oldInputTable: { [KI]: KO }, - _outputTable: { [KO]: any }, - _keyOIMap: { [KO]: KI }, - _meta: { [KO]: M? }, - _keyData: { - [KI]: { - dependencySet: Set, - oldDependencySet: Set, - dependencyValues: { [PubTypes.Dependency]: any }, - }, - }, -} - --- A state object whose value is derived from other objects using a callback. -export type ForValues = PubTypes.ForValues & { - _oldDependencySet: Set, - _processor: (PubTypes.Use, VI) -> (VO), - _destructor: (VO, M?) -> (), - _inputIsState: boolean, - _inputTable: PubTypes.CanBeState<{ [VI]: VO }>, - _outputTable: { [any]: VI }, - _valueCache: { [VO]: any }, - _oldValueCache: { [VO]: any }, - _meta: { [VO]: M? }, - _valueData: { - [VI]: { - dependencySet: Set, - oldDependencySet: Set, - dependencyValues: { [PubTypes.Dependency]: any }, - }, - }, +type For_Processor = { + inputKey: PubTypes.Value, + inputValue: PubTypes.Value, + outputKey: PubTypes.StateObject, + outputValue: PubTypes.StateObject, + cleanupTask: any } -- A state object which follows another state object using tweens. diff --git a/test/State/For.spec.lua b/test/State/For.spec.lua index 4aaf79c3a..e3f89569e 100644 --- a/test/State/For.spec.lua +++ b/test/State/For.spec.lua @@ -6,8 +6,6 @@ local peek = require(Package.State.peek) local doCleanup = require(Package.Memory.doCleanup) return function() - FOCUS() - it("constructs in scopes", function() local scope = {} local genericFor = For(scope, {}, function() diff --git a/test/State/ForKeys.spec.lua b/test/State/ForKeys.spec.lua index 4e6d22ed5..a8a84b766 100644 --- a/test/State/ForKeys.spec.lua +++ b/test/State/ForKeys.spec.lua @@ -5,171 +5,171 @@ local peek = require(Package.State.peek) local doCleanup = require(Package.Memory.doCleanup) return function() - -- it("constructs in scopes", function() - -- local scope = {} - -- local forkeys = ForKeys(scope, {}, function() - -- -- intentionally blank - -- end) + it("constructs in scopes", function() + local scope = {} + local forkeys = ForKeys(scope, {}, function() + -- intentionally blank + end) - -- expect(forkeys).to.be.a("table") - -- expect(forkeys.type).to.equal("State") - -- expect(forkeys.kind).to.equal("ForKeys") - -- expect(scope[1]).to.equal(forkeys) + expect(forkeys).to.be.a("table") + expect(forkeys.type).to.equal("State") + expect(forkeys.kind).to.equal("ForKeys") + expect(scope[1]).to.equal(forkeys) - -- doCleanup(scope) - -- end) + doCleanup(scope) + end) - -- it("is destroyable", function() - -- local scope = {} - -- local forkeys = ForKeys(scope, {}, function() - -- -- intentionally blank - -- end) - -- expect(function() - -- forkeys:destroy() - -- end).to.never.throw() - -- end) + it("is destroyable", function() + local scope = {} + local forkeys = ForKeys(scope, {}, function() + -- intentionally blank + end) + expect(function() + forkeys:destroy() + end).to.never.throw() + end) - -- it("iterates on constants", function() - -- local scope = {} - -- local data = {foo = 1, bar = 2} - -- local forkeys = ForKeys(scope, data, function(_, key) - -- return key:upper() - -- end) - -- expect(peek(forkeys)).to.be.a("table") - -- expect(peek(forkeys).FOO).to.equal(1) - -- expect(peek(forkeys).BAR).to.equal(2) - -- doCleanup(scope) - -- end) + it("iterates on constants", function() + local scope = {} + local data = {foo = 1, bar = 2} + local forkeys = ForKeys(scope, data, function(_, key) + return key:upper() + end) + expect(peek(forkeys)).to.be.a("table") + expect(peek(forkeys).FOO).to.equal(1) + expect(peek(forkeys).BAR).to.equal(2) + doCleanup(scope) + end) - -- it("iterates on state objects", function() - -- local scope = {} - -- local data = Value(scope, {foo = 1, bar = 2}) - -- local forkeys = ForKeys(scope, data, function(_, key) - -- return key:upper() - -- end) - -- expect(peek(forkeys)).to.be.a("table") - -- expect(peek(forkeys).FOO).to.equal(1) - -- expect(peek(forkeys).BAR).to.equal(2) - -- doCleanup(scope) - -- end) + it("iterates on state objects", function() + local scope = {} + local data = Value(scope, {foo = 1, bar = 2}) + local forkeys = ForKeys(scope, data, function(_, key) + return key:upper() + end) + expect(peek(forkeys)).to.be.a("table") + expect(peek(forkeys).FOO).to.equal(1) + expect(peek(forkeys).BAR).to.equal(2) + doCleanup(scope) + end) - -- it("computes with constants", function() - -- local scope = {} - -- local data = {foo = 1, bar = 2} - -- local forkeys = ForKeys(scope, data, function(use, key) - -- return key .. use("baz") - -- end) - -- expect(peek(forkeys).foobaz).to.equal(1) - -- expect(peek(forkeys).barbaz).to.equal(2) - -- doCleanup(scope) - -- end) + it("computes with constants", function() + local scope = {} + local data = {foo = 1, bar = 2} + local forkeys = ForKeys(scope, data, function(use, key) + return key .. use("baz") + end) + expect(peek(forkeys).foobaz).to.equal(1) + expect(peek(forkeys).barbaz).to.equal(2) + doCleanup(scope) + end) - -- it("computes with state objects", function() - -- local scope = {} - -- local data = {foo = 1, bar = 2} - -- local suffix = Value(scope, "first") - -- local forkeys = ForKeys(scope, data, function(use, key) - -- return key .. use(suffix) - -- end) - -- expect(peek(forkeys).foofirst).to.equal(1) - -- expect(peek(forkeys).barfirst).to.equal(2) - -- suffix:set("second") - -- expect(peek(forkeys).foofirst).to.equal(nil) - -- expect(peek(forkeys).barfirst).to.equal(nil) - -- expect(peek(forkeys).foosecond).to.equal(1) - -- expect(peek(forkeys).barsecond).to.equal(2) - -- doCleanup(scope) - -- end) + it("computes with state objects", function() + local scope = {} + local data = {foo = 1, bar = 2} + local suffix = Value(scope, "first") + local forkeys = ForKeys(scope, data, function(use, key) + return key .. use(suffix) + end) + expect(peek(forkeys).foofirst).to.equal(1) + expect(peek(forkeys).barfirst).to.equal(2) + suffix:set("second") + expect(peek(forkeys).foofirst).to.equal(nil) + expect(peek(forkeys).barfirst).to.equal(nil) + expect(peek(forkeys).foosecond).to.equal(1) + expect(peek(forkeys).barsecond).to.equal(2) + doCleanup(scope) + end) - -- it("rejects key collisions", function() - -- expect(function() - -- local scope = {} - -- local data = {foo = 1, bar = 2} - -- local _ = ForKeys(scope, data, function(use, key) - -- return "samuel" - -- end) - -- doCleanup(scope) - -- end).to.throw("forKeysKeyCollision") - -- end) + it("rejects key collisions", function() + expect(function() + local scope = {} + local data = {foo = 1, bar = 2} + local _ = ForKeys(scope, data, function(use, key) + return "samuel" + end) + doCleanup(scope) + end).to.throw("forKeysKeyCollision") + end) - -- it("preserves value on error", function() - -- local scope = {} - -- local data = {foo = 1, bar = 2} - -- local suffix = Value(scope, "first") - -- local forkeys = ForKeys(scope, data, function(use, key) - -- assert(use(suffix) ~= "second", "This is an intentional error from a unit test") - -- return key .. use(suffix) - -- end) - -- expect(peek(forkeys).foofirst).to.equal(1) - -- expect(peek(forkeys).barfirst).to.equal(2) - -- suffix:set("second") -- will invoke the error - -- expect(peek(forkeys).foofirst).to.equal(1) - -- expect(peek(forkeys).barfirst).to.equal(2) - -- expect(peek(forkeys).foosecond).to.equal(nil) - -- expect(peek(forkeys).barsecond).to.equal(nil) - -- suffix:set("third") - -- expect(peek(forkeys).foofirst).to.equal(nil) - -- expect(peek(forkeys).barfirst).to.equal(nil) - -- expect(peek(forkeys).foosecond).to.equal(nil) - -- expect(peek(forkeys).barsecond).to.equal(nil) - -- expect(peek(forkeys).foothird).to.equal(1) - -- expect(peek(forkeys).barthird).to.equal(2) - -- doCleanup(scope) - -- end) + it("preserves value on error", function() + local scope = {} + local data = {foo = 1, bar = 2} + local suffix = Value(scope, "first") + local forkeys = ForKeys(scope, data, function(use, key) + assert(use(suffix) ~= "second", "This is an intentional error from a unit test") + return key .. use(suffix) + end) + expect(peek(forkeys).foofirst).to.equal(1) + expect(peek(forkeys).barfirst).to.equal(2) + suffix:set("second") -- will invoke the error + expect(peek(forkeys).foofirst).to.equal(1) + expect(peek(forkeys).barfirst).to.equal(2) + expect(peek(forkeys).foosecond).to.equal(nil) + expect(peek(forkeys).barsecond).to.equal(nil) + suffix:set("third") + expect(peek(forkeys).foofirst).to.equal(nil) + expect(peek(forkeys).barfirst).to.equal(nil) + expect(peek(forkeys).foosecond).to.equal(nil) + expect(peek(forkeys).barsecond).to.equal(nil) + expect(peek(forkeys).foothird).to.equal(1) + expect(peek(forkeys).barthird).to.equal(2) + doCleanup(scope) + end) - -- it("doesn't call destructor on creation", function() - -- local scope = {} - -- local destructed = {} - -- local data = Value(scope, {foo = 1, bar = 2}) - -- local _ = ForKeys(scope, data, function(use, key) - -- return key, "meta" .. key - -- end, function(key, meta) - -- destructed[key] = true - -- destructed[meta] = true - -- end) - -- expect(destructed.foo).to.equal(nil) - -- expect(destructed.metafoo).to.equal(nil) - -- expect(destructed.bar).to.equal(nil) - -- expect(destructed.metabar).to.equal(nil) - -- doCleanup(scope) - -- end) + it("doesn't call destructor on creation", function() + local scope = {} + local destructed = {} + local data = Value(scope, {foo = 1, bar = 2}) + local _ = ForKeys(scope, data, function(use, key) + return key, "meta" .. key + end, function(key, meta) + destructed[key] = true + destructed[meta] = true + end) + expect(destructed.foo).to.equal(nil) + expect(destructed.metafoo).to.equal(nil) + expect(destructed.bar).to.equal(nil) + expect(destructed.metabar).to.equal(nil) + doCleanup(scope) + end) - -- it("calls destructor on update", function() - -- local scope = {} - -- local destructed = {} - -- local data = Value(scope, {foo = 1, bar = 2}) - -- local _ = ForKeys(scope, data, function(use, key) - -- return key, "meta" .. key - -- end, function(key, meta) - -- destructed[key] = true - -- destructed[meta] = true - -- end) - -- data:set({foo = 100, baz = 3}) - -- expect(destructed.foo).to.equal(nil) - -- expect(destructed.metafoo).to.equal(nil) - -- expect(destructed.bar).to.equal(true) - -- expect(destructed.metabar).to.equal(true) - -- doCleanup(scope) - -- end) + it("calls destructor on update", function() + local scope = {} + local destructed = {} + local data = Value(scope, {foo = 1, bar = 2}) + local _ = ForKeys(scope, data, function(use, key) + return key, "meta" .. key + end, function(key, meta) + destructed[key] = true + destructed[meta] = true + end) + data:set({foo = 100, baz = 3}) + expect(destructed.foo).to.equal(nil) + expect(destructed.metafoo).to.equal(nil) + expect(destructed.bar).to.equal(true) + expect(destructed.metabar).to.equal(true) + doCleanup(scope) + end) - -- it("calls destructor on destroy", function() - -- local scope = {} - -- local destructed = {} - -- local data = Value(scope, {foo = 1, bar = 2}) - -- local _ = ForKeys(scope, data, function(use, key) - -- return key, "meta" .. key - -- end, function(key, meta) - -- destructed[key] = true - -- destructed[meta] = true - -- end) - -- expect(destructed.foo).to.equal(nil) - -- expect(destructed.metafoo).to.equal(nil) - -- expect(destructed.bar).to.equal(nil) - -- expect(destructed.metabar).to.equal(nil) - -- doCleanup(scope) - -- expect(destructed.foo).to.equal(true) - -- expect(destructed.metafoo).to.equal(true) - -- expect(destructed.bar).to.equal(true) - -- expect(destructed.metabar).to.equal(true) - -- end) + it("calls destructor on destroy", function() + local scope = {} + local destructed = {} + local data = Value(scope, {foo = 1, bar = 2}) + local _ = ForKeys(scope, data, function(use, key) + return key, "meta" .. key + end, function(key, meta) + destructed[key] = true + destructed[meta] = true + end) + expect(destructed.foo).to.equal(nil) + expect(destructed.metafoo).to.equal(nil) + expect(destructed.bar).to.equal(nil) + expect(destructed.metabar).to.equal(nil) + doCleanup(scope) + expect(destructed.foo).to.equal(true) + expect(destructed.metafoo).to.equal(true) + expect(destructed.bar).to.equal(true) + expect(destructed.metabar).to.equal(true) + end) end diff --git a/test/State/ForPairs.spec.lua b/test/State/ForPairs.spec.lua index b9cdf0ed3..ec1b3fb54 100644 --- a/test/State/ForPairs.spec.lua +++ b/test/State/ForPairs.spec.lua @@ -6,262 +6,262 @@ local Value = require(Package.State.Value) local peek = require(Package.State.peek) return function() - -- it("should construct a ForPairs object", function() - -- local forPairs = ForPairs({}, function(use) end) + it("should construct a ForPairs object", function() + local forPairs = ForPairs({}, function(use) end) - -- expect(forPairs).to.be.a("table") - -- expect(forPairs.type).to.equal("State") - -- expect(forPairs.kind).to.equal("ForPairs") - -- end) + expect(forPairs).to.be.a("table") + expect(forPairs.type).to.equal("State") + expect(forPairs.kind).to.equal("ForPairs") + end) - -- it("should calculate and retrieve its value", function() - -- local computedPair = ForPairs({ ["foo"] = "bar" }, function(use, key, value) - -- return key .. "baz", value .. "biz" - -- end) + it("should calculate and retrieve its value", function() + local computedPair = ForPairs({ ["foo"] = "bar" }, function(use, key, value) + return key .. "baz", value .. "biz" + end) - -- local state = peek(computedPair) + local state = peek(computedPair) - -- expect(state["foobaz"]).to.be.ok() - -- expect(state["foobaz"]).to.equal("barbiz") - -- end) + expect(state["foobaz"]).to.be.ok() + expect(state["foobaz"]).to.equal("barbiz") + end) - -- it("should not recalculate its KO/VO in response to an unchanged KI/VI", function() - -- local state = Value({ - -- ["foo"] = "bar", - -- }) + it("should not recalculate its KO/VO in response to an unchanged KI/VI", function() + local state = Value({ + ["foo"] = "bar", + }) - -- local computedPair = ForPairs(state, function(use, key, value) - -- return key .. "biz", { value } - -- end) + local computedPair = ForPairs(state, function(use, key, value) + return key .. "biz", { value } + end) - -- local foobiz = peek(computedPair)["foobiz"] + local foobiz = peek(computedPair)["foobiz"] - -- state:set({ - -- ["foo"] = "bar", - -- ["baz"] = "bar", - -- }) + state:set({ + ["foo"] = "bar", + ["baz"] = "bar", + }) - -- expect(peek(computedPair)["foobiz"]).to.equal(foobiz) - -- end) + expect(peek(computedPair)["foobiz"]).to.equal(foobiz) + end) - -- it("should call the destructor when a key/value pair gets changed", function() - -- local state = Value({ - -- ["foo"] = "bar", - -- ["baz"] = "bar", - -- }) + it("should call the destructor when a key/value pair gets changed", function() + local state = Value({ + ["foo"] = "bar", + ["baz"] = "bar", + }) - -- local destructions = 0 + local destructions = 0 - -- local computedPair = ForPairs(state, function(use, key, value) - -- return key .. "biz", value .. "biz" - -- end, function(key, value) - -- destructions += 1 - -- end) + local computedPair = ForPairs(state, function(use, key, value) + return key .. "biz", value .. "biz" + end, function(key, value) + destructions += 1 + end) - -- state:set({ - -- ["foo"] = "bar", - -- }) + state:set({ + ["foo"] = "bar", + }) - -- expect(destructions).to.equal(1) + expect(destructions).to.equal(1) - -- state:set({ - -- ["baz"] = "bar", - -- }) + state:set({ + ["baz"] = "bar", + }) - -- expect(destructions).to.equal(2) + expect(destructions).to.equal(2) - -- state:set({ - -- ["foo"] = "bar", - -- ["baz"] = "bar", - -- }) + state:set({ + ["foo"] = "bar", + ["baz"] = "bar", + }) - -- expect(destructions).to.equal(2) + expect(destructions).to.equal(2) - -- state:set({}) + state:set({}) - -- expect(destructions).to.equal(4) - -- end) + expect(destructions).to.equal(4) + end) - -- it( - -- "should not call the destructor when an output key/value pair still remains with a new input key/value pair", - -- function() - -- local state = Value({ - -- ["foo"] = "bar", - -- }) + it( + "should not call the destructor when an output key/value pair still remains with a new input key/value pair", + function() + local state = Value({ + ["foo"] = "bar", + }) - -- local destructions = 0 + local destructions = 0 - -- local computedPair = ForPairs(state, function(use, key, value) - -- return value, value - -- end, function(key, value) - -- destructions += 1 - -- end) + local computedPair = ForPairs(state, function(use, key, value) + return value, value + end, function(key, value) + destructions += 1 + end) - -- state:set({ - -- ["baz"] = "bar", - -- }) + state:set({ + ["baz"] = "bar", + }) - -- expect(destructions).to.equal(0) + expect(destructions).to.equal(0) - -- state:set({ - -- ["biz"] = "baz", - -- }) + state:set({ + ["biz"] = "baz", + }) - -- expect(destructions).to.equal(1) + expect(destructions).to.equal(1) - -- state:set({ - -- ["foo"] = "bar", - -- ["biz"] = "baz", - -- }) + state:set({ + ["foo"] = "bar", + ["biz"] = "baz", + }) - -- expect(destructions).to.equal(1) + expect(destructions).to.equal(1) - -- state:set({ - -- ["foo"] = "baz", - -- ["biz"] = "bar", - -- }) + state:set({ + ["foo"] = "baz", + ["biz"] = "bar", + }) - -- expect(destructions).to.equal(1) + expect(destructions).to.equal(1) - -- state:set({ - -- ["biz"] = "bar", - -- }) + state:set({ + ["biz"] = "bar", + }) - -- expect(destructions).to.equal(2) + expect(destructions).to.equal(2) - -- state:set({}) + state:set({}) - -- expect(destructions).to.equal(3) - -- end - -- ) + expect(destructions).to.equal(3) + end + ) - -- it("should throw if there is a key collision", function() - -- expect(function() - -- local state = Value({ - -- ["foo"] = "bar", - -- ["baz"] = "bar", - -- }) + it("should throw if there is a key collision", function() + expect(function() + local state = Value({ + ["foo"] = "bar", + ["baz"] = "bar", + }) - -- local computed = ForPairs(state, function(use, key, value) - -- return value, key - -- end) - -- end).to.throw("forPairsKeyCollision") + local computed = ForPairs(state, function(use, key, value) + return value, key + end) + end).to.throw("forPairsKeyCollision") - -- local state = Value({ - -- ["foo"] = "bar", - -- }) + local state = Value({ + ["foo"] = "bar", + }) - -- local computed = ForPairs(state, function(use, key, value) - -- return value, key - -- end) + local computed = ForPairs(state, function(use, key, value) + return value, key + end) - -- expect(function() - -- state:set({ - -- ["foo"] = "bar", - -- ["baz"] = "bar", - -- }) - -- end).to.throw("forPairsKeyCollision") - -- end) + expect(function() + state:set({ + ["foo"] = "bar", + ["baz"] = "bar", + }) + end).to.throw("forPairsKeyCollision") + end) - -- it("should call the destructor with meta data", function() - -- local state = Value({ - -- ["foo"] = "bar", - -- }) + it("should call the destructor with meta data", function() + local state = Value({ + ["foo"] = "bar", + }) - -- local destructions = 0 + local destructions = 0 - -- local computedPair = ForPairs(state, function(use, key, value) - -- local newKey = key .. "biz" - -- local newValue = value .. "biz" + local computedPair = ForPairs(state, function(use, key, value) + local newKey = key .. "biz" + local newValue = value .. "biz" - -- return newKey, newValue, newKey .. newValue - -- end, function(key, value, meta) - -- expect(meta).to.equal(key .. value) - -- destructions += 1 - -- end) + return newKey, newValue, newKey .. newValue + end, function(key, value, meta) + expect(meta).to.equal(key .. value) + destructions += 1 + end) - -- state:set({ - -- ["foo"] = "baz", - -- }) + state:set({ + ["foo"] = "baz", + }) - -- -- this verifies that the meta expectation passed - -- expect(destructions).to.equal(1) + -- this verifies that the meta expectation passed + expect(destructions).to.equal(1) - -- state:set({}) + state:set({}) - -- -- this verifies that the meta expectation passed - -- expect(destructions).to.equal(2) - -- end) + -- this verifies that the meta expectation passed + expect(destructions).to.equal(2) + end) - -- it("should recalculate its value in response to State objects", function() - -- local currentNumber = Value({ ["foo"] = 2 }) - -- local doubled = ForPairs(currentNumber, function(use, key, value) - -- return key .. "bar", value * 2 - -- end) + it("should recalculate its value in response to State objects", function() + local currentNumber = Value({ ["foo"] = 2 }) + local doubled = ForPairs(currentNumber, function(use, key, value) + return key .. "bar", value * 2 + end) - -- expect(peek(doubled)["foobar"]).to.equal(4) + expect(peek(doubled)["foobar"]).to.equal(4) - -- currentNumber:set({ ["foo"] = 4 }) - -- expect(peek(doubled)["foobar"]).to.equal(8) - -- end) + currentNumber:set({ ["foo"] = 4 }) + expect(peek(doubled)["foobar"]).to.equal(8) + end) - -- it("should recalculate its value in response to ForPairs objects", function() - -- local currentNumbers = Value({ 1, 2 }) - -- local doubled = ForPairs(currentNumbers, function(use, key, value) - -- return key * 2, value * 2 - -- end) - -- local tripled = ForPairs(doubled, function(use, key, value) - -- return key * 2, value * 2 - -- end) + it("should recalculate its value in response to ForPairs objects", function() + local currentNumbers = Value({ 1, 2 }) + local doubled = ForPairs(currentNumbers, function(use, key, value) + return key * 2, value * 2 + end) + local tripled = ForPairs(doubled, function(use, key, value) + return key * 2, value * 2 + end) - -- expect(peek(tripled)[4]).to.equal(4) - -- expect(peek(tripled)[8]).to.equal(8) - - -- currentNumbers:set({ 2, 4 }) - -- expect(peek(tripled)[4]).to.equal(8) - -- expect(peek(tripled)[8]).to.equal(16) - -- end) + expect(peek(tripled)[4]).to.equal(4) + expect(peek(tripled)[8]).to.equal(8) + + currentNumbers:set({ 2, 4 }) + expect(peek(tripled)[4]).to.equal(8) + expect(peek(tripled)[8]).to.equal(16) + end) - -- itSKIP("should not corrupt dependencies after an error", function() - -- -- needs rewrite - -- end) + itSKIP("should not corrupt dependencies after an error", function() + -- needs rewrite + end) - -- it("should garbage-collect unused objects", function() - -- local state = Value({ 2 }) - - -- local counter = 0 - - -- do - -- local computedPairs = ForPairs(state, function(use, key, value) - -- counter += 1 - -- return key, value - -- end) - -- end - - -- state:set({ 5 }) - - -- expect(counter).to.equal(1) - -- end) - - -- it("should not garbage-collect objects in use", function() - -- local state = Value({ 2 }) - -- local computed2 + it("should garbage-collect unused objects", function() + local state = Value({ 2 }) + + local counter = 0 + + do + local computedPairs = ForPairs(state, function(use, key, value) + counter += 1 + return key, value + end) + end + + state:set({ 5 }) + + expect(counter).to.equal(1) + end) + + it("should not garbage-collect objects in use", function() + local state = Value({ 2 }) + local computed2 - -- local counter = 0 - - -- do - -- local computed = ForPairs(state, function(use, key, value) - -- counter += 1 - -- return key, value - -- end) + local counter = 0 + + do + local computed = ForPairs(state, function(use, key, value) + counter += 1 + return key, value + end) - -- computed2 = ForPairs(computed, function(use, key, value) - -- return key, value - -- end) - -- end + computed2 = ForPairs(computed, function(use, key, value) + return key, value + end) + end - -- state:set({ 5 }) + state:set({ 5 }) - -- expect(counter).to.equal(2) - -- end) + expect(counter).to.equal(2) + end) end diff --git a/test/State/ForValues.spec.lua b/test/State/ForValues.spec.lua index ac9f74884..6323a32d4 100644 --- a/test/State/ForValues.spec.lua +++ b/test/State/ForValues.spec.lua @@ -6,348 +6,348 @@ local Value = require(Package.State.Value) local peek = require(Package.State.peek) return function() - -- it("should construct a ForValues object", function() - -- local forKeys = ForValues({}, function(use) end) + it("should construct a ForValues object", function() + local forKeys = ForValues({}, function(use) end) - -- expect(forKeys).to.be.a("table") - -- expect(forKeys.type).to.equal("State") - -- expect(forKeys.kind).to.equal("ForValues") - -- end) + expect(forKeys).to.be.a("table") + expect(forKeys.type).to.equal("State") + expect(forKeys.kind).to.equal("ForValues") + end) - -- it("should calculate and retrieve its value", function() - -- local computed = ForValues({ 1 }, function(use, value) - -- return value - -- end) + it("should calculate and retrieve its value", function() + local computed = ForValues({ 1 }, function(use, value) + return value + end) - -- local state = peek(computed) + local state = peek(computed) - -- expect(state[1]).to.be.ok() - -- expect(state[1]).to.equal(1) - -- end) + expect(state[1]).to.be.ok() + expect(state[1]).to.equal(1) + end) - -- it("should not recalculate its VO in response to a changed VI", function() - -- local state = Value({ - -- [1] = "foo", - -- }) + it("should not recalculate its VO in response to a changed VI", function() + local state = Value({ + [1] = "foo", + }) - -- local calculations = 0 + local calculations = 0 - -- local computed = ForValues(state, function(use, value) - -- calculations += 1 - -- return value - -- end) + local computed = ForValues(state, function(use, value) + calculations += 1 + return value + end) - -- expect(calculations).to.equal(1) + expect(calculations).to.equal(1) - -- state:set({ - -- [1] = "bar", - -- [2] = "foo", - -- }) + state:set({ + [1] = "bar", + [2] = "foo", + }) - -- expect(calculations).to.equal(2) - -- end) + expect(calculations).to.equal(2) + end) - -- it("should only call the processor the first time a constant output value is added", function() - -- local state = Value({ - -- [1] = "foo", - -- }) + it("should only call the processor the first time a constant output value is added", function() + local state = Value({ + [1] = "foo", + }) - -- local processorCalls = 0 + local processorCalls = 0 - -- local computed = ForValues(state, function(use, value) - -- processorCalls += 1 + local computed = ForValues(state, function(use, value) + processorCalls += 1 - -- return value .. "biz" - -- end) + return value .. "biz" + end) - -- expect(processorCalls).to.equal(1) + expect(processorCalls).to.equal(1) - -- state:set({ - -- [1] = "bar", - -- }) + state:set({ + [1] = "bar", + }) - -- expect(processorCalls).to.equal(2) + expect(processorCalls).to.equal(2) - -- state:set({ - -- [2] = "bar", - -- [3] = "bar", - -- }) + state:set({ + [2] = "bar", + [3] = "bar", + }) - -- expect(processorCalls).to.equal(2) + expect(processorCalls).to.equal(2) - -- state:set({ - -- [1] = "bar", - -- [2] = "bar", - -- }) + state:set({ + [1] = "bar", + [2] = "bar", + }) - -- expect(processorCalls).to.equal(2) + expect(processorCalls).to.equal(2) - -- state:set({}) + state:set({}) - -- expect(processorCalls).to.equal(2) + expect(processorCalls).to.equal(2) - -- state:set({ - -- [1] = "bar", - -- [2] = "foo", - -- }) + state:set({ + [1] = "bar", + [2] = "foo", + }) - -- expect(processorCalls).to.equal(4) - -- end) + expect(processorCalls).to.equal(4) + end) - -- it("should only call the destructor when a constant value gets removed from all indices", function() - -- local state = Value({ - -- [1] = "foo", - -- }) + it("should only call the destructor when a constant value gets removed from all indices", function() + local state = Value({ + [1] = "foo", + }) - -- local destructions = 0 + local destructions = 0 - -- local computed = ForValues(state, function(use, value) - -- return value .. "biz" - -- end, function(key) - -- destructions += 1 - -- end) + local computed = ForValues(state, function(use, value) + return value .. "biz" + end, function(key) + destructions += 1 + end) - -- state:set({ - -- [1] = "bar", - -- }) + state:set({ + [1] = "bar", + }) - -- expect(destructions).to.equal(1) + expect(destructions).to.equal(1) - -- state:set({ - -- [2] = "bar", - -- [3] = "bar", - -- }) + state:set({ + [2] = "bar", + [3] = "bar", + }) - -- expect(destructions).to.equal(1) + expect(destructions).to.equal(1) - -- state:set({ - -- [1] = "bar", - -- [2] = "bar", - -- }) + state:set({ + [1] = "bar", + [2] = "bar", + }) - -- expect(destructions).to.equal(1) + expect(destructions).to.equal(1) - -- state:set({}) + state:set({}) - -- expect(destructions).to.equal(2) - -- end) + expect(destructions).to.equal(2) + end) - -- it("should only call the destructor when a non-constant value gets removed from all indices", function() - -- local mem1 = Instance.new("Folder") - -- local mem2 = Instance.new("Folder") - -- local mem3 = Instance.new("Folder") + it("should only call the destructor when a non-constant value gets removed from all indices", function() + local mem1 = Instance.new("Folder") + local mem2 = Instance.new("Folder") + local mem3 = Instance.new("Folder") - -- local state = Value({ - -- [1] = mem1, - -- }) + local state = Value({ + [1] = mem1, + }) - -- local destructions = 0 + local destructions = 0 - -- local computed = ForValues(state, function(use, value) - -- local obj = Instance.new("Folder") - -- obj.Parent = value + local computed = ForValues(state, function(use, value) + local obj = Instance.new("Folder") + obj.Parent = value - -- return obj - -- end, function(value) - -- destructions += 1 - -- value:Destroy() - -- end) + return obj + end, function(value) + destructions += 1 + value:Destroy() + end) - -- state:set({ - -- [1] = mem2, - -- }) + state:set({ + [1] = mem2, + }) - -- expect(destructions).to.equal(1) + expect(destructions).to.equal(1) - -- state:set({ - -- [2] = mem3, - -- [3] = mem2, - -- }) + state:set({ + [2] = mem3, + [3] = mem2, + }) - -- expect(destructions).to.equal(1) + expect(destructions).to.equal(1) - -- state:set({ - -- [1] = mem2, - -- [2] = mem3, - -- }) + state:set({ + [1] = mem2, + [2] = mem3, + }) - -- expect(destructions).to.equal(1) + expect(destructions).to.equal(1) - -- state:set({}) + state:set({}) - -- expect(destructions).to.equal(3) - -- end) + expect(destructions).to.equal(3) + end) - -- it("should call the destructor with meta data", function() - -- local state = Value({ - -- [1] = "foo", - -- }) + it("should call the destructor with meta data", function() + local state = Value({ + [1] = "foo", + }) - -- local destructions = 0 + local destructions = 0 - -- local computed = ForValues(state, function(use, value) - -- local newValue = value .. "biz" - -- return newValue, newValue - -- end, function(value, meta) - -- expect(meta).to.equal(value) - -- destructions += 1 - -- end) + local computed = ForValues(state, function(use, value) + local newValue = value .. "biz" + return newValue, newValue + end, function(value, meta) + expect(meta).to.equal(value) + destructions += 1 + end) - -- state:set({ - -- ["baz"] = "bar", - -- }) + state:set({ + ["baz"] = "bar", + }) - -- -- this verifies that the meta expectation passed - -- expect(destructions).to.equal(1) + -- this verifies that the meta expectation passed + expect(destructions).to.equal(1) - -- state:set({}) - - -- -- this verifies that the meta expectation passed - -- expect(destructions).to.equal(2) - -- end) + state:set({}) + + -- this verifies that the meta expectation passed + expect(destructions).to.equal(2) + end) - -- it("should not make any value changes or processor/destructor calls when only the input key changes", function() - -- local state = Value({ - -- ["foo"] = "bar", - -- ["bar"] = "baz", - -- ["biz"] = "buzz", - -- }) + it("should not make any value changes or processor/destructor calls when only the input key changes", function() + local state = Value({ + ["foo"] = "bar", + ["bar"] = "baz", + ["biz"] = "buzz", + }) - -- local processorCalls = 0 - -- local destructorCalls = 0 - - -- local computed = ForValues(state, function(use, value) - -- processorCalls += 1 - -- return value - -- end, function(value) - -- destructorCalls += 1 - -- end) - - -- expect(processorCalls).to.equal(3) - -- expect(destructorCalls).to.equal(0) - - -- state:set({ - -- [1] = "buzz", - -- ["bar"] = "bar", - -- ["fiz"] = "baz", - -- }) - - -- expect(processorCalls).to.equal(3) - -- expect(destructorCalls).to.equal(0) - - -- state:set({ - -- [1] = "bar", - -- ["bar"] = "bar", - -- ["fiz"] = "bar", - -- }) - - -- expect(processorCalls).to.equal(3) - -- expect(destructorCalls).to.equal(2) - - -- state:set({ - -- [2] = "bar", - -- [3] = "baz", - -- }) - - -- expect(processorCalls).to.equal(4) - -- expect(destructorCalls).to.equal(2) - - -- state:set({}) - - -- expect(processorCalls).to.equal(4) - -- expect(destructorCalls).to.equal(4) - -- end) - - -- it("should recalculate its value in response to State objects", function() - -- local state = Value({ - -- [1] = "baz", - -- }) - -- local barMap = ForValues(state, function(use, value) - -- return value .. "bar" - -- end) - - -- expect(peek(barMap)[1]).to.equal("bazbar") - - -- state:set({ - -- [1] = "bar", - -- }) - - -- expect(peek(barMap)[1]).to.equal("barbar") - -- end) - - -- it("should recalculate its value in response to ForValues objects", function() - -- local state = Value({ - -- [1] = 1, - -- }) - -- local doubled = ForValues(state, function(use, value) - -- return value * 2 - -- end) - -- local tripled = ForValues(doubled, function(use, value) - -- return value * 2 - -- end) - - -- expect(peek(doubled)[1]).to.equal(2) - -- expect(peek(tripled)[1]).to.equal(4) - - -- state:set({ - -- [1] = 2, - -- [2] = 3, - -- }) - - -- expect(peek(doubled)[1]).to.equal(4) - -- expect(peek(tripled)[1]).to.equal(8) - -- expect(peek(doubled)[2]).to.equal(6) - -- expect(peek(tripled)[2]).to.equal(12) - -- end) - - -- itSKIP("should not corrupt dependencies after an error", function() - -- -- needs rewrite - -- end) - - -- it("should garbage-collect unused objects", function() - -- local state = Value({ - -- [1] = "bar", - -- }) - - -- local counter = 0 - - -- do - -- local computedKeys = ForValues(state, function(use, value) - -- counter += 1 - -- return value - -- end) - -- end - - -- state:set({ - -- [1] = "biz", - -- }) - - -- expect(counter).to.equal(1) - -- end) - - -- it("should not garbage-collect objects in use", function() - -- local state = Value({ - -- [1] = 1, - -- }) - -- local computed2 - - -- local counter = 0 - - -- do - -- local computed = ForValues(state, function(use, value) - -- counter += 1 - -- return value - -- end) - - -- computed2 = ForValues(computed, function(use, value) - -- return value - -- end) - -- end + local processorCalls = 0 + local destructorCalls = 0 + + local computed = ForValues(state, function(use, value) + processorCalls += 1 + return value + end, function(value) + destructorCalls += 1 + end) + + expect(processorCalls).to.equal(3) + expect(destructorCalls).to.equal(0) + + state:set({ + [1] = "buzz", + ["bar"] = "bar", + ["fiz"] = "baz", + }) + + expect(processorCalls).to.equal(3) + expect(destructorCalls).to.equal(0) + + state:set({ + [1] = "bar", + ["bar"] = "bar", + ["fiz"] = "bar", + }) + + expect(processorCalls).to.equal(3) + expect(destructorCalls).to.equal(2) + + state:set({ + [2] = "bar", + [3] = "baz", + }) + + expect(processorCalls).to.equal(4) + expect(destructorCalls).to.equal(2) + + state:set({}) + + expect(processorCalls).to.equal(4) + expect(destructorCalls).to.equal(4) + end) + + it("should recalculate its value in response to State objects", function() + local state = Value({ + [1] = "baz", + }) + local barMap = ForValues(state, function(use, value) + return value .. "bar" + end) + + expect(peek(barMap)[1]).to.equal("bazbar") + + state:set({ + [1] = "bar", + }) + + expect(peek(barMap)[1]).to.equal("barbar") + end) + + it("should recalculate its value in response to ForValues objects", function() + local state = Value({ + [1] = 1, + }) + local doubled = ForValues(state, function(use, value) + return value * 2 + end) + local tripled = ForValues(doubled, function(use, value) + return value * 2 + end) + + expect(peek(doubled)[1]).to.equal(2) + expect(peek(tripled)[1]).to.equal(4) + + state:set({ + [1] = 2, + [2] = 3, + }) + + expect(peek(doubled)[1]).to.equal(4) + expect(peek(tripled)[1]).to.equal(8) + expect(peek(doubled)[2]).to.equal(6) + expect(peek(tripled)[2]).to.equal(12) + end) + + itSKIP("should not corrupt dependencies after an error", function() + -- needs rewrite + end) + + it("should garbage-collect unused objects", function() + local state = Value({ + [1] = "bar", + }) + + local counter = 0 + + do + local computedKeys = ForValues(state, function(use, value) + counter += 1 + return value + end) + end + + state:set({ + [1] = "biz", + }) + + expect(counter).to.equal(1) + end) + + it("should not garbage-collect objects in use", function() + local state = Value({ + [1] = 1, + }) + local computed2 + + local counter = 0 + + do + local computed = ForValues(state, function(use, value) + counter += 1 + return value + end) + + computed2 = ForValues(computed, function(use, value) + return value + end) + end - -- state:set({ - -- [1] = 2, - -- }) + state:set({ + [1] = 2, + }) - -- expect(counter).to.equal(2) - -- end) + expect(counter).to.equal(2) + end) end From 89ae64f251239eda5d0a435ab809534cf24107d1 Mon Sep 17 00:00:00 2001 From: Elttob Date: Mon, 27 Nov 2023 21:04:02 +0000 Subject: [PATCH 038/287] Even more unit tests --- src/Logging/messages.lua | 15 +- src/State/Computed.lua | 20 +- src/State/For.lua | 9 +- src/Types.lua | 5 +- src/init.lua | 12 +- test/State/Computed.spec.lua | 19 ++ test/State/For.spec.lua | 206 +++++++++----------- test/State/ForKeys.spec.lua | 148 +++++++++------ test/State/ForPairs.spec.lua | 257 ------------------------- test/State/ForValues.spec.lua | 346 +--------------------------------- 10 files changed, 230 insertions(+), 807 deletions(-) diff --git a/src/Logging/messages.lua b/src/Logging/messages.lua index 7476c899c..ce2ef0c80 100644 --- a/src/Logging/messages.lua +++ b/src/Logging/messages.lua @@ -13,21 +13,10 @@ return { cannotCreateClass = "Can't create a new instance of class '%s'.", cleanupWasRenamed = "`Fusion.cleanup` was renamed to `Fusion.doCleanup`. This will be an error in future versions of Fusion.", computedCallbackError = "Computed callback error: ERROR_MESSAGE", - destructorNeededValue = "To save instances into Values, provide a destructor function. This will be an error soon - see discussion #183 on GitHub.", destructorNeededComputed = "To return instances from Computeds, provide a destructor function. This will be an error soon - see discussion #183 on GitHub.", multiReturnComputed = "Returning multiple values from Computeds is discouraged, as behaviour will change soon - see discussion #189 on GitHub.", - destructorNeededForKeys = "To return instances from ForKeys, provide a destructor function. This will be an error soon - see discussion #183 on GitHub.", - destructorNeededForValues = "To return instances from ForValues, provide a destructor function. This will be an error soon - see discussion #183 on GitHub.", - destructorNeededForPairs = "To return instances from ForPairs, provide a destructor function. This will be an error soon - see discussion #183 on GitHub.", - forKeysProcessorError = "ForKeys callback error: ERROR_MESSAGE", - forKeysKeyCollision = "ForKeys should only write to output key '%s' once when processing key changes, but it wrote to it twice. Previously input key: '%s'; New input key: '%s'", - forKeysDestructorError = "ForKeys destructor error: ERROR_MESSAGE", - forPairsDestructorError = "ForPairs destructor error: ERROR_MESSAGE", - forPairsKeyCollision = "ForPairs should only write to output key '%s' once when processing key changes, but it wrote to it twice. Previous input pair: '[%s] = %s'; New input pair: '[%s] = %s'", - forPairsProcessorError = "ForPairs callback error: ERROR_MESSAGE", - forValuesProcessorError = "ForValues callback error: ERROR_MESSAGE", - forValuesDestructorError = "ForValues destructor error: ERROR_MESSAGE", - forProcessorError = "For (internal) callback error: ERROR_MESSAGE", + forKeyCollision = "The key '%s' was returned multiple times simultaneously, which is not allowed in `For` objects.", + forProcessorError = "Error while processing `For` object: ERROR_MESSAGE", invalidChangeHandler = "The change handler for the '%s' property must be a function.", invalidAttributeChangeHandler = "The change handler for the '%s' attribute must be a function.", invalidEventHandler = "The handler for the '%s' event must be a function.", diff --git a/src/State/Computed.lua b/src/State/Computed.lua index 4e4988535..272816007 100644 --- a/src/State/Computed.lua +++ b/src/State/Computed.lua @@ -58,16 +58,13 @@ function class:_recalculate( logWarn("destructorNeededComputed") end - if newMetaValue ~= nil then - logWarn("multiReturnComputed") - end - local oldValue = self._value local similar = isSimilar(oldValue, newValue) if self._destructor ~= nil and not firstTime then - self._destructor(oldValue) + self._destructor(oldValue, self._meta) end self._value = newValue + self._meta = newMetaValue -- add this object to the dependencies' dependent sets for dependency in pairs(self.dependencySet) do @@ -108,15 +105,15 @@ function class:destroy() dependency.dependentSet[self] = nil end if self._destructor ~= nil then - self._destructor(self._value) + self._destructor(self._value, self._meta) end end -local function Computed( +local function Computed( cleanupTable: {PubTypes.Task}, - processor: () -> T, - destructor: ((T) -> ())? -): Types.Computed + processor: () -> (T, M?), + destructor: ((T, M?) -> ())? +): Types.Computed local self = setmetatable({ type = "State", kind = "Computed", @@ -125,7 +122,8 @@ local function Computed( _oldDependencySet = {}, _processor = processor, _destructor = destructor, - _value = nil + _value = nil, + _meta = nil }, CLASS_METATABLE) self:_recalculate(true) diff --git a/src/State/For.lua b/src/State/For.lua index 67ccd3cad..b75c24fc5 100644 --- a/src/State/For.lua +++ b/src/State/For.lua @@ -147,7 +147,14 @@ function class:update(): boolean local key, value = processor.outputKey, processor.outputValue key.dependentSet[self], self.dependencySet[key] = true, true value.dependentSet[self], self.dependencySet[value] = true, true - newOutputTable[peek(key)] = peek(value) + local keyValue, valueValue = peek(key), peek(value) + if keyValue == nil or valueValue == nil then + continue + elseif newOutputTable[keyValue] == nil then + newOutputTable[keyValue] = valueValue + else + logErrorNonFatal("forKeyCollision", keyValue) + end end self._existingProcessors = newProcessors diff --git a/src/Types.lua b/src/Types.lua index b1510a2ba..bd259b447 100644 --- a/src/Types.lua +++ b/src/Types.lua @@ -48,10 +48,11 @@ export type State = PubTypes.Value & { } -- A state object whose value is derived from other objects using a callback. -export type Computed = PubTypes.Computed & { +export type Computed = PubTypes.Computed & { _oldDependencySet: Set, _callback: (PubTypes.Use) -> T, - _value: T + _value: T, + _meta: M } -- A state object which maps over keys and/or values in another table. diff --git a/src/init.lua b/src/init.lua index e8cc6aa8e..5bbd39fe4 100644 --- a/src/init.lua +++ b/src/init.lua @@ -51,9 +51,7 @@ export type CanBeState = PubTypes.CanBeState export type Symbol = PubTypes.Symbol export type Value = PubTypes.Value export type Computed = PubTypes.Computed -export type ForPairs = PubTypes.ForPairs -export type ForKeys = PubTypes.ForKeys -export type ForValues = PubTypes.ForKeys +export type For = PubTypes.For export type Observer = PubTypes.Observer export type Tween = PubTypes.Tween export type Spring = PubTypes.Spring @@ -75,10 +73,10 @@ type Fusion = { AttributeOut: (attributeName: string) -> PubTypes.SpecialKey, Value: (initialValue: T) -> Value, - Computed: (callback: (Use) -> T, destructor: (T) -> ()?) -> Computed, - ForPairs: (inputTable: CanBeState<{[KI]: VI}>, processor: (Use, KI, VI) -> (KO, VO, M?), destructor: (KO, VO, M?) -> ()?) -> ForPairs, - ForKeys: (inputTable: CanBeState<{[KI]: any}>, processor: (Use, KI) -> (KO, M?), destructor: (KO, M?) -> ()?) -> ForKeys, - ForValues: (inputTable: CanBeState<{[any]: VI}>, processor: (Use, VI) -> (VO, M?), destructor: (VO, M?) -> ()?) -> ForValues, + Computed: (callback: (Use) -> (T, M?), destructor: (T, M?) -> ()?) -> Computed, + ForPairs: (inputTable: CanBeState<{[KI]: VI}>, processor: (Use, KI, VI) -> (KO, VO, M?), destructor: (KO, VO, M?) -> ()?) -> For, + ForKeys: (inputTable: CanBeState<{[KI]: V}>, processor: (Use, KI) -> (KO, M?), destructor: (KO, M?) -> ()?) -> For, + ForValues: (inputTable: CanBeState<{[K]: VI}>, processor: (Use, VI) -> (VO, M?), destructor: (VO, M?) -> ()?) -> For, Observer: (watchedState: StateObject) -> Observer, Tween: (goalState: StateObject, tweenInfo: TweenInfo?) -> Tween, diff --git a/test/State/Computed.spec.lua b/test/State/Computed.spec.lua index f62cf03c0..2ebca263d 100644 --- a/test/State/Computed.spec.lua +++ b/test/State/Computed.spec.lua @@ -97,6 +97,25 @@ return function() doCleanup(scope) end) + it("calls destructor with metadata", function() + local scope = {} + local destructed = {} + local dependency = Value(scope, 1) + local _ = Computed(scope, function(use) + return 1, use(dependency) + end, function(_, meta) + destructed[meta] = true + end) + expect(destructed[1]).to.equal(nil) + dependency:set(2) + expect(destructed[1]).to.equal(true) + expect(destructed[2]).to.equal(nil) + dependency:set(3) + expect(destructed[2]).to.equal(true) + + doCleanup(scope) + end) + it("calls destructor on destroy", function() local scope = {} local destructed = {} diff --git a/test/State/For.spec.lua b/test/State/For.spec.lua index e3f89569e..b7b6e4135 100644 --- a/test/State/For.spec.lua +++ b/test/State/For.spec.lua @@ -8,26 +8,26 @@ local doCleanup = require(Package.Memory.doCleanup) return function() it("constructs in scopes", function() local scope = {} - local genericFor = For(scope, {}, function() + local forObject = For(scope, {}, function() -- intentionally blank end) - expect(genericFor).to.be.a("table") - expect(genericFor.type).to.equal("State") - expect(genericFor.kind).to.equal("For") - expect(scope[1]).to.equal(genericFor) + expect(forObject).to.be.a("table") + expect(forObject.type).to.equal("State") + expect(forObject.kind).to.equal("For") + expect(scope[1]).to.equal(forObject) doCleanup(scope) end) it("is destroyable", function() local scope = {} - local genericFor = For(scope, {}, function() + local forObject = For(scope, {}, function() -- intentionally blank end) expect(function() - genericFor:destroy() + forObject:destroy() end).to.never.throw() end) @@ -36,7 +36,7 @@ return function() local data = {foo = 1, bar = 2} local seen = {} local numCalls = 0 - local genericFor = For(scope, data, function(scope, inputKey, inputValue) + local forObject = For(scope, data, function(scope, inputKey, inputValue) numCalls += 1 local k, v = peek(inputKey), peek(inputValue) seen[k] = v @@ -52,9 +52,9 @@ return function() expect(seen.foo).to.equal(1) expect(seen.bar).to.equal(2) - expect(peek(genericFor)).to.be.a("table") - expect(peek(genericFor).FOO).to.equal(10) - expect(peek(genericFor).BAR).to.equal(20) + expect(peek(forObject)).to.be.a("table") + expect(peek(forObject).FOO).to.equal(10) + expect(peek(forObject).BAR).to.equal(20) doCleanup(scope) end) @@ -62,7 +62,7 @@ return function() local scope = {} local data = Value(scope, {foo = 1, bar = 2}) local numCalls = 0 - local genericFor = For(scope, data, function(scope, inputKey, inputValue) + local forObject = For(scope, data, function(scope, inputKey, inputValue) numCalls += 1 local outputKey = Computed(scope, function(use) return string.upper(use(inputKey)) @@ -74,130 +74,100 @@ return function() end) expect(numCalls).to.equal(2) - expect(peek(genericFor)).to.be.a("table") - expect(peek(genericFor).FOO).to.equal(10) - expect(peek(genericFor).BAR).to.equal(20) + expect(peek(forObject)).to.be.a("table") + expect(peek(forObject).FOO).to.equal(10) + expect(peek(forObject).BAR).to.equal(20) data:set({frob = 3, garb = 4}) expect(numCalls).to.equal(2) - expect(peek(genericFor).FOO).to.equal(nil) - expect(peek(genericFor).BAR).to.equal(nil) - expect(peek(genericFor).FROB).to.equal(30) - expect(peek(genericFor).GARB).to.equal(40) + expect(peek(forObject).FOO).to.equal(nil) + expect(peek(forObject).BAR).to.equal(nil) + expect(peek(forObject).FROB).to.equal(30) + expect(peek(forObject).GARB).to.equal(40) data:set({frob = 5, garb = 6, baz = 7}) expect(numCalls).to.equal(3) - expect(peek(genericFor).FROB).to.equal(50) - expect(peek(genericFor).GARB).to.equal(60) - expect(peek(genericFor).BAZ).to.equal(70) + expect(peek(forObject).FROB).to.equal(50) + expect(peek(forObject).GARB).to.equal(60) + expect(peek(forObject).BAZ).to.equal(70) data:set({garb = 6, baz = 7}) expect(numCalls).to.equal(3) - expect(peek(genericFor).FROB).to.equal(nil) - expect(peek(genericFor).GARB).to.equal(60) - expect(peek(genericFor).BAZ).to.equal(70) + expect(peek(forObject).FROB).to.equal(nil) + expect(peek(forObject).GARB).to.equal(60) + expect(peek(forObject).BAZ).to.equal(70) data:set({}) expect(numCalls).to.equal(3) - expect(peek(genericFor).GARB).to.equal(nil) - expect(peek(genericFor).BAZ).to.equal(nil) + expect(peek(forObject).GARB).to.equal(nil) + expect(peek(forObject).BAZ).to.equal(nil) doCleanup(scope) end) - -- itSKIP("rejects key collisions", function() - -- expect(function() - -- local scope = {} - -- local data = {foo = 1, bar = 2} - -- local _ = ForKeys(scope, data, function(use, key) - -- return "samuel" - -- end) - -- doCleanup(scope) - -- end).to.throw("forKeysKeyCollision") - -- end) - - -- itSKIP("preserves value on error", function() - -- local scope = {} - -- local data = {foo = 1, bar = 2} - -- local suffix = Value(scope, "first") - -- local forkeys = ForKeys(scope, data, function(use, key) - -- assert(use(suffix) ~= "second", "This is an intentional error from a unit test") - -- return key .. use(suffix) - -- end) - -- expect(peek(forkeys).foofirst).to.equal(1) - -- expect(peek(forkeys).barfirst).to.equal(2) - -- suffix:set("second") -- will invoke the error - -- expect(peek(forkeys).foofirst).to.equal(1) - -- expect(peek(forkeys).barfirst).to.equal(2) - -- expect(peek(forkeys).foosecond).to.equal(nil) - -- expect(peek(forkeys).barsecond).to.equal(nil) - -- suffix:set("third") - -- expect(peek(forkeys).foofirst).to.equal(nil) - -- expect(peek(forkeys).barfirst).to.equal(nil) - -- expect(peek(forkeys).foosecond).to.equal(nil) - -- expect(peek(forkeys).barsecond).to.equal(nil) - -- expect(peek(forkeys).foothird).to.equal(1) - -- expect(peek(forkeys).barthird).to.equal(2) - -- doCleanup(scope) - -- end) - - -- itSKIP("doesn't call destructor on creation", function() - -- local scope = {} - -- local destructed = {} - -- local data = Value(scope, {foo = 1, bar = 2}) - -- local _ = ForKeys(scope, data, function(use, key) - -- return key, "meta" .. key - -- end, function(key, meta) - -- destructed[key] = true - -- destructed[meta] = true - -- end) - -- expect(destructed.foo).to.equal(nil) - -- expect(destructed.metafoo).to.equal(nil) - -- expect(destructed.bar).to.equal(nil) - -- expect(destructed.metabar).to.equal(nil) - -- doCleanup(scope) - -- end) - - -- itSKIP("calls destructor on update", function() - -- local scope = {} - -- local destructed = {} - -- local data = Value(scope, {foo = 1, bar = 2}) - -- local _ = ForKeys(scope, data, function(use, key) - -- return key, "meta" .. key - -- end, function(key, meta) - -- destructed[key] = true - -- destructed[meta] = true - -- end) - -- data:set({foo = 100, baz = 3}) - -- expect(destructed.foo).to.equal(nil) - -- expect(destructed.metafoo).to.equal(nil) - -- expect(destructed.bar).to.equal(true) - -- expect(destructed.metabar).to.equal(true) - -- doCleanup(scope) - -- end) - - -- itSKIP("calls destructor on destroy", function() - -- local scope = {} - -- local destructed = {} - -- local data = Value(scope, {foo = 1, bar = 2}) - -- local _ = ForKeys(scope, data, function(use, key) - -- return key, "meta" .. key - -- end, function(key, meta) - -- destructed[key] = true - -- destructed[meta] = true - -- end) - -- expect(destructed.foo).to.equal(nil) - -- expect(destructed.metafoo).to.equal(nil) - -- expect(destructed.bar).to.equal(nil) - -- expect(destructed.metabar).to.equal(nil) - -- doCleanup(scope) - -- expect(destructed.foo).to.equal(true) - -- expect(destructed.metafoo).to.equal(true) - -- expect(destructed.bar).to.equal(true) - -- expect(destructed.metabar).to.equal(true) - -- end) + it("omits pairs that error", function() + local scope = {} + local data = {first = 1, second = 2, third = 3} + local forObject = For(scope, data, function(scope, inputKey, inputValue) + assert(peek(inputKey) ~= "second", "This is an intentional error from a unit test") + return inputKey, inputValue + end) + expect(peek(forObject).first).to.equal(1) + expect(peek(forObject).second).to.equal(nil) + expect(peek(forObject).third).to.equal(3) + doCleanup(scope) + end) + + it("omits pairs when their key or value is nil", function() + local scope = {} + local data = {first = 1, second = 2, third = 3} + local omitThird = Value(scope, false) + local forObject1 = For(scope, data, function(scope, inputKey, inputValue) + return inputKey, Computed(scope, function(use) + if use(inputKey) == "second" then + return nil + elseif use(inputKey) == "third" and use(omitThird) then + return nil + else + return use(inputValue) + end + end) + end) + local forObject2 = For(scope, data, function(scope, inputKey, inputValue) + return Computed(scope, function(use) + if use(inputKey) == "second" then + return nil + elseif use(inputKey) == "third" and use(omitThird) then + return nil + else + return use(inputKey) + end + end), inputValue + end) + expect(peek(forObject1).first).to.equal(1) + expect(peek(forObject1).second).to.equal(nil) + expect(peek(forObject1).third).to.equal(3) + expect(peek(forObject2).first).to.equal(1) + expect(peek(forObject2).second).to.equal(nil) + expect(peek(forObject2).third).to.equal(3) + omitThird:set(true) + expect(peek(forObject1).first).to.equal(1) + expect(peek(forObject1).second).to.equal(nil) + expect(peek(forObject1).third).to.equal(nil) + expect(peek(forObject2).first).to.equal(1) + expect(peek(forObject2).second).to.equal(nil) + expect(peek(forObject2).third).to.equal(nil) + omitThird:set(false) + expect(peek(forObject1).first).to.equal(1) + expect(peek(forObject1).second).to.equal(nil) + expect(peek(forObject1).third).to.equal(3) + expect(peek(forObject2).first).to.equal(1) + expect(peek(forObject2).second).to.equal(nil) + expect(peek(forObject2).third).to.equal(3) + doCleanup(scope) + end) end diff --git a/test/State/ForKeys.spec.lua b/test/State/ForKeys.spec.lua index a8a84b766..9625ec078 100644 --- a/test/State/ForKeys.spec.lua +++ b/test/State/ForKeys.spec.lua @@ -5,62 +5,64 @@ local peek = require(Package.State.peek) local doCleanup = require(Package.Memory.doCleanup) return function() + + FOCUS() it("constructs in scopes", function() local scope = {} - local forkeys = ForKeys(scope, {}, function() + local forObject = ForKeys(scope, {}, function() -- intentionally blank end) - expect(forkeys).to.be.a("table") - expect(forkeys.type).to.equal("State") - expect(forkeys.kind).to.equal("ForKeys") - expect(scope[1]).to.equal(forkeys) + expect(forObject).to.be.a("table") + expect(forObject.type).to.equal("State") + expect(forObject.kind).to.equal("For") + expect(scope[1]).to.equal(forObject) doCleanup(scope) end) it("is destroyable", function() local scope = {} - local forkeys = ForKeys(scope, {}, function() + local forObject = ForKeys(scope, {}, function() -- intentionally blank end) expect(function() - forkeys:destroy() + forObject:destroy() end).to.never.throw() end) it("iterates on constants", function() local scope = {} local data = {foo = 1, bar = 2} - local forkeys = ForKeys(scope, data, function(_, key) + local forObject = ForKeys(scope, data, function(_, key) return key:upper() end) - expect(peek(forkeys)).to.be.a("table") - expect(peek(forkeys).FOO).to.equal(1) - expect(peek(forkeys).BAR).to.equal(2) + expect(peek(forObject)).to.be.a("table") + expect(peek(forObject).FOO).to.equal(1) + expect(peek(forObject).BAR).to.equal(2) doCleanup(scope) end) it("iterates on state objects", function() local scope = {} local data = Value(scope, {foo = 1, bar = 2}) - local forkeys = ForKeys(scope, data, function(_, key) + local forObject = ForKeys(scope, data, function(_, key) return key:upper() end) - expect(peek(forkeys)).to.be.a("table") - expect(peek(forkeys).FOO).to.equal(1) - expect(peek(forkeys).BAR).to.equal(2) + expect(peek(forObject)).to.be.a("table") + expect(peek(forObject).FOO).to.equal(1) + expect(peek(forObject).BAR).to.equal(2) doCleanup(scope) end) it("computes with constants", function() local scope = {} local data = {foo = 1, bar = 2} - local forkeys = ForKeys(scope, data, function(use, key) + local forObject = ForKeys(scope, data, function(use, key) return key .. use("baz") end) - expect(peek(forkeys).foobaz).to.equal(1) - expect(peek(forkeys).barbaz).to.equal(2) + expect(peek(forObject).foobaz).to.equal(1) + expect(peek(forObject).barbaz).to.equal(2) doCleanup(scope) end) @@ -68,52 +70,70 @@ return function() local scope = {} local data = {foo = 1, bar = 2} local suffix = Value(scope, "first") - local forkeys = ForKeys(scope, data, function(use, key) + local forObject = ForKeys(scope, data, function(use, key) return key .. use(suffix) end) - expect(peek(forkeys).foofirst).to.equal(1) - expect(peek(forkeys).barfirst).to.equal(2) + expect(peek(forObject).foofirst).to.equal(1) + expect(peek(forObject).barfirst).to.equal(2) suffix:set("second") - expect(peek(forkeys).foofirst).to.equal(nil) - expect(peek(forkeys).barfirst).to.equal(nil) - expect(peek(forkeys).foosecond).to.equal(1) - expect(peek(forkeys).barsecond).to.equal(2) + expect(peek(forObject).foofirst).to.equal(nil) + expect(peek(forObject).barfirst).to.equal(nil) + expect(peek(forObject).foosecond).to.equal(1) + expect(peek(forObject).barsecond).to.equal(2) doCleanup(scope) end) - it("rejects key collisions", function() - expect(function() - local scope = {} - local data = {foo = 1, bar = 2} - local _ = ForKeys(scope, data, function(use, key) - return "samuel" - end) - doCleanup(scope) - end).to.throw("forKeysKeyCollision") + it("omits keys that error", function() + local scope = {} + local data = {foo = 1, bar = 2, baz = 3} + local omitThird = Value(scope, false) + local forObject = ForKeys(scope, data, function(use, key) + assert(key ~= "bar", "This is an intentional error from a unit test") + if use(omitThird) then + assert(key ~= "bar", "This is an intentional error from a unit test") + end + return key + end) + expect(peek(forObject).foo).to.equal(1) + expect(peek(forObject).bar).to.equal(nil) + expect(peek(forObject).baz).to.equal(3) + omitThird:set(true) + expect(peek(forObject).foo).to.equal(1) + expect(peek(forObject).bar).to.equal(nil) + expect(peek(forObject).baz).to.equal(nil) + omitThird:set(false) + expect(peek(forObject).foo).to.equal(1) + expect(peek(forObject).bar).to.equal(nil) + expect(peek(forObject).baz).to.equal(3) + doCleanup(scope) end) - it("preserves value on error", function() + it("omits keys that return nil", function() local scope = {} - local data = {foo = 1, bar = 2} - local suffix = Value(scope, "first") - local forkeys = ForKeys(scope, data, function(use, key) - assert(use(suffix) ~= "second", "This is an intentional error from a unit test") - return key .. use(suffix) + local data = {foo = 1, bar = 2, baz = 3} + local omitThird = Value(scope, false) + local forObject = ForKeys(scope, data, function(use, key) + if key == "bar" then + return nil + end + if use(omitThird) then + if key == "baz" then + return nil + end + end + return key end) - expect(peek(forkeys).foofirst).to.equal(1) - expect(peek(forkeys).barfirst).to.equal(2) - suffix:set("second") -- will invoke the error - expect(peek(forkeys).foofirst).to.equal(1) - expect(peek(forkeys).barfirst).to.equal(2) - expect(peek(forkeys).foosecond).to.equal(nil) - expect(peek(forkeys).barsecond).to.equal(nil) - suffix:set("third") - expect(peek(forkeys).foofirst).to.equal(nil) - expect(peek(forkeys).barfirst).to.equal(nil) - expect(peek(forkeys).foosecond).to.equal(nil) - expect(peek(forkeys).barsecond).to.equal(nil) - expect(peek(forkeys).foothird).to.equal(1) - expect(peek(forkeys).barthird).to.equal(2) + expect(peek(forObject).foo).to.equal(1) + expect(peek(forObject).bar).to.equal(nil) + expect(peek(forObject).baz).to.equal(3) + omitThird:set(true) + expect(peek(forObject).foo).to.equal(1) + expect(peek(forObject).bar).to.equal(nil) + expect(peek(forObject).baz).to.equal(nil) + omitThird:set(false) + expect(peek(forObject).foo).to.equal(1) + expect(peek(forObject).bar).to.equal(nil) + expect(peek(forObject).baz).to.equal(3) doCleanup(scope) end) @@ -172,4 +192,24 @@ return function() expect(destructed.bar).to.equal(true) expect(destructed.metabar).to.equal(true) end) + + it("doesn't recompute when keys are preserved", function() + local scope = {} + local data = Value(scope, {foo = 1, bar = 2}) + local computations = 0 + local forObject = ForKeys(scope, data, function(_, key) + computations += 1 + return string.upper(key) + end) + expect(computations).to.equal(2) + data:set({foo = 3, bar = 4}) + expect(computations).to.equal(2) + data:set({foo = 3, bar = 4, baz = 5}) + expect(computations).to.equal(3) + data:set({foo = 3, bar = 4, baz = 5}) + expect(computations).to.equal(3) + data:set({garb = 6}) + expect(computations).to.equal(4) + doCleanup(scope) + end) end diff --git a/test/State/ForPairs.spec.lua b/test/State/ForPairs.spec.lua index ec1b3fb54..8640826c1 100644 --- a/test/State/ForPairs.spec.lua +++ b/test/State/ForPairs.spec.lua @@ -6,262 +6,5 @@ local Value = require(Package.State.Value) local peek = require(Package.State.peek) return function() - it("should construct a ForPairs object", function() - local forPairs = ForPairs({}, function(use) end) - expect(forPairs).to.be.a("table") - expect(forPairs.type).to.equal("State") - expect(forPairs.kind).to.equal("ForPairs") - end) - - it("should calculate and retrieve its value", function() - local computedPair = ForPairs({ ["foo"] = "bar" }, function(use, key, value) - return key .. "baz", value .. "biz" - end) - - local state = peek(computedPair) - - expect(state["foobaz"]).to.be.ok() - expect(state["foobaz"]).to.equal("barbiz") - end) - - it("should not recalculate its KO/VO in response to an unchanged KI/VI", function() - local state = Value({ - ["foo"] = "bar", - }) - - local computedPair = ForPairs(state, function(use, key, value) - return key .. "biz", { value } - end) - - local foobiz = peek(computedPair)["foobiz"] - - state:set({ - ["foo"] = "bar", - ["baz"] = "bar", - }) - - expect(peek(computedPair)["foobiz"]).to.equal(foobiz) - end) - - it("should call the destructor when a key/value pair gets changed", function() - local state = Value({ - ["foo"] = "bar", - ["baz"] = "bar", - }) - - local destructions = 0 - - local computedPair = ForPairs(state, function(use, key, value) - return key .. "biz", value .. "biz" - end, function(key, value) - destructions += 1 - end) - - state:set({ - ["foo"] = "bar", - }) - - expect(destructions).to.equal(1) - - state:set({ - ["baz"] = "bar", - }) - - expect(destructions).to.equal(2) - - state:set({ - ["foo"] = "bar", - ["baz"] = "bar", - }) - - expect(destructions).to.equal(2) - - state:set({}) - - expect(destructions).to.equal(4) - end) - - it( - "should not call the destructor when an output key/value pair still remains with a new input key/value pair", - function() - local state = Value({ - ["foo"] = "bar", - }) - - local destructions = 0 - - local computedPair = ForPairs(state, function(use, key, value) - return value, value - end, function(key, value) - destructions += 1 - end) - - state:set({ - ["baz"] = "bar", - }) - - expect(destructions).to.equal(0) - - state:set({ - ["biz"] = "baz", - }) - - expect(destructions).to.equal(1) - - state:set({ - ["foo"] = "bar", - ["biz"] = "baz", - }) - - expect(destructions).to.equal(1) - - state:set({ - ["foo"] = "baz", - ["biz"] = "bar", - }) - - expect(destructions).to.equal(1) - - state:set({ - ["biz"] = "bar", - }) - - expect(destructions).to.equal(2) - - state:set({}) - - expect(destructions).to.equal(3) - end - ) - - it("should throw if there is a key collision", function() - expect(function() - local state = Value({ - ["foo"] = "bar", - ["baz"] = "bar", - }) - - local computed = ForPairs(state, function(use, key, value) - return value, key - end) - end).to.throw("forPairsKeyCollision") - - local state = Value({ - ["foo"] = "bar", - }) - - local computed = ForPairs(state, function(use, key, value) - return value, key - end) - - expect(function() - state:set({ - ["foo"] = "bar", - ["baz"] = "bar", - }) - end).to.throw("forPairsKeyCollision") - end) - - it("should call the destructor with meta data", function() - local state = Value({ - ["foo"] = "bar", - }) - - local destructions = 0 - - local computedPair = ForPairs(state, function(use, key, value) - local newKey = key .. "biz" - local newValue = value .. "biz" - - return newKey, newValue, newKey .. newValue - end, function(key, value, meta) - expect(meta).to.equal(key .. value) - destructions += 1 - end) - - state:set({ - ["foo"] = "baz", - }) - - -- this verifies that the meta expectation passed - expect(destructions).to.equal(1) - - state:set({}) - - -- this verifies that the meta expectation passed - expect(destructions).to.equal(2) - end) - - it("should recalculate its value in response to State objects", function() - local currentNumber = Value({ ["foo"] = 2 }) - local doubled = ForPairs(currentNumber, function(use, key, value) - return key .. "bar", value * 2 - end) - - expect(peek(doubled)["foobar"]).to.equal(4) - - currentNumber:set({ ["foo"] = 4 }) - expect(peek(doubled)["foobar"]).to.equal(8) - end) - - it("should recalculate its value in response to ForPairs objects", function() - local currentNumbers = Value({ 1, 2 }) - local doubled = ForPairs(currentNumbers, function(use, key, value) - return key * 2, value * 2 - end) - local tripled = ForPairs(doubled, function(use, key, value) - return key * 2, value * 2 - end) - - expect(peek(tripled)[4]).to.equal(4) - expect(peek(tripled)[8]).to.equal(8) - - currentNumbers:set({ 2, 4 }) - expect(peek(tripled)[4]).to.equal(8) - expect(peek(tripled)[8]).to.equal(16) - end) - - itSKIP("should not corrupt dependencies after an error", function() - -- needs rewrite - end) - - it("should garbage-collect unused objects", function() - local state = Value({ 2 }) - - local counter = 0 - - do - local computedPairs = ForPairs(state, function(use, key, value) - counter += 1 - return key, value - end) - end - - state:set({ 5 }) - - expect(counter).to.equal(1) - end) - - it("should not garbage-collect objects in use", function() - local state = Value({ 2 }) - local computed2 - - local counter = 0 - - do - local computed = ForPairs(state, function(use, key, value) - counter += 1 - return key, value - end) - - computed2 = ForPairs(computed, function(use, key, value) - return key, value - end) - end - - state:set({ 5 }) - - expect(counter).to.equal(2) - end) end diff --git a/test/State/ForValues.spec.lua b/test/State/ForValues.spec.lua index 6323a32d4..6e95e100d 100644 --- a/test/State/ForValues.spec.lua +++ b/test/State/ForValues.spec.lua @@ -4,350 +4,8 @@ local Package = game:GetService("ReplicatedStorage").Fusion local ForValues = require(Package.State.ForValues) local Value = require(Package.State.Value) local peek = require(Package.State.peek) +local doCleanup = require(Package.Memory.doCleanup) return function() - it("should construct a ForValues object", function() - local forKeys = ForValues({}, function(use) end) - - expect(forKeys).to.be.a("table") - expect(forKeys.type).to.equal("State") - expect(forKeys.kind).to.equal("ForValues") - end) - - it("should calculate and retrieve its value", function() - local computed = ForValues({ 1 }, function(use, value) - return value - end) - - local state = peek(computed) - - expect(state[1]).to.be.ok() - expect(state[1]).to.equal(1) - end) - - it("should not recalculate its VO in response to a changed VI", function() - local state = Value({ - [1] = "foo", - }) - - local calculations = 0 - - local computed = ForValues(state, function(use, value) - calculations += 1 - return value - end) - - expect(calculations).to.equal(1) - - state:set({ - [1] = "bar", - [2] = "foo", - }) - - expect(calculations).to.equal(2) - end) - - it("should only call the processor the first time a constant output value is added", function() - local state = Value({ - [1] = "foo", - }) - - local processorCalls = 0 - - local computed = ForValues(state, function(use, value) - processorCalls += 1 - - return value .. "biz" - end) - - expect(processorCalls).to.equal(1) - - state:set({ - [1] = "bar", - }) - - expect(processorCalls).to.equal(2) - - state:set({ - [2] = "bar", - [3] = "bar", - }) - - expect(processorCalls).to.equal(2) - - state:set({ - [1] = "bar", - [2] = "bar", - }) - - expect(processorCalls).to.equal(2) - - state:set({}) - - expect(processorCalls).to.equal(2) - - state:set({ - [1] = "bar", - [2] = "foo", - }) - - expect(processorCalls).to.equal(4) - end) - - it("should only call the destructor when a constant value gets removed from all indices", function() - local state = Value({ - [1] = "foo", - }) - - local destructions = 0 - - local computed = ForValues(state, function(use, value) - return value .. "biz" - end, function(key) - destructions += 1 - end) - - state:set({ - [1] = "bar", - }) - - expect(destructions).to.equal(1) - - state:set({ - [2] = "bar", - [3] = "bar", - }) - - expect(destructions).to.equal(1) - - state:set({ - [1] = "bar", - [2] = "bar", - }) - - expect(destructions).to.equal(1) - - state:set({}) - - expect(destructions).to.equal(2) - end) - - it("should only call the destructor when a non-constant value gets removed from all indices", function() - local mem1 = Instance.new("Folder") - local mem2 = Instance.new("Folder") - local mem3 = Instance.new("Folder") - - local state = Value({ - [1] = mem1, - }) - - local destructions = 0 - - local computed = ForValues(state, function(use, value) - local obj = Instance.new("Folder") - obj.Parent = value - - return obj - end, function(value) - destructions += 1 - value:Destroy() - end) - - state:set({ - [1] = mem2, - }) - - expect(destructions).to.equal(1) - - state:set({ - [2] = mem3, - [3] = mem2, - }) - - expect(destructions).to.equal(1) - - state:set({ - [1] = mem2, - [2] = mem3, - }) - - expect(destructions).to.equal(1) - - state:set({}) - - expect(destructions).to.equal(3) - end) - - it("should call the destructor with meta data", function() - local state = Value({ - [1] = "foo", - }) - - local destructions = 0 - - local computed = ForValues(state, function(use, value) - local newValue = value .. "biz" - return newValue, newValue - end, function(value, meta) - expect(meta).to.equal(value) - destructions += 1 - end) - - state:set({ - ["baz"] = "bar", - }) - - -- this verifies that the meta expectation passed - expect(destructions).to.equal(1) - - state:set({}) - - -- this verifies that the meta expectation passed - expect(destructions).to.equal(2) - end) - - it("should not make any value changes or processor/destructor calls when only the input key changes", function() - local state = Value({ - ["foo"] = "bar", - ["bar"] = "baz", - ["biz"] = "buzz", - }) - - local processorCalls = 0 - local destructorCalls = 0 - - local computed = ForValues(state, function(use, value) - processorCalls += 1 - return value - end, function(value) - destructorCalls += 1 - end) - - expect(processorCalls).to.equal(3) - expect(destructorCalls).to.equal(0) - - state:set({ - [1] = "buzz", - ["bar"] = "bar", - ["fiz"] = "baz", - }) - - expect(processorCalls).to.equal(3) - expect(destructorCalls).to.equal(0) - - state:set({ - [1] = "bar", - ["bar"] = "bar", - ["fiz"] = "bar", - }) - - expect(processorCalls).to.equal(3) - expect(destructorCalls).to.equal(2) - - state:set({ - [2] = "bar", - [3] = "baz", - }) - - expect(processorCalls).to.equal(4) - expect(destructorCalls).to.equal(2) - - state:set({}) - - expect(processorCalls).to.equal(4) - expect(destructorCalls).to.equal(4) - end) - - it("should recalculate its value in response to State objects", function() - local state = Value({ - [1] = "baz", - }) - local barMap = ForValues(state, function(use, value) - return value .. "bar" - end) - - expect(peek(barMap)[1]).to.equal("bazbar") - - state:set({ - [1] = "bar", - }) - - expect(peek(barMap)[1]).to.equal("barbar") - end) - - it("should recalculate its value in response to ForValues objects", function() - local state = Value({ - [1] = 1, - }) - local doubled = ForValues(state, function(use, value) - return value * 2 - end) - local tripled = ForValues(doubled, function(use, value) - return value * 2 - end) - - expect(peek(doubled)[1]).to.equal(2) - expect(peek(tripled)[1]).to.equal(4) - - state:set({ - [1] = 2, - [2] = 3, - }) - - expect(peek(doubled)[1]).to.equal(4) - expect(peek(tripled)[1]).to.equal(8) - expect(peek(doubled)[2]).to.equal(6) - expect(peek(tripled)[2]).to.equal(12) - end) - - itSKIP("should not corrupt dependencies after an error", function() - -- needs rewrite - end) - - it("should garbage-collect unused objects", function() - local state = Value({ - [1] = "bar", - }) - - local counter = 0 - - do - local computedKeys = ForValues(state, function(use, value) - counter += 1 - return value - end) - end - - state:set({ - [1] = "biz", - }) - - expect(counter).to.equal(1) - end) - - it("should not garbage-collect objects in use", function() - local state = Value({ - [1] = 1, - }) - local computed2 - - local counter = 0 - - do - local computed = ForValues(state, function(use, value) - counter += 1 - return value - end) - - computed2 = ForValues(computed, function(use, value) - return value - end) - end - - state:set({ - [1] = 2, - }) - - expect(counter).to.equal(2) - end) + end From 44ddfaf11272277f9c4aa55c59b73e4c198de7bd Mon Sep 17 00:00:00 2001 From: Elttob Date: Mon, 27 Nov 2023 21:17:14 +0000 Subject: [PATCH 039/287] ForKeys now passes unit tests --- src/State/ForKeys.lua | 11 ++++++++++- test/State/For.spec.lua | 2 ++ test/State/ForKeys.spec.lua | 2 +- 3 files changed, 13 insertions(+), 2 deletions(-) diff --git a/src/State/ForKeys.lua b/src/State/ForKeys.lua index 44779de11..3ea0988a0 100644 --- a/src/State/ForKeys.lua +++ b/src/State/ForKeys.lua @@ -17,6 +17,9 @@ local Types = require(Package.Types) -- State local For = require(Package.State.For) local Computed = require(Package.State.Computed) +-- Logging +local parseError = require(Package.Logging.parseError) +local logErrorNonFatal = require(Package.Logging.logErrorNonFatal) local function ForKeys( cleanupTable: {PubTypes.Task}, @@ -30,7 +33,13 @@ local function ForKeys( inputTable, function(scope, inputKey, inputValue) return Computed(scope, function(use) - return processor(use, use(inputKey)) + local ok, key, meta = xpcall(processor, parseError, use, use(inputKey)) + if ok then + return key, meta + else + logErrorNonFatal("forProcessorError", parseError) + return nil + end end, destructor), inputValue end ) diff --git a/test/State/For.spec.lua b/test/State/For.spec.lua index b7b6e4135..7c864f957 100644 --- a/test/State/For.spec.lua +++ b/test/State/For.spec.lua @@ -6,6 +6,8 @@ local peek = require(Package.State.peek) local doCleanup = require(Package.Memory.doCleanup) return function() + + FOCUS() it("constructs in scopes", function() local scope = {} local forObject = For(scope, {}, function() diff --git a/test/State/ForKeys.spec.lua b/test/State/ForKeys.spec.lua index 9625ec078..9c1d83aaa 100644 --- a/test/State/ForKeys.spec.lua +++ b/test/State/ForKeys.spec.lua @@ -90,7 +90,7 @@ return function() local forObject = ForKeys(scope, data, function(use, key) assert(key ~= "bar", "This is an intentional error from a unit test") if use(omitThird) then - assert(key ~= "bar", "This is an intentional error from a unit test") + assert(key ~= "baz", "This is an intentional error from a unit test") end return key end) From e57cd95c0a58a3d6247a83ac703e94aeb65708a8 Mon Sep 17 00:00:00 2001 From: Elttob Date: Mon, 27 Nov 2023 21:33:23 +0000 Subject: [PATCH 040/287] ForValues unit tests --- test/State/ForValues.spec.lua | 204 ++++++++++++++++++++++++++++++++++ 1 file changed, 204 insertions(+) diff --git a/test/State/ForValues.spec.lua b/test/State/ForValues.spec.lua index 6e95e100d..cf4f0d3ff 100644 --- a/test/State/ForValues.spec.lua +++ b/test/State/ForValues.spec.lua @@ -8,4 +8,208 @@ local doCleanup = require(Package.Memory.doCleanup) return function() + FOCUS() + it("constructs in scopes", function() + local scope = {} + local forObject = ForValues(scope, {}, function() + -- intentionally blank + end) + + expect(forObject).to.be.a("table") + expect(forObject.type).to.equal("State") + expect(forObject.kind).to.equal("For") + expect(scope[1]).to.equal(forObject) + + doCleanup(scope) + end) + + it("is destroyable", function() + local scope = {} + local forObject = ForValues(scope, {}, function() + -- intentionally blank + end) + expect(function() + forObject:destroy() + end).to.never.throw() + end) + + it("iterates on constants", function() + local scope = {} + local data = {"foo", "bar"} + local forObject = ForValues(scope, data, function(_, value) + return value:upper() + end) + expect(peek(forObject)).to.be.a("table") + expect(peek(forObject)[1]).to.equal("FOO") + expect(peek(forObject)[2]).to.equal("BAR") + doCleanup(scope) + end) + + it("iterates on state objects", function() + local scope = {} + local data = Value(scope, {"foo", "bar"}) + local forObject = ForValues(scope, data, function(_, value) + return value:upper() + end) + expect(peek(forObject)).to.be.a("table") + expect(peek(forObject)[1]).to.equal("FOO") + expect(peek(forObject)[2]).to.equal("BAR") + doCleanup(scope) + end) + + it("computes with constants", function() + local scope = {} + local data = {"foo", "bar"} + local forObject = ForValues(scope, data, function(use, value) + return value .. use("baz") + end) + expect(peek(forObject)[1]).to.equal("foobaz") + expect(peek(forObject)[2]).to.equal("barbaz") + doCleanup(scope) + end) + + it("computes with state objects", function() + local scope = {} + local data = {"foo", "bar"} + local suffix = Value(scope, "first") + local forObject = ForValues(scope, data, function(use, value) + return value .. use(suffix) + end) + expect(peek(forObject)[1]).to.equal("foofirst") + expect(peek(forObject)[2]).to.equal("barfirst") + suffix:set("second") + expect(peek(forObject)[1]).to.equal("foosecond") + expect(peek(forObject)[2]).to.equal("barsecond") + doCleanup(scope) + end) + + it("omits values that error", function() + local scope = {} + local data = {"foo", "bar", "baz"} + local omitThird = Value(scope, false) + local forObject = ForValues(scope, data, function(use, value) + assert(value ~= "bar", "This is an intentional error from a unit test") + if use(omitThird) then + assert(value ~= "baz", "This is an intentional error from a unit test") + end + return value + end) + expect(peek(forObject)[1]).to.equal("foo") + expect(peek(forObject)[2]).to.equal("baz") + expect(peek(forObject)[3]).to.equal(nil) + omitThird:set(true) + expect(peek(forObject)[1]).to.equal("foo") + expect(peek(forObject)[2]).to.equal(nil) + expect(peek(forObject)[3]).to.equal(nil) + omitThird:set(false) + expect(peek(forObject)[1]).to.equal("foo") + expect(peek(forObject)[2]).to.equal("baz") + expect(peek(forObject)[3]).to.equal(nil) + doCleanup(scope) + end) + + it("omits values that return nil", function() + local scope = {} + local data = {"foo", "bar", "baz"} + local omitThird = Value(scope, false) + local forObject = ForValues(scope, data, function(use, value) + if value == "bar" then + return nil + end + if use(omitThird) then + if value == "baz" then + return nil + end + end + return value + end) + expect(peek(forObject)[1]).to.equal("foo") + expect(peek(forObject)[2]).to.equal("baz") + expect(peek(forObject)[3]).to.equal(nil) + omitThird:set(true) + expect(peek(forObject)[1]).to.equal("foo") + expect(peek(forObject)[2]).to.equal(nil) + expect(peek(forObject)[3]).to.equal(nil) + omitThird:set(false) + expect(peek(forObject)[1]).to.equal("foo") + expect(peek(forObject)[2]).to.equal("baz") + expect(peek(forObject)[3]).to.equal(nil) + doCleanup(scope) + end) + + it("doesn't call destructor on creation", function() + local scope = {} + local destructed = {} + local data = Value(scope, {"foo", "bar"}) + local _ = ForValues(scope, data, function(use, value) + return value, "meta" .. value + end, function(value, meta) + destructed[value] = true + destructed[meta] = true + end) + expect(destructed.foo).to.equal(nil) + expect(destructed.metafoo).to.equal(nil) + expect(destructed.bar).to.equal(nil) + expect(destructed.metabar).to.equal(nil) + doCleanup(scope) + end) + + it("calls destructor on update", function() + local scope = {} + local destructed = {} + local data = Value(scope, {"foo", "bar"}) + local _ = ForValues(scope, data, function(use, value) + return value, "meta" .. value + end, function(value, meta) + destructed[value] = true + destructed[meta] = true + end) + data:set({"foo", "baz"}) + expect(destructed.foo).to.equal(nil) + expect(destructed.metafoo).to.equal(nil) + expect(destructed.bar).to.equal(true) + expect(destructed.metabar).to.equal(true) + doCleanup(scope) + end) + + it("calls destructor on destroy", function() + local scope = {} + local destructed = {} + local data = Value(scope, {"foo", "bar"}) + local _ = ForValues(scope, data, function(use, value) + return value, "meta" .. value + end, function(value, meta) + destructed[value] = true + destructed[meta] = true + end) + expect(destructed.foo).to.equal(nil) + expect(destructed.metafoo).to.equal(nil) + expect(destructed.bar).to.equal(nil) + expect(destructed.metabar).to.equal(nil) + doCleanup(scope) + expect(destructed.foo).to.equal(true) + expect(destructed.metafoo).to.equal(true) + expect(destructed.bar).to.equal(true) + expect(destructed.metabar).to.equal(true) + end) + + it("doesn't recompute when values are preserved", function() + local scope = {} + local data = Value(scope, {"foo", "bar"}) + local computations = 0 + local forObject = ForValues(scope, data, function(_, value) + computations += 1 + return string.upper(value) + end) + expect(computations).to.equal(2) + data:set({"bar", "foo"}) + expect(computations).to.equal(2) + data:set({"baz", "bar", "foo"}) + expect(computations).to.equal(3) + data:set({"foo", "baz", "bar"}) + expect(computations).to.equal(3) + data:set({"garb"}) + expect(computations).to.equal(4) + doCleanup(scope) + end) end From d8890cb81b8f0c14101fa19322efe88145903a3f Mon Sep 17 00:00:00 2001 From: Elttob Date: Mon, 27 Nov 2023 21:44:46 +0000 Subject: [PATCH 041/287] Arbitrary keys for ForValues --- src/State/For.lua | 8 +++- src/State/ForValues.lua | 4 +- src/Types.lua | 2 +- test/State/ForValues.spec.lua | 72 +++++++++++++++++++++-------------- 4 files changed, 53 insertions(+), 33 deletions(-) diff --git a/src/State/For.lua b/src/State/For.lua index b75c24fc5..fa7f35aa6 100644 --- a/src/State/For.lua +++ b/src/State/For.lua @@ -145,7 +145,11 @@ function class:update(): boolean for processor in newProcessors do local key, value = processor.outputKey, processor.outputValue - key.dependentSet[self], self.dependencySet[key] = true, true + if key == nil then + key = #newOutputTable + 1 + else + key.dependentSet[self], self.dependencySet[key] = true, true + end value.dependentSet[self], self.dependencySet[value] = true, true local keyValue, valueValue = peek(key), peek(value) if keyValue == nil or valueValue == nil then @@ -195,7 +199,7 @@ local function For( {any}, PubTypes.StateObject, PubTypes.StateObject - ) -> (PubTypes.StateObject, PubTypes.StateObject) + ) -> (PubTypes.StateObject?, PubTypes.StateObject) ): Types.For local self = setmetatable({ diff --git a/src/State/ForValues.lua b/src/State/ForValues.lua index 71c1929f6..5f9e1ece1 100644 --- a/src/State/ForValues.lua +++ b/src/State/ForValues.lua @@ -28,8 +28,8 @@ local function ForValues( return For( cleanupTable, inputTable, - function(scope, inputKey, inputValue) - return inputKey, Computed(scope, function(use) + function(scope, _, inputValue) + return nil, Computed(scope, function(use) return processor(use, use(inputValue)) end, destructor) end diff --git a/src/Types.lua b/src/Types.lua index bd259b447..74110b151 100644 --- a/src/Types.lua +++ b/src/Types.lua @@ -61,7 +61,7 @@ export type For = PubTypes.For & { {any}, PubTypes.StateObject, PubTypes.StateObject - ) -> (PubTypes.StateObject, PubTypes.StateObject), + ) -> (PubTypes.StateObject?, PubTypes.StateObject), _inputTable: PubTypes.CanBeState<{[KI]: VI}>, _existingInputTable: {[KI]: VI}?, _existingOutputTable: {[KO]: VO}, diff --git a/test/State/ForValues.spec.lua b/test/State/ForValues.spec.lua index cf4f0d3ff..b38e5af42 100644 --- a/test/State/ForValues.spec.lua +++ b/test/State/ForValues.spec.lua @@ -40,8 +40,8 @@ return function() return value:upper() end) expect(peek(forObject)).to.be.a("table") - expect(peek(forObject)[1]).to.equal("FOO") - expect(peek(forObject)[2]).to.equal("BAR") + expect(table.find(peek(forObject), "FOO")).to.be.ok() + expect(table.find(peek(forObject), "BAR")).to.be.ok() doCleanup(scope) end) @@ -52,8 +52,8 @@ return function() return value:upper() end) expect(peek(forObject)).to.be.a("table") - expect(peek(forObject)[1]).to.equal("FOO") - expect(peek(forObject)[2]).to.equal("BAR") + expect(table.find(peek(forObject), "FOO")).to.be.ok() + expect(table.find(peek(forObject), "BAR")).to.be.ok() doCleanup(scope) end) @@ -63,8 +63,8 @@ return function() local forObject = ForValues(scope, data, function(use, value) return value .. use("baz") end) - expect(peek(forObject)[1]).to.equal("foobaz") - expect(peek(forObject)[2]).to.equal("barbaz") + expect(table.find(peek(forObject), "foobaz")).to.be.ok() + expect(table.find(peek(forObject), "barbaz")).to.be.ok() doCleanup(scope) end) @@ -75,11 +75,11 @@ return function() local forObject = ForValues(scope, data, function(use, value) return value .. use(suffix) end) - expect(peek(forObject)[1]).to.equal("foofirst") - expect(peek(forObject)[2]).to.equal("barfirst") + expect(table.find(peek(forObject), "foofirst")).to.be.ok() + expect(table.find(peek(forObject), "barfirst")).to.be.ok() suffix:set("second") - expect(peek(forObject)[1]).to.equal("foosecond") - expect(peek(forObject)[2]).to.equal("barsecond") + expect(table.find(peek(forObject), "foosecond")).to.be.ok() + expect(table.find(peek(forObject), "barsecond")).to.be.ok() doCleanup(scope) end) @@ -94,17 +94,17 @@ return function() end return value end) - expect(peek(forObject)[1]).to.equal("foo") - expect(peek(forObject)[2]).to.equal("baz") - expect(peek(forObject)[3]).to.equal(nil) + expect(table.find(peek(forObject), "foo")).to.be.ok() + expect(table.find(peek(forObject), "bar")).to.never.be.ok() + expect(table.find(peek(forObject), "baz")).to.be.ok() omitThird:set(true) - expect(peek(forObject)[1]).to.equal("foo") - expect(peek(forObject)[2]).to.equal(nil) - expect(peek(forObject)[3]).to.equal(nil) + expect(table.find(peek(forObject), "foo")).to.be.ok() + expect(table.find(peek(forObject), "bar")).to.never.be.ok() + expect(table.find(peek(forObject), "baz")).to.never.be.ok() omitThird:set(false) - expect(peek(forObject)[1]).to.equal("foo") - expect(peek(forObject)[2]).to.equal("baz") - expect(peek(forObject)[3]).to.equal(nil) + expect(table.find(peek(forObject), "foo")).to.be.ok() + expect(table.find(peek(forObject), "bar")).to.never.be.ok() + expect(table.find(peek(forObject), "baz")).to.be.ok() doCleanup(scope) end) @@ -123,17 +123,17 @@ return function() end return value end) - expect(peek(forObject)[1]).to.equal("foo") - expect(peek(forObject)[2]).to.equal("baz") - expect(peek(forObject)[3]).to.equal(nil) + expect(table.find(peek(forObject), "foo")).to.be.ok() + expect(table.find(peek(forObject), "bar")).to.never.be.ok() + expect(table.find(peek(forObject), "baz")).to.be.ok() omitThird:set(true) - expect(peek(forObject)[1]).to.equal("foo") - expect(peek(forObject)[2]).to.equal(nil) - expect(peek(forObject)[3]).to.equal(nil) + expect(table.find(peek(forObject), "foo")).to.be.ok() + expect(table.find(peek(forObject), "bar")).to.never.be.ok() + expect(table.find(peek(forObject), "baz")).to.never.be.ok() omitThird:set(false) - expect(peek(forObject)[1]).to.equal("foo") - expect(peek(forObject)[2]).to.equal("baz") - expect(peek(forObject)[3]).to.equal(nil) + expect(table.find(peek(forObject), "foo")).to.be.ok() + expect(table.find(peek(forObject), "bar")).to.never.be.ok() + expect(table.find(peek(forObject), "baz")).to.be.ok() doCleanup(scope) end) @@ -212,4 +212,20 @@ return function() expect(computations).to.equal(4) doCleanup(scope) end) + + it("does not reuse values for duplicated items", function() + local scope = {} + local data = Value(scope, {"foo", "foo", "foo"}) + local computations = 0 + local forObject = ForValues(scope, data, function(_, value) + computations += 1 + return string.upper(value) + end) + expect(computations).to.equal(3) + data:set({"foo", "foo", "foo", "foo"}) + expect(computations).to.equal(4) + data:set({"bar", "foo", "foo"}) + expect(computations).to.equal(5) + doCleanup(scope) + end) end From 7fdea3f73cfafdf2e994df87c0fbed7d5419d75e Mon Sep 17 00:00:00 2001 From: Elttob Date: Mon, 27 Nov 2023 21:49:50 +0000 Subject: [PATCH 042/287] ForValues error handling --- src/State/ForValues.lua | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/src/State/ForValues.lua b/src/State/ForValues.lua index 5f9e1ece1..0608e6dab 100644 --- a/src/State/ForValues.lua +++ b/src/State/ForValues.lua @@ -17,6 +17,9 @@ local Types = require(Package.Types) -- State local For = require(Package.State.For) local Computed = require(Package.State.Computed) +-- Logging +local parseError = require(Package.Logging.parseError) +local logErrorNonFatal = require(Package.Logging.logErrorNonFatal) local function ForValues( cleanupTable: {PubTypes.Task}, @@ -30,7 +33,13 @@ local function ForValues( inputTable, function(scope, _, inputValue) return nil, Computed(scope, function(use) - return processor(use, use(inputValue)) + local ok, value, meta = xpcall(processor, parseError, use, use(inputValue)) + if ok then + return value, meta + else + logErrorNonFatal("forProcessorError", parseError) + return nil + end end, destructor) end ) From 5e89dd1193bf3bb5a042e0636d36c12c5c55be58 Mon Sep 17 00:00:00 2001 From: Elttob Date: Tue, 28 Nov 2023 10:50:15 +0000 Subject: [PATCH 043/287] Computed inner scopes --- src/Animation/Spring.lua | 4 +-- src/Animation/Tween.lua | 4 +-- src/Memory/deriveScope.lua | 16 +++++++++ src/Memory/scoped.lua | 2 +- src/PubTypes.lua | 57 +++++++++++++++++++++++++++++ src/State/Computed.lua | 52 ++++++++++++--------------- src/State/For.lua | 4 +-- src/State/ForKeys.lua | 4 +-- src/State/ForPairs.lua | 4 +-- src/State/ForValues.lua | 4 +-- src/State/Observer.lua | 4 +-- src/State/Value.lua | 4 +-- src/Types.lua | 7 ++-- src/init.lua | 30 ---------------- test/State/Computed.spec.lua | 69 ++++++++++++++++++++---------------- 15 files changed, 154 insertions(+), 111 deletions(-) create mode 100644 src/Memory/deriveScope.lua diff --git a/src/Animation/Spring.lua b/src/Animation/Spring.lua index d707f9d12..4db7ee4ca 100644 --- a/src/Animation/Spring.lua +++ b/src/Animation/Spring.lua @@ -167,7 +167,7 @@ function class:destroy() end local function Spring( - cleanupTable: {PubTypes.Task}, + scope: {PubTypes.Task}, goalState: PubTypes.Value, speed: PubTypes.CanBeState?, damping: PubTypes.CanBeState? @@ -214,7 +214,7 @@ local function Spring( -- add this object to the goal state's dependent set goalState.dependentSet[self] = true self:update() - table.insert(self, cleanupTable) + table.insert(self, scope) return self end diff --git a/src/Animation/Tween.lua b/src/Animation/Tween.lua index 19d59a33d..39c34826a 100644 --- a/src/Animation/Tween.lua +++ b/src/Animation/Tween.lua @@ -78,7 +78,7 @@ function class:destroy() end local function Tween( - cleanupTable: {PubTypes.Task}, + scope: {PubTypes.Task}, goalState: PubTypes.StateObject, tweenInfo: PubTypes.CanBeState? ): Types.Tween @@ -126,7 +126,7 @@ local function Tween( -- add this object to the goal state's dependent set goalState.dependentSet[self] = true - table.insert(cleanupTable, self) + table.insert(scope, self) return self end diff --git a/src/Memory/deriveScope.lua b/src/Memory/deriveScope.lua new file mode 100644 index 000000000..4c6aa8f0a --- /dev/null +++ b/src/Memory/deriveScope.lua @@ -0,0 +1,16 @@ +--!strict + +--[[ + Creates an empty scope with the same metatables as the original scope. Used + for preserving access to constructors when creating inner scopes. +]] +local Package = script.Parent.Parent +local PubTypes = require(Package.PubTypes) + +-- This return type is technically a lie, but it's required for useful type +-- checking behaviour. +local function deriveScope(scope: S & {PubTypes.Task}): S + return setmetatable({}, getmetatable(scope)) :: any +end + +return deriveScope \ No newline at end of file diff --git a/src/Memory/scoped.lua b/src/Memory/scoped.lua index a0a3a3b4c..635ca12f6 100644 --- a/src/Memory/scoped.lua +++ b/src/Memory/scoped.lua @@ -9,7 +9,7 @@ local PubTypes = require(Package.PubTypes) -- This return type is technically a lie, but it's required for useful type -- checking behaviour. -local function scoped(constructors: T): {PubTypes.Task} & T +local function scoped(constructors: T): PubTypes.Scope return setmetatable({}, {__index = constructors}) :: any end diff --git a/src/PubTypes.lua b/src/PubTypes.lua index 7450dbf2c..c0661617b 100644 --- a/src/PubTypes.lua +++ b/src/PubTypes.lua @@ -46,6 +46,9 @@ export type Task = {Destroy: (any) -> ()} | {Task} +-- A scope of tasks to clean up. +export type Scope = {Task} & Constructors + -- Script-readable version information. export type Version = { major: number, @@ -89,18 +92,42 @@ export type Value = StateObject & { set: (Value, newValue: any, force: boolean?) -> (), destroy: () -> () } +type ValueConstructor = ( + scope: Scope, + initialValue: T +) -> Value -- A state object whose value is derived from other objects using a callback. export type Computed = StateObject & Dependent & { kind: "Computed", destroy: () -> () } +type ComputedConstructor = ( + scope: Scope, + callback: (Scope, Use) -> T +) -> Computed -- A state object which maps over keys and/or values in another table. export type For = StateObject<{[KO]: VO}> & Dependent & { kind: "For", destroy: () -> () } +type ForPairsConstructor = ( + scope: Scope, + inputTable: CanBeState<{[KI]: VI}>, + processor: (Scope, Use, KI, VI) -> (KO, VO) +) -> For +type ForKeysConstructor = ( + inputTable: CanBeState<{[KI]: V}>, + processor: (Use, KI) -> (KO, M?), + destructor: (KO, M?) -> ()? +) -> For +type ForValuesConstructor = ( + inputTable: CanBeState<{[K]: VI}>, + processor: (Use, VI) -> (VO, M?), + destructor: (VO, M?) -> ()? +) -> For + -- A state object which follows another state object using tweens. export type Tween = StateObject & Dependent & { @@ -142,4 +169,34 @@ export type Children = Instance | StateObject | {[any]: Children} -- A table that defines an instance's properties, handlers and children. export type PropertyTable = {[string | SpecialKey]: any} +export type Fusion = { + version: Version, + + New: (className: string) -> ((propertyTable: PropertyTable) -> Instance), + Hydrate: (target: Instance) -> ((propertyTable: PropertyTable) -> Instance), + Ref: SpecialKey, + Cleanup: SpecialKey, + Children: SpecialKey, + Out: (propertyName: string) -> SpecialKey, + OnEvent: (eventName: string) -> SpecialKey, + OnChange: (propertyName: string) -> SpecialKey, + Attribute: (attributeName: string) -> SpecialKey, + AttributeChange: (attributeName: string) -> SpecialKey, + AttributeOut: (attributeName: string) -> SpecialKey, + + Value: ValueConstructor, + Computed: ComputedConstructor, + ForPairs: (inputTable: CanBeState<{[KI]: VI}>, processor: (Use, KI, VI) -> (KO, VO, M?), destructor: (KO, VO, M?) -> ()?) -> For, + ForKeys: (inputTable: CanBeState<{[KI]: V}>, processor: (Use, KI) -> (KO, M?), destructor: (KO, M?) -> ()?) -> For, + ForValues: (inputTable: CanBeState<{[K]: VI}>, processor: (Use, VI) -> (VO, M?), destructor: (VO, M?) -> ()?) -> For, + Observer: (watchedState: StateObject) -> Observer, + + Tween: (goalState: StateObject, tweenInfo: TweenInfo?) -> Tween, + Spring: (goalState: StateObject, speed: CanBeState?, damping: CanBeState?) -> Spring, + + doCleanup: (...any) -> (), + doNothing: (...any) -> (), + peek: Use +} + return nil \ No newline at end of file diff --git a/src/State/Computed.lua b/src/State/Computed.lua index 272816007..d33ac252b 100644 --- a/src/State/Computed.lua +++ b/src/State/Computed.lua @@ -15,9 +15,11 @@ local logWarn = require(Package.Logging.logWarn) local parseError = require(Package.Logging.parseError) -- Utility local isSimilar = require(Package.Utility.isSimilar) -local needsDestruction = require(Package.Memory.needsDestruction) -- State local makeUseCallback = require(Package.State.makeUseCallback) +-- Memory +local doCleanup = require(Package.Memory.doCleanup) +local deriveScope = require(Package.Memory.deriveScope) local class = {} @@ -25,19 +27,10 @@ local CLASS_METATABLE = {__index = class} --[[ Called when a dependency changes value. - Returns true if this object changed value. -]] -function class:update(): boolean - return self:_recalculate(false) -end - ---[[ Recalculates this Computed's cached value and dependencies. Returns true if it changed, or false if it's identical. ]] -function class:_recalculate( - firstTime: boolean -): boolean +function class:update(): boolean -- remove this object from its dependencies' dependent sets for dependency in pairs(self.dependencySet) do dependency.dependentSet[self] = nil @@ -50,21 +43,18 @@ function class:_recalculate( self._oldDependencySet, self.dependencySet = self.dependencySet, self._oldDependencySet table.clear(self.dependencySet) + local innerScope = deriveScope(self._outerScope) local use = makeUseCallback(self.dependencySet) - local ok, newValue, newMetaValue = xpcall(self._processor, parseError, use) + local ok, newValue = xpcall(self._processor, parseError, innerScope, use) if ok then - if self._destructor == nil and needsDestruction(newValue) then - logWarn("destructorNeededComputed") - end - local oldValue = self._value local similar = isSimilar(oldValue, newValue) - if self._destructor ~= nil and not firstTime then - self._destructor(oldValue, self._meta) + if self._innerScope ~= nil then + doCleanup(self._innerScope) end self._value = newValue - self._meta = newMetaValue + self._innerScope = innerScope -- add this object to the dependencies' dependent sets for dependency in pairs(self.dependencySet) do @@ -77,6 +67,8 @@ function class:_recalculate( -- update process logErrorNonFatal("computedCallbackError", newValue) + doCleanup(innerScope) + -- restore old dependencies, because the new dependencies may be corrupt self._oldDependencySet, self.dependencySet = self.dependencySet, self._oldDependencySet @@ -104,16 +96,16 @@ function class:destroy() for dependency in pairs(self.dependencySet) do dependency.dependentSet[self] = nil end - if self._destructor ~= nil then - self._destructor(self._value, self._meta) + if self._innerScope ~= nil then + doCleanup(self._innerScope) end end -local function Computed( - cleanupTable: {PubTypes.Task}, - processor: () -> (T, M?), - destructor: ((T, M?) -> ())? -): Types.Computed +local function Computed( + scope: PubTypes.Scope, + processor: (PubTypes.Scope, PubTypes.Use) -> T, + destructor: any -- TODO: warn for this +): Types.Computed local self = setmetatable({ type = "State", kind = "Computed", @@ -121,13 +113,13 @@ local function Computed( dependentSet = {}, _oldDependencySet = {}, _processor = processor, - _destructor = destructor, _value = nil, - _meta = nil + _outerScope = scope, + _innerScope = nil }, CLASS_METATABLE) - self:_recalculate(true) - table.insert(cleanupTable, self) + self:update() + table.insert(scope, self) return self end diff --git a/src/State/For.lua b/src/State/For.lua index fa7f35aa6..0e47f377a 100644 --- a/src/State/For.lua +++ b/src/State/For.lua @@ -193,7 +193,7 @@ function class:destroy() end local function For( - cleanupTable: {PubTypes.Task}, + scope: {PubTypes.Task}, inputTable: PubTypes.CanBeState<{ [KI]: VI }>, processor: ( {any}, @@ -221,7 +221,7 @@ local function For( self:update() - table.insert(cleanupTable, self) + table.insert(scope, self) return self end diff --git a/src/State/ForKeys.lua b/src/State/ForKeys.lua index 3ea0988a0..75e6c9fd0 100644 --- a/src/State/ForKeys.lua +++ b/src/State/ForKeys.lua @@ -22,14 +22,14 @@ local parseError = require(Package.Logging.parseError) local logErrorNonFatal = require(Package.Logging.logErrorNonFatal) local function ForKeys( - cleanupTable: {PubTypes.Task}, + scope: {PubTypes.Task}, inputTable: PubTypes.CanBeState<{[KI]: V}>, processor: (PubTypes.Use, KI) -> (KO, M?), destructor: (KO, M?) -> ()? ): Types.For return For( - cleanupTable, + scope, inputTable, function(scope, inputKey, inputValue) return Computed(scope, function(use) diff --git a/src/State/ForPairs.lua b/src/State/ForPairs.lua index 4779837b9..3b87ed1fa 100644 --- a/src/State/ForPairs.lua +++ b/src/State/ForPairs.lua @@ -21,14 +21,14 @@ local Computed = require(Package.State.Computed) local doNothing = require(Package.Memory.doNothing) local function ForPairs( - cleanupTable: {PubTypes.Task}, + scope: {PubTypes.Task}, inputTable: PubTypes.CanBeState<{[KI]: VI}>, processor: (PubTypes.Use, KI, VI) -> (KO, VO, M?), destructor: (KO, VO, M?) -> ()? ): Types.For return For( - cleanupTable, + scope, inputTable, function(scope, inputKey, inputValue) local pair = Computed(scope, function(use) diff --git a/src/State/ForValues.lua b/src/State/ForValues.lua index 0608e6dab..8790fd468 100644 --- a/src/State/ForValues.lua +++ b/src/State/ForValues.lua @@ -22,14 +22,14 @@ local parseError = require(Package.Logging.parseError) local logErrorNonFatal = require(Package.Logging.logErrorNonFatal) local function ForValues( - cleanupTable: {PubTypes.Task}, + scope: {PubTypes.Task}, inputTable: PubTypes.CanBeState<{[K]: VI}>, processor: (PubTypes.Use, VI) -> (VO, M?), destructor: (VO, M?) -> ()? ): Types.For return For( - cleanupTable, + scope, inputTable, function(scope, _, inputValue) return nil, Computed(scope, function(use) diff --git a/src/State/Observer.lua b/src/State/Observer.lua index 9991b49d2..f017de878 100644 --- a/src/State/Observer.lua +++ b/src/State/Observer.lua @@ -59,7 +59,7 @@ function class:destroy() end local function Observer( - cleanupTable: {PubTypes.Task}, + scope: {PubTypes.Task}, watchedState: PubTypes.Value ): Types.Observer local self = setmetatable({ @@ -72,7 +72,7 @@ local function Observer( -- add this object to the watched state's dependent set watchedState.dependentSet[self] = true - table.insert(cleanupTable, self) + table.insert(scope, self) return self end diff --git a/src/State/Value.lua b/src/State/Value.lua index 5b74d4bc9..855e74385 100644 --- a/src/State/Value.lua +++ b/src/State/Value.lua @@ -49,7 +49,7 @@ function class:destroy() end local function Value( - cleanupTable: {PubTypes.Task}, + scope: {PubTypes.Task}, initialValue: T ): Types.State local self = setmetatable({ @@ -59,7 +59,7 @@ local function Value( _value = initialValue }, CLASS_METATABLE) - table.insert(cleanupTable, self) + table.insert(scope, self) return self end diff --git a/src/Types.lua b/src/Types.lua index 74110b151..2ffb93e73 100644 --- a/src/Types.lua +++ b/src/Types.lua @@ -48,11 +48,12 @@ export type State = PubTypes.Value & { } -- A state object whose value is derived from other objects using a callback. -export type Computed = PubTypes.Computed & { +export type Computed = PubTypes.Computed & { _oldDependencySet: Set, - _callback: (PubTypes.Use) -> T, + _processor: (PubTypes.Scope, PubTypes.Use) -> T, _value: T, - _meta: M + _outerScope: PubTypes.Scope, + _innerScope: PubTypes.Scope? } -- A state object which maps over keys and/or values in another table. diff --git a/src/init.lua b/src/init.lua index 5bbd39fe4..47e10718c 100644 --- a/src/init.lua +++ b/src/init.lua @@ -57,34 +57,4 @@ export type Tween = PubTypes.Tween export type Spring = PubTypes.Spring export type Use = PubTypes.Use -type Fusion = { - version: PubTypes.Version, - - New: (className: string) -> ((propertyTable: PubTypes.PropertyTable) -> Instance), - Hydrate: (target: Instance) -> ((propertyTable: PubTypes.PropertyTable) -> Instance), - Ref: PubTypes.SpecialKey, - Cleanup: PubTypes.SpecialKey, - Children: PubTypes.SpecialKey, - Out: (propertyName: string) -> PubTypes.SpecialKey, - OnEvent: (eventName: string) -> PubTypes.SpecialKey, - OnChange: (propertyName: string) -> PubTypes.SpecialKey, - Attribute: (attributeName: string) -> PubTypes.SpecialKey, - AttributeChange: (attributeName: string) -> PubTypes.SpecialKey, - AttributeOut: (attributeName: string) -> PubTypes.SpecialKey, - - Value: (initialValue: T) -> Value, - Computed: (callback: (Use) -> (T, M?), destructor: (T, M?) -> ()?) -> Computed, - ForPairs: (inputTable: CanBeState<{[KI]: VI}>, processor: (Use, KI, VI) -> (KO, VO, M?), destructor: (KO, VO, M?) -> ()?) -> For, - ForKeys: (inputTable: CanBeState<{[KI]: V}>, processor: (Use, KI) -> (KO, M?), destructor: (KO, M?) -> ()?) -> For, - ForValues: (inputTable: CanBeState<{[K]: VI}>, processor: (Use, VI) -> (VO, M?), destructor: (VO, M?) -> ()?) -> For, - Observer: (watchedState: StateObject) -> Observer, - - Tween: (goalState: StateObject, tweenInfo: TweenInfo?) -> Tween, - Spring: (goalState: StateObject, speed: CanBeState?, damping: CanBeState?) -> Spring, - - doCleanup: (...any) -> (), - doNothing: (...any) -> (), - peek: Use -} - return Fusion diff --git a/test/State/Computed.spec.lua b/test/State/Computed.spec.lua index 2ebca263d..40758cfa9 100644 --- a/test/State/Computed.spec.lua +++ b/test/State/Computed.spec.lua @@ -5,6 +5,7 @@ local peek = require(Package.State.peek) local doCleanup = require(Package.Memory.doCleanup) return function() + FOCUS() it("constructs in scopes", function() local scope = {} local computed = Computed(scope, function() @@ -31,7 +32,7 @@ return function() it("computes with constants", function() local scope = {} - local computed = Computed(scope, function(use) + local computed = Computed(scope, function(_, use) return use(5) end) expect(peek(computed)).to.equal(5) @@ -41,7 +42,7 @@ return function() it("computes with state objects", function() local scope = {} local dependency = Value(scope, 5) - local computed = Computed(scope, function(use) + local computed = Computed(scope, function(_, use) return use(dependency) end) expect(peek(computed)).to.equal(5) @@ -53,7 +54,7 @@ return function() it("preserves value on error", function() local scope = {} local dependency = Value(scope, 5) - local computed = Computed(scope, function(use) + local computed = Computed(scope, function(_, use) assert(use(dependency) ~= 13, "This is an intentional error from a unit test") return use(dependency) end) @@ -65,27 +66,29 @@ return function() doCleanup(scope) end) - it("doesn't call destructor on creation", function() + it("doesn't destroy inner scope on creation", function() local scope = {} local destructed = false - local _ = Computed(scope, function() - -- intentionally blank - end, function() - destructed = true + local _ = Computed(scope, function(innerScope) + table.insert(innerScope, function() + destructed = true + end) end) expect(destructed).to.equal(false) doCleanup(scope) end) - it("calls destructor on update", function() + it("destroys inner scope on update", function() local scope = {} local destructed = {} local dependency = Value(scope, 1) - local _ = Computed(scope, function(use) + local _ = Computed(scope, function(innerScope, use) + local value = use(dependency) + table.insert(innerScope, function() + destructed[value] = true + end) return use(dependency) - end, function(value) - destructed[value] = true end) expect(destructed[1]).to.equal(nil) dependency:set(2) @@ -97,36 +100,40 @@ return function() doCleanup(scope) end) - it("calls destructor with metadata", function() + it("destroys errored values and preserves the last non-error value", function() local scope = {} - local destructed = {} + local numDestructions = {} local dependency = Value(scope, 1) - local _ = Computed(scope, function(use) - return 1, use(dependency) - end, function(_, meta) - destructed[meta] = true + local _ = Computed(scope, function(innerScope, use) + local value = use(dependency) + table.insert(innerScope, function() + numDestructions[value] = (numDestructions[value] or 0) + 1 + end) + assert(value ~= 2, "This is an intentional error from a unit test") + return use(dependency) end) - expect(destructed[1]).to.equal(nil) + expect(numDestructions[1]).to.equal(nil) dependency:set(2) - expect(destructed[1]).to.equal(true) - expect(destructed[2]).to.equal(nil) + expect(numDestructions[1]).to.equal(nil) + expect(numDestructions[2]).to.equal(1) dependency:set(3) - expect(destructed[2]).to.equal(true) + expect(numDestructions[2]).to.equal(1) + expect(numDestructions[3]).to.equal(nil) + dependency:set(4) + expect(numDestructions[3]).to.equal(1) doCleanup(scope) end) - it("calls destructor on destroy", function() + it("destroys inner scope on destroy", function() local scope = {} - local destructed = {} - local dependency = Value(scope, 1) - local _ = Computed(scope, function(use) - return use(dependency) - end, function(value) - destructed[value] = true + local destructed = false + local _ = Computed(scope, function(innerScope, use) + table.insert(innerScope, function() + destructed = true + end) end) - expect(destructed[1]).to.equal(nil) doCleanup(scope) - expect(destructed[1]).to.equal(true) + expect(destructed).to.equal(true) end) end From e05bf5b2b985a5c38587b503a3b59def923e2e21 Mon Sep 17 00:00:00 2001 From: Elttob Date: Tue, 28 Nov 2023 11:06:22 +0000 Subject: [PATCH 044/287] Replace destructors for Computed/For/ForKeys --- src/State/ForKeys.lua | 14 ++--- test/State/For.spec.lua | 12 ++-- test/State/ForKeys.spec.lua | 108 ++++++++++++------------------------ 3 files changed, 46 insertions(+), 88 deletions(-) diff --git a/src/State/ForKeys.lua b/src/State/ForKeys.lua index 75e6c9fd0..29fe1f17c 100644 --- a/src/State/ForKeys.lua +++ b/src/State/ForKeys.lua @@ -19,28 +19,26 @@ local For = require(Package.State.For) local Computed = require(Package.State.Computed) -- Logging local parseError = require(Package.Logging.parseError) -local logErrorNonFatal = require(Package.Logging.logErrorNonFatal) +local logError = require(Package.Logging.logError) local function ForKeys( scope: {PubTypes.Task}, inputTable: PubTypes.CanBeState<{[KI]: V}>, - processor: (PubTypes.Use, KI) -> (KO, M?), - destructor: (KO, M?) -> ()? + processor: (PubTypes.Use, KI) -> (KO, M?) ): Types.For return For( scope, inputTable, function(scope, inputKey, inputValue) - return Computed(scope, function(use) - local ok, key, meta = xpcall(processor, parseError, use, use(inputKey)) + return Computed(scope, function(scope, use) + local ok, key, meta = xpcall(processor, parseError, scope, use, use(inputKey)) if ok then return key, meta else - logErrorNonFatal("forProcessorError", parseError) - return nil + logError("forProcessorError", parseError) end - end, destructor), inputValue + end), inputValue end ) end diff --git a/test/State/For.spec.lua b/test/State/For.spec.lua index 7c864f957..3718aefea 100644 --- a/test/State/For.spec.lua +++ b/test/State/For.spec.lua @@ -42,10 +42,10 @@ return function() numCalls += 1 local k, v = peek(inputKey), peek(inputValue) seen[k] = v - local outputKey = Computed(scope, function(use) + local outputKey = Computed(scope, function(_, use) return string.upper(use(inputKey)) end) - local outputValue = Computed(scope, function(use) + local outputValue = Computed(scope, function(_, use) return use(inputValue) * 10 end) return outputKey, outputValue @@ -66,10 +66,10 @@ return function() local numCalls = 0 local forObject = For(scope, data, function(scope, inputKey, inputValue) numCalls += 1 - local outputKey = Computed(scope, function(use) + local outputKey = Computed(scope, function(_, use) return string.upper(use(inputKey)) end) - local outputValue = Computed(scope, function(use) + local outputValue = Computed(scope, function(_, use) return use(inputValue) * 10 end) return outputKey, outputValue @@ -129,7 +129,7 @@ return function() local data = {first = 1, second = 2, third = 3} local omitThird = Value(scope, false) local forObject1 = For(scope, data, function(scope, inputKey, inputValue) - return inputKey, Computed(scope, function(use) + return inputKey, Computed(scope, function(_, use) if use(inputKey) == "second" then return nil elseif use(inputKey) == "third" and use(omitThird) then @@ -140,7 +140,7 @@ return function() end) end) local forObject2 = For(scope, data, function(scope, inputKey, inputValue) - return Computed(scope, function(use) + return Computed(scope, function(_, use) if use(inputKey) == "second" then return nil elseif use(inputKey) == "third" and use(omitThird) then diff --git a/test/State/ForKeys.spec.lua b/test/State/ForKeys.spec.lua index 9c1d83aaa..cee71e8ff 100644 --- a/test/State/ForKeys.spec.lua +++ b/test/State/ForKeys.spec.lua @@ -34,7 +34,7 @@ return function() it("iterates on constants", function() local scope = {} local data = {foo = 1, bar = 2} - local forObject = ForKeys(scope, data, function(_, key) + local forObject = ForKeys(scope, data, function(_, _, key) return key:upper() end) expect(peek(forObject)).to.be.a("table") @@ -46,7 +46,7 @@ return function() it("iterates on state objects", function() local scope = {} local data = Value(scope, {foo = 1, bar = 2}) - local forObject = ForKeys(scope, data, function(_, key) + local forObject = ForKeys(scope, data, function(_, _, key) return key:upper() end) expect(peek(forObject)).to.be.a("table") @@ -58,7 +58,7 @@ return function() it("computes with constants", function() local scope = {} local data = {foo = 1, bar = 2} - local forObject = ForKeys(scope, data, function(use, key) + local forObject = ForKeys(scope, data, function(_, use, key) return key .. use("baz") end) expect(peek(forObject).foobaz).to.equal(1) @@ -70,7 +70,7 @@ return function() local scope = {} local data = {foo = 1, bar = 2} local suffix = Value(scope, "first") - local forObject = ForKeys(scope, data, function(use, key) + local forObject = ForKeys(scope, data, function(_, use, key) return key .. use(suffix) end) expect(peek(forObject).foofirst).to.equal(1) @@ -83,36 +83,15 @@ return function() doCleanup(scope) end) - it("omits keys that error", function() - local scope = {} - local data = {foo = 1, bar = 2, baz = 3} - local omitThird = Value(scope, false) - local forObject = ForKeys(scope, data, function(use, key) - assert(key ~= "bar", "This is an intentional error from a unit test") - if use(omitThird) then - assert(key ~= "baz", "This is an intentional error from a unit test") - end - return key - end) - expect(peek(forObject).foo).to.equal(1) - expect(peek(forObject).bar).to.equal(nil) - expect(peek(forObject).baz).to.equal(3) - omitThird:set(true) - expect(peek(forObject).foo).to.equal(1) - expect(peek(forObject).bar).to.equal(nil) - expect(peek(forObject).baz).to.equal(nil) - omitThird:set(false) - expect(peek(forObject).foo).to.equal(1) - expect(peek(forObject).bar).to.equal(nil) - expect(peek(forObject).baz).to.equal(3) - doCleanup(scope) + it("destroys and reverts keys that error during processing", function() + error("TODO") end) it("omits keys that return nil", function() local scope = {} local data = {foo = 1, bar = 2, baz = 3} local omitThird = Value(scope, false) - local forObject = ForKeys(scope, data, function(use, key) + local forObject = ForKeys(scope, data, function(_, use, key) if key == "bar" then return nil end @@ -137,79 +116,60 @@ return function() doCleanup(scope) end) - it("doesn't call destructor on creation", function() + it("doesn't destroy inner scope on creation", function() local scope = {} local destructed = {} local data = Value(scope, {foo = 1, bar = 2}) - local _ = ForKeys(scope, data, function(use, key) - return key, "meta" .. key - end, function(key, meta) - destructed[key] = true - destructed[meta] = true + local _ = ForKeys(scope, data, function(innerScope, _, key) + table.insert(innerScope, function() + destructed[key] = true + end) + return key end) expect(destructed.foo).to.equal(nil) - expect(destructed.metafoo).to.equal(nil) expect(destructed.bar).to.equal(nil) - expect(destructed.metabar).to.equal(nil) + data:set({foo = 1, bar = 2, baz = 3}) + expect(destructed.foo).to.equal(nil) + expect(destructed.bar).to.equal(nil) + expect(destructed.baz).to.equal(nil) doCleanup(scope) end) - it("calls destructor on update", function() + it("destroys inner scope on update", function() local scope = {} local destructed = {} local data = Value(scope, {foo = 1, bar = 2}) - local _ = ForKeys(scope, data, function(use, key) - return key, "meta" .. key - end, function(key, meta) - destructed[key] = true - destructed[meta] = true + local _ = ForKeys(scope, data, function(innerScope, _, key) + table.insert(innerScope, function() + destructed[key] = true + end) + return key end) - data:set({foo = 100, baz = 3}) expect(destructed.foo).to.equal(nil) - expect(destructed.metafoo).to.equal(nil) + expect(destructed.bar).to.equal(nil) + data:set({baz = 3}) + expect(destructed.foo).to.equal(true) expect(destructed.bar).to.equal(true) - expect(destructed.metabar).to.equal(true) + expect(destructed.baz).to.equal(nil) doCleanup(scope) end) - it("calls destructor on destroy", function() + it("destroys inner scope on destroy", function() local scope = {} local destructed = {} local data = Value(scope, {foo = 1, bar = 2}) - local _ = ForKeys(scope, data, function(use, key) - return key, "meta" .. key - end, function(key, meta) - destructed[key] = true - destructed[meta] = true + local _ = ForKeys(scope, data, function(innerScope, _, key) + table.insert(innerScope, function() + destructed[key] = true + end) + return key end) expect(destructed.foo).to.equal(nil) - expect(destructed.metafoo).to.equal(nil) expect(destructed.bar).to.equal(nil) - expect(destructed.metabar).to.equal(nil) doCleanup(scope) expect(destructed.foo).to.equal(true) - expect(destructed.metafoo).to.equal(true) expect(destructed.bar).to.equal(true) - expect(destructed.metabar).to.equal(true) end) - it("doesn't recompute when keys are preserved", function() - local scope = {} - local data = Value(scope, {foo = 1, bar = 2}) - local computations = 0 - local forObject = ForKeys(scope, data, function(_, key) - computations += 1 - return string.upper(key) - end) - expect(computations).to.equal(2) - data:set({foo = 3, bar = 4}) - expect(computations).to.equal(2) - data:set({foo = 3, bar = 4, baz = 5}) - expect(computations).to.equal(3) - data:set({foo = 3, bar = 4, baz = 5}) - expect(computations).to.equal(3) - data:set({garb = 6}) - expect(computations).to.equal(4) - doCleanup(scope) - end) + end From 258a3cd246ff8d07eab924da5e0b28e2e8f45d0e Mon Sep 17 00:00:00 2001 From: Elttob Date: Tue, 28 Nov 2023 11:23:52 +0000 Subject: [PATCH 045/287] Update public types --- src/PubTypes.lua | 88 +++++++++++++++++++++++++++++++----------------- src/init.lua | 62 ++++++++++++++++++++-------------- 2 files changed, 93 insertions(+), 57 deletions(-) diff --git a/src/PubTypes.lua b/src/PubTypes.lua index 86690add3..33c412cb4 100644 --- a/src/PubTypes.lua +++ b/src/PubTypes.lua @@ -93,8 +93,8 @@ export type Value = StateObject & { set: (Value, newValue: any, force: boolean?) -> (), destroy: () -> () } -type ValueConstructor = ( - scope: Scope, +type ValueConstructor = ( + scope: Scope, initialValue: T ) -> Value @@ -118,23 +118,38 @@ type ForPairsConstructor = ( inputTable: CanBeState<{[KI]: VI}>, processor: (Scope, Use, KI, VI) -> (KO, VO) ) -> For -type ForKeysConstructor = ( +type ForKeysConstructor = ( + scope: Scope, inputTable: CanBeState<{[KI]: V}>, - processor: (Use, KI) -> (KO, M?), - destructor: (KO, M?) -> ()? + processor: (Scope, Use, KI) -> (KO, M?) ) -> For -type ForValuesConstructor = ( +type ForValuesConstructor = ( + scope: Scope, inputTable: CanBeState<{[K]: VI}>, - processor: (Use, VI) -> (VO, M?), - destructor: (VO, M?) -> ()? + processor: (Scope, Use, VI) -> (VO, M?) ) -> For +-- An object which can listen for updates on another state object. +export type Observer = Dependent & { + kind: "Observer", + onChange: (Observer, callback: () -> ()) -> (() -> ()), + destroy: () -> () +} +type ObserverConstructor = ( + scope: Scope, + watchedState: StateObject +) -> Observer -- A state object which follows another state object using tweens. export type Tween = StateObject & Dependent & { kind: "Tween", destroy: () -> () } +type TweenConstructor = ( + scope: Scope, + goalState: StateObject, + tweenInfo: TweenInfo? +) -> Tween -- A state object which follows another state object using spring simulation. export type Spring = StateObject & Dependent & { @@ -144,13 +159,12 @@ export type Spring = StateObject & Dependent & { addVelocity: (Spring, deltaVelocity: Animatable) -> (), destroy: () -> () } - --- An object which can listen for updates on another state object. -export type Observer = Dependent & { - kind: "Observer", - onChange: (Observer, callback: () -> ()) -> (() -> ()), - destroy: () -> () -} +type SpringConstructor = ( + scope: Scope, + goalState: StateObject, + speed: CanBeState?, + damping: CanBeState? +) -> Spring --[[ Instance related types @@ -170,11 +184,36 @@ export type Children = Instance | StateObject | {[any]: Children} -- A table that defines an instance's properties, handlers and children. export type PropertyTable = {[string | SpecialKey]: any} +type NewConstructor = ( + scope: Scope, + className: string +) -> (propertyTable: PropertyTable) -> Instance + +type HydrateConstructor = ( + scope: Scope, + target: Instance +) -> (propertyTable: PropertyTable) -> Instance + export type Fusion = { version: Version, - New: (className: string) -> ((propertyTable: PropertyTable) -> Instance), - Hydrate: (target: Instance) -> ((propertyTable: PropertyTable) -> Instance), + doCleanup: (...any) -> (), + doNothing: (...any) -> (), + peek: Use, + + Value: ValueConstructor, + Computed: ComputedConstructor, + ForPairs: ForPairsConstructor, + ForKeys: ForKeysConstructor, + ForValues: ForValuesConstructor, + Observer: ObserverConstructor, + + Tween: TweenConstructor, + Spring: SpringConstructor, + + New: NewConstructor, + Hydrate: HydrateConstructor, + Ref: SpecialKey, Cleanup: SpecialKey, Children: SpecialKey, @@ -184,20 +223,7 @@ export type Fusion = { Attribute: (attributeName: string) -> SpecialKey, AttributeChange: (attributeName: string) -> SpecialKey, AttributeOut: (attributeName: string) -> SpecialKey, - - Value: ValueConstructor, - Computed: ComputedConstructor, - ForPairs: (inputTable: CanBeState<{[KI]: VI}>, processor: (Use, KI, VI) -> (KO, VO, M?), destructor: (KO, VO, M?) -> ()?) -> For, - ForKeys: (inputTable: CanBeState<{[KI]: V}>, processor: (Use, KI) -> (KO, M?), destructor: (KO, M?) -> ()?) -> For, - ForValues: (inputTable: CanBeState<{[K]: VI}>, processor: (Use, VI) -> (VO, M?), destructor: (VO, M?) -> ()?) -> For, - Observer: (watchedState: StateObject) -> Observer, - - Tween: (goalState: StateObject, tweenInfo: TweenInfo?) -> Tween, - Spring: (goalState: StateObject, speed: CanBeState?, damping: CanBeState?) -> Spring, - - doCleanup: (...any) -> (), - doNothing: (...any) -> (), - peek: Use + } return nil diff --git a/src/init.lua b/src/init.lua index 47e10718c..700594deb 100644 --- a/src/init.lua +++ b/src/init.lua @@ -8,6 +8,26 @@ local PubTypes = require(script.PubTypes) local External = require(script.External) local restrictRead = require(script.Utility.restrictRead) +export type Symbol = PubTypes.Symbol +export type Animatable = PubTypes.Animatable +export type Task = PubTypes.Task +export type Scope = PubTypes.Scope +export type Version = PubTypes.Version +export type Dependency = PubTypes.Dependency +export type Dependent = PubTypes.Dependent +export type StateObject = PubTypes.StateObject +export type CanBeState = PubTypes.CanBeState +export type Use = PubTypes.Use +export type Value = PubTypes.Value +export type Computed = PubTypes.Computed +export type For = PubTypes.For +export type Observer = PubTypes.Observer +export type Tween = PubTypes.Tween +export type Spring = PubTypes.Spring +export type SpecialKey = PubTypes.SpecialKey +export type Children = PubTypes.Children +export type PropertyTable = PubTypes.PropertyTable + -- Down the line, this will be conditional based on whether Fusion is being -- compiled for Roblox. do @@ -18,17 +38,10 @@ end local Fusion = restrictRead("Fusion", { version = {major = 0, minor = 3, isRelease = false}, - New = require(script.Instances.New), - Hydrate = require(script.Instances.Hydrate), - Ref = require(script.Instances.Ref), - Out = require(script.Instances.Out), - Cleanup = require(script.Instances.Cleanup), - Children = require(script.Instances.Children), - OnEvent = require(script.Instances.OnEvent), - OnChange = require(script.Instances.OnChange), - Attribute = require(script.Instances.Attribute), - AttributeChange = require(script.Instances.AttributeChange), - AttributeOut = require(script.Instances.AttributeOut), + cleanup = require(script.Memory.legacyCleanup), + doCleanup = require(script.Memory.doCleanup), + doNothing = require(script.Memory.doNothing), + peek = require(script.State.peek), Value = require(script.State.Value), Computed = require(script.State.Computed), @@ -40,21 +53,18 @@ local Fusion = restrictRead("Fusion", { Tween = require(script.Animation.Tween), Spring = require(script.Animation.Spring), - cleanup = require(script.Memory.legacyCleanup), - doCleanup = require(script.Memory.doCleanup), - doNothing = require(script.Memory.doNothing), - peek = require(script.State.peek) -}) :: Fusion + New = require(script.Instances.New), + Hydrate = require(script.Instances.Hydrate), -export type StateObject = PubTypes.StateObject -export type CanBeState = PubTypes.CanBeState -export type Symbol = PubTypes.Symbol -export type Value = PubTypes.Value -export type Computed = PubTypes.Computed -export type For = PubTypes.For -export type Observer = PubTypes.Observer -export type Tween = PubTypes.Tween -export type Spring = PubTypes.Spring -export type Use = PubTypes.Use + Ref = require(script.Instances.Ref), + Out = require(script.Instances.Out), + Cleanup = require(script.Instances.Cleanup), + Children = require(script.Instances.Children), + OnEvent = require(script.Instances.OnEvent), + OnChange = require(script.Instances.OnChange), + Attribute = require(script.Instances.Attribute), + AttributeChange = require(script.Instances.AttributeChange), + AttributeOut = require(script.Instances.AttributeOut) +}) :: PubTypes.Fusion return Fusion From ce10e0e494c4e9debed1f928ce1de6a3e954171c Mon Sep 17 00:00:00 2001 From: Elttob Date: Tue, 28 Nov 2023 11:28:09 +0000 Subject: [PATCH 046/287] Expose scoped() and deriveScope() --- src/Memory/deriveScope.lua | 2 +- src/PubTypes.lua | 6 ++++-- src/init.lua | 4 +++- 3 files changed, 8 insertions(+), 4 deletions(-) diff --git a/src/Memory/deriveScope.lua b/src/Memory/deriveScope.lua index 4c6aa8f0a..3f2c7992f 100644 --- a/src/Memory/deriveScope.lua +++ b/src/Memory/deriveScope.lua @@ -9,7 +9,7 @@ local PubTypes = require(Package.PubTypes) -- This return type is technically a lie, but it's required for useful type -- checking behaviour. -local function deriveScope(scope: S & {PubTypes.Task}): S +local function deriveScope(scope: PubTypes.Scope): PubTypes.Scope return setmetatable({}, getmetatable(scope)) :: any end diff --git a/src/PubTypes.lua b/src/PubTypes.lua index 33c412cb4..158de8354 100644 --- a/src/PubTypes.lua +++ b/src/PubTypes.lua @@ -199,15 +199,17 @@ export type Fusion = { doCleanup: (...any) -> (), doNothing: (...any) -> (), - peek: Use, + scoped: (constructors: T) -> Scope, + deriveScope: (scope: Scope) -> Scope, + peek: Use, Value: ValueConstructor, Computed: ComputedConstructor, ForPairs: ForPairsConstructor, ForKeys: ForKeysConstructor, ForValues: ForValuesConstructor, Observer: ObserverConstructor, - + Tween: TweenConstructor, Spring: SpringConstructor, diff --git a/src/init.lua b/src/init.lua index 700594deb..94449fc6e 100644 --- a/src/init.lua +++ b/src/init.lua @@ -41,8 +41,10 @@ local Fusion = restrictRead("Fusion", { cleanup = require(script.Memory.legacyCleanup), doCleanup = require(script.Memory.doCleanup), doNothing = require(script.Memory.doNothing), + scoped = require(script.Memory.scoped), + deriveScope = require(script.Memory.deriveScope), + peek = require(script.State.peek), - Value = require(script.State.Value), Computed = require(script.State.Computed), ForPairs = require(script.State.ForPairs), From 2d956e319e3a9b3e9a96d598a65b5c58ab663ce2 Mon Sep 17 00:00:00 2001 From: Elttob Date: Tue, 28 Nov 2023 11:31:14 +0000 Subject: [PATCH 047/287] Update export unit test --- test/State/Computed.spec.lua | 1 - test/State/For.spec.lua | 2 -- test/State/ForKeys.spec.lua | 2 -- test/State/ForValues.spec.lua | 2 -- test/init.spec.lua | 36 +++++++++++++++++++---------------- 5 files changed, 20 insertions(+), 23 deletions(-) diff --git a/test/State/Computed.spec.lua b/test/State/Computed.spec.lua index 40758cfa9..8075a9ab0 100644 --- a/test/State/Computed.spec.lua +++ b/test/State/Computed.spec.lua @@ -5,7 +5,6 @@ local peek = require(Package.State.peek) local doCleanup = require(Package.Memory.doCleanup) return function() - FOCUS() it("constructs in scopes", function() local scope = {} local computed = Computed(scope, function() diff --git a/test/State/For.spec.lua b/test/State/For.spec.lua index 3718aefea..4369fb76d 100644 --- a/test/State/For.spec.lua +++ b/test/State/For.spec.lua @@ -6,8 +6,6 @@ local peek = require(Package.State.peek) local doCleanup = require(Package.Memory.doCleanup) return function() - - FOCUS() it("constructs in scopes", function() local scope = {} local forObject = For(scope, {}, function() diff --git a/test/State/ForKeys.spec.lua b/test/State/ForKeys.spec.lua index cee71e8ff..db0a43e21 100644 --- a/test/State/ForKeys.spec.lua +++ b/test/State/ForKeys.spec.lua @@ -5,8 +5,6 @@ local peek = require(Package.State.peek) local doCleanup = require(Package.Memory.doCleanup) return function() - - FOCUS() it("constructs in scopes", function() local scope = {} local forObject = ForKeys(scope, {}, function() diff --git a/test/State/ForValues.spec.lua b/test/State/ForValues.spec.lua index b38e5af42..88bbedc7e 100644 --- a/test/State/ForValues.spec.lua +++ b/test/State/ForValues.spec.lua @@ -7,8 +7,6 @@ local peek = require(Package.State.peek) local doCleanup = require(Package.Memory.doCleanup) return function() - - FOCUS() it("constructs in scopes", function() local scope = {} local forObject = ForValues(scope, {}, function() diff --git a/test/init.spec.lua b/test/init.spec.lua index a4719ae43..3450369f3 100644 --- a/test/init.spec.lua +++ b/test/init.spec.lua @@ -2,37 +2,41 @@ local Package = game:GetService("ReplicatedStorage").Fusion local Fusion = require(Package) return function() - it("should load with the correct public APIs", function() + itFOCUS("should load with the correct public APIs", function() expect(Fusion).to.be.a("table") local api = { version = "table", - New = "function", - Hydrate = "function", - Ref = "table", - Out = "function", - Cleanup = "table", - Children = "table", - OnEvent = "function", - OnChange = "function", - Attribute = "function", - AttributeChange = "function", - AttributeOut = "function", + cleanup = "function", + doCleanup = "function", + doNothing = "function", + scoped = "function", + deriveScope = "function", + peek = "function", Value = "function", Computed = "function", ForPairs = "function", ForKeys = "function", ForValues = "function", Observer = "function", - + Tween = "function", Spring = "function", - doCleanup = "function", - doNothing = "function", - peek = "function" + New = "function", + Hydrate = "function", + + Ref = "table", + Out = "function", + Cleanup = "table", + Children = "table", + OnEvent = "function", + OnChange = "function", + Attribute = "function", + AttributeChange = "function", + AttributeOut = "function" } for apiName, apiType in pairs(api) do From 714819aef9440490c98c6be1a784653f73acabc2 Mon Sep 17 00:00:00 2001 From: Elttob Date: Tue, 28 Nov 2023 13:07:00 +0000 Subject: [PATCH 048/287] ForKeys error handling update --- src/State/ForKeys.lua | 5 +++-- test/State/For.spec.lua | 1 + test/State/ForKeys.spec.lua | 34 ++++++++++++++++++++++++++++++++-- test/init.spec.lua | 2 +- 4 files changed, 37 insertions(+), 5 deletions(-) diff --git a/src/State/ForKeys.lua b/src/State/ForKeys.lua index 29fe1f17c..9a54e2d22 100644 --- a/src/State/ForKeys.lua +++ b/src/State/ForKeys.lua @@ -19,7 +19,7 @@ local For = require(Package.State.For) local Computed = require(Package.State.Computed) -- Logging local parseError = require(Package.Logging.parseError) -local logError = require(Package.Logging.logError) +local logErrorNonFatal = require(Package.Logging.logErrorNonFatal) local function ForKeys( scope: {PubTypes.Task}, @@ -36,7 +36,8 @@ local function ForKeys( if ok then return key, meta else - logError("forProcessorError", parseError) + logErrorNonFatal("forProcessorError", parseError) + return nil end end), inputValue end diff --git a/test/State/For.spec.lua b/test/State/For.spec.lua index 4369fb76d..292f719cc 100644 --- a/test/State/For.spec.lua +++ b/test/State/For.spec.lua @@ -6,6 +6,7 @@ local peek = require(Package.State.peek) local doCleanup = require(Package.Memory.doCleanup) return function() + FOCUS() it("constructs in scopes", function() local scope = {} local forObject = For(scope, {}, function() diff --git a/test/State/ForKeys.spec.lua b/test/State/ForKeys.spec.lua index db0a43e21..74a529f96 100644 --- a/test/State/ForKeys.spec.lua +++ b/test/State/ForKeys.spec.lua @@ -5,6 +5,7 @@ local peek = require(Package.State.peek) local doCleanup = require(Package.Memory.doCleanup) return function() + FOCUS() it("constructs in scopes", function() local scope = {} local forObject = ForKeys(scope, {}, function() @@ -81,8 +82,37 @@ return function() doCleanup(scope) end) - it("destroys and reverts keys that error during processing", function() - error("TODO") + it("destroys and omits keys that error during processing", function() + local scope = {} + local data = {foo = 1, bar = 2, baz = 3} + local suffix = Value(scope, "first") + local forObject = ForKeys(scope, data, function(_, use, key) + if key == "bar" and use(suffix) == "second" then + error("This is an intentional error from a unit test") + end + return key .. use(suffix) + end) + expect(peek(forObject).foofirst).to.equal(1) + expect(peek(forObject).barfirst).to.equal(2) + expect(peek(forObject).bazfirst).to.equal(3) + suffix:set("second") + expect(peek(forObject).foofirst).to.equal(nil) + expect(peek(forObject).barfirst).to.equal(nil) + expect(peek(forObject).bazfirst).to.equal(nil) + expect(peek(forObject).foosecond).to.equal(1) + expect(peek(forObject).barsecond).to.equal(nil) + expect(peek(forObject).bazsecond).to.equal(3) + suffix:set("third") + expect(peek(forObject).foofirst).to.equal(nil) + expect(peek(forObject).barfirst).to.equal(nil) + expect(peek(forObject).bazfirst).to.equal(nil) + expect(peek(forObject).foosecond).to.equal(nil) + expect(peek(forObject).barsecond).to.equal(nil) + expect(peek(forObject).bazsecond).to.equal(nil) + expect(peek(forObject).foothird).to.equal(1) + expect(peek(forObject).barthird).to.equal(2) + expect(peek(forObject).bazthird).to.equal(3) + doCleanup(scope) end) it("omits keys that return nil", function() diff --git a/test/init.spec.lua b/test/init.spec.lua index 3450369f3..7933c6096 100644 --- a/test/init.spec.lua +++ b/test/init.spec.lua @@ -2,7 +2,7 @@ local Package = game:GetService("ReplicatedStorage").Fusion local Fusion = require(Package) return function() - itFOCUS("should load with the correct public APIs", function() + it("should load with the correct public APIs", function() expect(Fusion).to.be.a("table") local api = { From 920dee7e3c3c7b068f36826f3f34f4deca824666 Mon Sep 17 00:00:00 2001 From: Elttob Date: Tue, 28 Nov 2023 13:36:30 +0000 Subject: [PATCH 049/287] Fix destruction behaviour of ForKeys/Values --- src/State/ForKeys.lua | 10 +- src/State/ForValues.lua | 17 +- test/State/ForKeys.spec.lua | 17 +- test/State/ForValues.spec.lua | 300 +++++++++++++++++----------------- 4 files changed, 181 insertions(+), 163 deletions(-) diff --git a/src/State/ForKeys.lua b/src/State/ForKeys.lua index 9a54e2d22..f0b94f42b 100644 --- a/src/State/ForKeys.lua +++ b/src/State/ForKeys.lua @@ -20,11 +20,13 @@ local Computed = require(Package.State.Computed) -- Logging local parseError = require(Package.Logging.parseError) local logErrorNonFatal = require(Package.Logging.logErrorNonFatal) +-- Memory +local doCleanup = require(Package.Memory.doCleanup) -local function ForKeys( - scope: {PubTypes.Task}, +local function ForKeys( + scope: PubTypes.Scope, inputTable: PubTypes.CanBeState<{[KI]: V}>, - processor: (PubTypes.Use, KI) -> (KO, M?) + processor: (PubTypes.Scope, PubTypes.Use, KI) -> KO ): Types.For return For( @@ -37,6 +39,8 @@ local function ForKeys( return key, meta else logErrorNonFatal("forProcessorError", parseError) + doCleanup(scope) + table.clear(scope) return nil end end), inputValue diff --git a/src/State/ForValues.lua b/src/State/ForValues.lua index 8790fd468..95b6ba9fd 100644 --- a/src/State/ForValues.lua +++ b/src/State/ForValues.lua @@ -20,27 +20,30 @@ local Computed = require(Package.State.Computed) -- Logging local parseError = require(Package.Logging.parseError) local logErrorNonFatal = require(Package.Logging.logErrorNonFatal) +-- Memory +local doCleanup = require(Package.Memory.doCleanup) -local function ForValues( - scope: {PubTypes.Task}, +local function ForValues( + scope: PubTypes.Scope, inputTable: PubTypes.CanBeState<{[K]: VI}>, - processor: (PubTypes.Use, VI) -> (VO, M?), - destructor: (VO, M?) -> ()? + processor: (PubTypes.Scope, PubTypes.Use, VI) -> VO ): Types.For return For( scope, inputTable, function(scope, _, inputValue) - return nil, Computed(scope, function(use) - local ok, value, meta = xpcall(processor, parseError, use, use(inputValue)) + return nil, Computed(scope, function(scope, use) + local ok, value, meta = xpcall(processor, parseError, scope, use, use(inputValue)) if ok then return value, meta else logErrorNonFatal("forProcessorError", parseError) + doCleanup(scope) + table.clear(scope) return nil end - end, destructor) + end) end ) end diff --git a/test/State/ForKeys.spec.lua b/test/State/ForKeys.spec.lua index 74a529f96..79d48d3b0 100644 --- a/test/State/ForKeys.spec.lua +++ b/test/State/ForKeys.spec.lua @@ -86,11 +86,16 @@ return function() local scope = {} local data = {foo = 1, bar = 2, baz = 3} local suffix = Value(scope, "first") - local forObject = ForKeys(scope, data, function(_, use, key) + local destroyed = {} + local forObject = ForKeys(scope, data, function(innerScope, use, key) + local value = key .. use(suffix) + table.insert(innerScope, function() + destroyed[value] = true + end) if key == "bar" and use(suffix) == "second" then error("This is an intentional error from a unit test") end - return key .. use(suffix) + return value end) expect(peek(forObject).foofirst).to.equal(1) expect(peek(forObject).barfirst).to.equal(2) @@ -102,6 +107,12 @@ return function() expect(peek(forObject).foosecond).to.equal(1) expect(peek(forObject).barsecond).to.equal(nil) expect(peek(forObject).bazsecond).to.equal(3) + expect(destroyed.foofirst).to.equal(true) + expect(destroyed.barfirst).to.equal(true) + expect(destroyed.bazfirst).to.equal(true) + expect(destroyed.foosecond).to.equal(nil) + expect(destroyed.barsecond).to.equal(true) + expect(destroyed.bazsecond).to.equal(nil) suffix:set("third") expect(peek(forObject).foofirst).to.equal(nil) expect(peek(forObject).barfirst).to.equal(nil) @@ -198,6 +209,4 @@ return function() expect(destructed.foo).to.equal(true) expect(destructed.bar).to.equal(true) end) - - end diff --git a/test/State/ForValues.spec.lua b/test/State/ForValues.spec.lua index 88bbedc7e..e96bf3dae 100644 --- a/test/State/ForValues.spec.lua +++ b/test/State/ForValues.spec.lua @@ -7,6 +7,8 @@ local peek = require(Package.State.peek) local doCleanup = require(Package.Memory.doCleanup) return function() + FOCUS() + it("constructs in scopes", function() local scope = {} local forObject = ForValues(scope, {}, function() @@ -34,7 +36,7 @@ return function() it("iterates on constants", function() local scope = {} local data = {"foo", "bar"} - local forObject = ForValues(scope, data, function(_, value) + local forObject = ForValues(scope, data, function(_, _, value) return value:upper() end) expect(peek(forObject)).to.be.a("table") @@ -46,7 +48,7 @@ return function() it("iterates on state objects", function() local scope = {} local data = Value(scope, {"foo", "bar"}) - local forObject = ForValues(scope, data, function(_, value) + local forObject = ForValues(scope, data, function(_, _, value) return value:upper() end) expect(peek(forObject)).to.be.a("table") @@ -58,7 +60,7 @@ return function() it("computes with constants", function() local scope = {} local data = {"foo", "bar"} - local forObject = ForValues(scope, data, function(use, value) + local forObject = ForValues(scope, data, function(_, use, value) return value .. use("baz") end) expect(table.find(peek(forObject), "foobaz")).to.be.ok() @@ -70,7 +72,7 @@ return function() local scope = {} local data = {"foo", "bar"} local suffix = Value(scope, "first") - local forObject = ForValues(scope, data, function(use, value) + local forObject = ForValues(scope, data, function(_, use, value) return value .. use(suffix) end) expect(table.find(peek(forObject), "foofirst")).to.be.ok() @@ -81,149 +83,149 @@ return function() doCleanup(scope) end) - it("omits values that error", function() - local scope = {} - local data = {"foo", "bar", "baz"} - local omitThird = Value(scope, false) - local forObject = ForValues(scope, data, function(use, value) - assert(value ~= "bar", "This is an intentional error from a unit test") - if use(omitThird) then - assert(value ~= "baz", "This is an intentional error from a unit test") - end - return value - end) - expect(table.find(peek(forObject), "foo")).to.be.ok() - expect(table.find(peek(forObject), "bar")).to.never.be.ok() - expect(table.find(peek(forObject), "baz")).to.be.ok() - omitThird:set(true) - expect(table.find(peek(forObject), "foo")).to.be.ok() - expect(table.find(peek(forObject), "bar")).to.never.be.ok() - expect(table.find(peek(forObject), "baz")).to.never.be.ok() - omitThird:set(false) - expect(table.find(peek(forObject), "foo")).to.be.ok() - expect(table.find(peek(forObject), "bar")).to.never.be.ok() - expect(table.find(peek(forObject), "baz")).to.be.ok() - doCleanup(scope) - end) - - it("omits values that return nil", function() - local scope = {} - local data = {"foo", "bar", "baz"} - local omitThird = Value(scope, false) - local forObject = ForValues(scope, data, function(use, value) - if value == "bar" then - return nil - end - if use(omitThird) then - if value == "baz" then - return nil - end - end - return value - end) - expect(table.find(peek(forObject), "foo")).to.be.ok() - expect(table.find(peek(forObject), "bar")).to.never.be.ok() - expect(table.find(peek(forObject), "baz")).to.be.ok() - omitThird:set(true) - expect(table.find(peek(forObject), "foo")).to.be.ok() - expect(table.find(peek(forObject), "bar")).to.never.be.ok() - expect(table.find(peek(forObject), "baz")).to.never.be.ok() - omitThird:set(false) - expect(table.find(peek(forObject), "foo")).to.be.ok() - expect(table.find(peek(forObject), "bar")).to.never.be.ok() - expect(table.find(peek(forObject), "baz")).to.be.ok() - doCleanup(scope) - end) - - it("doesn't call destructor on creation", function() - local scope = {} - local destructed = {} - local data = Value(scope, {"foo", "bar"}) - local _ = ForValues(scope, data, function(use, value) - return value, "meta" .. value - end, function(value, meta) - destructed[value] = true - destructed[meta] = true - end) - expect(destructed.foo).to.equal(nil) - expect(destructed.metafoo).to.equal(nil) - expect(destructed.bar).to.equal(nil) - expect(destructed.metabar).to.equal(nil) - doCleanup(scope) - end) - - it("calls destructor on update", function() - local scope = {} - local destructed = {} - local data = Value(scope, {"foo", "bar"}) - local _ = ForValues(scope, data, function(use, value) - return value, "meta" .. value - end, function(value, meta) - destructed[value] = true - destructed[meta] = true - end) - data:set({"foo", "baz"}) - expect(destructed.foo).to.equal(nil) - expect(destructed.metafoo).to.equal(nil) - expect(destructed.bar).to.equal(true) - expect(destructed.metabar).to.equal(true) - doCleanup(scope) - end) - - it("calls destructor on destroy", function() - local scope = {} - local destructed = {} - local data = Value(scope, {"foo", "bar"}) - local _ = ForValues(scope, data, function(use, value) - return value, "meta" .. value - end, function(value, meta) - destructed[value] = true - destructed[meta] = true - end) - expect(destructed.foo).to.equal(nil) - expect(destructed.metafoo).to.equal(nil) - expect(destructed.bar).to.equal(nil) - expect(destructed.metabar).to.equal(nil) - doCleanup(scope) - expect(destructed.foo).to.equal(true) - expect(destructed.metafoo).to.equal(true) - expect(destructed.bar).to.equal(true) - expect(destructed.metabar).to.equal(true) - end) - - it("doesn't recompute when values are preserved", function() - local scope = {} - local data = Value(scope, {"foo", "bar"}) - local computations = 0 - local forObject = ForValues(scope, data, function(_, value) - computations += 1 - return string.upper(value) - end) - expect(computations).to.equal(2) - data:set({"bar", "foo"}) - expect(computations).to.equal(2) - data:set({"baz", "bar", "foo"}) - expect(computations).to.equal(3) - data:set({"foo", "baz", "bar"}) - expect(computations).to.equal(3) - data:set({"garb"}) - expect(computations).to.equal(4) - doCleanup(scope) - end) - - it("does not reuse values for duplicated items", function() - local scope = {} - local data = Value(scope, {"foo", "foo", "foo"}) - local computations = 0 - local forObject = ForValues(scope, data, function(_, value) - computations += 1 - return string.upper(value) - end) - expect(computations).to.equal(3) - data:set({"foo", "foo", "foo", "foo"}) - expect(computations).to.equal(4) - data:set({"bar", "foo", "foo"}) - expect(computations).to.equal(5) - doCleanup(scope) - end) +-- it("destroys and omits values that error during processing", function() +-- local scope = {} +-- local data = {"foo", "bar", "baz"} +-- local omitThird = Value(scope, false) +-- local forObject = ForValues(scope, data, function(use, value) +-- assert(value ~= "bar", "This is an intentional error from a unit test") +-- if use(omitThird) then +-- assert(value ~= "baz", "This is an intentional error from a unit test") +-- end +-- return value +-- end) +-- expect(table.find(peek(forObject), "foo")).to.be.ok() +-- expect(table.find(peek(forObject), "bar")).to.never.be.ok() +-- expect(table.find(peek(forObject), "baz")).to.be.ok() +-- omitThird:set(true) +-- expect(table.find(peek(forObject), "foo")).to.be.ok() +-- expect(table.find(peek(forObject), "bar")).to.never.be.ok() +-- expect(table.find(peek(forObject), "baz")).to.never.be.ok() +-- omitThird:set(false) +-- expect(table.find(peek(forObject), "foo")).to.be.ok() +-- expect(table.find(peek(forObject), "bar")).to.never.be.ok() +-- expect(table.find(peek(forObject), "baz")).to.be.ok() +-- doCleanup(scope) +-- end) + +-- it("omits values that return nil", function() +-- local scope = {} +-- local data = {"foo", "bar", "baz"} +-- local omitThird = Value(scope, false) +-- local forObject = ForValues(scope, data, function(use, value) +-- if value == "bar" then +-- return nil +-- end +-- if use(omitThird) then +-- if value == "baz" then +-- return nil +-- end +-- end +-- return value +-- end) +-- expect(table.find(peek(forObject), "foo")).to.be.ok() +-- expect(table.find(peek(forObject), "bar")).to.never.be.ok() +-- expect(table.find(peek(forObject), "baz")).to.be.ok() +-- omitThird:set(true) +-- expect(table.find(peek(forObject), "foo")).to.be.ok() +-- expect(table.find(peek(forObject), "bar")).to.never.be.ok() +-- expect(table.find(peek(forObject), "baz")).to.never.be.ok() +-- omitThird:set(false) +-- expect(table.find(peek(forObject), "foo")).to.be.ok() +-- expect(table.find(peek(forObject), "bar")).to.never.be.ok() +-- expect(table.find(peek(forObject), "baz")).to.be.ok() +-- doCleanup(scope) +-- end) + +-- it("doesn't call destructor on creation", function() +-- local scope = {} +-- local destructed = {} +-- local data = Value(scope, {"foo", "bar"}) +-- local _ = ForValues(scope, data, function(use, value) +-- return value, "meta" .. value +-- end, function(value, meta) +-- destructed[value] = true +-- destructed[meta] = true +-- end) +-- expect(destructed.foo).to.equal(nil) +-- expect(destructed.metafoo).to.equal(nil) +-- expect(destructed.bar).to.equal(nil) +-- expect(destructed.metabar).to.equal(nil) +-- doCleanup(scope) +-- end) + +-- it("calls destructor on update", function() +-- local scope = {} +-- local destructed = {} +-- local data = Value(scope, {"foo", "bar"}) +-- local _ = ForValues(scope, data, function(use, value) +-- return value, "meta" .. value +-- end, function(value, meta) +-- destructed[value] = true +-- destructed[meta] = true +-- end) +-- data:set({"foo", "baz"}) +-- expect(destructed.foo).to.equal(nil) +-- expect(destructed.metafoo).to.equal(nil) +-- expect(destructed.bar).to.equal(true) +-- expect(destructed.metabar).to.equal(true) +-- doCleanup(scope) +-- end) + +-- it("calls destructor on destroy", function() +-- local scope = {} +-- local destructed = {} +-- local data = Value(scope, {"foo", "bar"}) +-- local _ = ForValues(scope, data, function(use, value) +-- return value, "meta" .. value +-- end, function(value, meta) +-- destructed[value] = true +-- destructed[meta] = true +-- end) +-- expect(destructed.foo).to.equal(nil) +-- expect(destructed.metafoo).to.equal(nil) +-- expect(destructed.bar).to.equal(nil) +-- expect(destructed.metabar).to.equal(nil) +-- doCleanup(scope) +-- expect(destructed.foo).to.equal(true) +-- expect(destructed.metafoo).to.equal(true) +-- expect(destructed.bar).to.equal(true) +-- expect(destructed.metabar).to.equal(true) +-- end) + +-- it("doesn't recompute when values are preserved", function() +-- local scope = {} +-- local data = Value(scope, {"foo", "bar"}) +-- local computations = 0 +-- local forObject = ForValues(scope, data, function(_, value) +-- computations += 1 +-- return string.upper(value) +-- end) +-- expect(computations).to.equal(2) +-- data:set({"bar", "foo"}) +-- expect(computations).to.equal(2) +-- data:set({"baz", "bar", "foo"}) +-- expect(computations).to.equal(3) +-- data:set({"foo", "baz", "bar"}) +-- expect(computations).to.equal(3) +-- data:set({"garb"}) +-- expect(computations).to.equal(4) +-- doCleanup(scope) +-- end) + +-- it("does not reuse values for duplicated items", function() +-- local scope = {} +-- local data = Value(scope, {"foo", "foo", "foo"}) +-- local computations = 0 +-- local forObject = ForValues(scope, data, function(_, value) +-- computations += 1 +-- return string.upper(value) +-- end) +-- expect(computations).to.equal(3) +-- data:set({"foo", "foo", "foo", "foo"}) +-- expect(computations).to.equal(4) +-- data:set({"bar", "foo", "foo"}) +-- expect(computations).to.equal(5) +-- doCleanup(scope) +-- end) end From 3db08eff639f6442c11bf3d5519a974f077e731c Mon Sep 17 00:00:00 2001 From: Elttob Date: Tue, 28 Nov 2023 14:11:54 +0000 Subject: [PATCH 050/287] Remove ForKeys/Values meta --- src/State/ForKeys.lua | 4 ++-- src/State/ForValues.lua | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/State/ForKeys.lua b/src/State/ForKeys.lua index f0b94f42b..206e3c5f7 100644 --- a/src/State/ForKeys.lua +++ b/src/State/ForKeys.lua @@ -34,9 +34,9 @@ local function ForKeys( inputTable, function(scope, inputKey, inputValue) return Computed(scope, function(scope, use) - local ok, key, meta = xpcall(processor, parseError, scope, use, use(inputKey)) + local ok, key = xpcall(processor, parseError, scope, use, use(inputKey)) if ok then - return key, meta + return key else logErrorNonFatal("forProcessorError", parseError) doCleanup(scope) diff --git a/src/State/ForValues.lua b/src/State/ForValues.lua index 95b6ba9fd..ffd174319 100644 --- a/src/State/ForValues.lua +++ b/src/State/ForValues.lua @@ -34,9 +34,9 @@ local function ForValues( inputTable, function(scope, _, inputValue) return nil, Computed(scope, function(scope, use) - local ok, value, meta = xpcall(processor, parseError, scope, use, use(inputValue)) + local ok, value = xpcall(processor, parseError, scope, use, use(inputValue)) if ok then - return value, meta + return value else logErrorNonFatal("forProcessorError", parseError) doCleanup(scope) From a5daf4fdb7a8f5696c77c653ae951bcfd656688c Mon Sep 17 00:00:00 2001 From: Elttob Date: Tue, 28 Nov 2023 14:12:08 +0000 Subject: [PATCH 051/287] Start working on ForValues complete unit tests --- test/State/ForKeys.spec.lua | 6 +- test/State/ForValues.spec.lua | 309 ++++++++++++++++++---------------- 2 files changed, 167 insertions(+), 148 deletions(-) diff --git a/test/State/ForKeys.spec.lua b/test/State/ForKeys.spec.lua index 79d48d3b0..023a721d4 100644 --- a/test/State/ForKeys.spec.lua +++ b/test/State/ForKeys.spec.lua @@ -88,14 +88,14 @@ return function() local suffix = Value(scope, "first") local destroyed = {} local forObject = ForKeys(scope, data, function(innerScope, use, key) - local value = key .. use(suffix) + local generated = key .. use(suffix) table.insert(innerScope, function() - destroyed[value] = true + destroyed[generated] = true end) if key == "bar" and use(suffix) == "second" then error("This is an intentional error from a unit test") end - return value + return generated end) expect(peek(forObject).foofirst).to.equal(1) expect(peek(forObject).barfirst).to.equal(2) diff --git a/test/State/ForValues.spec.lua b/test/State/ForValues.spec.lua index e96bf3dae..1f37029ec 100644 --- a/test/State/ForValues.spec.lua +++ b/test/State/ForValues.spec.lua @@ -83,149 +83,168 @@ return function() doCleanup(scope) end) --- it("destroys and omits values that error during processing", function() --- local scope = {} --- local data = {"foo", "bar", "baz"} --- local omitThird = Value(scope, false) --- local forObject = ForValues(scope, data, function(use, value) --- assert(value ~= "bar", "This is an intentional error from a unit test") --- if use(omitThird) then --- assert(value ~= "baz", "This is an intentional error from a unit test") --- end --- return value --- end) --- expect(table.find(peek(forObject), "foo")).to.be.ok() --- expect(table.find(peek(forObject), "bar")).to.never.be.ok() --- expect(table.find(peek(forObject), "baz")).to.be.ok() --- omitThird:set(true) --- expect(table.find(peek(forObject), "foo")).to.be.ok() --- expect(table.find(peek(forObject), "bar")).to.never.be.ok() --- expect(table.find(peek(forObject), "baz")).to.never.be.ok() --- omitThird:set(false) --- expect(table.find(peek(forObject), "foo")).to.be.ok() --- expect(table.find(peek(forObject), "bar")).to.never.be.ok() --- expect(table.find(peek(forObject), "baz")).to.be.ok() --- doCleanup(scope) --- end) - --- it("omits values that return nil", function() --- local scope = {} --- local data = {"foo", "bar", "baz"} --- local omitThird = Value(scope, false) --- local forObject = ForValues(scope, data, function(use, value) --- if value == "bar" then --- return nil --- end --- if use(omitThird) then --- if value == "baz" then --- return nil --- end --- end --- return value --- end) --- expect(table.find(peek(forObject), "foo")).to.be.ok() --- expect(table.find(peek(forObject), "bar")).to.never.be.ok() --- expect(table.find(peek(forObject), "baz")).to.be.ok() --- omitThird:set(true) --- expect(table.find(peek(forObject), "foo")).to.be.ok() --- expect(table.find(peek(forObject), "bar")).to.never.be.ok() --- expect(table.find(peek(forObject), "baz")).to.never.be.ok() --- omitThird:set(false) --- expect(table.find(peek(forObject), "foo")).to.be.ok() --- expect(table.find(peek(forObject), "bar")).to.never.be.ok() --- expect(table.find(peek(forObject), "baz")).to.be.ok() --- doCleanup(scope) --- end) - --- it("doesn't call destructor on creation", function() --- local scope = {} --- local destructed = {} --- local data = Value(scope, {"foo", "bar"}) --- local _ = ForValues(scope, data, function(use, value) --- return value, "meta" .. value --- end, function(value, meta) --- destructed[value] = true --- destructed[meta] = true --- end) --- expect(destructed.foo).to.equal(nil) --- expect(destructed.metafoo).to.equal(nil) --- expect(destructed.bar).to.equal(nil) --- expect(destructed.metabar).to.equal(nil) --- doCleanup(scope) --- end) - --- it("calls destructor on update", function() --- local scope = {} --- local destructed = {} --- local data = Value(scope, {"foo", "bar"}) --- local _ = ForValues(scope, data, function(use, value) --- return value, "meta" .. value --- end, function(value, meta) --- destructed[value] = true --- destructed[meta] = true --- end) --- data:set({"foo", "baz"}) --- expect(destructed.foo).to.equal(nil) --- expect(destructed.metafoo).to.equal(nil) --- expect(destructed.bar).to.equal(true) --- expect(destructed.metabar).to.equal(true) --- doCleanup(scope) --- end) - --- it("calls destructor on destroy", function() --- local scope = {} --- local destructed = {} --- local data = Value(scope, {"foo", "bar"}) --- local _ = ForValues(scope, data, function(use, value) --- return value, "meta" .. value --- end, function(value, meta) --- destructed[value] = true --- destructed[meta] = true --- end) --- expect(destructed.foo).to.equal(nil) --- expect(destructed.metafoo).to.equal(nil) --- expect(destructed.bar).to.equal(nil) --- expect(destructed.metabar).to.equal(nil) --- doCleanup(scope) --- expect(destructed.foo).to.equal(true) --- expect(destructed.metafoo).to.equal(true) --- expect(destructed.bar).to.equal(true) --- expect(destructed.metabar).to.equal(true) --- end) - --- it("doesn't recompute when values are preserved", function() --- local scope = {} --- local data = Value(scope, {"foo", "bar"}) --- local computations = 0 --- local forObject = ForValues(scope, data, function(_, value) --- computations += 1 --- return string.upper(value) --- end) --- expect(computations).to.equal(2) --- data:set({"bar", "foo"}) --- expect(computations).to.equal(2) --- data:set({"baz", "bar", "foo"}) --- expect(computations).to.equal(3) --- data:set({"foo", "baz", "bar"}) --- expect(computations).to.equal(3) --- data:set({"garb"}) --- expect(computations).to.equal(4) --- doCleanup(scope) --- end) - --- it("does not reuse values for duplicated items", function() --- local scope = {} --- local data = Value(scope, {"foo", "foo", "foo"}) --- local computations = 0 --- local forObject = ForValues(scope, data, function(_, value) --- computations += 1 --- return string.upper(value) --- end) --- expect(computations).to.equal(3) --- data:set({"foo", "foo", "foo", "foo"}) --- expect(computations).to.equal(4) --- data:set({"bar", "foo", "foo"}) --- expect(computations).to.equal(5) --- doCleanup(scope) --- end) + it("destroys and omits values that error during processing", function() + local scope = {} + local data = {"foo", "bar", "baz"} + local suffix = Value(scope, "first") + local destroyed = {} + local forObject = ForValues(scope, data, function(innerScope, use, value) + local generated = value .. use(suffix) + table.insert(innerScope, function() + destroyed[generated] = true + end) + if value == "bar" and use(suffix) == "second" then + error("This is an intentional error from a unit test") + end + return generated + end) + expect(table.find(peek(forObject), "foofirst")).to.be.ok() + expect(table.find(peek(forObject), "barfirst")).to.be.ok() + expect(table.find(peek(forObject), "bazfirst")).to.be.ok() + suffix:set("second") + expect(table.find(peek(forObject), "foofirst")).to.never.be.ok() + expect(table.find(peek(forObject), "barfirst")).to.never.be.ok() + expect(table.find(peek(forObject), "bazfirst")).to.never.be.ok() + expect(table.find(peek(forObject), "foosecond")).to.be.ok() + expect(table.find(peek(forObject), "barsecond")).to.never.be.ok() + expect(table.find(peek(forObject), "bazsecond")).to.be.ok() + expect(destroyed.foofirst).to.equal(true) + expect(destroyed.barfirst).to.equal(true) + expect(destroyed.bazfirst).to.equal(true) + expect(destroyed.foosecond).to.equal(nil) + expect(destroyed.barsecond).to.equal(true) + expect(destroyed.bazsecond).to.equal(nil) + suffix:set("third") + expect(table.find(peek(forObject), "foofirst")).to.never.be.ok() + expect(table.find(peek(forObject), "barfirst")).to.never.be.ok() + expect(table.find(peek(forObject), "bazfirst")).to.never.be.ok() + expect(table.find(peek(forObject), "foosecond")).to.never.be.ok() + expect(table.find(peek(forObject), "barsecond")).to.never.be.ok() + expect(table.find(peek(forObject), "bazsecond")).to.never.be.ok() + expect(table.find(peek(forObject), "foothird")).to.be.ok() + expect(table.find(peek(forObject), "barthird")).to.be.ok() + expect(table.find(peek(forObject), "bazthird")).to.be.ok() + doCleanup(scope) + end) + + it("omits values that return nil", function() + local scope = {} + local data = {"foo", "bar", "baz"} + local omitThird = Value(scope, false) + local forObject = ForValues(scope, data, function(use, value) + if value == "bar" then + return nil + end + if use(omitThird) then + if value == "baz" then + return nil + end + end + return value + end) + expect(table.find(peek(forObject), "foo")).to.be.ok() + expect(table.find(peek(forObject), "bar")).to.never.be.ok() + expect(table.find(peek(forObject), "baz")).to.be.ok() + omitThird:set(true) + expect(table.find(peek(forObject), "foo")).to.be.ok() + expect(table.find(peek(forObject), "bar")).to.never.be.ok() + expect(table.find(peek(forObject), "baz")).to.never.be.ok() + omitThird:set(false) + expect(table.find(peek(forObject), "foo")).to.be.ok() + expect(table.find(peek(forObject), "bar")).to.never.be.ok() + expect(table.find(peek(forObject), "baz")).to.be.ok() + doCleanup(scope) + end) + + it("doesn't call destructor on creation", function() + local scope = {} + local destructed = {} + local data = Value(scope, {"foo", "bar"}) + local _ = ForValues(scope, data, function(use, value) + return value, "meta" .. value + end, function(value, meta) + destructed[value] = true + destructed[meta] = true + end) + expect(destructed.foo).to.equal(nil) + expect(destructed.metafoo).to.equal(nil) + expect(destructed.bar).to.equal(nil) + expect(destructed.metabar).to.equal(nil) + doCleanup(scope) + end) + + it("calls destructor on update", function() + local scope = {} + local destructed = {} + local data = Value(scope, {"foo", "bar"}) + local _ = ForValues(scope, data, function(use, value) + return value, "meta" .. value + end, function(value, meta) + destructed[value] = true + destructed[meta] = true + end) + data:set({"foo", "baz"}) + expect(destructed.foo).to.equal(nil) + expect(destructed.metafoo).to.equal(nil) + expect(destructed.bar).to.equal(true) + expect(destructed.metabar).to.equal(true) + doCleanup(scope) + end) + + it("calls destructor on destroy", function() + local scope = {} + local destructed = {} + local data = Value(scope, {"foo", "bar"}) + local _ = ForValues(scope, data, function(use, value) + return value, "meta" .. value + end, function(value, meta) + destructed[value] = true + destructed[meta] = true + end) + expect(destructed.foo).to.equal(nil) + expect(destructed.metafoo).to.equal(nil) + expect(destructed.bar).to.equal(nil) + expect(destructed.metabar).to.equal(nil) + doCleanup(scope) + expect(destructed.foo).to.equal(true) + expect(destructed.metafoo).to.equal(true) + expect(destructed.bar).to.equal(true) + expect(destructed.metabar).to.equal(true) + end) + + it("doesn't recompute when values are preserved", function() + local scope = {} + local data = Value(scope, {"foo", "bar"}) + local computations = 0 + local forObject = ForValues(scope, data, function(_, value) + computations += 1 + return string.upper(value) + end) + expect(computations).to.equal(2) + data:set({"bar", "foo"}) + expect(computations).to.equal(2) + data:set({"baz", "bar", "foo"}) + expect(computations).to.equal(3) + data:set({"foo", "baz", "bar"}) + expect(computations).to.equal(3) + data:set({"garb"}) + expect(computations).to.equal(4) + doCleanup(scope) + end) + + it("does not reuse values for duplicated items", function() + local scope = {} + local data = Value(scope, {"foo", "foo", "foo"}) + local computations = 0 + local forObject = ForValues(scope, data, function(_, value) + computations += 1 + return string.upper(value) + end) + expect(computations).to.equal(3) + data:set({"foo", "foo", "foo", "foo"}) + expect(computations).to.equal(4) + data:set({"bar", "foo", "foo"}) + expect(computations).to.equal(5) + doCleanup(scope) + end) end From d35b5e52cff80b4540c59cc50c8855521dbc2c51 Mon Sep 17 00:00:00 2001 From: Elttob Date: Tue, 28 Nov 2023 14:15:23 +0000 Subject: [PATCH 052/287] Update ForPairs impl --- src/State/ForPairs.lua | 35 ++++++++++++++++++++--------------- 1 file changed, 20 insertions(+), 15 deletions(-) diff --git a/src/State/ForPairs.lua b/src/State/ForPairs.lua index 3b87ed1fa..dad13bd3f 100644 --- a/src/State/ForPairs.lua +++ b/src/State/ForPairs.lua @@ -17,33 +17,38 @@ local Types = require(Package.Types) -- State local For = require(Package.State.For) local Computed = require(Package.State.Computed) +-- Logging +local parseError = require(Package.Logging.parseError) +local logErrorNonFatal = require(Package.Logging.logErrorNonFatal) -- Memory -local doNothing = require(Package.Memory.doNothing) +local doCleanup = require(Package.Memory.doCleanup) -local function ForPairs( - scope: {PubTypes.Task}, +local function ForPairs( + scope: PubTypes.Scope, inputTable: PubTypes.CanBeState<{[KI]: VI}>, - processor: (PubTypes.Use, KI, VI) -> (KO, VO, M?), - destructor: (KO, VO, M?) -> ()? + processor: (PubTypes.Scope, PubTypes.Use, KI, VI) -> (KO, VO) ): Types.For return For( scope, inputTable, function(scope, inputKey, inputValue) - local pair = Computed(scope, function(use) - -- TODO: error checking - local key, value, meta = processor(use, use(inputKey), use(inputValue)) - return {key = key, value = value}, meta - end, function(data, meta) - -- TODO: error checking - destructor(data.key, meta) + local pair = Computed(scope, function(scope, use) + local ok, key, value = xpcall(processor, parseError, scope, use, use(inputKey), use(inputValue)) + if ok then + return {key = key, value = value} + else + logErrorNonFatal("forProcessorError", parseError) + doCleanup(scope) + table.clear(scope) + return nil + end end) - return Computed(function(use) + return Computed(function(_, use) return use(pair).key - end, doNothing), Computed(function(use) + end), Computed(function(_, use) return use(pair).value - end, doNothing) + end) end ) end From 3d47b9476440b94d0ee9e97e2df29277e5dfcd20 Mon Sep 17 00:00:00 2001 From: Elttob Date: Tue, 28 Nov 2023 14:15:45 +0000 Subject: [PATCH 053/287] Properly handle errors in ForPairs --- src/State/ForPairs.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/State/ForPairs.lua b/src/State/ForPairs.lua index dad13bd3f..20528d0d0 100644 --- a/src/State/ForPairs.lua +++ b/src/State/ForPairs.lua @@ -41,7 +41,7 @@ local function ForPairs( logErrorNonFatal("forProcessorError", parseError) doCleanup(scope) table.clear(scope) - return nil + return {key = nil, value = nil} end end) return Computed(function(_, use) From 25ffe42c8d37d13a0570cc4ea39ce470052de0ae Mon Sep 17 00:00:00 2001 From: Elttob Date: Tue, 28 Nov 2023 14:39:18 +0000 Subject: [PATCH 054/287] ForValues passes more unit tests --- test/State/ForValues.spec.lua | 59 +++++++++++++++++------------------ 1 file changed, 29 insertions(+), 30 deletions(-) diff --git a/test/State/ForValues.spec.lua b/test/State/ForValues.spec.lua index 1f37029ec..c287fcd93 100644 --- a/test/State/ForValues.spec.lua +++ b/test/State/ForValues.spec.lua @@ -131,7 +131,7 @@ return function() local scope = {} local data = {"foo", "bar", "baz"} local omitThird = Value(scope, false) - local forObject = ForValues(scope, data, function(use, value) + local forObject = ForValues(scope, data, function(_, use, value) if value == "bar" then return nil end @@ -156,67 +156,66 @@ return function() doCleanup(scope) end) - it("doesn't call destructor on creation", function() + it("doesn't destroy inner scope on creation", function() local scope = {} local destructed = {} local data = Value(scope, {"foo", "bar"}) - local _ = ForValues(scope, data, function(use, value) - return value, "meta" .. value - end, function(value, meta) - destructed[value] = true - destructed[meta] = true + local _ = ForValues(scope, data, function(innerScope, _, value) + table.insert(innerScope, function() + destructed[value] = true + end) + return value end) expect(destructed.foo).to.equal(nil) - expect(destructed.metafoo).to.equal(nil) expect(destructed.bar).to.equal(nil) - expect(destructed.metabar).to.equal(nil) + data:set({"foo", "bar", "baz"}) + expect(destructed.foo).to.equal(nil) + expect(destructed.bar).to.equal(nil) + expect(destructed.baz).to.equal(nil) doCleanup(scope) end) - it("calls destructor on update", function() + it("destroys inner scope on update", function() local scope = {} local destructed = {} local data = Value(scope, {"foo", "bar"}) - local _ = ForValues(scope, data, function(use, value) - return value, "meta" .. value - end, function(value, meta) - destructed[value] = true - destructed[meta] = true + local _ = ForValues(scope, data, function(innerScope, _, value) + table.insert(innerScope, function() + destructed[value] = true + end) + return value end) - data:set({"foo", "baz"}) expect(destructed.foo).to.equal(nil) - expect(destructed.metafoo).to.equal(nil) + expect(destructed.bar).to.equal(nil) + data:set({"baz"}) + expect(destructed.foo).to.equal(true) expect(destructed.bar).to.equal(true) - expect(destructed.metabar).to.equal(true) + expect(destructed.baz).to.equal(nil) doCleanup(scope) end) - it("calls destructor on destroy", function() + it("destroys inner scope on destroy", function() local scope = {} local destructed = {} local data = Value(scope, {"foo", "bar"}) - local _ = ForValues(scope, data, function(use, value) - return value, "meta" .. value - end, function(value, meta) - destructed[value] = true - destructed[meta] = true + local _ = ForValues(scope, data, function(innerScope, _, value) + table.insert(innerScope, function() + destructed[value] = true + end) + return value end) expect(destructed.foo).to.equal(nil) - expect(destructed.metafoo).to.equal(nil) expect(destructed.bar).to.equal(nil) - expect(destructed.metabar).to.equal(nil) doCleanup(scope) expect(destructed.foo).to.equal(true) - expect(destructed.metafoo).to.equal(true) expect(destructed.bar).to.equal(true) - expect(destructed.metabar).to.equal(true) end) it("doesn't recompute when values are preserved", function() local scope = {} local data = Value(scope, {"foo", "bar"}) local computations = 0 - local forObject = ForValues(scope, data, function(_, value) + local forObject = ForValues(scope, data, function(_, _, value) computations += 1 return string.upper(value) end) @@ -236,7 +235,7 @@ return function() local scope = {} local data = Value(scope, {"foo", "foo", "foo"}) local computations = 0 - local forObject = ForValues(scope, data, function(_, value) + local forObject = ForValues(scope, data, function(_, _, value) computations += 1 return string.upper(value) end) From 1b8e4cd3b1cb562158244648a0ce6fc1f01a38a3 Mon Sep 17 00:00:00 2001 From: Elttob Date: Tue, 28 Nov 2023 14:46:23 +0000 Subject: [PATCH 055/287] Processors roam between keys without recomputation --- src/State/For.lua | 31 ++++++++++++++++++++++++------- 1 file changed, 24 insertions(+), 7 deletions(-) diff --git a/src/State/For.lua b/src/State/For.lua index 0e47f377a..ee8ab68c1 100644 --- a/src/State/For.lua +++ b/src/State/For.lua @@ -28,6 +28,7 @@ local WEAK_KEYS_METATABLE = { __mode = "k" } ]] function class:update(): boolean + local existingInputTable = self._existingInputTable local existingOutputTable = self._existingOutputTable local existingProcessors = self._existingProcessors @@ -57,15 +58,31 @@ function class:update(): boolean -- First, try and reuse processors who match both the key and value of a -- remaining pair. This can be done with no recomputation. + -- NOTE: we also reuse processors with nil output keys here, so long as + -- they match values. This ensures they don't get recomputed either. for tryReuseProcessor in existingProcessors do - local key = peek(tryReuseProcessor.inputKey) - local value = peek(tryReuseProcessor.inputValue) - local remainingValues = remainingPairs[key] - if remainingValues ~= nil and remainingValues[value] ~= nil then - remainingValues[value] = nil - newProcessors[tryReuseProcessor] = true - existingProcessors[tryReuseProcessor] = nil + if tryReuseProcessor.outputKey == nil then + local value = peek(tryReuseProcessor.inputValue) + for key, remainingValues in remainingPairs do + if remainingValues[value] ~= nil then + remainingValues[value] = nil + tryReuseProcessor.inputKey:set(key) + newProcessors[tryReuseProcessor] = true + existingProcessors[tryReuseProcessor] = nil + break + end + end + else + local key = peek(tryReuseProcessor.inputKey) + local value = peek(tryReuseProcessor.inputValue) + local remainingValues = remainingPairs[key] + if remainingValues ~= nil and remainingValues[value] ~= nil then + remainingValues[value] = nil + newProcessors[tryReuseProcessor] = true + existingProcessors[tryReuseProcessor] = nil + end end + end -- Next, try and reuse processors who match the key of a remaining pair. -- The value will change but the key will stay stable. From ac18f9bf7cb8a81b548e212ff7ac7035ccf9f8d6 Mon Sep 17 00:00:00 2001 From: Elttob Date: Tue, 28 Nov 2023 15:06:55 +0000 Subject: [PATCH 056/287] Improved state iteration unit tests --- test/State/ForKeys.spec.lua | 5 +++++ test/State/ForValues.spec.lua | 5 +++++ 2 files changed, 10 insertions(+) diff --git a/test/State/ForKeys.spec.lua b/test/State/ForKeys.spec.lua index 023a721d4..f8f1796b4 100644 --- a/test/State/ForKeys.spec.lua +++ b/test/State/ForKeys.spec.lua @@ -51,6 +51,11 @@ return function() expect(peek(forObject)).to.be.a("table") expect(peek(forObject).FOO).to.equal(1) expect(peek(forObject).BAR).to.equal(2) + data:set({baz = 3, garb = 4}) + expect(peek(forObject).FOO).to.equal(nil) + expect(peek(forObject).BAR).to.equal(nil) + expect(peek(forObject).BAZ).to.equal(3) + expect(peek(forObject).GARB).to.equal(4) doCleanup(scope) end) diff --git a/test/State/ForValues.spec.lua b/test/State/ForValues.spec.lua index c287fcd93..52fb98f3a 100644 --- a/test/State/ForValues.spec.lua +++ b/test/State/ForValues.spec.lua @@ -54,6 +54,11 @@ return function() expect(peek(forObject)).to.be.a("table") expect(table.find(peek(forObject), "FOO")).to.be.ok() expect(table.find(peek(forObject), "BAR")).to.be.ok() + data:set({"baz", "garb"}) + expect(table.find(peek(forObject), "FOO")).to.never.be.ok() + expect(table.find(peek(forObject), "BAR")).to.never.be.ok() + expect(table.find(peek(forObject), "BAZ")).to.be.ok() + expect(table.find(peek(forObject), "GARB")).to.be.ok() doCleanup(scope) end) From 6455c97421caca5f329cb4bf6b0bd4d166f5ea05 Mon Sep 17 00:00:00 2001 From: Elttob Date: Tue, 28 Nov 2023 15:07:11 +0000 Subject: [PATCH 057/287] Fix ForPairs computed scope argument --- src/State/ForPairs.lua | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/State/ForPairs.lua b/src/State/ForPairs.lua index 20528d0d0..975f666af 100644 --- a/src/State/ForPairs.lua +++ b/src/State/ForPairs.lua @@ -44,9 +44,9 @@ local function ForPairs( return {key = nil, value = nil} end end) - return Computed(function(_, use) + return Computed(scope, function(_, use) return use(pair).key - end), Computed(function(_, use) + end), Computed(scope, function(_, use) return use(pair).value end) end From 38adbed1cd8e184afd924d96ca37848d3efdd75f Mon Sep 17 00:00:00 2001 From: Elttob Date: Tue, 28 Nov 2023 17:09:13 +0000 Subject: [PATCH 058/287] More ForPairs unit tests --- src/State/For.lua | 4 + test/State/ForPairs.spec.lua | 211 +++++++++++++++++++++++++++++++++++ 2 files changed, 215 insertions(+) diff --git a/src/State/For.lua b/src/State/For.lua index ee8ab68c1..78518e9e9 100644 --- a/src/State/For.lua +++ b/src/State/For.lua @@ -143,6 +143,8 @@ function class:update(): boolean local scope = {} local inputKey = Value(scope, key) local inputValue = Value(scope, value) + + if _G.VERBOSE then print("MAKING", key, value) end local processOK, outputKey, outputValue = xpcall(self._processor, parseError, scope, inputKey, inputValue) if processOK then local processor = { @@ -154,6 +156,7 @@ function class:update(): boolean } newProcessors[processor] = true else + if _G.VERBOSE then print("PROCESS NOT OK", outputKey) end logErrorNonFatal("forProcessorError", outputKey) end end @@ -173,6 +176,7 @@ function class:update(): boolean continue elseif newOutputTable[keyValue] == nil then newOutputTable[keyValue] = valueValue + if _G.VERBOSE then print(keyValue, valueValue) end else logErrorNonFatal("forKeyCollision", keyValue) end diff --git a/test/State/ForPairs.spec.lua b/test/State/ForPairs.spec.lua index 8640826c1..1700b4095 100644 --- a/test/State/ForPairs.spec.lua +++ b/test/State/ForPairs.spec.lua @@ -4,7 +4,218 @@ local Package = game:GetService("ReplicatedStorage").Fusion local ForPairs = require(Package.State.ForPairs) local Value = require(Package.State.Value) local peek = require(Package.State.peek) +local doCleanup = require(Package.Memory.doCleanup) return function() + FOCUS() + it("constructs in scopes", function() + local scope = {} + local forObject = ForPairs(scope, {}, function() + -- intentionally blank + end) + + expect(forObject).to.be.a("table") + expect(forObject.type).to.equal("State") + expect(forObject.kind).to.equal("For") + expect(scope[1]).to.equal(forObject) + + doCleanup(scope) + end) + + it("is destroyable", function() + local scope = {} + local forObject = ForPairs(scope, {}, function() + -- intentionally blank + end) + expect(function() + forObject:destroy() + end).to.never.throw() + end) + + it("iterates on constants", function() + local scope = {} + local data = {foo = "oof", bar = "rab"} + local forObject = ForPairs(scope, data, function(_, _, key, value) + return value, key + end) + expect(peek(forObject)).to.be.a("table") + expect(peek(forObject).oof).to.equal("foo") + expect(peek(forObject).rab).to.equal("bar") + doCleanup(scope) + end) + + it("iterates on state objects", function() + local scope = {} + local data = Value(scope, {foo = "oof", bar = "rab"}) + local forObject = ForPairs(scope, data, function(_, _, key, value) + return value, key + end) + expect(peek(forObject)).to.be.a("table") + expect(peek(forObject).oof).to.equal("foo") + expect(peek(forObject).rab).to.equal("bar") + data:set({baz = "zab", garb = "brag"}) + expect(peek(forObject).oof).to.equal(nil) + expect(peek(forObject).rab).to.equal(nil) + expect(peek(forObject).zab).to.equal("baz") + expect(peek(forObject).brag).to.equal("garb") + doCleanup(scope) + end) + + it("computes with constants", function() + local scope = {} + local data = {foo = "oof", bar = "rab"} + local forObject = ForPairs(scope, data, function(_, use, key, value) + return value .. use("baz"), key .. use("baz") + end) + expect(peek(forObject).oofbaz).to.equal("foobaz") + expect(peek(forObject).rabbaz).to.equal("barbaz") + doCleanup(scope) + end) + + it("computes with state objects", function() + local scope = {} + local data = {foo = "oof", bar = "rab"} + local suffix = Value(scope, "first") + local forObject = ForPairs(scope, data, function(_, use, key, value) + return value .. use(suffix), key .. use(suffix) + end) + expect(peek(forObject).ooffirst).to.equal("foofirst") + expect(peek(forObject).rabfirst).to.equal("barfirst") + suffix:set("second") + expect(peek(forObject).ooffirst).to.equal(nil) + expect(peek(forObject).rabfirst).to.equal(nil) + expect(peek(forObject).oofsecond).to.equal("foosecond") + expect(peek(forObject).rabsecond).to.equal("barsecond") + doCleanup(scope) + end) + + it("destroys and omits pair that error during processing", function() + local scope = {} + local data = {foo = "oof", bar = "rab", baz = "zab"} + local suffix = Value(scope, "first") + local destroyed = {} + local forObject = ForPairs(scope, data, function(innerScope, use, key, value) + local generatedKey = value .. use(suffix) + local generatedValue = key .. use(suffix) + table.insert(innerScope, function() + destroyed[generatedKey] = true + end) + if key == "bar" and use(suffix) == "second" then + error("This is an intentional error from a unit test") + end + return generatedKey, generatedValue + end) + expect(peek(forObject).ooffirst).to.equal("foofirst") + expect(peek(forObject).rabfirst).to.equal("barfirst") + expect(peek(forObject).zabfirst).to.equal("bazfirst") + suffix:set("second") + expect(peek(forObject).ooffirst).to.never.equal("foofirst") + expect(peek(forObject).rabfirst).to.never.equal("barfirst") + expect(peek(forObject).zabfirst).to.never.equal("bazfirst") + expect(peek(forObject).oofsecond).to.equal("foosecond") + expect(peek(forObject).rabsecond).to.never.equal("barsecond") + expect(peek(forObject).zabsecond).to.equal("bazsecond") + expect(destroyed.ooffirst).to.equal(true) + expect(destroyed.rabfirst).to.equal(true) + expect(destroyed.zabfirst).to.equal(true) + expect(destroyed.oofsecond).to.equal(nil) + expect(destroyed.rabsecond).to.equal(true) + expect(destroyed.zabsecond).to.equal(nil) + suffix:set("third") + expect(peek(forObject).ooffirst).to.never.equal("foofirst") + expect(peek(forObject).rabfirst).to.never.equal("barfirst") + expect(peek(forObject).zabfirst).to.never.equal("bazfirst") + expect(peek(forObject).oofsecond).to.never.equal("foosecond") + expect(peek(forObject).rabsecond).to.never.equal("barsecond") + expect(peek(forObject).zabsecond).to.never.equal("bazsecond") + expect(peek(forObject).oofthird).to.equal("foothird") + expect(peek(forObject).rabthird).to.equal("barthird") + expect(peek(forObject).zabthird).to.equal("bazthird") + doCleanup(scope) + end) + + it("omits values that return nil", function() + local scope = {} + local data = {foo = "oof", bar = "rab", baz = "zab"} + local omitThird = Value(scope, false) + local forObject = ForPairs(scope, data, function(_, use, key, value) + if key == "bar" then + return nil + end + if use(omitThird) then + if key == "baz" then + return value, nil + end + end + return value, key + end) + expect(peek(forObject).oof).to.equal("foo") + expect(peek(forObject).rab).to.never.equal("bar") + expect(peek(forObject).zab).to.equal("baz") + omitThird:set(true) + expect(peek(forObject).oof).to.equal("foo") + expect(peek(forObject).rab).to.never.equal("bar") + expect(peek(forObject).zab).to.never.equal("baz") + omitThird:set(false) + expect(peek(forObject).oof).to.equal("foo") + expect(peek(forObject).rab).to.never.equal("bar") + expect(peek(forObject).zab).to.equal("baz") + doCleanup(scope) + end) + + it("doesn't destroy inner scope on creation", function() + local scope = {} + local destructed = {} + local data = Value(scope, {foo = "oof", bar = "rab", baz = "zab"}) + local _ = ForPairs(scope, data, function(innerScope, _, key, value) + table.insert(innerScope, function() + destructed[key] = true + end) + return value, key + end) + expect(destructed.foo).to.equal(nil) + expect(destructed.bar).to.equal(nil) + data:set({foo = "oof", bar = "rab", baz = "zab"}) + expect(destructed.foo).to.equal(nil) + expect(destructed.bar).to.equal(nil) + expect(destructed.baz).to.equal(nil) + doCleanup(scope) + end) + + it("destroys inner scope on update", function() + local scope = {} + local destructed = {} + local data = Value(scope, {foo = "oof", bar = "rab"}) + local _ = ForPairs(scope, data, function(innerScope, _, key, value) + table.insert(innerScope, function() + destructed[key] = true + end) + return value, key + end) + expect(destructed.foo).to.equal(nil) + expect(destructed.bar).to.equal(nil) + data:set({baz = "zab"}) + expect(destructed.foo).to.equal(true) + expect(destructed.bar).to.equal(true) + expect(destructed.baz).to.equal(nil) + doCleanup(scope) + end) + + it("destroys inner scope on destroy", function() + local scope = {} + local destructed = {} + local data = Value(scope, {foo = "oof", bar = "rab"}) + local _ = ForPairs(scope, data, function(innerScope, _, key, value) + table.insert(innerScope, function() + destructed[key] = true + end) + return value, key + end) + expect(destructed.foo).to.equal(nil) + expect(destructed.bar).to.equal(nil) + doCleanup(scope) + expect(destructed.foo).to.equal(true) + expect(destructed.bar).to.equal(true) + end) end From 0415b76975ac663fc03185a9c78ff40a58304f65 Mon Sep 17 00:00:00 2001 From: Elttob Date: Tue, 28 Nov 2023 17:36:48 +0000 Subject: [PATCH 059/287] Process pairs in single state objects --- src/State/For.lua | 59 +++++++++------------- src/State/ForKeys.lua | 11 +++-- src/State/ForPairs.lua | 11 ++--- src/State/ForValues.lua | 10 ++-- src/Types.lua | 17 +++---- test/State/For.spec.lua | 106 +++++++++++++++++++--------------------- 6 files changed, 97 insertions(+), 117 deletions(-) diff --git a/src/State/For.lua b/src/State/For.lua index 78518e9e9..2011f5cd8 100644 --- a/src/State/For.lua +++ b/src/State/For.lua @@ -61,8 +61,9 @@ function class:update(): boolean -- NOTE: we also reuse processors with nil output keys here, so long as -- they match values. This ensures they don't get recomputed either. for tryReuseProcessor in existingProcessors do - if tryReuseProcessor.outputKey == nil then - local value = peek(tryReuseProcessor.inputValue) + local key = peek(tryReuseProcessor.inputPair).key + local value = peek(tryReuseProcessor.inputPair).value + if key == nil then for key, remainingValues in remainingPairs do if remainingValues[value] ~= nil then remainingValues[value] = nil @@ -73,8 +74,6 @@ function class:update(): boolean end end else - local key = peek(tryReuseProcessor.inputKey) - local value = peek(tryReuseProcessor.inputValue) local remainingValues = remainingPairs[key] if remainingValues ~= nil and remainingValues[value] ~= nil then remainingValues[value] = nil @@ -87,13 +86,13 @@ function class:update(): boolean -- Next, try and reuse processors who match the key of a remaining pair. -- The value will change but the key will stay stable. for tryReuseProcessor in existingProcessors do - local key = peek(tryReuseProcessor.inputKey) + local key = peek(tryReuseProcessor.inputPair).key local remainingValues = remainingPairs[key] if remainingValues ~= nil then local value = next(remainingValues) if value ~= nil then remainingValues[value] = nil - tryReuseProcessor.inputValue:set(value) + tryReuseProcessor.inputPair:set({key = key, value = value}) newProcessors[tryReuseProcessor] = true existingProcessors[tryReuseProcessor] = nil end @@ -102,11 +101,11 @@ function class:update(): boolean -- Next, try and reuse processors who match the value of a remaining pair. -- The key will change but the value will stay stable. for tryReuseProcessor in existingProcessors do - local value = peek(tryReuseProcessor.inputValue) + local value = peek(tryReuseProcessor.inputPair).value for key, remainingValues in remainingPairs do if remainingValues[value] ~= nil then remainingValues[value] = nil - tryReuseProcessor.inputKey:set(key) + tryReuseProcessor.inputPair:set({key = key, value = value}) newProcessors[tryReuseProcessor] = true existingProcessors[tryReuseProcessor] = nil break @@ -120,8 +119,7 @@ function class:update(): boolean local value = next(remainingValues) if value ~= nil then remainingValues[value] = nil - tryReuseProcessor.inputKey:set(key) - tryReuseProcessor.inputValue:set(value) + tryReuseProcessor.inputPair:set({key = key, value = value}) newProcessors[tryReuseProcessor] = true existingProcessors[tryReuseProcessor] = nil break @@ -141,44 +139,36 @@ function class:update(): boolean for key, remainingValues in remainingPairs do for value in remainingValues do local scope = {} - local inputKey = Value(scope, key) - local inputValue = Value(scope, value) - - if _G.VERBOSE then print("MAKING", key, value) end - local processOK, outputKey, outputValue = xpcall(self._processor, parseError, scope, inputKey, inputValue) + local inputPair = Value(scope, {key = key, value = value}) + local processOK, outputPair = xpcall(self._processor, parseError, scope, inputPair) if processOK then local processor = { - inputKey = inputKey, - inputValue = inputValue, - outputKey = outputKey, - outputValue = outputValue, + inputPair = inputPair, + outputPair = outputPair, cleanupTask = scope } newProcessors[processor] = true else - if _G.VERBOSE then print("PROCESS NOT OK", outputKey) end - logErrorNonFatal("forProcessorError", outputKey) + logErrorNonFatal("forProcessorError", outputPair) end end end end for processor in newProcessors do - local key, value = processor.outputKey, processor.outputValue + local pair = processor.outputPair + pair.dependentSet[self], self.dependencySet[pair] = true, true + local key, value = peek(pair).key, peek(pair).value + if value == nil then + continue + end if key == nil then key = #newOutputTable + 1 - else - key.dependentSet[self], self.dependencySet[key] = true, true end - value.dependentSet[self], self.dependencySet[value] = true, true - local keyValue, valueValue = peek(key), peek(value) - if keyValue == nil or valueValue == nil then - continue - elseif newOutputTable[keyValue] == nil then - newOutputTable[keyValue] = valueValue - if _G.VERBOSE then print(keyValue, valueValue) end + if newOutputTable[key] == nil then + newOutputTable[key] = value else - logErrorNonFatal("forKeyCollision", keyValue) + logErrorNonFatal("forKeyCollision", key) end end @@ -218,9 +208,8 @@ local function For( inputTable: PubTypes.CanBeState<{ [KI]: VI }>, processor: ( {any}, - PubTypes.StateObject, - PubTypes.StateObject - ) -> (PubTypes.StateObject?, PubTypes.StateObject) + PubTypes.StateObject<{key: KI, value: VI}> + ) -> (PubTypes.StateObject<{key: KO?, value: VO}>) ): Types.For local self = setmetatable({ diff --git a/src/State/ForKeys.lua b/src/State/ForKeys.lua index 206e3c5f7..9ce6d9914 100644 --- a/src/State/ForKeys.lua +++ b/src/State/ForKeys.lua @@ -32,9 +32,9 @@ local function ForKeys( return For( scope, inputTable, - function(scope, inputKey, inputValue) - return Computed(scope, function(scope, use) - local ok, key = xpcall(processor, parseError, scope, use, use(inputKey)) + function(scope, inputPair) + local outputKey = Computed(scope, function(scope, use) + local ok, key = xpcall(processor, parseError, scope, use, use(inputPair).key) if ok then return key else @@ -43,7 +43,10 @@ local function ForKeys( table.clear(scope) return nil end - end), inputValue + end) + return Computed(scope, function(scope, use) + return {key = use(outputKey), value = use(inputPair).value} + end) end ) end diff --git a/src/State/ForPairs.lua b/src/State/ForPairs.lua index 975f666af..e6671fa31 100644 --- a/src/State/ForPairs.lua +++ b/src/State/ForPairs.lua @@ -32,9 +32,9 @@ local function ForPairs( return For( scope, inputTable, - function(scope, inputKey, inputValue) - local pair = Computed(scope, function(scope, use) - local ok, key, value = xpcall(processor, parseError, scope, use, use(inputKey), use(inputValue)) + function(scope, inputPair) + return Computed(scope, function(scope, use) + local ok, key, value = xpcall(processor, parseError, scope, use, use(inputPair).key, use(inputPair).value) if ok then return {key = key, value = value} else @@ -44,11 +44,6 @@ local function ForPairs( return {key = nil, value = nil} end end) - return Computed(scope, function(_, use) - return use(pair).key - end), Computed(scope, function(_, use) - return use(pair).value - end) end ) end diff --git a/src/State/ForValues.lua b/src/State/ForValues.lua index ffd174319..cc7d49ca0 100644 --- a/src/State/ForValues.lua +++ b/src/State/ForValues.lua @@ -32,16 +32,16 @@ local function ForValues( return For( scope, inputTable, - function(scope, _, inputValue) - return nil, Computed(scope, function(scope, use) - local ok, value = xpcall(processor, parseError, scope, use, use(inputValue)) + function(scope, inputPair) + return Computed(scope, function(scope, use) + local ok, value = xpcall(processor, parseError, scope, use, use(inputPair).value) if ok then - return value + return {key = nil, value = value} else logErrorNonFatal("forProcessorError", parseError) doCleanup(scope) table.clear(scope) - return nil + return {key = nil, value = nil} end end) end diff --git a/src/Types.lua b/src/Types.lua index 2ffb93e73..8d3793e4a 100644 --- a/src/Types.lua +++ b/src/Types.lua @@ -60,22 +60,19 @@ export type Computed = PubTypes.Computed & { export type For = PubTypes.For & { _processor: ( {any}, - PubTypes.StateObject, - PubTypes.StateObject - ) -> (PubTypes.StateObject?, PubTypes.StateObject), + PubTypes.StateObject<{key: KI, value: VI}> + ) -> (PubTypes.StateObject<{key: KO?, value: VO}>), _inputTable: PubTypes.CanBeState<{[KI]: VI}>, _existingInputTable: {[KI]: VI}?, _existingOutputTable: {[KO]: VO}, - _existingProcessors: {[For_Processor]: true}, + _existingProcessors: {[ForProcessor]: true}, _newOutputTable: {[KO]: VO}, - _newProcessors: {[For_Processor]: true}, + _newProcessors: {[ForProcessor]: true}, _remainingPairs: {[KI]: {[VI]: true}} } -type For_Processor = { - inputKey: PubTypes.Value, - inputValue: PubTypes.Value, - outputKey: PubTypes.StateObject, - outputValue: PubTypes.StateObject, +type ForProcessor = { + inputPair: PubTypes.Value<{key: any, value: any}>, + outputPair: PubTypes.StateObject<{key: any, value: any}>, cleanupTask: any } diff --git a/test/State/For.spec.lua b/test/State/For.spec.lua index 292f719cc..f4f93f5d4 100644 --- a/test/State/For.spec.lua +++ b/test/State/For.spec.lua @@ -37,17 +37,13 @@ return function() local data = {foo = 1, bar = 2} local seen = {} local numCalls = 0 - local forObject = For(scope, data, function(scope, inputKey, inputValue) + local forObject = For(scope, data, function(scope, inputPair) numCalls += 1 - local k, v = peek(inputKey), peek(inputValue) + local k, v = peek(inputPair).key, peek(inputPair).value seen[k] = v - local outputKey = Computed(scope, function(_, use) - return string.upper(use(inputKey)) - end) - local outputValue = Computed(scope, function(_, use) - return use(inputValue) * 10 + return Computed(scope, function(_, use) + return {key = string.upper(use(inputPair).key), value = use(inputPair).value * 10} end) - return outputKey, outputValue end) expect(numCalls).to.equal(2) expect(seen.foo).to.equal(1) @@ -63,15 +59,11 @@ return function() local scope = {} local data = Value(scope, {foo = 1, bar = 2}) local numCalls = 0 - local forObject = For(scope, data, function(scope, inputKey, inputValue) + local forObject = For(scope, data, function(scope, inputPair) numCalls += 1 - local outputKey = Computed(scope, function(_, use) - return string.upper(use(inputKey)) - end) - local outputValue = Computed(scope, function(_, use) - return use(inputValue) * 10 + return Computed(scope, function(_, use) + return {key = string.upper(use(inputPair).key), value = use(inputPair).value * 10} end) - return outputKey, outputValue end) expect(numCalls).to.equal(2) @@ -113,9 +105,9 @@ return function() it("omits pairs that error", function() local scope = {} local data = {first = 1, second = 2, third = 3} - local forObject = For(scope, data, function(scope, inputKey, inputValue) - assert(peek(inputKey) ~= "second", "This is an intentional error from a unit test") - return inputKey, inputValue + local forObject = For(scope, data, function(scope, inputPair) + assert(peek(inputPair).key ~= "second", "This is an intentional error from a unit test") + return inputPair end) expect(peek(forObject).first).to.equal(1) expect(peek(forObject).second).to.equal(nil) @@ -123,52 +115,56 @@ return function() doCleanup(scope) end) - it("omits pairs when their key or value is nil", function() + it("omits pairs when their value is nil", function() local scope = {} local data = {first = 1, second = 2, third = 3} local omitThird = Value(scope, false) - local forObject1 = For(scope, data, function(scope, inputKey, inputValue) - return inputKey, Computed(scope, function(_, use) - if use(inputKey) == "second" then - return nil - elseif use(inputKey) == "third" and use(omitThird) then - return nil - else - return use(inputValue) - end - end) - end) - local forObject2 = For(scope, data, function(scope, inputKey, inputValue) + local forObject = For(scope, data, function(scope, inputPair) return Computed(scope, function(_, use) - if use(inputKey) == "second" then - return nil - elseif use(inputKey) == "third" and use(omitThird) then - return nil + if use(inputPair).key == "second" then + return {key = use(inputPair).key, value = nil} + elseif use(inputPair).key == "third" and use(omitThird) then + return {key = use(inputPair).key, value = nil} else - return use(inputKey) + return use(inputPair) end - end), inputValue + end) end) - expect(peek(forObject1).first).to.equal(1) - expect(peek(forObject1).second).to.equal(nil) - expect(peek(forObject1).third).to.equal(3) - expect(peek(forObject2).first).to.equal(1) - expect(peek(forObject2).second).to.equal(nil) - expect(peek(forObject2).third).to.equal(3) + expect(peek(forObject).first).to.equal(1) + expect(peek(forObject).second).to.equal(nil) + expect(peek(forObject).third).to.equal(3) omitThird:set(true) - expect(peek(forObject1).first).to.equal(1) - expect(peek(forObject1).second).to.equal(nil) - expect(peek(forObject1).third).to.equal(nil) - expect(peek(forObject2).first).to.equal(1) - expect(peek(forObject2).second).to.equal(nil) - expect(peek(forObject2).third).to.equal(nil) + expect(peek(forObject).first).to.equal(1) + expect(peek(forObject).second).to.equal(nil) + expect(peek(forObject).third).to.equal(nil) omitThird:set(false) - expect(peek(forObject1).first).to.equal(1) - expect(peek(forObject1).second).to.equal(nil) - expect(peek(forObject1).third).to.equal(3) - expect(peek(forObject2).first).to.equal(1) - expect(peek(forObject2).second).to.equal(nil) - expect(peek(forObject2).third).to.equal(3) + expect(peek(forObject).first).to.equal(1) + expect(peek(forObject).second).to.equal(nil) + expect(peek(forObject).third).to.equal(3) doCleanup(scope) end) + + it("allows values to roam when their key is nil", function() + error("TODO") + -- local scope = {} + -- local data = {first = 1, second = 2, third = 3} + -- local omitThird = Value(scope, false) + -- local forObject = For(scope, data, function(scope, inputPair) + -- return Computed(scope, function(_, use) + -- return {key = nil, value = use(inputPair).value} + -- end) + -- end) + -- expect(peek(forObject).first).to.equal(1) + -- expect(peek(forObject).second).to.equal(nil) + -- expect(peek(forObject).third).to.equal(3) + -- omitThird:set(true) + -- expect(peek(forObject).first).to.equal(1) + -- expect(peek(forObject).second).to.equal(nil) + -- expect(peek(forObject).third).to.equal(nil) + -- omitThird:set(false) + -- expect(peek(forObject).first).to.equal(1) + -- expect(peek(forObject).second).to.equal(nil) + -- expect(peek(forObject).third).to.equal(3) + -- doCleanup(scope) + end) end From 6356dbbd63d485846a2b8f0acab2be72c4b4d4f1 Mon Sep 17 00:00:00 2001 From: Elttob Date: Tue, 28 Nov 2023 18:33:32 +0000 Subject: [PATCH 060/287] For spec roaming test --- test/State/For.spec.lua | 49 +++++++++++++++++++++++------------------ 1 file changed, 28 insertions(+), 21 deletions(-) diff --git a/test/State/For.spec.lua b/test/State/For.spec.lua index f4f93f5d4..f2e5310e9 100644 --- a/test/State/For.spec.lua +++ b/test/State/For.spec.lua @@ -145,26 +145,33 @@ return function() end) it("allows values to roam when their key is nil", function() - error("TODO") - -- local scope = {} - -- local data = {first = 1, second = 2, third = 3} - -- local omitThird = Value(scope, false) - -- local forObject = For(scope, data, function(scope, inputPair) - -- return Computed(scope, function(_, use) - -- return {key = nil, value = use(inputPair).value} - -- end) - -- end) - -- expect(peek(forObject).first).to.equal(1) - -- expect(peek(forObject).second).to.equal(nil) - -- expect(peek(forObject).third).to.equal(3) - -- omitThird:set(true) - -- expect(peek(forObject).first).to.equal(1) - -- expect(peek(forObject).second).to.equal(nil) - -- expect(peek(forObject).third).to.equal(nil) - -- omitThird:set(false) - -- expect(peek(forObject).first).to.equal(1) - -- expect(peek(forObject).second).to.equal(nil) - -- expect(peek(forObject).third).to.equal(3) - -- doCleanup(scope) + local scope = {} + local data = Value(scope, {"first", "second", "third"}) + local numCalls = 0 + local forObject = For(scope, data, function(scope, inputPair) + numCalls += 1 + return Computed(scope, function(_, use) + return {key = nil, value = use(inputPair).value} + end) + end) + expect(table.find(peek(forObject), "first")).to.be.ok() + expect(table.find(peek(forObject), "second")).to.be.ok() + expect(table.find(peek(forObject), "third")).to.be.ok() + expect(numCalls).to.equal(3) + data:set({"third", "first", "second"}) + expect(table.find(peek(forObject), "first")).to.be.ok() + expect(table.find(peek(forObject), "second")).to.be.ok() + expect(table.find(peek(forObject), "third")).to.be.ok() + expect(numCalls).to.equal(3) + data:set({"second", "first"}) + expect(table.find(peek(forObject), "first")).to.be.ok() + expect(table.find(peek(forObject), "second")).to.be.ok() + expect(table.find(peek(forObject), "third")).to.never.be.ok() + expect(numCalls).to.equal(3) + data:set({"first"}) + expect(table.find(peek(forObject), "first")).to.be.ok() + expect(table.find(peek(forObject), "second")).to.never.be.ok() + expect(numCalls).to.equal(3) + doCleanup(scope) end) end From 1eb098ce43613f09244bfc0715e2a6ef5902788d Mon Sep 17 00:00:00 2001 From: Elttob Date: Tue, 28 Nov 2023 18:37:32 +0000 Subject: [PATCH 061/287] Add more computation centric tests --- src/State/ForValues.lua | 5 ++++- test/State/ForKeys.spec.lua | 18 ++++++++++++++++++ test/State/ForValues.spec.lua | 2 +- 3 files changed, 23 insertions(+), 2 deletions(-) diff --git a/src/State/ForValues.lua b/src/State/ForValues.lua index cc7d49ca0..d30e22a11 100644 --- a/src/State/ForValues.lua +++ b/src/State/ForValues.lua @@ -33,8 +33,11 @@ local function ForValues( scope, inputTable, function(scope, inputPair) + local inputValue = Computed(scope, function(scope, use) + return use(inputPair).value + end) return Computed(scope, function(scope, use) - local ok, value = xpcall(processor, parseError, scope, use, use(inputPair).value) + local ok, value = xpcall(processor, parseError, scope, use, use(inputValue)) if ok then return {key = nil, value = value} else diff --git a/test/State/ForKeys.spec.lua b/test/State/ForKeys.spec.lua index f8f1796b4..da1d45bc6 100644 --- a/test/State/ForKeys.spec.lua +++ b/test/State/ForKeys.spec.lua @@ -214,4 +214,22 @@ return function() expect(destructed.foo).to.equal(true) expect(destructed.bar).to.equal(true) end) + + it("doesn't recompute when values chaneg", function() + local scope = {} + local data = Value(scope, {foo = 1, bar = 2}) + local computations = 0 + local _ = ForKeys(scope, data, function(innerScope, _, key) + computations += 1 + return string.upper(key) + end) + expect(computations).to.equal(2) + data:set({foo = 3, bar = 4}) + expect(computations).to.equal(2) + data:set({foo = 3, bar = 4, baz = 5}) + expect(computations).to.equal(3) + data:set({foo = 4, bar = 5, baz = 6}) + expect(computations).to.equal(3) + doCleanup(scope) + end) end diff --git a/test/State/ForValues.spec.lua b/test/State/ForValues.spec.lua index 52fb98f3a..edd0e2f91 100644 --- a/test/State/ForValues.spec.lua +++ b/test/State/ForValues.spec.lua @@ -216,7 +216,7 @@ return function() expect(destructed.bar).to.equal(true) end) - it("doesn't recompute when values are preserved", function() + it("doesn't recompute when values roam between keys", function() local scope = {} local data = Value(scope, {"foo", "bar"}) local computations = 0 From bd23d0c9684e7890eb72e72fb415cd9e50e6331f Mon Sep 17 00:00:00 2001 From: Elttob Date: Tue, 28 Nov 2023 18:39:05 +0000 Subject: [PATCH 062/287] Fix ForKeys multiple invocation --- src/State/ForKeys.lua | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/State/ForKeys.lua b/src/State/ForKeys.lua index 9ce6d9914..afdfdf230 100644 --- a/src/State/ForKeys.lua +++ b/src/State/ForKeys.lua @@ -33,8 +33,11 @@ local function ForKeys( scope, inputTable, function(scope, inputPair) + local inputKey = Computed(scope, function(scope, use) + return use(inputPair).key + end) local outputKey = Computed(scope, function(scope, use) - local ok, key = xpcall(processor, parseError, scope, use, use(inputPair).key) + local ok, key = xpcall(processor, parseError, scope, use, use(inputKey)) if ok then return key else From a29a49e60daadefa24c7a7b7de0ef0d920319c6e Mon Sep 17 00:00:00 2001 From: Elttob Date: Tue, 28 Nov 2023 18:45:31 +0000 Subject: [PATCH 063/287] All For unit tests pass --- src/State/For.lua | 4 ++-- test/State/ForValues.spec.lua | 2 ++ 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/src/State/For.lua b/src/State/For.lua index 2011f5cd8..ab0d60073 100644 --- a/src/State/For.lua +++ b/src/State/For.lua @@ -63,11 +63,11 @@ function class:update(): boolean for tryReuseProcessor in existingProcessors do local key = peek(tryReuseProcessor.inputPair).key local value = peek(tryReuseProcessor.inputPair).value - if key == nil then + if peek(tryReuseProcessor.outputPair).key == nil then for key, remainingValues in remainingPairs do if remainingValues[value] ~= nil then remainingValues[value] = nil - tryReuseProcessor.inputKey:set(key) + tryReuseProcessor.inputPair:set({key = key, value = value}) newProcessors[tryReuseProcessor] = true existingProcessors[tryReuseProcessor] = nil break diff --git a/test/State/ForValues.spec.lua b/test/State/ForValues.spec.lua index edd0e2f91..718a8a0b6 100644 --- a/test/State/ForValues.spec.lua +++ b/test/State/ForValues.spec.lua @@ -225,7 +225,9 @@ return function() return string.upper(value) end) expect(computations).to.equal(2) + _G.VERBOSE = true data:set({"bar", "foo"}) + _G.VERBOSE = false expect(computations).to.equal(2) data:set({"baz", "bar", "foo"}) expect(computations).to.equal(3) From 7901e2b56efa0938f0caef7a19d687a038bebc3c Mon Sep 17 00:00:00 2001 From: Elttob Date: Tue, 28 Nov 2023 18:52:52 +0000 Subject: [PATCH 064/287] learn to type properly, dan --- test/State/ForKeys.spec.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/State/ForKeys.spec.lua b/test/State/ForKeys.spec.lua index da1d45bc6..7c28e0606 100644 --- a/test/State/ForKeys.spec.lua +++ b/test/State/ForKeys.spec.lua @@ -215,7 +215,7 @@ return function() expect(destructed.bar).to.equal(true) end) - it("doesn't recompute when values chaneg", function() + it("doesn't recompute when values change", function() local scope = {} local data = Value(scope, {foo = 1, bar = 2}) local computations = 0 From b2dded63b2811acdd908375eb79aacb149300a80 Mon Sep 17 00:00:00 2001 From: Elttob Date: Tue, 28 Nov 2023 18:53:35 +0000 Subject: [PATCH 065/287] Remove verbose globals --- test/State/ForValues.spec.lua | 2 -- 1 file changed, 2 deletions(-) diff --git a/test/State/ForValues.spec.lua b/test/State/ForValues.spec.lua index 718a8a0b6..edd0e2f91 100644 --- a/test/State/ForValues.spec.lua +++ b/test/State/ForValues.spec.lua @@ -225,9 +225,7 @@ return function() return string.upper(value) end) expect(computations).to.equal(2) - _G.VERBOSE = true data:set({"bar", "foo"}) - _G.VERBOSE = false expect(computations).to.equal(2) data:set({"baz", "bar", "foo"}) expect(computations).to.equal(3) From d1376d7e0a956f55b402483ce62edf30226974d6 Mon Sep 17 00:00:00 2001 From: Elttob Date: Tue, 28 Nov 2023 18:58:47 +0000 Subject: [PATCH 066/287] Replace {Task} with Scope --- src/Animation/Spring.lua | 2 +- src/Animation/Tween.lua | 2 +- src/Instances/Attribute.lua | 4 ++-- src/Instances/AttributeChange.lua | 2 +- src/Instances/Children.lua | 2 +- src/Instances/Cleanup.lua | 2 +- src/Instances/OnChange.lua | 2 +- src/Instances/OnEvent.lua | 2 +- src/Instances/Ref.lua | 2 +- src/Instances/applyInstanceProps.lua | 2 +- src/State/For.lua | 2 +- src/State/Observer.lua | 2 +- src/State/Value.lua | 2 +- src/Types.lua | 2 +- test/State/For.spec.lua | 1 - test/State/ForKeys.spec.lua | 1 - test/State/ForPairs.spec.lua | 2 -- test/State/ForValues.spec.lua | 2 -- 18 files changed, 15 insertions(+), 21 deletions(-) diff --git a/src/Animation/Spring.lua b/src/Animation/Spring.lua index 4db7ee4ca..4620da0eb 100644 --- a/src/Animation/Spring.lua +++ b/src/Animation/Spring.lua @@ -167,7 +167,7 @@ function class:destroy() end local function Spring( - scope: {PubTypes.Task}, + scope: PubTypes.Scope, goalState: PubTypes.Value, speed: PubTypes.CanBeState?, damping: PubTypes.CanBeState? diff --git a/src/Animation/Tween.lua b/src/Animation/Tween.lua index 39c34826a..ce8957bdb 100644 --- a/src/Animation/Tween.lua +++ b/src/Animation/Tween.lua @@ -78,7 +78,7 @@ function class:destroy() end local function Tween( - scope: {PubTypes.Task}, + scope: PubTypes.Scope, goalState: PubTypes.StateObject, tweenInfo: PubTypes.CanBeState? ): Types.Tween diff --git a/src/Instances/Attribute.lua b/src/Instances/Attribute.lua index d9a968b0f..b625b98bd 100644 --- a/src/Instances/Attribute.lua +++ b/src/Instances/Attribute.lua @@ -17,7 +17,7 @@ local function setAttribute(instance: Instance, attribute: string, value: any) instance:SetAttribute(attribute, value) end -local function bindAttribute(instance: Instance, attribute: string, value: any, cleanupTasks: {PubTypes.Task}) +local function bindAttribute(instance: Instance, attribute: string, value: any, cleanupTasks: PubTypes.Scope) if xtypeof(value) == "State" then local didDefer = false local function update() @@ -46,7 +46,7 @@ local function Attribute(attributeName: string): PubTypes.SpecialKey logError("attributeNameNil") end - function AttributeKey:apply(attributeValue: any, applyTo: Instance, cleanupTasks: {PubTypes.Task}) + function AttributeKey:apply(attributeValue: any, applyTo: Instance, cleanupTasks: PubTypes.Scope) bindAttribute(applyTo, attributeName, attributeValue, cleanupTasks) end return AttributeKey diff --git a/src/Instances/AttributeChange.lua b/src/Instances/AttributeChange.lua index 29c1e2332..cac6a8e75 100644 --- a/src/Instances/AttributeChange.lua +++ b/src/Instances/AttributeChange.lua @@ -20,7 +20,7 @@ local function AttributeChange(attributeName: string): PubTypes.SpecialKey logError("attributeNameNil") end - function attributeKey:apply(callback: any, applyTo: Instance, cleanupTasks: {PubTypes.Task}) + function attributeKey:apply(callback: any, applyTo: Instance, cleanupTasks: PubTypes.Scope) if typeof(callback) ~= "function" then logError("invalidAttributeChangeHandler", nil, attributeName) end diff --git a/src/Instances/Children.lua b/src/Instances/Children.lua index 2e756a8c9..afab84779 100644 --- a/src/Instances/Children.lua +++ b/src/Instances/Children.lua @@ -23,7 +23,7 @@ Children.type = "SpecialKey" Children.kind = "Children" Children.stage = "descendants" -function Children:apply(propValue: any, applyTo: Instance, cleanupTasks: {PubTypes.Task}) +function Children:apply(propValue: any, applyTo: Instance, cleanupTasks: PubTypes.Scope) local newParented: Set = {} local oldParented: Set = {} diff --git a/src/Instances/Cleanup.lua b/src/Instances/Cleanup.lua index 5fd71002b..71823a138 100644 --- a/src/Instances/Cleanup.lua +++ b/src/Instances/Cleanup.lua @@ -13,7 +13,7 @@ Cleanup.type = "SpecialKey" Cleanup.kind = "Cleanup" Cleanup.stage = "observer" -function Cleanup:apply(userTask: any, applyTo: Instance, cleanupTasks: {PubTypes.Task}) +function Cleanup:apply(userTask: any, applyTo: Instance, cleanupTasks: PubTypes.Scope) table.insert(cleanupTasks, userTask) end diff --git a/src/Instances/OnChange.lua b/src/Instances/OnChange.lua index 2561eeeb4..0e060f617 100644 --- a/src/Instances/OnChange.lua +++ b/src/Instances/OnChange.lua @@ -15,7 +15,7 @@ local function OnChange(propertyName: string): PubTypes.SpecialKey changeKey.kind = "OnChange" changeKey.stage = "observer" - function changeKey:apply(callback: any, applyTo: Instance, cleanupTasks: {PubTypes.Task}) + function changeKey:apply(callback: any, applyTo: Instance, cleanupTasks: PubTypes.Scope) local ok, event = pcall(applyTo.GetPropertyChangedSignal, applyTo, propertyName) if not ok then logError("cannotConnectChange", nil, applyTo.ClassName, propertyName) diff --git a/src/Instances/OnEvent.lua b/src/Instances/OnEvent.lua index 92a5880c4..bfad69877 100644 --- a/src/Instances/OnEvent.lua +++ b/src/Instances/OnEvent.lua @@ -19,7 +19,7 @@ local function OnEvent(eventName: string): PubTypes.SpecialKey eventKey.kind = "OnEvent" eventKey.stage = "observer" - function eventKey:apply(callback: any, applyTo: Instance, cleanupTasks: {PubTypes.Task}) + function eventKey:apply(callback: any, applyTo: Instance, cleanupTasks: PubTypes.Scope) local ok, event = pcall(getProperty_unsafe, applyTo, eventName) if not ok or typeof(event) ~= "RBXScriptSignal" then logError("cannotConnectEvent", nil, applyTo.ClassName, eventName) diff --git a/src/Instances/Ref.lua b/src/Instances/Ref.lua index 4bcab0bcd..aa7d46f65 100644 --- a/src/Instances/Ref.lua +++ b/src/Instances/Ref.lua @@ -15,7 +15,7 @@ Ref.type = "SpecialKey" Ref.kind = "Ref" Ref.stage = "observer" -function Ref:apply(refState: any, applyTo: Instance, cleanupTasks: {PubTypes.Task}) +function Ref:apply(refState: any, applyTo: Instance, cleanupTasks: PubTypes.Scope) if xtypeof(refState) ~= "State" or refState.kind ~= "Value" then logError("invalidRefType") else diff --git a/src/Instances/applyInstanceProps.lua b/src/Instances/applyInstanceProps.lua index 890f62de2..9aa2da619 100644 --- a/src/Instances/applyInstanceProps.lua +++ b/src/Instances/applyInstanceProps.lua @@ -50,7 +50,7 @@ local function setProperty(instance: Instance, property: string, value: any) end end -local function bindProperty(instance: Instance, property: string, value: PubTypes.CanBeState, cleanupTasks: {PubTypes.Task}) +local function bindProperty(instance: Instance, property: string, value: PubTypes.CanBeState, cleanupTasks: PubTypes.Scope) if xtypeof(value) == "State" then -- value is a state object - assign and observe for changes local willUpdate = false diff --git a/src/State/For.lua b/src/State/For.lua index ab0d60073..a4a1cacf2 100644 --- a/src/State/For.lua +++ b/src/State/For.lua @@ -204,7 +204,7 @@ function class:destroy() end local function For( - scope: {PubTypes.Task}, + scope: PubTypes.Scope, inputTable: PubTypes.CanBeState<{ [KI]: VI }>, processor: ( {any}, diff --git a/src/State/Observer.lua b/src/State/Observer.lua index f017de878..34fc99cad 100644 --- a/src/State/Observer.lua +++ b/src/State/Observer.lua @@ -59,7 +59,7 @@ function class:destroy() end local function Observer( - scope: {PubTypes.Task}, + scope: PubTypes.Scope, watchedState: PubTypes.Value ): Types.Observer local self = setmetatable({ diff --git a/src/State/Value.lua b/src/State/Value.lua index 855e74385..0b36e7ef1 100644 --- a/src/State/Value.lua +++ b/src/State/Value.lua @@ -49,7 +49,7 @@ function class:destroy() end local function Value( - scope: {PubTypes.Task}, + scope: PubTypes.Scope, initialValue: T ): Types.State local self = setmetatable({ diff --git a/src/Types.lua b/src/Types.lua index 8d3793e4a..a4c4d9bc2 100644 --- a/src/Types.lua +++ b/src/Types.lua @@ -59,7 +59,7 @@ export type Computed = PubTypes.Computed & { -- A state object which maps over keys and/or values in another table. export type For = PubTypes.For & { _processor: ( - {any}, + PubTypes.Scope, PubTypes.StateObject<{key: KI, value: VI}> ) -> (PubTypes.StateObject<{key: KO?, value: VO}>), _inputTable: PubTypes.CanBeState<{[KI]: VI}>, diff --git a/test/State/For.spec.lua b/test/State/For.spec.lua index f2e5310e9..7c792f495 100644 --- a/test/State/For.spec.lua +++ b/test/State/For.spec.lua @@ -6,7 +6,6 @@ local peek = require(Package.State.peek) local doCleanup = require(Package.Memory.doCleanup) return function() - FOCUS() it("constructs in scopes", function() local scope = {} local forObject = For(scope, {}, function() diff --git a/test/State/ForKeys.spec.lua b/test/State/ForKeys.spec.lua index 7c28e0606..99608ad93 100644 --- a/test/State/ForKeys.spec.lua +++ b/test/State/ForKeys.spec.lua @@ -5,7 +5,6 @@ local peek = require(Package.State.peek) local doCleanup = require(Package.Memory.doCleanup) return function() - FOCUS() it("constructs in scopes", function() local scope = {} local forObject = ForKeys(scope, {}, function() diff --git a/test/State/ForPairs.spec.lua b/test/State/ForPairs.spec.lua index 1700b4095..ae33a4ff9 100644 --- a/test/State/ForPairs.spec.lua +++ b/test/State/ForPairs.spec.lua @@ -7,8 +7,6 @@ local peek = require(Package.State.peek) local doCleanup = require(Package.Memory.doCleanup) return function() - FOCUS() - it("constructs in scopes", function() local scope = {} local forObject = ForPairs(scope, {}, function() diff --git a/test/State/ForValues.spec.lua b/test/State/ForValues.spec.lua index edd0e2f91..533142e36 100644 --- a/test/State/ForValues.spec.lua +++ b/test/State/ForValues.spec.lua @@ -7,8 +7,6 @@ local peek = require(Package.State.peek) local doCleanup = require(Package.Memory.doCleanup) return function() - FOCUS() - it("constructs in scopes", function() local scope = {} local forObject = ForValues(scope, {}, function() From 037f8c596c39645dc5393acdd5d3e133d253cabb Mon Sep 17 00:00:00 2001 From: Elttob Date: Tue, 28 Nov 2023 19:08:21 +0000 Subject: [PATCH 067/287] Update New/Hydrate to use scopes --- src/Instances/Hydrate.lua | 11 +- src/Instances/New.lua | 12 +- src/Instances/applyInstanceProps.lua | 23 ++-- test/Instances/applyInstanceProps.spec.lua | 140 ++++++++++++++------- 4 files changed, 126 insertions(+), 60 deletions(-) diff --git a/src/Instances/Hydrate.lua b/src/Instances/Hydrate.lua index b913d27d5..bcd848e57 100644 --- a/src/Instances/Hydrate.lua +++ b/src/Instances/Hydrate.lua @@ -9,9 +9,14 @@ local Package = script.Parent.Parent local PubTypes = require(Package.PubTypes) local applyInstanceProps = require(Package.Instances.applyInstanceProps) -local function Hydrate(target: Instance) - return function(props: PubTypes.PropertyTable): Instance - applyInstanceProps(props, target) +local function Hydrate( + scope: PubTypes.Scope, + target: Instance +) + return function( + props: PubTypes.PropertyTable + ): Instance + applyInstanceProps(scope, props, target) return target end end diff --git a/src/Instances/New.lua b/src/Instances/New.lua index 6fc46f9cd..52cb14a8a 100644 --- a/src/Instances/New.lua +++ b/src/Instances/New.lua @@ -11,10 +11,14 @@ local defaultProps = require(Package.Instances.defaultProps) local applyInstanceProps = require(Package.Instances.applyInstanceProps) local logError= require(Package.Logging.logError) -local function New(className: string) - return function(props: PubTypes.PropertyTable): Instance +local function New( + scope: PubTypes.Scope, + className: string +) + return function( + props: PubTypes.PropertyTable + ): Instance local ok, instance = pcall(Instance.new, className) - if not ok then logError("cannotCreateClass", nil, className) end @@ -26,7 +30,7 @@ local function New(className: string) end end - applyInstanceProps(props, instance) + applyInstanceProps(scope, props, instance) return instance end diff --git a/src/Instances/applyInstanceProps.lua b/src/Instances/applyInstanceProps.lua index 9aa2da619..78940e230 100644 --- a/src/Instances/applyInstanceProps.lua +++ b/src/Instances/applyInstanceProps.lua @@ -65,28 +65,31 @@ local function bindProperty(instance: Instance, property: string, value: PubType end setProperty(instance, property, peek(value)) - table.insert(cleanupTasks, Observer(value :: any):onChange(updateLater)) + table.insert(cleanupTasks, Observer(cleanupTasks, value :: any):onChange(updateLater)) else -- value is a constant - assign once only setProperty(instance, property, value) end end -local function applyInstanceProps(props: PubTypes.PropertyTable, applyTo: Instance) +local function applyInstanceProps( + scope: PubTypes.Scope, + props: PubTypes.PropertyTable, + applyTo: Instance +) local specialKeys = { self = {} :: {[PubTypes.SpecialKey]: any}, descendants = {} :: {[PubTypes.SpecialKey]: any}, ancestor = {} :: {[PubTypes.SpecialKey]: any}, observer = {} :: {[PubTypes.SpecialKey]: any} } - local cleanupTasks = {} for key, value in pairs(props) do local keyType = xtypeof(key) if keyType == "string" then if key ~= "Parent" then - bindProperty(applyTo, key :: string, value, cleanupTasks) + bindProperty(applyTo, key :: string, value, scope) end elseif keyType == "SpecialKey" then local stage = (key :: PubTypes.SpecialKey).stage @@ -103,25 +106,25 @@ local function applyInstanceProps(props: PubTypes.PropertyTable, applyTo: Instan end for key, value in pairs(specialKeys.self) do - key:apply(value, applyTo, cleanupTasks) + key:apply(value, applyTo, scope) end for key, value in pairs(specialKeys.descendants) do - key:apply(value, applyTo, cleanupTasks) + key:apply(value, applyTo, scope) end if props.Parent ~= nil then - bindProperty(applyTo, "Parent", props.Parent, cleanupTasks) + bindProperty(applyTo, "Parent", props.Parent, scope) end for key, value in pairs(specialKeys.ancestor) do - key:apply(value, applyTo, cleanupTasks) + key:apply(value, applyTo, scope) end for key, value in pairs(specialKeys.observer) do - key:apply(value, applyTo, cleanupTasks) + key:apply(value, applyTo, scope) end applyTo.Destroying:Connect(function() - doCleanup(cleanupTasks) + doCleanup(scope) end) end diff --git a/test/Instances/applyInstanceProps.spec.lua b/test/Instances/applyInstanceProps.spec.lua index a43278fa2..8c8ec4cd3 100644 --- a/test/Instances/applyInstanceProps.spec.lua +++ b/test/Instances/applyInstanceProps.spec.lua @@ -1,143 +1,197 @@ local Package = game:GetService("ReplicatedStorage").Fusion local applyInstanceProps = require(Package.Instances.applyInstanceProps) local Value = require(Package.State.Value) +local doCleanup = require(Package.Memory.doCleanup) return function() + FOCUS() it("should assign properties (constant)", function() + local scope = {} local instance = Instance.new("Folder") - applyInstanceProps({ - Name = "Bob", - }, instance) + applyInstanceProps( + scope, + { Name = "Bob" }, + instance + ) expect(instance.Name).to.equal("Bob") + doCleanup(scope) end) it("should assign properties (state)", function() - local value = Value("Bob") + local scope = {} + local value = Value(scope, "Bob") local instance = Instance.new("Folder") - applyInstanceProps({ - Name = value, - }, instance) + applyInstanceProps( + scope, + { Name = value }, + instance + ) expect(instance.Name).to.equal("Bob") value:set("Maya") task.wait() -- property changes are deferred expect(instance.Name).to.equal("Maya") + doCleanup(scope) end) it("should assign Parent (constant)", function() + local scope = {} local parent = Instance.new("Folder") local instance = Instance.new("Folder") - applyInstanceProps({ - Parent = parent, - }, instance) + applyInstanceProps( + scope, + { Parent = parent }, + instance + ) expect(instance.Parent).to.equal(parent) + doCleanup(scope) end) it("should assign Parent (state)", function() + local scope = {} local parent1 = Instance.new("Folder") local parent2 = Instance.new("Folder") - local value = Value(parent1) + local value = Value(scope, parent1) local instance = Instance.new("Folder") - applyInstanceProps({ - Parent = value, - }, instance) + applyInstanceProps( + scope, + { Parent = value }, + instance + ) expect(instance.Parent).to.equal(parent1) value:set(parent2) task.wait() -- property changes are deferred expect(instance.Parent).to.equal(parent2) + doCleanup(scope) end) it("should throw for non-existent properties (constant)", function() expect(function() + local scope = {} local instance = Instance.new("Folder") - applyInstanceProps({ - NotARealProperty = true, - }, instance) + applyInstanceProps( + scope, + { NotARealProperty = true }, + instance + ) + doCleanup(scope) end).to.throw("cannotAssignProperty") end) it("should throw for non-existent properties (state)", function() expect(function() + local scope = {} local instance = Instance.new("Folder") - applyInstanceProps({ - NotARealProperty = Value(true), - }, instance) + applyInstanceProps( + scope, + { NotARealProperty = Value(scope, true) }, + instance + ) + doCleanup(scope) end).to.throw("cannotAssignProperty") end) it("should throw for invalid property types (constant)", function() expect(function() + local scope = {} local instance = Instance.new("Folder") - applyInstanceProps({ - Name = Vector3.new(), - }, instance) + applyInstanceProps( + scope, + { Name = Vector3.new() }, + instance + ) + doCleanup(scope) end).to.throw("invalidPropertyType") end) it("should throw for invalid property types (state)", function() expect(function() + local scope = {} local instance = Instance.new("Folder") - applyInstanceProps({ - Name = Value(Vector3.new()), - }, instance) + applyInstanceProps( + scope, + { Name = Value(scope, Vector3.new()) }, + instance + ) + doCleanup(scope) end).to.throw("invalidPropertyType") end) it("should throw for invalid Parent types (constant)", function() expect(function() + local scope = {} local instance = Instance.new("Folder") - applyInstanceProps({ - Parent = Vector3.new(), - }, instance) + applyInstanceProps( + scope, + { Parent = Vector3.new() }, + instance + ) + doCleanup(scope) end).to.throw("invalidPropertyType") end) it("should throw for invalid Parent types (state)", function() expect(function() + local scope = {} local instance = Instance.new("Folder") - applyInstanceProps({ - Parent = Value(Vector3.new()), - }, instance) + applyInstanceProps( + scope, + { Parent = Value(scope, Vector3.new()) }, + instance + ) + doCleanup(scope) end).to.throw("invalidPropertyType") end) it("should throw for unrecognised keys in the property table", function() expect(function() + local scope = {} local instance = Instance.new("Folder") - applyInstanceProps({ - [2] = true, - }, instance) + applyInstanceProps( + scope, + { [2] = true }, + instance + ) + doCleanup(scope) end).to.throw("unrecognisedPropertyKey") end) it("should defer property changes", function() - local value = Value("Bob") + local scope = {} + local value = Value(scope, "Bob") local instance = Instance.new("Folder") - applyInstanceProps({ - Name = value, - }, instance) + applyInstanceProps( + scope, + { Name = value }, + instance + ) value:set("Maya") expect(instance.Name).to.equal("Bob") task.wait() expect(instance.Name).to.equal("Maya") + doCleanup(scope) end) it("should defer Parent changes", function() + local scope = {} local parent1 = Instance.new("Folder") local parent2 = Instance.new("Folder") - local value = Value(parent1) + local value = Value(scope, parent1) local instance = Instance.new("Folder") - applyInstanceProps({ - Parent = value, - }, instance) + applyInstanceProps( + scope, + { Parent = value }, + instance + ) value:set(parent2) expect(instance.Parent).to.equal(parent1) task.wait() expect(instance.Parent).to.equal(parent2) + doCleanup(scope) end) end From 5c7943a1681c012c6d8bfd37f74ec8a5d262440b Mon Sep 17 00:00:00 2001 From: Elttob Date: Tue, 28 Nov 2023 19:11:09 +0000 Subject: [PATCH 068/287] Update naming convention for scopes --- src/Instances/Attribute.lua | 8 ++++---- src/Instances/AttributeChange.lua | 4 ++-- src/Instances/AttributeOut.lua | 6 +++--- src/Instances/Children.lua | 4 ++-- src/Instances/Cleanup.lua | 4 ++-- src/Instances/OnChange.lua | 4 ++-- src/Instances/OnEvent.lua | 4 ++-- src/Instances/Out.lua | 6 +++--- src/Instances/Ref.lua | 4 ++-- src/Instances/applyInstanceProps.lua | 4 ++-- src/PubTypes.lua | 2 +- test/Instances/applyInstanceProps.spec.lua | 1 - 12 files changed, 25 insertions(+), 26 deletions(-) diff --git a/src/Instances/Attribute.lua b/src/Instances/Attribute.lua index b625b98bd..05f68a887 100644 --- a/src/Instances/Attribute.lua +++ b/src/Instances/Attribute.lua @@ -17,7 +17,7 @@ local function setAttribute(instance: Instance, attribute: string, value: any) instance:SetAttribute(attribute, value) end -local function bindAttribute(instance: Instance, attribute: string, value: any, cleanupTasks: PubTypes.Scope) +local function bindAttribute(instance: Instance, attribute: string, value: any, scope: PubTypes.Scope) if xtypeof(value) == "State" then local didDefer = false local function update() @@ -30,7 +30,7 @@ local function bindAttribute(instance: Instance, attribute: string, value: any, end end setAttribute(instance, attribute, peek(value)) - table.insert(cleanupTasks, Observer(value :: any):onChange(update)) + table.insert(scope, Observer(value :: any):onChange(update)) else setAttribute(instance, attribute, value) end @@ -46,8 +46,8 @@ local function Attribute(attributeName: string): PubTypes.SpecialKey logError("attributeNameNil") end - function AttributeKey:apply(attributeValue: any, applyTo: Instance, cleanupTasks: PubTypes.Scope) - bindAttribute(applyTo, attributeName, attributeValue, cleanupTasks) + function AttributeKey:apply(attributeValue: any, applyTo: Instance, scope: PubTypes.Scope) + bindAttribute(applyTo, attributeName, attributeValue, scope) end return AttributeKey end diff --git a/src/Instances/AttributeChange.lua b/src/Instances/AttributeChange.lua index cac6a8e75..14d17894a 100644 --- a/src/Instances/AttributeChange.lua +++ b/src/Instances/AttributeChange.lua @@ -20,7 +20,7 @@ local function AttributeChange(attributeName: string): PubTypes.SpecialKey logError("attributeNameNil") end - function attributeKey:apply(callback: any, applyTo: Instance, cleanupTasks: PubTypes.Scope) + function attributeKey:apply(callback: any, applyTo: Instance, scope: PubTypes.Scope) if typeof(callback) ~= "function" then logError("invalidAttributeChangeHandler", nil, attributeName) end @@ -29,7 +29,7 @@ local function AttributeChange(attributeName: string): PubTypes.SpecialKey logError("cannotConnectAttributeChange", nil, applyTo.ClassName, attributeName) else callback((applyTo :: any):GetAttribute(attributeName)) - table.insert(cleanupTasks, event:Connect(function() + table.insert(scope, event:Connect(function() callback((applyTo :: any):GetAttribute(attributeName)) end)) end diff --git a/src/Instances/AttributeOut.lua b/src/Instances/AttributeOut.lua index 257b1c66d..f787bd55a 100644 --- a/src/Instances/AttributeOut.lua +++ b/src/Instances/AttributeOut.lua @@ -16,7 +16,7 @@ local function AttributeOut(attributeName: string): PubTypes.SpecialKey attributeOutKey.kind = "AttributeOut" attributeOutKey.stage = "observer" - function attributeOutKey:apply(stateObject: PubTypes.StateObject, applyTo: Instance, cleanupTasks: { PubTypes.Task }) + function attributeOutKey:apply(stateObject: PubTypes.StateObject, applyTo: Instance, scope: { PubTypes.Task }) if xtypeof(stateObject) ~= "State" or stateObject.kind ~= "Value" then logError("invalidAttributeOutType") end @@ -28,10 +28,10 @@ local function AttributeOut(attributeName: string): PubTypes.SpecialKey logError("invalidOutAttributeName", applyTo.ClassName, attributeName) else stateObject:set((applyTo :: any):GetAttribute(attributeName)) - table.insert(cleanupTasks, event:Connect(function() + table.insert(scope, event:Connect(function() stateObject:set((applyTo :: any):GetAttribute(attributeName)) end)) - table.insert(cleanupTasks, function() + table.insert(scope, function() stateObject:set(nil) end) end diff --git a/src/Instances/Children.lua b/src/Instances/Children.lua index afab84779..2891814c0 100644 --- a/src/Instances/Children.lua +++ b/src/Instances/Children.lua @@ -23,7 +23,7 @@ Children.type = "SpecialKey" Children.kind = "Children" Children.stage = "descendants" -function Children:apply(propValue: any, applyTo: Instance, cleanupTasks: PubTypes.Scope) +function Children:apply(propValue: any, applyTo: Instance, scope: PubTypes.Scope) local newParented: Set = {} local oldParented: Set = {} @@ -136,7 +136,7 @@ function Children:apply(propValue: any, applyTo: Instance, cleanupTasks: PubType end end - table.insert(cleanupTasks, function() + table.insert(scope, function() propValue = nil updateQueued = true updateChildren() diff --git a/src/Instances/Cleanup.lua b/src/Instances/Cleanup.lua index 71823a138..b1093e924 100644 --- a/src/Instances/Cleanup.lua +++ b/src/Instances/Cleanup.lua @@ -13,8 +13,8 @@ Cleanup.type = "SpecialKey" Cleanup.kind = "Cleanup" Cleanup.stage = "observer" -function Cleanup:apply(userTask: any, applyTo: Instance, cleanupTasks: PubTypes.Scope) - table.insert(cleanupTasks, userTask) +function Cleanup:apply(userTask: any, applyTo: Instance, scope: PubTypes.Scope) + table.insert(scope, userTask) end return Cleanup \ No newline at end of file diff --git a/src/Instances/OnChange.lua b/src/Instances/OnChange.lua index 0e060f617..523f9c930 100644 --- a/src/Instances/OnChange.lua +++ b/src/Instances/OnChange.lua @@ -15,14 +15,14 @@ local function OnChange(propertyName: string): PubTypes.SpecialKey changeKey.kind = "OnChange" changeKey.stage = "observer" - function changeKey:apply(callback: any, applyTo: Instance, cleanupTasks: PubTypes.Scope) + function changeKey:apply(callback: any, applyTo: Instance, scope: PubTypes.Scope) local ok, event = pcall(applyTo.GetPropertyChangedSignal, applyTo, propertyName) if not ok then logError("cannotConnectChange", nil, applyTo.ClassName, propertyName) elseif typeof(callback) ~= "function" then logError("invalidChangeHandler", nil, propertyName) else - table.insert(cleanupTasks, event:Connect(function() + table.insert(scope, event:Connect(function() callback((applyTo :: any)[propertyName]) end)) end diff --git a/src/Instances/OnEvent.lua b/src/Instances/OnEvent.lua index bfad69877..d6ea0491a 100644 --- a/src/Instances/OnEvent.lua +++ b/src/Instances/OnEvent.lua @@ -19,14 +19,14 @@ local function OnEvent(eventName: string): PubTypes.SpecialKey eventKey.kind = "OnEvent" eventKey.stage = "observer" - function eventKey:apply(callback: any, applyTo: Instance, cleanupTasks: PubTypes.Scope) + function eventKey:apply(callback: any, applyTo: Instance, scope: PubTypes.Scope) local ok, event = pcall(getProperty_unsafe, applyTo, eventName) if not ok or typeof(event) ~= "RBXScriptSignal" then logError("cannotConnectEvent", nil, applyTo.ClassName, eventName) elseif typeof(callback) ~= "function" then logError("invalidEventHandler", nil, eventName) else - table.insert(cleanupTasks, event:Connect(callback)) + table.insert(scope, event:Connect(callback)) end end diff --git a/src/Instances/Out.lua b/src/Instances/Out.lua index c205f1d27..581a010e1 100644 --- a/src/Instances/Out.lua +++ b/src/Instances/Out.lua @@ -16,7 +16,7 @@ local function Out(propertyName: string): PubTypes.SpecialKey outKey.kind = "Out" outKey.stage = "observer" - function outKey:apply(outState: any, applyTo: Instance, cleanupTasks: { PubTypes.Task }) + function outKey:apply(outState: any, applyTo: Instance, scope: { PubTypes.Task }) local ok, event = pcall(applyTo.GetPropertyChangedSignal, applyTo, propertyName) if not ok then logError("invalidOutProperty", nil, applyTo.ClassName, propertyName) @@ -25,12 +25,12 @@ local function Out(propertyName: string): PubTypes.SpecialKey else outState:set((applyTo :: any)[propertyName]) table.insert( - cleanupTasks, + scope, event:Connect(function() outState:set((applyTo :: any)[propertyName]) end) ) - table.insert(cleanupTasks, function() + table.insert(scope, function() outState:set(nil) end) end diff --git a/src/Instances/Ref.lua b/src/Instances/Ref.lua index aa7d46f65..3b97d2d35 100644 --- a/src/Instances/Ref.lua +++ b/src/Instances/Ref.lua @@ -15,12 +15,12 @@ Ref.type = "SpecialKey" Ref.kind = "Ref" Ref.stage = "observer" -function Ref:apply(refState: any, applyTo: Instance, cleanupTasks: PubTypes.Scope) +function Ref:apply(refState: any, applyTo: Instance, scope: PubTypes.Scope) if xtypeof(refState) ~= "State" or refState.kind ~= "Value" then logError("invalidRefType") else refState:set(applyTo) - table.insert(cleanupTasks, function() + table.insert(scope, function() refState:set(nil) end) end diff --git a/src/Instances/applyInstanceProps.lua b/src/Instances/applyInstanceProps.lua index 78940e230..d53b015c4 100644 --- a/src/Instances/applyInstanceProps.lua +++ b/src/Instances/applyInstanceProps.lua @@ -50,7 +50,7 @@ local function setProperty(instance: Instance, property: string, value: any) end end -local function bindProperty(instance: Instance, property: string, value: PubTypes.CanBeState, cleanupTasks: PubTypes.Scope) +local function bindProperty(instance: Instance, property: string, value: PubTypes.CanBeState, scope: PubTypes.Scope) if xtypeof(value) == "State" then -- value is a state object - assign and observe for changes local willUpdate = false @@ -65,7 +65,7 @@ local function bindProperty(instance: Instance, property: string, value: PubType end setProperty(instance, property, peek(value)) - table.insert(cleanupTasks, Observer(cleanupTasks, value :: any):onChange(updateLater)) + table.insert(scope, Observer(scope, value :: any):onChange(updateLater)) else -- value is a constant - assign once only setProperty(instance, property, value) diff --git a/src/PubTypes.lua b/src/PubTypes.lua index 158de8354..8c8a0c596 100644 --- a/src/PubTypes.lua +++ b/src/PubTypes.lua @@ -175,7 +175,7 @@ export type SpecialKey = { type: "SpecialKey", kind: string, stage: "self" | "descendants" | "ancestor" | "observer", - apply: (SpecialKey, value: any, applyTo: Instance, cleanupTasks: {Task}) -> () + apply: (SpecialKey, value: any, applyTo: Instance, scope: {Task}) -> () } -- A collection of instances that may be parented to another instance. diff --git a/test/Instances/applyInstanceProps.spec.lua b/test/Instances/applyInstanceProps.spec.lua index 8c8ec4cd3..e8569f9b5 100644 --- a/test/Instances/applyInstanceProps.spec.lua +++ b/test/Instances/applyInstanceProps.spec.lua @@ -4,7 +4,6 @@ local Value = require(Package.State.Value) local doCleanup = require(Package.Memory.doCleanup) return function() - FOCUS() it("should assign properties (constant)", function() local scope = {} local instance = Instance.new("Folder") From 0d7215e78a249f1befd77e2148fef65125aaf8ff Mon Sep 17 00:00:00 2001 From: Elttob Date: Tue, 28 Nov 2023 19:11:32 +0000 Subject: [PATCH 069/287] Lingering use of {Task} --- src/Instances/AttributeOut.lua | 2 +- src/Instances/Out.lua | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Instances/AttributeOut.lua b/src/Instances/AttributeOut.lua index f787bd55a..2f0a5175a 100644 --- a/src/Instances/AttributeOut.lua +++ b/src/Instances/AttributeOut.lua @@ -16,7 +16,7 @@ local function AttributeOut(attributeName: string): PubTypes.SpecialKey attributeOutKey.kind = "AttributeOut" attributeOutKey.stage = "observer" - function attributeOutKey:apply(stateObject: PubTypes.StateObject, applyTo: Instance, scope: { PubTypes.Task }) + function attributeOutKey:apply(stateObject: PubTypes.StateObject, applyTo: Instance, scope: PubTypes.Scope) if xtypeof(stateObject) ~= "State" or stateObject.kind ~= "Value" then logError("invalidAttributeOutType") end diff --git a/src/Instances/Out.lua b/src/Instances/Out.lua index 581a010e1..fd8f93b14 100644 --- a/src/Instances/Out.lua +++ b/src/Instances/Out.lua @@ -16,7 +16,7 @@ local function Out(propertyName: string): PubTypes.SpecialKey outKey.kind = "Out" outKey.stage = "observer" - function outKey:apply(outState: any, applyTo: Instance, scope: { PubTypes.Task }) + function outKey:apply(outState: any, applyTo: Instance, scope: PubTypes.Scope) local ok, event = pcall(applyTo.GetPropertyChangedSignal, applyTo, propertyName) if not ok then logError("invalidOutProperty", nil, applyTo.ClassName, propertyName) From 5305829598ee18b8dd76a8fc1258f280fe74824c Mon Sep 17 00:00:00 2001 From: Elttob Date: Tue, 28 Nov 2023 19:16:08 +0000 Subject: [PATCH 070/287] Update New/Hydrate unit tests --- test/Instances/Hydrate.spec.lua | 12 +++++++----- test/Instances/New.spec.lua | 14 +++++++++----- 2 files changed, 16 insertions(+), 10 deletions(-) diff --git a/test/Instances/Hydrate.spec.lua b/test/Instances/Hydrate.spec.lua index c39019e53..66a5adfe0 100644 --- a/test/Instances/Hydrate.spec.lua +++ b/test/Instances/Hydrate.spec.lua @@ -1,20 +1,22 @@ local Package = game:GetService("ReplicatedStorage").Fusion local Hydrate = require(Package.Instances.Hydrate) +local doCleanup = require(Package.Memory.doCleanup) return function() it("should return the instance it was passed", function() + local scope = {} local ins = Instance.new("Folder") - - expect(Hydrate(ins) {}).to.equal(ins) + expect(Hydrate(scope, ins) {}).to.equal(ins) + doCleanup(scope) end) it("should apply properties to the instance", function() + local scope = {} local ins = Instance.new("Folder") - - Hydrate(ins) { + Hydrate(scope, ins) { Name = "Jeremy" } - expect(ins.Name).to.equal("Jeremy") + doCleanup(scope) end) end diff --git a/test/Instances/New.spec.lua b/test/Instances/New.spec.lua index fb5ef78d1..43ef6098d 100644 --- a/test/Instances/New.spec.lua +++ b/test/Instances/New.spec.lua @@ -1,28 +1,32 @@ local Package = game:GetService("ReplicatedStorage").Fusion local New = require(Package.Instances.New) local defaultProps = require(Package.Instances.defaultProps) +local doCleanup = require(Package.Memory.doCleanup) return function() it("should create a new instance", function() - local ins = New "Frame" {} - + local scope = {} + local ins = New (scope, "Frame") {} expect(typeof(ins) == "Instance").to.be.ok() expect(ins:IsA("Frame")).to.be.ok() end) it("should throw for non-existent class types", function() expect(function() - New "This is not a valid class type" {} + local scope = {} + New (scope, "This is not a valid class type") {} + doCleanup(scope) end).to.throw("cannotCreateClass") end) it("should apply 'sensible default' properties", function() for className, defaults in pairs(defaultProps) do - local ins = New (className) {} - + local scope = {} + local ins = New (scope, className) {} for propName, propValue in pairs(defaults) do expect(ins[propName]).to.equal(propValue) end + doCleanup(scope) end end) end From 744d82b8cde57d0ce1cc3c0b51337ee7dedab4b4 Mon Sep 17 00:00:00 2001 From: Elttob Date: Tue, 28 Nov 2023 19:29:06 +0000 Subject: [PATCH 071/287] Update SpecialKey signature + Attribute --- src/Instances/Attribute.lua | 48 ++++++------ src/Instances/applyInstanceProps.lua | 47 +++++++----- src/PubTypes.lua | 7 +- test/Instances/Attribute.spec.lua | 110 +++++++++++++++------------ 4 files changed, 118 insertions(+), 94 deletions(-) diff --git a/src/Instances/Attribute.lua b/src/Instances/Attribute.lua index 05f68a887..02cd98bd5 100644 --- a/src/Instances/Attribute.lua +++ b/src/Instances/Attribute.lua @@ -9,33 +9,10 @@ local Package = script.Parent.Parent local PubTypes = require(Package.PubTypes) local External = require(Package.External) local logError = require(Package.Logging.logError) -local xtypeof = require(Package.Utility.xtypeof) +local isState = require(Package.State.isState) local Observer = require(Package.State.Observer) local peek = require(Package.State.peek) -local function setAttribute(instance: Instance, attribute: string, value: any) - instance:SetAttribute(attribute, value) -end - -local function bindAttribute(instance: Instance, attribute: string, value: any, scope: PubTypes.Scope) - if xtypeof(value) == "State" then - local didDefer = false - local function update() - if not didDefer then - didDefer = true - External.doTaskDeferred(function() - didDefer = false - setAttribute(instance, attribute, peek(value)) - end) - end - end - setAttribute(instance, attribute, peek(value)) - table.insert(scope, Observer(value :: any):onChange(update)) - else - setAttribute(instance, attribute, value) - end -end - local function Attribute(attributeName: string): PubTypes.SpecialKey local AttributeKey = {} AttributeKey.type = "SpecialKey" @@ -46,8 +23,27 @@ local function Attribute(attributeName: string): PubTypes.SpecialKey logError("attributeNameNil") end - function AttributeKey:apply(attributeValue: any, applyTo: Instance, scope: PubTypes.Scope) - bindAttribute(applyTo, attributeName, attributeValue, scope) + function AttributeKey:apply( + scope: PubTypes.Scope, + value: any, + applyTo: Instance + ) + if isState(value) then + local didDefer = false + local function update() + if not didDefer then + didDefer = true + External.doTaskDeferred(function() + didDefer = false + applyTo:SetAttribute(attributeName, peek(value)) + end) + end + end + applyTo:SetAttribute(attributeName, peek(value)) + table.insert(scope, Observer(scope, value :: any):onChange(update)) + else + applyTo:SetAttribute(attributeName, value) + end end return AttributeKey end diff --git a/src/Instances/applyInstanceProps.lua b/src/Instances/applyInstanceProps.lua index d53b015c4..197cbdbf2 100644 --- a/src/Instances/applyInstanceProps.lua +++ b/src/Instances/applyInstanceProps.lua @@ -17,29 +17,35 @@ local Package = script.Parent.Parent local PubTypes = require(Package.PubTypes) local doCleanup = require(Package.Memory.doCleanup) local External = require(Package.External) -local xtypeof = require(Package.Utility.xtypeof) +local isState = require(Package.State.isState) local logError = require(Package.Logging.logError) local Observer = require(Package.State.Observer) local peek = require(Package.State.peek) +local xtypeof = require(Package.Utility.xtypeof) -local function setProperty_unsafe(instance: Instance, property: string, value: any) +local function setProperty_unsafe( + instance: Instance, + property: string, + value: any +) (instance :: any)[property] = value end -local function testPropertyAssignable(instance: Instance, property: string) +local function testPropertyAssignable( + instance: Instance, + property: string +) (instance :: any)[property] = (instance :: any)[property] end -local function setProperty(instance: Instance, property: string, value: any) +local function setProperty( + instance: Instance, + property: string, + value: any +) if not pcall(setProperty_unsafe, instance, property, value) then if not pcall(testPropertyAssignable, instance, property) then - if instance == nil then - -- reference has been lost - logError("setPropertyNilRef", nil, property, tostring(value)) - else - -- property is not assignable - logError("cannotAssignProperty", nil, instance.ClassName, property) - end + logError("cannotAssignProperty", nil, instance.ClassName, property) else -- property is assignable, but this specific assignment failed -- this typically implies the wrong type was received @@ -50,8 +56,13 @@ local function setProperty(instance: Instance, property: string, value: any) end end -local function bindProperty(instance: Instance, property: string, value: PubTypes.CanBeState, scope: PubTypes.Scope) - if xtypeof(value) == "State" then +local function bindProperty( + scope: PubTypes.Scope, + instance: Instance, + property: string, + value: PubTypes.CanBeState +) + if isState(value) then -- value is a state object - assign and observe for changes local willUpdate = false local function updateLater() @@ -101,15 +112,15 @@ local function applyInstanceProps( end else -- we don't recognise what this key is supposed to be - logError("unrecognisedPropertyKey", nil, xtypeof(key)) + logError("unrecognisedPropertyKey", nil, keyType) end end for key, value in pairs(specialKeys.self) do - key:apply(value, applyTo, scope) + key:apply(scope, value, applyTo) end for key, value in pairs(specialKeys.descendants) do - key:apply(value, applyTo, scope) + key:apply(scope, value, applyTo) end if props.Parent ~= nil then @@ -117,10 +128,10 @@ local function applyInstanceProps( end for key, value in pairs(specialKeys.ancestor) do - key:apply(value, applyTo, scope) + key:apply(scope, value, applyTo) end for key, value in pairs(specialKeys.observer) do - key:apply(value, applyTo, scope) + key:apply(scope, value, applyTo) end applyTo.Destroying:Connect(function() diff --git a/src/PubTypes.lua b/src/PubTypes.lua index 8c8a0c596..0ddf811a7 100644 --- a/src/PubTypes.lua +++ b/src/PubTypes.lua @@ -175,7 +175,12 @@ export type SpecialKey = { type: "SpecialKey", kind: string, stage: "self" | "descendants" | "ancestor" | "observer", - apply: (SpecialKey, value: any, applyTo: Instance, scope: {Task}) -> () + apply: ( + self: SpecialKey, + scope: Scope, + value: any, + applyTo: Instance + ) -> () } -- A collection of instances that may be parented to another instance. diff --git a/test/Instances/Attribute.spec.lua b/test/Instances/Attribute.spec.lua index cdcdd8ece..15b878ee2 100644 --- a/test/Instances/Attribute.spec.lua +++ b/test/Instances/Attribute.spec.lua @@ -3,59 +3,71 @@ local Package = game:GetService("ReplicatedStorage").Fusion local New = require(Package.Instances.New) local Attribute = require(Package.Instances.Attribute) local Value = require(Package.State.Value) +local doCleanup = require(Package.Memory.doCleanup) return function() - it("should create attributes (constant)", function() - local child = New "Folder" { - [Attribute "Foo"] = "Bar" - } - expect(child:GetAttribute("Foo")).to.equal("Bar") - end) + FOCUS() + it("creates attributes (constant)", function() + local scope = {} + local child = New(scope, "Folder") { + [Attribute "Foo"] = "Bar" + } + expect(child:GetAttribute("Foo")).to.equal("Bar") + doCleanup(scope) + end) - it("should create attributes (state)", function() - local attributeValue = Value("Bar") - local child = New "Folder" { - [Attribute "Foo"] = attributeValue - } - expect(child:GetAttribute("Foo")).to.equal("Bar") - end) - - it("should update attributes when state objects are updated", function() - local attributeValue = Value("Bar") - local child = New "Folder" { - [Attribute "Foo"] = attributeValue - } - expect(child:GetAttribute("Foo")).to.equal("Bar") - attributeValue:set("Baz") - task.wait() - expect(child:GetAttribute("Foo")).to.equal("Baz") - end) + it("creates attributes (state)", function() + local scope = {} + local attributeValue = Value(scope, "Bar") + local child = New(scope, "Folder") { + [Attribute "Foo"] = attributeValue + } + expect(child:GetAttribute("Foo")).to.equal("Bar") + end) + + it("updates attributes when state objects are updated", function() + local scope = {} + local attributeValue = Value(scope, "Bar") + local child = New(scope, "Folder") { + [Attribute "Foo"] = attributeValue + } + expect(child:GetAttribute("Foo")).to.equal("Bar") + attributeValue:set("Baz") + task.wait() + expect(child:GetAttribute("Foo")).to.equal("Baz") + doCleanup(scope) + end) - it("should error when given nil names (constant)", function() - expect(function() - local child = New "Folder" { - [Attribute(nil)] = "foo" - } - end).to.throw("attributeNameNil") - end) + it("errors when given nil names (constant)", function() + expect(function() + local scope = {} + local child = New(scope, "Folder") { + [Attribute(nil)] = "foo" + } + doCleanup(scope) + end).to.throw("attributeNameNil") + end) - it("should error when given nil names (state)", function() - expect(function() - local attributeValue = Value("foo") - local child = New "Folder" { - [Attribute(nil)] = attributeValue - } - end).to.throw("attributeNameNil") - end) + it("errors when given nil names (state)", function() + expect(function() + local scope = {} + local attributeValue = Value(scope, "foo") + local child = New(scope, "Folder") { + [Attribute(nil)] = attributeValue + } + doCleanup(scope) + end).to.throw("attributeNameNil") + end) - it("should defer attribute changes", function() - local value = Value("Bar") - local child = New "Folder" { - [Attribute "Foo"] = value - } - value:set("Baz") - expect(child:GetAttribute("Foo")).to.equal("Bar") - task.wait() - expect(child:GetAttribute("Foo")).to.equal("Baz") - end) + it("defers attribute changes", function() + local scope = {} + local value = Value(scope, "Bar") + local child = New(scope, "Folder") { + [Attribute "Foo"] = value + } + value:set("Baz") + expect(child:GetAttribute("Foo")).to.equal("Bar") + task.wait() + expect(child:GetAttribute("Foo")).to.equal("Baz") + end) end From 2824b2b41a8cd4cc69f1928e439a6344a11dde01 Mon Sep 17 00:00:00 2001 From: Elttob Date: Tue, 28 Nov 2023 19:31:30 +0000 Subject: [PATCH 072/287] Fix applyInstanceProps --- src/Instances/applyInstanceProps.lua | 8 ++++---- test/Instances/Attribute.spec.lua | 1 - test/Instances/applyInstanceProps.spec.lua | 1 + 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/Instances/applyInstanceProps.lua b/src/Instances/applyInstanceProps.lua index 197cbdbf2..bdb55c550 100644 --- a/src/Instances/applyInstanceProps.lua +++ b/src/Instances/applyInstanceProps.lua @@ -24,8 +24,8 @@ local peek = require(Package.State.peek) local xtypeof = require(Package.Utility.xtypeof) local function setProperty_unsafe( - instance: Instance, - property: string, + instance: Instance, + property: string, value: any ) (instance :: any)[property] = value @@ -100,7 +100,7 @@ local function applyInstanceProps( if keyType == "string" then if key ~= "Parent" then - bindProperty(applyTo, key :: string, value, scope) + bindProperty(scope, applyTo, key :: string, value) end elseif keyType == "SpecialKey" then local stage = (key :: PubTypes.SpecialKey).stage @@ -124,7 +124,7 @@ local function applyInstanceProps( end if props.Parent ~= nil then - bindProperty(applyTo, "Parent", props.Parent, scope) + bindProperty(scope, applyTo, "Parent", props.Parent) end for key, value in pairs(specialKeys.ancestor) do diff --git a/test/Instances/Attribute.spec.lua b/test/Instances/Attribute.spec.lua index 15b878ee2..e32bcf6fc 100644 --- a/test/Instances/Attribute.spec.lua +++ b/test/Instances/Attribute.spec.lua @@ -6,7 +6,6 @@ local Value = require(Package.State.Value) local doCleanup = require(Package.Memory.doCleanup) return function() - FOCUS() it("creates attributes (constant)", function() local scope = {} local child = New(scope, "Folder") { diff --git a/test/Instances/applyInstanceProps.spec.lua b/test/Instances/applyInstanceProps.spec.lua index e8569f9b5..8c8ec4cd3 100644 --- a/test/Instances/applyInstanceProps.spec.lua +++ b/test/Instances/applyInstanceProps.spec.lua @@ -4,6 +4,7 @@ local Value = require(Package.State.Value) local doCleanup = require(Package.Memory.doCleanup) return function() + FOCUS() it("should assign properties (constant)", function() local scope = {} local instance = Instance.new("Folder") From 4287a7166aaafe6dcf5de507f4210cc6f9317e90 Mon Sep 17 00:00:00 2001 From: Elttob Date: Tue, 28 Nov 2023 19:33:00 +0000 Subject: [PATCH 073/287] Update doCleanup spec --- test/Instances/applyInstanceProps.spec.lua | 1 - test/Memory/doCleanup.spec.lua | 4 ++-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/test/Instances/applyInstanceProps.spec.lua b/test/Instances/applyInstanceProps.spec.lua index 8c8ec4cd3..e8569f9b5 100644 --- a/test/Instances/applyInstanceProps.spec.lua +++ b/test/Instances/applyInstanceProps.spec.lua @@ -4,7 +4,6 @@ local Value = require(Package.State.Value) local doCleanup = require(Package.Memory.doCleanup) return function() - FOCUS() it("should assign properties (constant)", function() local scope = {} local instance = Instance.new("Folder") diff --git a/test/Memory/doCleanup.spec.lua b/test/Memory/doCleanup.spec.lua index 72a9ee290..a6643cc2e 100644 --- a/test/Memory/doCleanup.spec.lua +++ b/test/Memory/doCleanup.spec.lua @@ -4,7 +4,7 @@ local doCleanup = require(Package.Memory.doCleanup) return function() it("should destroy instances", function() - local instance = New "Folder" {} + local instance = New({}, "Folder") {} -- one of the only reliable ways to test for proper destruction local conn = instance.AncestryChanged:Connect(function() end) @@ -14,7 +14,7 @@ return function() end) it("should disconnect connections", function() - local instance = New "Folder" {} + local instance = New({}, "Folder") {} local conn = instance.AncestryChanged:Connect(function() end) doCleanup(conn) From 9263e1ee25ee979a83120f303d9a7cd3ca99ae74 Mon Sep 17 00:00:00 2001 From: Elttob Date: Tue, 28 Nov 2023 19:33:27 +0000 Subject: [PATCH 074/287] Update doNothing spec --- test/Memory/doNothing.spec.lua | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/Memory/doNothing.spec.lua b/test/Memory/doNothing.spec.lua index 3e810ef3e..e322e6c59 100644 --- a/test/Memory/doNothing.spec.lua +++ b/test/Memory/doNothing.spec.lua @@ -4,7 +4,7 @@ local doNothing = require(Package.Memory.doNothing) return function() it("should not destroy instances", function() - local instance = New "Folder" {} + local instance = New({}, "Folder") {} -- one of the only reliable ways to test for proper destruction local conn = instance.AncestryChanged:Connect(function() end) @@ -14,7 +14,7 @@ return function() end) it("should not disconnect connections", function() - local instance = New "Folder" {} + local instance = New({}, "Folder") {} local conn = instance.AncestryChanged:Connect(function() end) doNothing(conn) From 78e56f69e5bdfda75bb090364b8ef0897c36a4fa Mon Sep 17 00:00:00 2001 From: Elttob Date: Wed, 29 Nov 2023 10:27:54 +0000 Subject: [PATCH 075/287] Update AttributeOut --- src/Instances/Attribute.lua | 22 ++--- src/Instances/AttributeChange.lua | 16 ++-- src/Instances/AttributeOut.lua | 6 +- src/Instances/Children.lua | 8 +- src/Instances/Cleanup.lua | 20 ----- src/init.lua | 1 - test/Animation/Tween.spec.lua | 2 +- test/Instances/AttributeChange.spec.lua | 14 ++- test/Instances/AttributeOut.spec.lua | 21 +++-- test/Instances/Children.spec.lua | 79 ++++++++++------- test/Instances/Cleanup.spec.lua | 110 ------------------------ test/Instances/OnChange.spec.lua | 17 +++- test/Instances/OnEvent.spec.lua | 19 ++-- test/Instances/Out.spec.lua | 13 ++- test/Instances/Ref.spec.lua | 7 +- test/init.spec.lua | 1 - 16 files changed, 146 insertions(+), 210 deletions(-) delete mode 100644 src/Instances/Cleanup.lua delete mode 100644 test/Instances/Cleanup.spec.lua diff --git a/src/Instances/Attribute.lua b/src/Instances/Attribute.lua index 02cd98bd5..79493cbc7 100644 --- a/src/Instances/Attribute.lua +++ b/src/Instances/Attribute.lua @@ -2,7 +2,7 @@ --[[ A special key for property tables, which allows users to apply custom - attributes to instances + attributes to instances ]] local Package = script.Parent.Parent @@ -14,16 +14,16 @@ local Observer = require(Package.State.Observer) local peek = require(Package.State.peek) local function Attribute(attributeName: string): PubTypes.SpecialKey - local AttributeKey = {} - AttributeKey.type = "SpecialKey" - AttributeKey.kind = "Attribute" - AttributeKey.stage = "self" + local AttributeKey = {} + AttributeKey.type = "SpecialKey" + AttributeKey.kind = "Attribute" + AttributeKey.stage = "self" - if attributeName == nil then - logError("attributeNameNil") - end + if attributeName == nil then + logError("attributeNameNil") + end - function AttributeKey:apply( + function AttributeKey:apply( scope: PubTypes.Scope, value: any, applyTo: Instance @@ -44,8 +44,8 @@ local function Attribute(attributeName: string): PubTypes.SpecialKey else applyTo:SetAttribute(attributeName, value) end - end - return AttributeKey + end + return AttributeKey end return Attribute \ No newline at end of file diff --git a/src/Instances/AttributeChange.lua b/src/Instances/AttributeChange.lua index 14d17894a..bb5b615a9 100644 --- a/src/Instances/AttributeChange.lua +++ b/src/Instances/AttributeChange.lua @@ -2,7 +2,7 @@ --[[ A special key for property tables, which allows users to connect to - an attribute change on an instance. + an attribute change on an instance. ]] local Package = script.Parent.Parent @@ -17,20 +17,24 @@ local function AttributeChange(attributeName: string): PubTypes.SpecialKey attributeKey.stage = "observer" if attributeName == nil then - logError("attributeNameNil") + logError("attributeNameNil") end - function attributeKey:apply(callback: any, applyTo: Instance, scope: PubTypes.Scope) - if typeof(callback) ~= "function" then + function attributeKey:apply( + scope: PubTypes.Scope, + value: any, + applyTo: Instance + ) + if typeof(value) ~= "function" then logError("invalidAttributeChangeHandler", nil, attributeName) end local ok, event = pcall(applyTo.GetAttributeChangedSignal, applyTo, attributeName) if not ok then logError("cannotConnectAttributeChange", nil, applyTo.ClassName, attributeName) else - callback((applyTo :: any):GetAttribute(attributeName)) + value((applyTo :: any):GetAttribute(attributeName)) table.insert(scope, event:Connect(function() - callback((applyTo :: any):GetAttribute(attributeName)) + value((applyTo :: any):GetAttribute(attributeName)) end)) end end diff --git a/src/Instances/AttributeOut.lua b/src/Instances/AttributeOut.lua index 2f0a5175a..ec3806e97 100644 --- a/src/Instances/AttributeOut.lua +++ b/src/Instances/AttributeOut.lua @@ -16,7 +16,11 @@ local function AttributeOut(attributeName: string): PubTypes.SpecialKey attributeOutKey.kind = "AttributeOut" attributeOutKey.stage = "observer" - function attributeOutKey:apply(stateObject: PubTypes.StateObject, applyTo: Instance, scope: PubTypes.Scope) + function attributeOutKey:apply( + scope: PubTypes.Scope, + stateObject: any, + applyTo: Instance + ) if xtypeof(stateObject) ~= "State" or stateObject.kind ~= "Value" then logError("invalidAttributeOutType") end diff --git a/src/Instances/Children.lua b/src/Instances/Children.lua index 2891814c0..57141360a 100644 --- a/src/Instances/Children.lua +++ b/src/Instances/Children.lua @@ -23,7 +23,11 @@ Children.type = "SpecialKey" Children.kind = "Children" Children.stage = "descendants" -function Children:apply(propValue: any, applyTo: Instance, scope: PubTypes.Scope) +function Children:apply( + scope: PubTypes.Scope, + propValue: any, + applyTo: Instance +) local newParented: Set = {} local oldParented: Set = {} @@ -82,7 +86,7 @@ function Children:apply(propValue: any, applyTo: Instance, scope: PubTypes.Scope local disconnect = oldDisconnects[child] if disconnect == nil then -- wasn't previously present - disconnect = Observer(child):onChange(queueUpdate) + disconnect = Observer(scope, child):onChange(queueUpdate) else -- previously here; we want to reuse, so remove from old -- set so we don't encounter it during unparenting diff --git a/src/Instances/Cleanup.lua b/src/Instances/Cleanup.lua deleted file mode 100644 index b1093e924..000000000 --- a/src/Instances/Cleanup.lua +++ /dev/null @@ -1,20 +0,0 @@ ---!strict - ---[[ - A special key for property tables, which adds user-specified tasks to be run - when the instance is destroyed. -]] - -local Package = script.Parent.Parent -local PubTypes = require(Package.PubTypes) - -local Cleanup = {} -Cleanup.type = "SpecialKey" -Cleanup.kind = "Cleanup" -Cleanup.stage = "observer" - -function Cleanup:apply(userTask: any, applyTo: Instance, scope: PubTypes.Scope) - table.insert(scope, userTask) -end - -return Cleanup \ No newline at end of file diff --git a/src/init.lua b/src/init.lua index 94449fc6e..158081c04 100644 --- a/src/init.lua +++ b/src/init.lua @@ -60,7 +60,6 @@ local Fusion = restrictRead("Fusion", { Ref = require(script.Instances.Ref), Out = require(script.Instances.Out), - Cleanup = require(script.Instances.Cleanup), Children = require(script.Instances.Children), OnEvent = require(script.Instances.OnEvent), OnChange = require(script.Instances.OnChange), diff --git a/test/Animation/Tween.spec.lua b/test/Animation/Tween.spec.lua index 2c69ed88c..a511f4baa 100644 --- a/test/Animation/Tween.spec.lua +++ b/test/Animation/Tween.spec.lua @@ -43,7 +43,7 @@ return function() -- local followerState = Value(UDim2.fromScale(1, 1)) -- local tween = Tween(followerState, TweenInfo.new(0.1)) - -- local testInstance = New "Frame" { Size = tween } + -- local testInstance = New(scope, "Frame") { Size = tween } -- followerState:set(UDim2.fromScale(0.5, 0.5)) diff --git a/test/Instances/AttributeChange.spec.lua b/test/Instances/AttributeChange.spec.lua index 90aba84c0..06a82e49e 100644 --- a/test/Instances/AttributeChange.spec.lua +++ b/test/Instances/AttributeChange.spec.lua @@ -2,12 +2,13 @@ local Package = game:GetService("ReplicatedStorage").Fusion local New = require(Package.Instances.New) local Attribute = require(Package.Instances.Attribute) local AttributeChange = require(Package.Instances.AttributeChange) -local Value = require(Package.State.Value) +local doCleanup = require(Package.Memory.doCleanup) return function() it("should connect attribute change handlers", function() + local scope = {} local changeCount = 0 - local child = New "Folder" { + local child = New(scope, "Folder") { [Attribute "Foo"] = "Bar", [AttributeChange "Foo"] = function() changeCount += 1 @@ -17,11 +18,13 @@ return function() child:SetAttribute("Foo", "Baz") task.wait() expect(changeCount).never.to.equal(0) + doCleanup(scope) end) it("should pass the updated value as an argument", function() + local scope = {} local updatedValue = "" - local child = New "Folder" { + local child = New(scope, "Folder") { [AttributeChange "Foo"] = function(newValue) updatedValue = newValue end @@ -30,13 +33,16 @@ return function() child:SetAttribute("Foo", "Baz") task.wait() expect(updatedValue).to.equal("Baz") + doCleanup(scope) end) it("should error when given an invalid handler", function() expect(function() - local child = New "Folder" { + local scope = {} + local child = New(scope, "Folder") { [AttributeChange "Foo"] = 0 } + doCleanup(scope) end).to.throw("invalidAttributeChangeHandler") end) end diff --git a/test/Instances/AttributeOut.spec.lua b/test/Instances/AttributeOut.spec.lua index 0cbf872a6..9da5b30f5 100644 --- a/test/Instances/AttributeOut.spec.lua +++ b/test/Instances/AttributeOut.spec.lua @@ -4,11 +4,13 @@ local Attribute = require(Package.Instances.Attribute) local AttributeOut = require(Package.Instances.AttributeOut) local Value = require(Package.State.Value) local peek = require(Package.State.peek) +local doCleanup = require(Package.Memory.doCleanup) return function() it("should update when attributes are changed externally", function() - local attributeValue = Value() - local child = New "Folder" { + local scope = {} + local attributeValue = Value(scope, nil) + local child = New(scope, "Folder") { [AttributeOut "Foo"] = attributeValue } @@ -16,12 +18,14 @@ return function() child:SetAttribute("Foo", "Bar") task.wait() expect(peek(attributeValue)).to.equal("Bar") + doCleanup(scope) end) it("should update when state objects linked update", function() - local attributeValue = Value("Foo") - local attributeOutValue = Value() - local child = New "Folder" { + local scope = {} + local attributeValue = Value(scope, "Foo") + local attributeOutValue = Value(scope) + local child = New(scope, "Folder") { [Attribute "Foo"] = attributeValue, [AttributeOut "Foo"] = attributeOutValue } @@ -29,11 +33,13 @@ return function() attributeValue:set("Bar") task.wait() expect(peek(attributeOutValue)).to.equal("Bar") + doCleanup(scope) end) it("should work with two-way connections", function() - local attributeValue = Value("Bar") - local child = New "Folder" { + local scope = {} + local attributeValue = Value(scope, "Bar") + local child = New(scope, "Folder") { [Attribute "Foo"] = attributeValue, [AttributeOut "Foo"] = attributeValue } @@ -45,5 +51,6 @@ return function() child:SetAttribute("Foo", "Biff") task.wait() expect(peek(attributeValue)).to.equal("Biff") + doCleanup(scope) end) end \ No newline at end of file diff --git a/test/Instances/Children.spec.lua b/test/Instances/Children.spec.lua index b77f58c4c..2396b6b0d 100644 --- a/test/Instances/Children.spec.lua +++ b/test/Instances/Children.spec.lua @@ -2,32 +2,36 @@ local Package = game:GetService("ReplicatedStorage").Fusion local New = require(Package.Instances.New) local Children = require(Package.Instances.Children) local Value = require(Package.State.Value) +local doCleanup = require(Package.Memory.doCleanup) return function() it("should assign single children to instances", function() - local ins = New "Folder" { + local scope = {} + local ins = New(scope, "Folder") { Name = "Bob", - [Children] = New "Folder" { + [Children] = New(scope, "Folder") { Name = "Fred" } } expect(ins:FindFirstChild("Fred")).to.be.ok() + doCleanup(scope) end) it("should assign multiple children to instances", function() - local ins = New "Folder" { + local scope = {} + local ins = New(scope, "Folder") { Name = "Bob", [Children] = { - New "Folder" { + New(scope, "Folder") { Name = "Fred" }, - New "Folder" { + New(scope, "Folder") { Name = "George" }, - New "Folder" { + New(scope, "Folder") { Name = "Harry" } } @@ -36,22 +40,24 @@ return function() expect(ins:FindFirstChild("Fred")).to.be.ok() expect(ins:FindFirstChild("George")).to.be.ok() expect(ins:FindFirstChild("Harry")).to.be.ok() + doCleanup(scope) end) it("should flatten children to be assigned", function() - local ins = New "Folder" { + local scope = {} + local ins = New(scope, "Folder") { Name = "Bob", [Children] = { - New "Folder" { + New(scope, "Folder") { Name = "Fred" }, { - New "Folder" { + New(scope, "Folder") { Name = "George" }, { - New "Folder" { + New(scope, "Folder") { Name = "Harry" } } @@ -62,17 +68,19 @@ return function() expect(ins:FindFirstChild("Fred")).to.be.ok() expect(ins:FindFirstChild("George")).to.be.ok() expect(ins:FindFirstChild("Harry")).to.be.ok() + doCleanup(scope) end) it("should bind State objects passed as children", function() - local child1 = New "Folder" {} - local child2 = New "Folder" {} - local child3 = New "Folder" {} - local child4 = New "Folder" {} + local scope = {} + local child1 = New(scope, "Folder") {} + local child2 = New(scope, "Folder") {} + local child3 = New(scope, "Folder") {} + local child4 = New(scope, "Folder") {} - local children = Value({child1}) + local children = Value(scope, {child1}) - local parent = New "Folder" { + local parent = New(scope, "Folder") { [Children] = { children } @@ -94,15 +102,17 @@ return function() expect(child2.Parent).to.equal(parent) expect(child3.Parent).to.equal(parent) expect(child4.Parent).to.equal(parent) + doCleanup(scope) end) it("should defer updates to State children", function() - local child1 = New "Folder" {} - local child2 = New "Folder" {} + local scope = {} + local child1 = New(scope, "Folder") {} + local child2 = New(scope, "Folder") {} - local children = Value(child1) + local children = Value(scope, child1) - local parent = New "Folder" { + local parent = New(scope, "Folder") { [Children] = { children } @@ -119,24 +129,26 @@ return function() expect(child1.Parent).to.equal(nil) expect(child2.Parent).to.equal(parent) + doCleanup(scope) end) it("should recursively bind State children", function() - local child1 = New "Folder" {} - local child2 = New "Folder" {} - local child3 = New "Folder" {} - local child4 = New "Folder" {} + local scope = {} + local child1 = New(scope, "Folder") {} + local child2 = New(scope, "Folder") {} + local child3 = New(scope, "Folder") {} + local child4 = New(scope, "Folder") {} - local children = Value({ + local children = Value(scope, { child1, - Value(child2), - Value({ + Value(scope, child2), + Value(scope, { child3, - Value(Value(child4)) + Value(scope, Value(scope, child4)) }) }) - local parent = New "Folder" { + local parent = New(scope, "Folder") { [Children] = { children } @@ -146,14 +158,16 @@ return function() expect(child2.Parent).to.equal(parent) expect(child3.Parent).to.equal(parent) expect(child4.Parent).to.equal(parent) + doCleanup(scope) end) it("should allow for State children to be nil", function() - local child = New "Folder" {} + local scope = {} + local child = New(scope, "Folder") {} - local children = Value(nil) + local children = Value(scope, nil) - local parent = New "Folder" { + local parent = New(scope, "Folder") { [Children] = { children } @@ -170,5 +184,6 @@ return function() task.wait() expect(child.Parent).to.equal(nil) + doCleanup(scope) end) end diff --git a/test/Instances/Cleanup.spec.lua b/test/Instances/Cleanup.spec.lua deleted file mode 100644 index 216720474..000000000 --- a/test/Instances/Cleanup.spec.lua +++ /dev/null @@ -1,110 +0,0 @@ -local Package = game:GetService("ReplicatedStorage").Fusion -local New = require(Package.Instances.New) -local Cleanup = require(Package.Instances.Cleanup) - -return function() - it("should destroy instances", function() - local instance = New "Folder" {} - -- one of the only reliable ways to test for proper destruction - local conn = instance.AncestryChanged:Connect(function() end) - - local child = New "Folder" { - [Cleanup] = instance - } - child:Destroy() - task.wait() - - expect(conn.Connected).to.equal(false) - end) - - it("should disconnect connections", function() - local instance = New "Folder" {} - local conn = instance.AncestryChanged:Connect(function() end) - - local child = New "Folder" { - [Cleanup] = conn - } - child:Destroy() - task.wait() - - expect(conn.Connected).to.equal(false) - end) - - it("should invoke callbacks", function() - local didRun = false - - local child = New "Folder" { - [Cleanup] = function() - didRun = true - end - } - child:Destroy() - task.wait() - - expect(didRun).to.equal(true) - end) - - it("should invoke :destroy() methods", function() - local didRun = false - - local child = New "Folder" { - [Cleanup] = { - destroy = function() - didRun = true - end - } - } - child:Destroy() - task.wait() - - expect(didRun).to.equal(true) - end) - - it("should invoke :Destroy() methods", function() - local didRun = false - - local child = New "Folder" { - [Cleanup] = { - Destroy = function() - didRun = true - end - } - } - child:Destroy() - task.wait() - - expect(didRun).to.equal(true) - end) - - it("should clean up contents of arrays", function() - local numRuns = 0 - - local function doRun() - numRuns += 1 - end - - local child = New "Folder" { - [Cleanup] = {doRun, doRun, doRun} - } - child:Destroy() - task.wait() - - expect(numRuns).to.equal(3) - end) - - it("should clean up contents of nested arrays", function() - local numRuns = 0 - - local function doRun() - numRuns += 1 - end - - local child = New "Folder" { - [Cleanup] = {{doRun, {doRun, {doRun}}}} - } - child:Destroy() - task.wait() - - expect(numRuns).to.equal(3) - end) -end diff --git a/test/Instances/OnChange.spec.lua b/test/Instances/OnChange.spec.lua index add8fdf5e..b905951f7 100644 --- a/test/Instances/OnChange.spec.lua +++ b/test/Instances/OnChange.spec.lua @@ -1,11 +1,13 @@ local Package = game:GetService("ReplicatedStorage").Fusion local New = require(Package.Instances.New) local OnChange = require(Package.Instances.OnChange) +local doCleanup = require(Package.Memory.doCleanup) return function() it("should connect property change handlers", function() + local scope = {} local fires = 0 - local ins = New "Folder" { + local ins = New(scope, "Folder") { Name = "Foo", [OnChange "Name"] = function(newName) @@ -16,11 +18,13 @@ return function() ins.Name = "Bar" task.wait() expect(fires).never.to.equal(0) + doCleanup(scope) end) it("should pass the new value to the handler", function() + local scope = {} local arg = nil - local ins = New "Folder" { + local ins = New(scope, "Folder") { Name = "Foo", [OnChange "Name"] = function(newName) @@ -31,21 +35,25 @@ return function() ins.Name = "Bar" task.wait() expect(arg).to.equal("Bar") + doCleanup(scope) end) it("should throw when connecting to non-existent property changes", function() + local scope = {} expect(function() - New "Folder" { + New(scope, "Folder") { Name = "Foo", [OnChange "Frobulate"] = function() end } + doCleanup(scope) end).to.throw("cannotConnectChange") end) it("shouldn't fire property changes during initialisation", function() + local scope = {} local fires = 0 - local ins = New "Folder" { + local ins = New(scope, "Folder") { Parent = game, Name = "Foo", @@ -62,5 +70,6 @@ return function() ins:Destroy() task.wait() expect(totalFires).to.equal(0) + doCleanup(scope) end) end diff --git a/test/Instances/OnEvent.spec.lua b/test/Instances/OnEvent.spec.lua index c8e93179c..43409057e 100644 --- a/test/Instances/OnEvent.spec.lua +++ b/test/Instances/OnEvent.spec.lua @@ -2,11 +2,13 @@ local Package = game:GetService("ReplicatedStorage").Fusion local New = require(Package.Instances.New) local Children = require(Package.Instances.Children) local OnEvent = require(Package.Instances.OnEvent) +local doCleanup = require(Package.Memory.doCleanup) return function() it("should connect event handlers", function() + local scope = {} local fires = 0 - local ins = New "Folder" { + local ins = New(scope, "Folder") { Name = "Foo", [OnEvent "AncestryChanged"] = function() @@ -20,31 +22,37 @@ return function() task.wait() expect(fires).never.to.equal(0) + doCleanup(scope) end) it("should throw for non-existent events", function() expect(function() - New "Folder" { + local scope = {} + New(scope, "Folder") { Name = "Foo", [OnEvent "Frobulate"] = function() end } + doCleanup(scope) end).to.throw("cannotConnectEvent") end) it("should throw for non-event event handlers", function() expect(function() - New "Folder" { + local scope = {} + New(scope, "Folder") { Name = "Foo", [OnEvent "Name"] = function() end } + doCleanup(scope) end).to.throw("cannotConnectEvent") end) it("shouldn't fire events during initialisation", function() + local scope = {} local fires = 0 - local ins = New "Folder" { + local ins = New(scope, "Folder") { Parent = game, Name = "Foo", @@ -60,7 +68,7 @@ return function() fires += 1 end, - [Children] = New "Folder" { + [Children] = New(scope, "Folder") { Name = "Bar" } } @@ -71,5 +79,6 @@ return function() task.wait() expect(totalFires).to.equal(0) + doCleanup(scope) end) end diff --git a/test/Instances/Out.spec.lua b/test/Instances/Out.spec.lua index 062fff59f..5be15d255 100644 --- a/test/Instances/Out.spec.lua +++ b/test/Instances/Out.spec.lua @@ -3,12 +3,14 @@ local New = require(Package.Instances.New) local Out = require(Package.Instances.Out) local Value = require(Package.State.Value) local peek = require(Package.State.peek) +local doCleanup = require(Package.Memory.doCleanup) return function() it("should reflect external property changes", function() + local scope = {} local outValue = Value() - local child = New "Folder" { + local child = New(scope, "Folder") { [Out "Name"] = outValue } expect(peek(outValue)).to.equal("Folder") @@ -16,13 +18,15 @@ return function() child.Name = "Mary" task.wait() expect(peek(outValue)).to.equal("Mary") + doCleanup(scope) end) it("should reflect property changes from bound state", function() + local scope = {} local outValue = Value() local inValue = Value("Gabriel") - local child = New "Folder" { + local child = New(scope, "Folder") { Name = inValue, [Out "Name"] = outValue } @@ -31,12 +35,14 @@ return function() inValue:set("Joseph") task.wait() expect(peek(outValue)).to.equal("Joseph") + doCleanup(scope) end) it("should support two-way data binding", function() + local scope = {} local twoWayValue = Value("Gabriel") - local child = New "Folder" { + local child = New(scope, "Folder") { Name = twoWayValue, [Out "Name"] = twoWayValue } @@ -49,5 +55,6 @@ return function() child.Name = "Elias" task.wait() expect(peek(twoWayValue)).to.equal("Elias") + doCleanup(scope) end) end diff --git a/test/Instances/Ref.spec.lua b/test/Instances/Ref.spec.lua index cfa395b24..5208255b0 100644 --- a/test/Instances/Ref.spec.lua +++ b/test/Instances/Ref.spec.lua @@ -3,15 +3,18 @@ local New = require(Package.Instances.New) local Ref = require(Package.Instances.Ref) local Value = require(Package.State.Value) local peek = require(Package.State.peek) +local doCleanup = require(Package.Memory.doCleanup) return function() it("should set State objects passed as [Ref]", function() - local refValue = Value() + local scope = {} + local refValue = Value(scope, nil) - local child = New "Folder" { + local child = New(scope, "Folder") { [Ref] = refValue } expect(peek(refValue)).to.equal(child) + doCleanup(scope) end) end diff --git a/test/init.spec.lua b/test/init.spec.lua index 7933c6096..6aa15bb4c 100644 --- a/test/init.spec.lua +++ b/test/init.spec.lua @@ -30,7 +30,6 @@ return function() Ref = "table", Out = "function", - Cleanup = "table", Children = "table", OnEvent = "function", OnChange = "function", From 77f8dc0c140ea09baa0d06a6570c548ae50eb306 Mon Sep 17 00:00:00 2001 From: Elttob Date: Wed, 29 Nov 2023 10:33:14 +0000 Subject: [PATCH 076/287] Update OnChange --- src/Instances/OnChange.lua | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/Instances/OnChange.lua b/src/Instances/OnChange.lua index 523f9c930..e7c0fdae8 100644 --- a/src/Instances/OnChange.lua +++ b/src/Instances/OnChange.lua @@ -15,7 +15,11 @@ local function OnChange(propertyName: string): PubTypes.SpecialKey changeKey.kind = "OnChange" changeKey.stage = "observer" - function changeKey:apply(callback: any, applyTo: Instance, scope: PubTypes.Scope) + function changeKey:apply( + scope: PubTypes.Scope, + callback: any, + applyTo: Instance + ) local ok, event = pcall(applyTo.GetPropertyChangedSignal, applyTo, propertyName) if not ok then logError("cannotConnectChange", nil, applyTo.ClassName, propertyName) From 82cbdab9c9ecaaab032b8596ded08f411ac15b1d Mon Sep 17 00:00:00 2001 From: Elttob Date: Wed, 29 Nov 2023 10:34:43 +0000 Subject: [PATCH 077/287] Update OnEvent/Out/Ref --- src/Instances/OnEvent.lua | 6 +++++- src/Instances/Out.lua | 6 +++++- src/Instances/Ref.lua | 6 +++++- 3 files changed, 15 insertions(+), 3 deletions(-) diff --git a/src/Instances/OnEvent.lua b/src/Instances/OnEvent.lua index d6ea0491a..b6054be57 100644 --- a/src/Instances/OnEvent.lua +++ b/src/Instances/OnEvent.lua @@ -19,7 +19,11 @@ local function OnEvent(eventName: string): PubTypes.SpecialKey eventKey.kind = "OnEvent" eventKey.stage = "observer" - function eventKey:apply(callback: any, applyTo: Instance, scope: PubTypes.Scope) + function eventKey:apply( + scope: PubTypes.Scope, + callback: any, + applyTo: Instance + ) local ok, event = pcall(getProperty_unsafe, applyTo, eventName) if not ok or typeof(event) ~= "RBXScriptSignal" then logError("cannotConnectEvent", nil, applyTo.ClassName, eventName) diff --git a/src/Instances/Out.lua b/src/Instances/Out.lua index fd8f93b14..510ab7970 100644 --- a/src/Instances/Out.lua +++ b/src/Instances/Out.lua @@ -16,7 +16,11 @@ local function Out(propertyName: string): PubTypes.SpecialKey outKey.kind = "Out" outKey.stage = "observer" - function outKey:apply(outState: any, applyTo: Instance, scope: PubTypes.Scope) + function outKey:apply( + scope: PubTypes.Scope, + outState: any, + applyTo: Instance + ) local ok, event = pcall(applyTo.GetPropertyChangedSignal, applyTo, propertyName) if not ok then logError("invalidOutProperty", nil, applyTo.ClassName, propertyName) diff --git a/src/Instances/Ref.lua b/src/Instances/Ref.lua index 3b97d2d35..14983ac04 100644 --- a/src/Instances/Ref.lua +++ b/src/Instances/Ref.lua @@ -15,7 +15,11 @@ Ref.type = "SpecialKey" Ref.kind = "Ref" Ref.stage = "observer" -function Ref:apply(refState: any, applyTo: Instance, scope: PubTypes.Scope) +function Ref:apply( + scope: PubTypes.Scope, + refState: any, + applyTo: Instance +) if xtypeof(refState) ~= "State" or refState.kind ~= "Value" then logError("invalidRefType") else From 92b295e50ee12ee2090cfd10c10df9acc8f14530 Mon Sep 17 00:00:00 2001 From: Elttob Date: Wed, 29 Nov 2023 10:37:11 +0000 Subject: [PATCH 078/287] Fix Out unit tests --- test/Instances/Out.spec.lua | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/test/Instances/Out.spec.lua b/test/Instances/Out.spec.lua index 5be15d255..8b311ff1d 100644 --- a/test/Instances/Out.spec.lua +++ b/test/Instances/Out.spec.lua @@ -8,7 +8,7 @@ local doCleanup = require(Package.Memory.doCleanup) return function() it("should reflect external property changes", function() local scope = {} - local outValue = Value() + local outValue = Value(scope, nil) local child = New(scope, "Folder") { [Out "Name"] = outValue @@ -23,8 +23,8 @@ return function() it("should reflect property changes from bound state", function() local scope = {} - local outValue = Value() - local inValue = Value("Gabriel") + local outValue = Value(scope, nil) + local inValue = Value(scope, "Gabriel") local child = New(scope, "Folder") { Name = inValue, @@ -40,7 +40,7 @@ return function() it("should support two-way data binding", function() local scope = {} - local twoWayValue = Value("Gabriel") + local twoWayValue = Value(scope, "Gabriel") local child = New(scope, "Folder") { Name = twoWayValue, From 0303f6b95651d659c63469acef20cf55bf99a49b Mon Sep 17 00:00:00 2001 From: Elttob Date: Wed, 29 Nov 2023 11:20:15 +0000 Subject: [PATCH 079/287] Add `childAppearsLater` for lifetime analysis --- src/Logging/messages.lua | 1 + src/Memory/childAppearsLater.lua | 42 ++++++++++++++++++++++++++++++++ 2 files changed, 43 insertions(+) create mode 100644 src/Memory/childAppearsLater.lua diff --git a/src/Logging/messages.lua b/src/Logging/messages.lua index ce2ef0c80..c4962d664 100644 --- a/src/Logging/messages.lua +++ b/src/Logging/messages.lua @@ -32,6 +32,7 @@ return { mistypedSpringSpeed = "The speed of a spring must be a number. (got a %s)", mistypedTweenInfo = "The tween info of a tween must be a TweenInfo. (got a %s)", noTaskScheduler = "Fusion is not connected to an external task scheduler.", + possiblyOutlives = "%s might outlive the %s that's using it; ensure they are in the same scope and created in the correct order.", springTypeMismatch = "The type '%s' doesn't match the spring's type '%s'.", stateGetWasRemoved = "`StateObject:get()` has been replaced by `use()` and `peek()` - see discussion #217 on GitHub.", strictReadError = "'%s' is not a valid member of '%s'.", diff --git a/src/Memory/childAppearsLater.lua b/src/Memory/childAppearsLater.lua new file mode 100644 index 000000000..b335c2772 --- /dev/null +++ b/src/Memory/childAppearsLater.lua @@ -0,0 +1,42 @@ +--!strict + +--[[ + Checks the scope of the parent to see if the child will be destroyed before + the parent is destroyed. +]] +local Package = script.Parent.Parent +local PubTypes = require(Package.PubTypes) +local logWarn = require(Package.Logging.logWarn) + +local function childAppearsLaterImpl( + haystack: {any}, + alreadyChecked: {[any]: true}, + parent: any, + child: any +): boolean? + for index = #haystack, 1, -1 do + local value = haystack[index] + if value == parent then + return false + elseif value == child then + return true + elseif typeof(value) == "table" and value[1] ~= nil and alreadyChecked[value] == nil then + alreadyChecked[value] = true + local appearsLater = childAppearsLaterImpl(value, alreadyChecked, parent, child) + if appearsLater ~= nil then + return appearsLater + end + end + end + return nil +end + +local function childAppearsLater( + scope: PubTypes.Scope, + parent: any, + child: any +): boolean + return childAppearsLaterImpl(scope, {}, parent, child) == true +end + +return childAppearsLater \ No newline at end of file From fbf5a6e8f98847e63216dcbbadd9af04badd3252 Mon Sep 17 00:00:00 2001 From: Elttob Date: Wed, 29 Nov 2023 11:33:58 +0000 Subject: [PATCH 080/287] Unit tests for `childAppearsLater` --- test/Memory/childAppearsLater.spec.lua | 49 ++++++++++++++++++++++++++ 1 file changed, 49 insertions(+) create mode 100644 test/Memory/childAppearsLater.spec.lua diff --git a/test/Memory/childAppearsLater.spec.lua b/test/Memory/childAppearsLater.spec.lua new file mode 100644 index 000000000..6d8da96e9 --- /dev/null +++ b/test/Memory/childAppearsLater.spec.lua @@ -0,0 +1,49 @@ +local Package = game:GetService("ReplicatedStorage").Fusion +local childAppearsLater = require(Package.Memory.childAppearsLater) + +return function() + it("allows correct order in flat arrays", function() + expect(childAppearsLater({"p", "c"}, "p", "c")).to.equal(true) + expect(childAppearsLater({1, 2, 3, "p", "c", 4, 5, 6}, "p", "c")).to.equal(true) + expect(childAppearsLater({1, "p", 2, 3, 4, "c", 5, 6}, "p", "c")).to.equal(true) + expect(childAppearsLater({"p", 1, 2, 3, 4, 5, 6, "c"}, "p", "c")).to.equal(true) + expect(childAppearsLater({"p", "c", 1, 2, 3, 4, 5, 6}, "p", "c")).to.equal(true) + expect(childAppearsLater({1, 2, 3, 4, 5, 6, "p", "c"}, "p", "c")).to.equal(true) + end) + it("disallows incorrect order in flat arrays", function() + expect(childAppearsLater({"c", "p"}, "p", "c")).to.equal(false) + expect(childAppearsLater({1, 2, 3, "c", "p", 4, 5, 6}, "p", "c")).to.equal(false) + expect(childAppearsLater({1, "c", 2, 3, 4, "p", 5, 6}, "p", "c")).to.equal(false) + expect(childAppearsLater({"c", 1, 2, 3, 4, 5, 6, "p"}, "p", "c")).to.equal(false) + expect(childAppearsLater({"c", "p", 1, 2, 3, 4, 5, 6}, "p", "c")).to.equal(false) + expect(childAppearsLater({1, 2, 3, 4, 5, 6, "c", "p"}, "p", "c")).to.equal(false) + end) + it("disallows absent children in flat arrays", function() + expect(childAppearsLater({"p"}, "p", "c")).to.equal(false) + expect(childAppearsLater({1, 2, 3, "p", 4, 5, 6}, "p", "c")).to.equal(false) + expect(childAppearsLater({"p", 1, 2, 3, 4, 5, 6}, "p", "c")).to.equal(false) + expect(childAppearsLater({1, 2, 3, 4, 5, 6, "p"}, "p", "c")).to.equal(false) + end) + it("allows correct order in nested arrays", function() + expect(childAppearsLater({{"p"}, {"c"}}, "p", "c")).to.equal(true) + expect(childAppearsLater({1, {2, 3, "p"}, "c", 4, 5, 6}, "p", "c")).to.equal(true) + expect(childAppearsLater({{1, {"p"}}, 2, {3, 4}, {"c", 5, 6}}, "p", "c")).to.equal(true) + expect(childAppearsLater({"p", 1, 2, 3, {4, 5, 6, "c"}}, "p", "c")).to.equal(true) + expect(childAppearsLater({"p", {"c", 1, 2, 3, 4}, 5, 6}, "p", "c")).to.equal(true) + expect(childAppearsLater({1, {{2, 3}, 4, 5, 6, "p"}, "c"}, "p", "c")).to.equal(true) + end) + it("disallows incorrect order in nested arrays", function() + expect(childAppearsLater({{"c"}, {"p"}}, "p", "c")).to.equal(false) + expect(childAppearsLater({1, {2, 3, "c"}, "p", 4, 5, 6}, "p", "c")).to.equal(false) + expect(childAppearsLater({{1, {"c"}}, 2, {3, 4}, {"p", 5, 6}}, "p", "c")).to.equal(false) + expect(childAppearsLater({"c", 1, 2, 3, {4, 5, 6, "p"}}, "p", "c")).to.equal(false) + expect(childAppearsLater({"c", {"p", 1, 2, 3, 4}, 5, 6}, "p", "c")).to.equal(false) + expect(childAppearsLater({1, {{2, 3}, 4, 5, 6, "c"}, "p"}, "p", "c")).to.equal(false) + end) + it("disallows absent children in nested arrays", function() + expect(childAppearsLater({{"p"}}, "p", "c")).to.equal(false) + expect(childAppearsLater({1, {2, 3, "p"}, 4, 5, 6}, "p", "c")).to.equal(false) + expect(childAppearsLater({"p", 1, 2, 3, {4, 5, 6}}, "p", "c")).to.equal(false) + expect(childAppearsLater({1, {{2, 3}, 4, 5, 6}, "p"}, "p", "c")).to.equal(false) + end) +end \ No newline at end of file From 1fd380e9dd6bef038eec1169cb2e87efd2fec341 Mon Sep 17 00:00:00 2001 From: Elttob Date: Wed, 29 Nov 2023 11:43:30 +0000 Subject: [PATCH 081/287] Rename to assertLifetime --- src/Logging/messages.lua | 2 +- ...ildAppearsLater.lua => assertLifetime.lua} | 20 ++++---- test/Memory/assertLifetime.spec.lua | 49 +++++++++++++++++++ test/Memory/childAppearsLater.spec.lua | 49 ------------------- 4 files changed, 60 insertions(+), 60 deletions(-) rename src/Memory/{childAppearsLater.lua => assertLifetime.lua} (60%) create mode 100644 test/Memory/assertLifetime.spec.lua delete mode 100644 test/Memory/childAppearsLater.spec.lua diff --git a/src/Logging/messages.lua b/src/Logging/messages.lua index c4962d664..3a5406837 100644 --- a/src/Logging/messages.lua +++ b/src/Logging/messages.lua @@ -32,7 +32,7 @@ return { mistypedSpringSpeed = "The speed of a spring must be a number. (got a %s)", mistypedTweenInfo = "The tween info of a tween must be a TweenInfo. (got a %s)", noTaskScheduler = "Fusion is not connected to an external task scheduler.", - possiblyOutlives = "%s might outlive the %s that's using it; ensure they are in the same scope and created in the correct order.", + possiblyOutlives = "%s might outlive the %s that's using it; ensure they are constructed in the correct order with the same scope.", springTypeMismatch = "The type '%s' doesn't match the spring's type '%s'.", stateGetWasRemoved = "`StateObject:get()` has been replaced by `use()` and `peek()` - see discussion #217 on GitHub.", strictReadError = "'%s' is not a valid member of '%s'.", diff --git a/src/Memory/childAppearsLater.lua b/src/Memory/assertLifetime.lua similarity index 60% rename from src/Memory/childAppearsLater.lua rename to src/Memory/assertLifetime.lua index b335c2772..7297b4a0f 100644 --- a/src/Memory/childAppearsLater.lua +++ b/src/Memory/assertLifetime.lua @@ -2,27 +2,27 @@ --[[ Checks the scope of the parent to see if the child will be destroyed before - the parent is destroyed. + the parent is destroyed. If the parent is omitted, the scope will only be + checked for presence of the child. ]] local Package = script.Parent.Parent local PubTypes = require(Package.PubTypes) -local logWarn = require(Package.Logging.logWarn) -local function childAppearsLaterImpl( +local function assertLifetimeImpl( haystack: {any}, alreadyChecked: {[any]: true}, - parent: any, + parent: any?, child: any ): boolean? for index = #haystack, 1, -1 do local value = haystack[index] - if value == parent then + if parent ~= nil and value == parent then return false elseif value == child then return true elseif typeof(value) == "table" and value[1] ~= nil and alreadyChecked[value] == nil then alreadyChecked[value] = true - local appearsLater = childAppearsLaterImpl(value, alreadyChecked, parent, child) + local appearsLater = assertLifetimeImpl(value, alreadyChecked, parent, child) if appearsLater ~= nil then return appearsLater end @@ -31,12 +31,12 @@ local function childAppearsLaterImpl( return nil end -local function childAppearsLater( +local function assertLifetime( scope: PubTypes.Scope, - parent: any, + parent: any?, child: any ): boolean - return childAppearsLaterImpl(scope, {}, parent, child) == true + return assertLifetimeImpl(scope, {}, parent, child) == true end -return childAppearsLater \ No newline at end of file +return assertLifetime \ No newline at end of file diff --git a/test/Memory/assertLifetime.spec.lua b/test/Memory/assertLifetime.spec.lua new file mode 100644 index 000000000..1c852abd6 --- /dev/null +++ b/test/Memory/assertLifetime.spec.lua @@ -0,0 +1,49 @@ +local Package = game:GetService("ReplicatedStorage").Fusion +local assertLifetime = require(Package.Memory.assertLifetime) + +return function() + it("allows correct order in flat arrays", function() + expect(assertLifetime({"p", "c"}, "p", "c")).to.equal(true) + expect(assertLifetime({1, 2, 3, "p", "c", 4, 5, 6}, "p", "c")).to.equal(true) + expect(assertLifetime({1, "p", 2, 3, 4, "c", 5, 6}, "p", "c")).to.equal(true) + expect(assertLifetime({"p", 1, 2, 3, 4, 5, 6, "c"}, "p", "c")).to.equal(true) + expect(assertLifetime({"p", "c", 1, 2, 3, 4, 5, 6}, "p", "c")).to.equal(true) + expect(assertLifetime({1, 2, 3, 4, 5, 6, "p", "c"}, "p", "c")).to.equal(true) + end) + it("disallows incorrect order in flat arrays", function() + expect(assertLifetime({"c", "p"}, "p", "c")).to.equal(false) + expect(assertLifetime({1, 2, 3, "c", "p", 4, 5, 6}, "p", "c")).to.equal(false) + expect(assertLifetime({1, "c", 2, 3, 4, "p", 5, 6}, "p", "c")).to.equal(false) + expect(assertLifetime({"c", 1, 2, 3, 4, 5, 6, "p"}, "p", "c")).to.equal(false) + expect(assertLifetime({"c", "p", 1, 2, 3, 4, 5, 6}, "p", "c")).to.equal(false) + expect(assertLifetime({1, 2, 3, 4, 5, 6, "c", "p"}, "p", "c")).to.equal(false) + end) + it("disallows absent children in flat arrays", function() + expect(assertLifetime({"p"}, "p", "c")).to.equal(false) + expect(assertLifetime({1, 2, 3, "p", 4, 5, 6}, "p", "c")).to.equal(false) + expect(assertLifetime({"p", 1, 2, 3, 4, 5, 6}, "p", "c")).to.equal(false) + expect(assertLifetime({1, 2, 3, 4, 5, 6, "p"}, "p", "c")).to.equal(false) + end) + it("allows correct order in nested arrays", function() + expect(assertLifetime({{"p"}, {"c"}}, "p", "c")).to.equal(true) + expect(assertLifetime({1, {2, 3, "p"}, "c", 4, 5, 6}, "p", "c")).to.equal(true) + expect(assertLifetime({{1, {"p"}}, 2, {3, 4}, {"c", 5, 6}}, "p", "c")).to.equal(true) + expect(assertLifetime({"p", 1, 2, 3, {4, 5, 6, "c"}}, "p", "c")).to.equal(true) + expect(assertLifetime({"p", {"c", 1, 2, 3, 4}, 5, 6}, "p", "c")).to.equal(true) + expect(assertLifetime({1, {{2, 3}, 4, 5, 6, "p"}, "c"}, "p", "c")).to.equal(true) + end) + it("disallows incorrect order in nested arrays", function() + expect(assertLifetime({{"c"}, {"p"}}, "p", "c")).to.equal(false) + expect(assertLifetime({1, {2, 3, "c"}, "p", 4, 5, 6}, "p", "c")).to.equal(false) + expect(assertLifetime({{1, {"c"}}, 2, {3, 4}, {"p", 5, 6}}, "p", "c")).to.equal(false) + expect(assertLifetime({"c", 1, 2, 3, {4, 5, 6, "p"}}, "p", "c")).to.equal(false) + expect(assertLifetime({"c", {"p", 1, 2, 3, 4}, 5, 6}, "p", "c")).to.equal(false) + expect(assertLifetime({1, {{2, 3}, 4, 5, 6, "c"}, "p"}, "p", "c")).to.equal(false) + end) + it("disallows absent children in nested arrays", function() + expect(assertLifetime({{"p"}}, "p", "c")).to.equal(false) + expect(assertLifetime({1, {2, 3, "p"}, 4, 5, 6}, "p", "c")).to.equal(false) + expect(assertLifetime({"p", 1, 2, 3, {4, 5, 6}}, "p", "c")).to.equal(false) + expect(assertLifetime({1, {{2, 3}, 4, 5, 6}, "p"}, "p", "c")).to.equal(false) + end) +end \ No newline at end of file diff --git a/test/Memory/childAppearsLater.spec.lua b/test/Memory/childAppearsLater.spec.lua deleted file mode 100644 index 6d8da96e9..000000000 --- a/test/Memory/childAppearsLater.spec.lua +++ /dev/null @@ -1,49 +0,0 @@ -local Package = game:GetService("ReplicatedStorage").Fusion -local childAppearsLater = require(Package.Memory.childAppearsLater) - -return function() - it("allows correct order in flat arrays", function() - expect(childAppearsLater({"p", "c"}, "p", "c")).to.equal(true) - expect(childAppearsLater({1, 2, 3, "p", "c", 4, 5, 6}, "p", "c")).to.equal(true) - expect(childAppearsLater({1, "p", 2, 3, 4, "c", 5, 6}, "p", "c")).to.equal(true) - expect(childAppearsLater({"p", 1, 2, 3, 4, 5, 6, "c"}, "p", "c")).to.equal(true) - expect(childAppearsLater({"p", "c", 1, 2, 3, 4, 5, 6}, "p", "c")).to.equal(true) - expect(childAppearsLater({1, 2, 3, 4, 5, 6, "p", "c"}, "p", "c")).to.equal(true) - end) - it("disallows incorrect order in flat arrays", function() - expect(childAppearsLater({"c", "p"}, "p", "c")).to.equal(false) - expect(childAppearsLater({1, 2, 3, "c", "p", 4, 5, 6}, "p", "c")).to.equal(false) - expect(childAppearsLater({1, "c", 2, 3, 4, "p", 5, 6}, "p", "c")).to.equal(false) - expect(childAppearsLater({"c", 1, 2, 3, 4, 5, 6, "p"}, "p", "c")).to.equal(false) - expect(childAppearsLater({"c", "p", 1, 2, 3, 4, 5, 6}, "p", "c")).to.equal(false) - expect(childAppearsLater({1, 2, 3, 4, 5, 6, "c", "p"}, "p", "c")).to.equal(false) - end) - it("disallows absent children in flat arrays", function() - expect(childAppearsLater({"p"}, "p", "c")).to.equal(false) - expect(childAppearsLater({1, 2, 3, "p", 4, 5, 6}, "p", "c")).to.equal(false) - expect(childAppearsLater({"p", 1, 2, 3, 4, 5, 6}, "p", "c")).to.equal(false) - expect(childAppearsLater({1, 2, 3, 4, 5, 6, "p"}, "p", "c")).to.equal(false) - end) - it("allows correct order in nested arrays", function() - expect(childAppearsLater({{"p"}, {"c"}}, "p", "c")).to.equal(true) - expect(childAppearsLater({1, {2, 3, "p"}, "c", 4, 5, 6}, "p", "c")).to.equal(true) - expect(childAppearsLater({{1, {"p"}}, 2, {3, 4}, {"c", 5, 6}}, "p", "c")).to.equal(true) - expect(childAppearsLater({"p", 1, 2, 3, {4, 5, 6, "c"}}, "p", "c")).to.equal(true) - expect(childAppearsLater({"p", {"c", 1, 2, 3, 4}, 5, 6}, "p", "c")).to.equal(true) - expect(childAppearsLater({1, {{2, 3}, 4, 5, 6, "p"}, "c"}, "p", "c")).to.equal(true) - end) - it("disallows incorrect order in nested arrays", function() - expect(childAppearsLater({{"c"}, {"p"}}, "p", "c")).to.equal(false) - expect(childAppearsLater({1, {2, 3, "c"}, "p", 4, 5, 6}, "p", "c")).to.equal(false) - expect(childAppearsLater({{1, {"c"}}, 2, {3, 4}, {"p", 5, 6}}, "p", "c")).to.equal(false) - expect(childAppearsLater({"c", 1, 2, 3, {4, 5, 6, "p"}}, "p", "c")).to.equal(false) - expect(childAppearsLater({"c", {"p", 1, 2, 3, 4}, 5, 6}, "p", "c")).to.equal(false) - expect(childAppearsLater({1, {{2, 3}, 4, 5, 6, "c"}, "p"}, "p", "c")).to.equal(false) - end) - it("disallows absent children in nested arrays", function() - expect(childAppearsLater({{"p"}}, "p", "c")).to.equal(false) - expect(childAppearsLater({1, {2, 3, "p"}, 4, 5, 6}, "p", "c")).to.equal(false) - expect(childAppearsLater({"p", 1, 2, 3, {4, 5, 6}}, "p", "c")).to.equal(false) - expect(childAppearsLater({1, {{2, 3}, 4, 5, 6}, "p"}, "p", "c")).to.equal(false) - end) -end \ No newline at end of file From 123621d73167d6ed1b444188c6b9af27f83d45d1 Mon Sep 17 00:00:00 2001 From: Elttob Date: Wed, 29 Nov 2023 11:43:43 +0000 Subject: [PATCH 082/287] Ref asserts lifetime of given value --- src/Instances/Ref.lua | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/Instances/Ref.lua b/src/Instances/Ref.lua index 14983ac04..a3cbc4cf4 100644 --- a/src/Instances/Ref.lua +++ b/src/Instances/Ref.lua @@ -7,8 +7,10 @@ local Package = script.Parent.Parent local PubTypes = require(Package.PubTypes) +local logWarn = require(Package.Logging.logWarn) local logError = require(Package.Logging.logError) local xtypeof = require(Package.Utility.xtypeof) +local assertLifetime = require(Package.Memory.assertLifetime) local Ref = {} Ref.type = "SpecialKey" @@ -23,10 +25,10 @@ function Ref:apply( if xtypeof(refState) ~= "State" or refState.kind ~= "Value" then logError("invalidRefType") else + if not assertLifetime(scope, nil, refState) then + logWarn("possiblyOutlives") + end refState:set(applyTo) - table.insert(scope, function() - refState:set(nil) - end) end end From 4ce13e7b3e4c5b13ad5279263b61c72b6b84b640 Mon Sep 17 00:00:00 2001 From: Elttob Date: Wed, 29 Nov 2023 11:46:05 +0000 Subject: [PATCH 083/287] Out asserts lifetime of given state --- src/Instances/Out.lua | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/Instances/Out.lua b/src/Instances/Out.lua index 510ab7970..3edaf133f 100644 --- a/src/Instances/Out.lua +++ b/src/Instances/Out.lua @@ -8,7 +8,9 @@ local Package = script.Parent.Parent local PubTypes = require(Package.PubTypes) local logError = require(Package.Logging.logError) +local logWarn = require(Package.Logging.logWarn) local xtypeof = require(Package.Utility.xtypeof) +local assertLifetime = require(Package.Memory.assertLifetime) local function Out(propertyName: string): PubTypes.SpecialKey local outKey = {} @@ -27,6 +29,9 @@ local function Out(propertyName: string): PubTypes.SpecialKey elseif xtypeof(outState) ~= "State" or outState.kind ~= "Value" then logError("invalidOutType") else + if not assertLifetime(scope, nil, outState) then + logWarn("possiblyOutlives") + end outState:set((applyTo :: any)[propertyName]) table.insert( scope, @@ -34,9 +39,6 @@ local function Out(propertyName: string): PubTypes.SpecialKey outState:set((applyTo :: any)[propertyName]) end) ) - table.insert(scope, function() - outState:set(nil) - end) end end From ad2b3d8ca6c9b028695f43ad618ad786e524d902 Mon Sep 17 00:00:00 2001 From: Elttob Date: Wed, 29 Nov 2023 11:58:15 +0000 Subject: [PATCH 084/287] AttributeOut asserts lifetime of given object --- src/Instances/AttributeOut.lua | 8 +++++--- src/Instances/Out.lua | 2 +- src/Instances/Ref.lua | 2 +- 3 files changed, 7 insertions(+), 5 deletions(-) diff --git a/src/Instances/AttributeOut.lua b/src/Instances/AttributeOut.lua index ec3806e97..a63357d3e 100644 --- a/src/Instances/AttributeOut.lua +++ b/src/Instances/AttributeOut.lua @@ -8,7 +8,9 @@ local Package = script.Parent.Parent local PubTypes = require(Package.PubTypes) local logError = require(Package.Logging.logError) +local logWarn = require(Package.Logging.logWarn) local xtypeof = require(Package.Utility.xtypeof) +local assertLifetime = require(Package.Memory.assertLifetime) local function AttributeOut(attributeName: string): PubTypes.SpecialKey local attributeOutKey = {} @@ -31,13 +33,13 @@ local function AttributeOut(attributeName: string): PubTypes.SpecialKey if not ok then logError("invalidOutAttributeName", applyTo.ClassName, attributeName) else + if not assertLifetime(scope, nil, stateObject) then + logWarn("possiblyOutlives", "Value object", `[AttributeOut "{attributeName}"]`) + end stateObject:set((applyTo :: any):GetAttribute(attributeName)) table.insert(scope, event:Connect(function() stateObject:set((applyTo :: any):GetAttribute(attributeName)) end)) - table.insert(scope, function() - stateObject:set(nil) - end) end end diff --git a/src/Instances/Out.lua b/src/Instances/Out.lua index 3edaf133f..705adb1e1 100644 --- a/src/Instances/Out.lua +++ b/src/Instances/Out.lua @@ -30,7 +30,7 @@ local function Out(propertyName: string): PubTypes.SpecialKey logError("invalidOutType") else if not assertLifetime(scope, nil, outState) then - logWarn("possiblyOutlives") + logWarn("possiblyOutlives", "Value object", `[Out "{propertyName}"]`) end outState:set((applyTo :: any)[propertyName]) table.insert( diff --git a/src/Instances/Ref.lua b/src/Instances/Ref.lua index a3cbc4cf4..eb1a78a8f 100644 --- a/src/Instances/Ref.lua +++ b/src/Instances/Ref.lua @@ -26,7 +26,7 @@ function Ref:apply( logError("invalidRefType") else if not assertLifetime(scope, nil, refState) then - logWarn("possiblyOutlives") + logWarn("possiblyOutlives", "Value object", "[Ref]") end refState:set(applyTo) end From e262109cffc8512945d353941e802fb120a03702 Mon Sep 17 00:00:00 2001 From: Elttob Date: Wed, 29 Nov 2023 12:00:27 +0000 Subject: [PATCH 085/287] Attribute asserts lifetime of bound values --- src/Instances/Attribute.lua | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/Instances/Attribute.lua b/src/Instances/Attribute.lua index 79493cbc7..435559b6f 100644 --- a/src/Instances/Attribute.lua +++ b/src/Instances/Attribute.lua @@ -9,9 +9,11 @@ local Package = script.Parent.Parent local PubTypes = require(Package.PubTypes) local External = require(Package.External) local logError = require(Package.Logging.logError) +local logWarn = require(Package.Logging.logWarn) local isState = require(Package.State.isState) local Observer = require(Package.State.Observer) local peek = require(Package.State.peek) +local assertLifetime = require(Package.Memory.assertLifetime) local function Attribute(attributeName: string): PubTypes.SpecialKey local AttributeKey = {} @@ -29,6 +31,9 @@ local function Attribute(attributeName: string): PubTypes.SpecialKey applyTo: Instance ) if isState(value) then + if not assertLifetime(scope, nil, value) then + logWarn("possiblyOutlives", value.kind .. " object", `[Attribute "{attributeName}"]`) + end local didDefer = false local function update() if not didDefer then From d4e5b40daaa65b9adfb79472f42ae9f0565d541f Mon Sep 17 00:00:00 2001 From: Elttob Date: Wed, 29 Nov 2023 12:01:11 +0000 Subject: [PATCH 086/287] More concise warns for outlives --- src/Instances/Attribute.lua | 2 +- src/Instances/AttributeOut.lua | 2 +- src/Instances/Out.lua | 2 +- src/Instances/Ref.lua | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Instances/Attribute.lua b/src/Instances/Attribute.lua index 435559b6f..d2b6701e6 100644 --- a/src/Instances/Attribute.lua +++ b/src/Instances/Attribute.lua @@ -32,7 +32,7 @@ local function Attribute(attributeName: string): PubTypes.SpecialKey ) if isState(value) then if not assertLifetime(scope, nil, value) then - logWarn("possiblyOutlives", value.kind .. " object", `[Attribute "{attributeName}"]`) + logWarn("possiblyOutlives", value.kind, `[Attribute "{attributeName}"]`) end local didDefer = false local function update() diff --git a/src/Instances/AttributeOut.lua b/src/Instances/AttributeOut.lua index a63357d3e..4ff70829e 100644 --- a/src/Instances/AttributeOut.lua +++ b/src/Instances/AttributeOut.lua @@ -34,7 +34,7 @@ local function AttributeOut(attributeName: string): PubTypes.SpecialKey logError("invalidOutAttributeName", applyTo.ClassName, attributeName) else if not assertLifetime(scope, nil, stateObject) then - logWarn("possiblyOutlives", "Value object", `[AttributeOut "{attributeName}"]`) + logWarn("possiblyOutlives", "Value", `[AttributeOut "{attributeName}"]`) end stateObject:set((applyTo :: any):GetAttribute(attributeName)) table.insert(scope, event:Connect(function() diff --git a/src/Instances/Out.lua b/src/Instances/Out.lua index 705adb1e1..df165f82a 100644 --- a/src/Instances/Out.lua +++ b/src/Instances/Out.lua @@ -30,7 +30,7 @@ local function Out(propertyName: string): PubTypes.SpecialKey logError("invalidOutType") else if not assertLifetime(scope, nil, outState) then - logWarn("possiblyOutlives", "Value object", `[Out "{propertyName}"]`) + logWarn("possiblyOutlives", "Value", `[Out "{propertyName}"]`) end outState:set((applyTo :: any)[propertyName]) table.insert( diff --git a/src/Instances/Ref.lua b/src/Instances/Ref.lua index eb1a78a8f..e5248cdd1 100644 --- a/src/Instances/Ref.lua +++ b/src/Instances/Ref.lua @@ -26,7 +26,7 @@ function Ref:apply( logError("invalidRefType") else if not assertLifetime(scope, nil, refState) then - logWarn("possiblyOutlives", "Value object", "[Ref]") + logWarn("possiblyOutlives", "Value", "[Ref]") end refState:set(applyTo) end From 47d50ab07218a991d22244410bc34543d23f6a48 Mon Sep 17 00:00:00 2001 From: Elttob Date: Wed, 29 Nov 2023 12:02:13 +0000 Subject: [PATCH 087/287] applyInstanceProps asserts lifetime of bound values --- src/Instances/applyInstanceProps.lua | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/Instances/applyInstanceProps.lua b/src/Instances/applyInstanceProps.lua index bdb55c550..c4fc6a944 100644 --- a/src/Instances/applyInstanceProps.lua +++ b/src/Instances/applyInstanceProps.lua @@ -19,9 +19,11 @@ local doCleanup = require(Package.Memory.doCleanup) local External = require(Package.External) local isState = require(Package.State.isState) local logError = require(Package.Logging.logError) +local logWarn = require(Package.Logging.logWarn) local Observer = require(Package.State.Observer) local peek = require(Package.State.peek) local xtypeof = require(Package.Utility.xtypeof) +local assertLifetime = require(Package.Memory.assertLifetime) local function setProperty_unsafe( instance: Instance, @@ -63,6 +65,10 @@ local function bindProperty( value: PubTypes.CanBeState ) if isState(value) then + if not assertLifetime(scope, nil, value) then + logWarn("possiblyOutlives", value.kind, `{instance.ClassName}.{property}`) + end + -- value is a state object - assign and observe for changes local willUpdate = false local function updateLater() From 86a633b65a7381f4b1c732add33fdbe5a5549319 Mon Sep 17 00:00:00 2001 From: Elttob Date: Wed, 29 Nov 2023 12:15:55 +0000 Subject: [PATCH 088/287] Observer asserts lifetime + improved message --- src/Instances/Attribute.lua | 2 +- src/Instances/AttributeOut.lua | 2 +- src/Instances/Out.lua | 2 +- src/Instances/Ref.lua | 2 +- src/Instances/applyInstanceProps.lua | 4 ++-- src/Logging/messages.lua | 2 +- src/Memory/assertLifetime.lua | 22 +++++++++++----------- src/State/Observer.lua | 8 +++++++- 8 files changed, 25 insertions(+), 19 deletions(-) diff --git a/src/Instances/Attribute.lua b/src/Instances/Attribute.lua index d2b6701e6..3a2809689 100644 --- a/src/Instances/Attribute.lua +++ b/src/Instances/Attribute.lua @@ -31,7 +31,7 @@ local function Attribute(attributeName: string): PubTypes.SpecialKey applyTo: Instance ) if isState(value) then - if not assertLifetime(scope, nil, value) then + if not assertLifetime(scope, applyTo, value) then logWarn("possiblyOutlives", value.kind, `[Attribute "{attributeName}"]`) end local didDefer = false diff --git a/src/Instances/AttributeOut.lua b/src/Instances/AttributeOut.lua index 4ff70829e..d0c22f938 100644 --- a/src/Instances/AttributeOut.lua +++ b/src/Instances/AttributeOut.lua @@ -33,7 +33,7 @@ local function AttributeOut(attributeName: string): PubTypes.SpecialKey if not ok then logError("invalidOutAttributeName", applyTo.ClassName, attributeName) else - if not assertLifetime(scope, nil, stateObject) then + if not assertLifetime(scope, stateObject, applyTo) then logWarn("possiblyOutlives", "Value", `[AttributeOut "{attributeName}"]`) end stateObject:set((applyTo :: any):GetAttribute(attributeName)) diff --git a/src/Instances/Out.lua b/src/Instances/Out.lua index df165f82a..3de0eb3f8 100644 --- a/src/Instances/Out.lua +++ b/src/Instances/Out.lua @@ -29,7 +29,7 @@ local function Out(propertyName: string): PubTypes.SpecialKey elseif xtypeof(outState) ~= "State" or outState.kind ~= "Value" then logError("invalidOutType") else - if not assertLifetime(scope, nil, outState) then + if not assertLifetime(scope, outState, applyTo) then logWarn("possiblyOutlives", "Value", `[Out "{propertyName}"]`) end outState:set((applyTo :: any)[propertyName]) diff --git a/src/Instances/Ref.lua b/src/Instances/Ref.lua index e5248cdd1..d5881d628 100644 --- a/src/Instances/Ref.lua +++ b/src/Instances/Ref.lua @@ -25,7 +25,7 @@ function Ref:apply( if xtypeof(refState) ~= "State" or refState.kind ~= "Value" then logError("invalidRefType") else - if not assertLifetime(scope, nil, refState) then + if not assertLifetime(scope, refState, applyTo) then logWarn("possiblyOutlives", "Value", "[Ref]") end refState:set(applyTo) diff --git a/src/Instances/applyInstanceProps.lua b/src/Instances/applyInstanceProps.lua index c4fc6a944..e8d014f2e 100644 --- a/src/Instances/applyInstanceProps.lua +++ b/src/Instances/applyInstanceProps.lua @@ -65,8 +65,8 @@ local function bindProperty( value: PubTypes.CanBeState ) if isState(value) then - if not assertLifetime(scope, nil, value) then - logWarn("possiblyOutlives", value.kind, `{instance.ClassName}.{property}`) + if not assertLifetime(scope, instance, value) then + logWarn("possiblyOutlives", `{instance.ClassName}.{property}`, value.kind) end -- value is a state object - assign and observe for changes diff --git a/src/Logging/messages.lua b/src/Logging/messages.lua index 3a5406837..792b352e3 100644 --- a/src/Logging/messages.lua +++ b/src/Logging/messages.lua @@ -32,7 +32,7 @@ return { mistypedSpringSpeed = "The speed of a spring must be a number. (got a %s)", mistypedTweenInfo = "The tween info of a tween must be a TweenInfo. (got a %s)", noTaskScheduler = "Fusion is not connected to an external task scheduler.", - possiblyOutlives = "%s might outlive the %s that's using it; ensure they are constructed in the correct order with the same scope.", + possiblyOutlives = "%s might outlive the %s it's associated with; ensure they are constructed in the correct order with the same scope.", springTypeMismatch = "The type '%s' doesn't match the spring's type '%s'.", stateGetWasRemoved = "`StateObject:get()` has been replaced by `use()` and `peek()` - see discussion #217 on GitHub.", strictReadError = "'%s' is not a valid member of '%s'.", diff --git a/src/Memory/assertLifetime.lua b/src/Memory/assertLifetime.lua index 7297b4a0f..526a103c1 100644 --- a/src/Memory/assertLifetime.lua +++ b/src/Memory/assertLifetime.lua @@ -1,9 +1,9 @@ --!strict --[[ - Checks the scope of the parent to see if the child will be destroyed before - the parent is destroyed. If the parent is omitted, the scope will only be - checked for presence of the child. + Given one argument, checks if the argument is destroyed by this scope. + Given two arguments, checks if the first argument is destroyed before the + second argument by this scope. ]] local Package = script.Parent.Parent local PubTypes = require(Package.PubTypes) @@ -11,18 +11,18 @@ local PubTypes = require(Package.PubTypes) local function assertLifetimeImpl( haystack: {any}, alreadyChecked: {[any]: true}, - parent: any?, - child: any + destroyedFirst: any, + destroyedSecond: any? ): boolean? for index = #haystack, 1, -1 do local value = haystack[index] - if parent ~= nil and value == parent then + if destroyedSecond ~= nil and value == destroyedSecond then return false - elseif value == child then + elseif value == destroyedFirst then return true elseif typeof(value) == "table" and value[1] ~= nil and alreadyChecked[value] == nil then alreadyChecked[value] = true - local appearsLater = assertLifetimeImpl(value, alreadyChecked, parent, child) + local appearsLater = assertLifetimeImpl(value, alreadyChecked, destroyedFirst, destroyedSecond) if appearsLater ~= nil then return appearsLater end @@ -33,10 +33,10 @@ end local function assertLifetime( scope: PubTypes.Scope, - parent: any?, - child: any + destroyedFirst: any, + destroyedSecond: any? ): boolean - return assertLifetimeImpl(scope, {}, parent, child) == true + return assertLifetimeImpl(scope, {}, destroyedFirst, destroyedSecond) == true end return assertLifetime \ No newline at end of file diff --git a/src/State/Observer.lua b/src/State/Observer.lua index 34fc99cad..6f2216084 100644 --- a/src/State/Observer.lua +++ b/src/State/Observer.lua @@ -11,6 +11,8 @@ local Package = script.Parent.Parent local PubTypes = require(Package.PubTypes) local Types = require(Package.Types) local External = require(Package.External) +local assertLifetime = require(Package.Memory.assertLifetime) +local logWarn = require(Package.Logging.logWarn) type Set = {[T]: any} @@ -60,7 +62,7 @@ end local function Observer( scope: PubTypes.Scope, - watchedState: PubTypes.Value + watchedState: PubTypes.StateObject ): Types.Observer local self = setmetatable({ type = "State", @@ -70,6 +72,10 @@ local function Observer( _changeListeners = {} }, CLASS_METATABLE) + if not assertLifetime(scope, self, watchedState) then + logWarn("possiblyOutlives", "Observer", watchedState.kind) + end + -- add this object to the watched state's dependent set watchedState.dependentSet[self] = true table.insert(scope, self) From 8c74394055fbd40efd35f70e5e12ffd434a3be86 Mon Sep 17 00:00:00 2001 From: Elttob Date: Wed, 29 Nov 2023 13:59:44 +0000 Subject: [PATCH 089/287] Add stack trace to warnings --- src/Logging/logWarn.lua | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/Logging/logWarn.lua b/src/Logging/logWarn.lua index ce7867b4e..317e5241e 100644 --- a/src/Logging/logWarn.lua +++ b/src/Logging/logWarn.lua @@ -17,7 +17,9 @@ local function logWarn(messageID, ...) formatString = messages[messageID] end - warn(string.format("[Fusion] " .. formatString .. "\n(ID: " .. messageID .. ")", ...)) + local warnMessage = string.format("[Fusion] " .. formatString .. "\n(ID: " .. messageID .. ")", ...) + warnMessage ..= "\n---- Stack trace ----\n" .. debug.traceback(nil, 3) + warn(warnMessage) end return logWarn \ No newline at end of file From 1e2c4207664a0276132ec856edb835b4fb366462 Mon Sep 17 00:00:00 2001 From: Elttob Date: Wed, 29 Nov 2023 13:59:58 +0000 Subject: [PATCH 090/287] Fix lifetime assertions --- src/Instances/Attribute.lua | 2 +- src/Instances/applyInstanceProps.lua | 2 +- src/State/Observer.lua | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Instances/Attribute.lua b/src/Instances/Attribute.lua index 3a2809689..46cbf7b0f 100644 --- a/src/Instances/Attribute.lua +++ b/src/Instances/Attribute.lua @@ -31,7 +31,7 @@ local function Attribute(attributeName: string): PubTypes.SpecialKey applyTo: Instance ) if isState(value) then - if not assertLifetime(scope, applyTo, value) then + if not assertLifetime(scope, value) then logWarn("possiblyOutlives", value.kind, `[Attribute "{attributeName}"]`) end local didDefer = false diff --git a/src/Instances/applyInstanceProps.lua b/src/Instances/applyInstanceProps.lua index e8d014f2e..8bddbf0c2 100644 --- a/src/Instances/applyInstanceProps.lua +++ b/src/Instances/applyInstanceProps.lua @@ -65,7 +65,7 @@ local function bindProperty( value: PubTypes.CanBeState ) if isState(value) then - if not assertLifetime(scope, instance, value) then + if not assertLifetime(scope, value) then logWarn("possiblyOutlives", `{instance.ClassName}.{property}`, value.kind) end diff --git a/src/State/Observer.lua b/src/State/Observer.lua index 6f2216084..b03f09d77 100644 --- a/src/State/Observer.lua +++ b/src/State/Observer.lua @@ -72,7 +72,7 @@ local function Observer( _changeListeners = {} }, CLASS_METATABLE) - if not assertLifetime(scope, self, watchedState) then + if not assertLifetime(scope, watchedState) then logWarn("possiblyOutlives", "Observer", watchedState.kind) end From e5988f06804f2ab218b3af3c904f8528de059c6a Mon Sep 17 00:00:00 2001 From: Elttob Date: Wed, 29 Nov 2023 14:00:57 +0000 Subject: [PATCH 091/287] Fix observer spec thanks to that lifetime warning --- test/State/Observer.spec.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/State/Observer.spec.lua b/test/State/Observer.spec.lua index f170e91f0..c97d7fa95 100644 --- a/test/State/Observer.spec.lua +++ b/test/State/Observer.spec.lua @@ -95,7 +95,7 @@ return function() it("disconnects on destroy", function() local scope = {} local dependency = Value(scope, 5) - local observer = Observer({}, dependency) + local observer = Observer(scope, dependency) local numFires = 0 local _ = observer:onChange(function() numFires += 1 From 2913acb42b28266b51581467d3bdd40fbf577024 Mon Sep 17 00:00:00 2001 From: Elttob Date: Wed, 29 Nov 2023 14:29:31 +0000 Subject: [PATCH 092/287] Fix assertLifetime unit tests --- test/Memory/assertLifetime.spec.lua | 84 +++++++++++++++++------------ 1 file changed, 49 insertions(+), 35 deletions(-) diff --git a/test/Memory/assertLifetime.spec.lua b/test/Memory/assertLifetime.spec.lua index 1c852abd6..b675c3e1c 100644 --- a/test/Memory/assertLifetime.spec.lua +++ b/test/Memory/assertLifetime.spec.lua @@ -2,48 +2,62 @@ local Package = game:GetService("ReplicatedStorage").Fusion local assertLifetime = require(Package.Memory.assertLifetime) return function() + it("disallows absent children in flat arrays", function() + expect(assertLifetime({}, "c")).to.equal(false) + expect(assertLifetime({1, 2, 3, 4, 5, 6}, "c")).to.equal(false) + expect(assertLifetime({"p"}, "c", "p")).to.equal(false) + expect(assertLifetime({1, 2, 3, "p", 4, 5, 6}, "c", "p")).to.equal(false) + expect(assertLifetime({"p", 1, 2, 3, 4, 5, 6}, "c", "p")).to.equal(false) + expect(assertLifetime({1, 2, 3, 4, 5, 6, "p"}, "c", "p")).to.equal(false) + end) + it("allows present children in flat arrays", function() + expect(assertLifetime({"c"}, "c")).to.equal(true) + expect(assertLifetime({"c", 1, 2, 3, 4, 5, 6}, "c")).to.equal(true) + expect(assertLifetime({1, 2, 3, "c", 4, 5, 6}, "c")).to.equal(true) + expect(assertLifetime({1, 2, 3, 4, 5, 6, "c"}, "c")).to.equal(true) + end) it("allows correct order in flat arrays", function() - expect(assertLifetime({"p", "c"}, "p", "c")).to.equal(true) - expect(assertLifetime({1, 2, 3, "p", "c", 4, 5, 6}, "p", "c")).to.equal(true) - expect(assertLifetime({1, "p", 2, 3, 4, "c", 5, 6}, "p", "c")).to.equal(true) - expect(assertLifetime({"p", 1, 2, 3, 4, 5, 6, "c"}, "p", "c")).to.equal(true) - expect(assertLifetime({"p", "c", 1, 2, 3, 4, 5, 6}, "p", "c")).to.equal(true) - expect(assertLifetime({1, 2, 3, 4, 5, 6, "p", "c"}, "p", "c")).to.equal(true) + expect(assertLifetime({"p", "c"}, "c", "p")).to.equal(true) + expect(assertLifetime({1, 2, 3, "p", "c", 4, 5, 6}, "c", "p")).to.equal(true) + expect(assertLifetime({1, "p", 2, 3, 4, "c", 5, 6}, "c", "p")).to.equal(true) + expect(assertLifetime({"p", 1, 2, 3, 4, 5, 6, "c"}, "c", "p")).to.equal(true) + expect(assertLifetime({"p", "c", 1, 2, 3, 4, 5, 6}, "c", "p")).to.equal(true) + expect(assertLifetime({1, 2, 3, 4, 5, 6, "p", "c"}, "c", "p")).to.equal(true) end) it("disallows incorrect order in flat arrays", function() - expect(assertLifetime({"c", "p"}, "p", "c")).to.equal(false) - expect(assertLifetime({1, 2, 3, "c", "p", 4, 5, 6}, "p", "c")).to.equal(false) - expect(assertLifetime({1, "c", 2, 3, 4, "p", 5, 6}, "p", "c")).to.equal(false) - expect(assertLifetime({"c", 1, 2, 3, 4, 5, 6, "p"}, "p", "c")).to.equal(false) - expect(assertLifetime({"c", "p", 1, 2, 3, 4, 5, 6}, "p", "c")).to.equal(false) - expect(assertLifetime({1, 2, 3, 4, 5, 6, "c", "p"}, "p", "c")).to.equal(false) + expect(assertLifetime({"c", "p"}, "c", "p")).to.equal(false) + expect(assertLifetime({1, 2, 3, "c", "p", 4, 5, 6}, "c", "p")).to.equal(false) + expect(assertLifetime({1, "c", 2, 3, 4, "p", 5, 6}, "c", "p")).to.equal(false) + expect(assertLifetime({"c", 1, 2, 3, 4, 5, 6, "p"}, "c", "p")).to.equal(false) + expect(assertLifetime({"c", "p", 1, 2, 3, 4, 5, 6}, "c", "p")).to.equal(false) + expect(assertLifetime({1, 2, 3, 4, 5, 6, "c", "p"}, "c", "p")).to.equal(false) end) - it("disallows absent children in flat arrays", function() - expect(assertLifetime({"p"}, "p", "c")).to.equal(false) - expect(assertLifetime({1, 2, 3, "p", 4, 5, 6}, "p", "c")).to.equal(false) - expect(assertLifetime({"p", 1, 2, 3, 4, 5, 6}, "p", "c")).to.equal(false) - expect(assertLifetime({1, 2, 3, 4, 5, 6, "p"}, "p", "c")).to.equal(false) + it("disallows absent children in nested arrays", function() + expect(assertLifetime({{"p"}}, "c", "p")).to.equal(false) + expect(assertLifetime({1, {2, 3, "p"}, 4, 5, 6}, "c", "p")).to.equal(false) + expect(assertLifetime({"p", 1, 2, 3, {4, 5, 6}}, "c", "p")).to.equal(false) + expect(assertLifetime({1, {{2, 3}, 4, 5, 6}, "p"}, "c", "p")).to.equal(false) + end) + it("allows present children in nested arrays", function() + expect(assertLifetime({{"c"}}, "c")).to.equal(true) + expect(assertLifetime({"c", {1, 2, 3}, 4, 5, 6}, "c")).to.equal(true) + expect(assertLifetime({1, 2, 3, {"c", 4, 5, 6}}, "c")).to.equal(true) + expect(assertLifetime({1, 2, 3, 4, 5, {6, "c"}}, "c")).to.equal(true) end) it("allows correct order in nested arrays", function() - expect(assertLifetime({{"p"}, {"c"}}, "p", "c")).to.equal(true) - expect(assertLifetime({1, {2, 3, "p"}, "c", 4, 5, 6}, "p", "c")).to.equal(true) - expect(assertLifetime({{1, {"p"}}, 2, {3, 4}, {"c", 5, 6}}, "p", "c")).to.equal(true) - expect(assertLifetime({"p", 1, 2, 3, {4, 5, 6, "c"}}, "p", "c")).to.equal(true) - expect(assertLifetime({"p", {"c", 1, 2, 3, 4}, 5, 6}, "p", "c")).to.equal(true) - expect(assertLifetime({1, {{2, 3}, 4, 5, 6, "p"}, "c"}, "p", "c")).to.equal(true) + expect(assertLifetime({{"p"}, {"c"}}, "c", "p")).to.equal(true) + expect(assertLifetime({1, {2, 3, "p"}, "c", 4, 5, 6}, "c", "p")).to.equal(true) + expect(assertLifetime({{1, {"p"}}, 2, {3, 4}, {"c", 5, 6}}, "c", "p")).to.equal(true) + expect(assertLifetime({"p", 1, 2, 3, {4, 5, 6, "c"}}, "c", "p")).to.equal(true) + expect(assertLifetime({"p", {"c", 1, 2, 3, 4}, 5, 6}, "c", "p")).to.equal(true) + expect(assertLifetime({1, {{2, 3}, 4, 5, 6, "p"}, "c"}, "c", "p")).to.equal(true) end) it("disallows incorrect order in nested arrays", function() - expect(assertLifetime({{"c"}, {"p"}}, "p", "c")).to.equal(false) - expect(assertLifetime({1, {2, 3, "c"}, "p", 4, 5, 6}, "p", "c")).to.equal(false) - expect(assertLifetime({{1, {"c"}}, 2, {3, 4}, {"p", 5, 6}}, "p", "c")).to.equal(false) - expect(assertLifetime({"c", 1, 2, 3, {4, 5, 6, "p"}}, "p", "c")).to.equal(false) - expect(assertLifetime({"c", {"p", 1, 2, 3, 4}, 5, 6}, "p", "c")).to.equal(false) - expect(assertLifetime({1, {{2, 3}, 4, 5, 6, "c"}, "p"}, "p", "c")).to.equal(false) - end) - it("disallows absent children in nested arrays", function() - expect(assertLifetime({{"p"}}, "p", "c")).to.equal(false) - expect(assertLifetime({1, {2, 3, "p"}, 4, 5, 6}, "p", "c")).to.equal(false) - expect(assertLifetime({"p", 1, 2, 3, {4, 5, 6}}, "p", "c")).to.equal(false) - expect(assertLifetime({1, {{2, 3}, 4, 5, 6}, "p"}, "p", "c")).to.equal(false) + expect(assertLifetime({{"c"}, {"p"}}, "c", "p")).to.equal(false) + expect(assertLifetime({1, {2, 3, "c"}, "p", 4, 5, 6}, "c", "p")).to.equal(false) + expect(assertLifetime({{1, {"c"}}, 2, {3, 4}, {"p", 5, 6}}, "c", "p")).to.equal(false) + expect(assertLifetime({"c", 1, 2, 3, {4, 5, 6, "p"}}, "c", "p")).to.equal(false) + expect(assertLifetime({"c", {"p", 1, 2, 3, 4}, 5, 6}, "c", "p")).to.equal(false) + expect(assertLifetime({1, {{2, 3}, 4, 5, 6, "c"}, "p"}, "c", "p")).to.equal(false) end) end \ No newline at end of file From b9e8ca1b50e0543e065b0fb4f0732780f6f43af4 Mon Sep 17 00:00:00 2001 From: Elttob Date: Wed, 29 Nov 2023 14:48:48 +0000 Subject: [PATCH 093/287] Computed asserts lifetime of used state --- src/State/Computed.lua | 20 +++++++++++++++----- src/State/makeUseCallback.lua | 27 --------------------------- 2 files changed, 15 insertions(+), 32 deletions(-) delete mode 100644 src/State/makeUseCallback.lua diff --git a/src/State/Computed.lua b/src/State/Computed.lua index d33ac252b..74b5aff21 100644 --- a/src/State/Computed.lua +++ b/src/State/Computed.lua @@ -16,10 +16,11 @@ local parseError = require(Package.Logging.parseError) -- Utility local isSimilar = require(Package.Utility.isSimilar) -- State -local makeUseCallback = require(Package.State.makeUseCallback) +local isState = require(Package.State.isState) -- Memory local doCleanup = require(Package.Memory.doCleanup) local deriveScope = require(Package.Memory.deriveScope) +local assertLifetime = require(Package.Memory.assertLifetime) local class = {} @@ -44,7 +45,17 @@ function class:update(): boolean table.clear(self.dependencySet) local innerScope = deriveScope(self._outerScope) - local use = makeUseCallback(self.dependencySet) + local function use(target: PubTypes.CanBeState): T + if isState(target) then + if not assertLifetime(self._outerScope, self, target) then + logWarn("possiblyOutlives", "Computed", target.kind) + end + self.dependencySet[target] = true + return (target :: Types.StateObject):_peek() + else + return target + end + end local ok, newValue = xpcall(self._processor, parseError, innerScope, use) if ok then @@ -117,10 +128,9 @@ local function Computed( _outerScope = scope, _innerScope = nil }, CLASS_METATABLE) - - self:update() table.insert(scope, self) - + self:update() + return self end diff --git a/src/State/makeUseCallback.lua b/src/State/makeUseCallback.lua deleted file mode 100644 index d1caca700..000000000 --- a/src/State/makeUseCallback.lua +++ /dev/null @@ -1,27 +0,0 @@ ---!strict - ---[[ - Constructs a 'use callback' for the purposes of collecting dependencies. -]] - -local Package = script.Parent.Parent -local PubTypes = require(Package.PubTypes) -local Types = require(Package.Types) --- State -local isState = require(Package.State.isState) - -type Set = {[T]: any} - -local function makeUseCallback(dependencySet: Set) - local function use(target: PubTypes.CanBeState): T - if isState(target) then - dependencySet[target] = true - return (target :: Types.StateObject):_peek() - else - return target - end - end - return use -end - -return makeUseCallback \ No newline at end of file From b9ee96849f6bf81feb10cc234e864cd28af78971 Mon Sep 17 00:00:00 2001 From: Elttob Date: Wed, 29 Nov 2023 19:00:02 +0000 Subject: [PATCH 094/287] Refactored lifetime messages --- src/Animation/Spring.lua | 1 + src/Animation/Tween.lua | 1 + src/Instances/Attribute.lua | 6 +- src/Instances/AttributeOut.lua | 6 +- src/Instances/Hydrate.lua | 2 + src/Instances/New.lua | 1 + src/Instances/Out.lua | 6 +- src/Instances/Ref.lua | 6 +- src/Instances/applyInstanceProps.lua | 10 +--- src/Logging/messages.lua | 2 +- src/Memory/assertLifetime.lua | 42 -------------- src/Memory/whichLivesLonger.lua | 64 ++++++++++++++++++++++ src/PubTypes.lua | 7 ++- src/State/Computed.lua | 10 ++-- src/State/For.lua | 1 + src/State/Observer.lua | 9 +-- src/State/Value.lua | 1 + src/Types.lua | 2 +- test/Instances/applyInstanceProps.spec.lua | 27 ++++++++- test/Memory/assertLifetime.spec.lua | 63 --------------------- 20 files changed, 128 insertions(+), 139 deletions(-) delete mode 100644 src/Memory/assertLifetime.lua create mode 100644 src/Memory/whichLivesLonger.lua delete mode 100644 test/Memory/assertLifetime.spec.lua diff --git a/src/Animation/Spring.lua b/src/Animation/Spring.lua index 4620da0eb..1bb020e15 100644 --- a/src/Animation/Spring.lua +++ b/src/Animation/Spring.lua @@ -191,6 +191,7 @@ local function Spring( local self = setmetatable({ type = "State", kind = "Spring", + scope = scope, dependencySet = dependencySet, -- if we held strong references to the dependents, then they wouldn't be -- able to get garbage collected when they fall out of scope diff --git a/src/Animation/Tween.lua b/src/Animation/Tween.lua index ce8957bdb..c6fe17603 100644 --- a/src/Animation/Tween.lua +++ b/src/Animation/Tween.lua @@ -104,6 +104,7 @@ local function Tween( local self = setmetatable({ type = "State", kind = "Tween", + scope = scope, dependencySet = dependencySet, -- if we held strong references to the dependents, then they wouldn't be -- able to get garbage collected when they fall out of scope diff --git a/src/Instances/Attribute.lua b/src/Instances/Attribute.lua index 46cbf7b0f..4c9a648d5 100644 --- a/src/Instances/Attribute.lua +++ b/src/Instances/Attribute.lua @@ -13,7 +13,7 @@ local logWarn = require(Package.Logging.logWarn) local isState = require(Package.State.isState) local Observer = require(Package.State.Observer) local peek = require(Package.State.peek) -local assertLifetime = require(Package.Memory.assertLifetime) +local whichLivesLonger = require(Package.Memory.whichLivesLonger) local function Attribute(attributeName: string): PubTypes.SpecialKey local AttributeKey = {} @@ -31,8 +31,8 @@ local function Attribute(attributeName: string): PubTypes.SpecialKey applyTo: Instance ) if isState(value) then - if not assertLifetime(scope, value) then - logWarn("possiblyOutlives", value.kind, `[Attribute "{attributeName}"]`) + if whichLivesLonger(scope, applyTo, value.scope, value) == "b" then + logWarn("possiblyOutlives", `The {value.kind} object, bound to [Attribute "{attributeName}"],`, `the {applyTo.ClassName} instance`) end local didDefer = false local function update() diff --git a/src/Instances/AttributeOut.lua b/src/Instances/AttributeOut.lua index d0c22f938..c87dffaa7 100644 --- a/src/Instances/AttributeOut.lua +++ b/src/Instances/AttributeOut.lua @@ -10,7 +10,7 @@ local PubTypes = require(Package.PubTypes) local logError = require(Package.Logging.logError) local logWarn = require(Package.Logging.logWarn) local xtypeof = require(Package.Utility.xtypeof) -local assertLifetime = require(Package.Memory.assertLifetime) +local whichLivesLonger = require(Package.Memory.whichLivesLonger) local function AttributeOut(attributeName: string): PubTypes.SpecialKey local attributeOutKey = {} @@ -33,8 +33,8 @@ local function AttributeOut(attributeName: string): PubTypes.SpecialKey if not ok then logError("invalidOutAttributeName", applyTo.ClassName, attributeName) else - if not assertLifetime(scope, stateObject, applyTo) then - logWarn("possiblyOutlives", "Value", `[AttributeOut "{attributeName}"]`) + if whichLivesLonger(scope, applyTo, stateObject.scope, stateObject) == "a" then + logWarn("possiblyOutlives", `The Value object, which [AttributeOut "{attributeName}"] outputs to,`, `the {applyTo.ClassName} instance`) end stateObject:set((applyTo :: any):GetAttribute(attributeName)) table.insert(scope, event:Connect(function() diff --git a/src/Instances/Hydrate.lua b/src/Instances/Hydrate.lua index bcd848e57..0ff618339 100644 --- a/src/Instances/Hydrate.lua +++ b/src/Instances/Hydrate.lua @@ -16,6 +16,8 @@ local function Hydrate( return function( props: PubTypes.PropertyTable ): Instance + + table.insert(scope, target) applyInstanceProps(scope, props, target) return target end diff --git a/src/Instances/New.lua b/src/Instances/New.lua index 52cb14a8a..f10625868 100644 --- a/src/Instances/New.lua +++ b/src/Instances/New.lua @@ -30,6 +30,7 @@ local function New( end end + table.insert(scope, instance) applyInstanceProps(scope, props, instance) return instance diff --git a/src/Instances/Out.lua b/src/Instances/Out.lua index 3de0eb3f8..c04308710 100644 --- a/src/Instances/Out.lua +++ b/src/Instances/Out.lua @@ -10,7 +10,7 @@ local PubTypes = require(Package.PubTypes) local logError = require(Package.Logging.logError) local logWarn = require(Package.Logging.logWarn) local xtypeof = require(Package.Utility.xtypeof) -local assertLifetime = require(Package.Memory.assertLifetime) +local whichLivesLonger = require(Package.Memory.whichLivesLonger) local function Out(propertyName: string): PubTypes.SpecialKey local outKey = {} @@ -29,8 +29,8 @@ local function Out(propertyName: string): PubTypes.SpecialKey elseif xtypeof(outState) ~= "State" or outState.kind ~= "Value" then logError("invalidOutType") else - if not assertLifetime(scope, outState, applyTo) then - logWarn("possiblyOutlives", "Value", `[Out "{propertyName}"]`) + if whichLivesLonger(scope, applyTo, outState.scope, outState) == "a" then + logWarn("possiblyOutlives", `The Value, which [Out "{propertyName}"] outputs to,`, `the {applyTo.ClassName} instance`) end outState:set((applyTo :: any)[propertyName]) table.insert( diff --git a/src/Instances/Ref.lua b/src/Instances/Ref.lua index d5881d628..18ad61376 100644 --- a/src/Instances/Ref.lua +++ b/src/Instances/Ref.lua @@ -10,7 +10,7 @@ local PubTypes = require(Package.PubTypes) local logWarn = require(Package.Logging.logWarn) local logError = require(Package.Logging.logError) local xtypeof = require(Package.Utility.xtypeof) -local assertLifetime = require(Package.Memory.assertLifetime) +local whichLivesLonger = require(Package.Memory.whichLivesLonger) local Ref = {} Ref.type = "SpecialKey" @@ -25,8 +25,8 @@ function Ref:apply( if xtypeof(refState) ~= "State" or refState.kind ~= "Value" then logError("invalidRefType") else - if not assertLifetime(scope, refState, applyTo) then - logWarn("possiblyOutlives", "Value", "[Ref]") + if whichLivesLonger(scope, applyTo, refState.scope, refState) == "a" then + logWarn("possiblyOutlives", "The Value object, which [Ref] outputs to,", `the {applyTo} instance`) end refState:set(applyTo) end diff --git a/src/Instances/applyInstanceProps.lua b/src/Instances/applyInstanceProps.lua index 8bddbf0c2..b027cd359 100644 --- a/src/Instances/applyInstanceProps.lua +++ b/src/Instances/applyInstanceProps.lua @@ -23,7 +23,7 @@ local logWarn = require(Package.Logging.logWarn) local Observer = require(Package.State.Observer) local peek = require(Package.State.peek) local xtypeof = require(Package.Utility.xtypeof) -local assertLifetime = require(Package.Memory.assertLifetime) +local whichLivesLonger = require(Package.Memory.whichLivesLonger) local function setProperty_unsafe( instance: Instance, @@ -65,8 +65,8 @@ local function bindProperty( value: PubTypes.CanBeState ) if isState(value) then - if not assertLifetime(scope, value) then - logWarn("possiblyOutlives", `{instance.ClassName}.{property}`, value.kind) + if whichLivesLonger(scope, instance, value.scope, value) ~= "b" then + logWarn("possiblyOutlives", `The {value.kind} object, bound to {property},`, `the {instance.ClassName} instance`) end -- value is a state object - assign and observe for changes @@ -139,10 +139,6 @@ local function applyInstanceProps( for key, value in pairs(specialKeys.observer) do key:apply(scope, value, applyTo) end - - applyTo.Destroying:Connect(function() - doCleanup(scope) - end) end return applyInstanceProps \ No newline at end of file diff --git a/src/Logging/messages.lua b/src/Logging/messages.lua index 792b352e3..0fe35449d 100644 --- a/src/Logging/messages.lua +++ b/src/Logging/messages.lua @@ -32,7 +32,7 @@ return { mistypedSpringSpeed = "The speed of a spring must be a number. (got a %s)", mistypedTweenInfo = "The tween info of a tween must be a TweenInfo. (got a %s)", noTaskScheduler = "Fusion is not connected to an external task scheduler.", - possiblyOutlives = "%s might outlive the %s it's associated with; ensure they are constructed in the correct order with the same scope.", + possiblyOutlives = "%s could be destroyed before %s. Ensure they're created in the correct scope & correct order.", springTypeMismatch = "The type '%s' doesn't match the spring's type '%s'.", stateGetWasRemoved = "`StateObject:get()` has been replaced by `use()` and `peek()` - see discussion #217 on GitHub.", strictReadError = "'%s' is not a valid member of '%s'.", diff --git a/src/Memory/assertLifetime.lua b/src/Memory/assertLifetime.lua deleted file mode 100644 index 526a103c1..000000000 --- a/src/Memory/assertLifetime.lua +++ /dev/null @@ -1,42 +0,0 @@ ---!strict - ---[[ - Given one argument, checks if the argument is destroyed by this scope. - Given two arguments, checks if the first argument is destroyed before the - second argument by this scope. -]] -local Package = script.Parent.Parent -local PubTypes = require(Package.PubTypes) - -local function assertLifetimeImpl( - haystack: {any}, - alreadyChecked: {[any]: true}, - destroyedFirst: any, - destroyedSecond: any? -): boolean? - for index = #haystack, 1, -1 do - local value = haystack[index] - if destroyedSecond ~= nil and value == destroyedSecond then - return false - elseif value == destroyedFirst then - return true - elseif typeof(value) == "table" and value[1] ~= nil and alreadyChecked[value] == nil then - alreadyChecked[value] = true - local appearsLater = assertLifetimeImpl(value, alreadyChecked, destroyedFirst, destroyedSecond) - if appearsLater ~= nil then - return appearsLater - end - end - end - return nil -end - -local function assertLifetime( - scope: PubTypes.Scope, - destroyedFirst: any, - destroyedSecond: any? -): boolean - return assertLifetimeImpl(scope, {}, destroyedFirst, destroyedSecond) == true -end - -return assertLifetime \ No newline at end of file diff --git a/src/Memory/whichLivesLonger.lua b/src/Memory/whichLivesLonger.lua new file mode 100644 index 000000000..8550f3364 --- /dev/null +++ b/src/Memory/whichLivesLonger.lua @@ -0,0 +1,64 @@ +--!strict + +--[[ + Calculates how the lifetimes of the two values relate. Specifically, it + calculates which value will be destroyed earlier or later, if it is possible + to infer this from their scopes. +]] +local Package = script.Parent.Parent +local PubTypes = require(Package.PubTypes) + +local function whichScopeLivesLonger( + scopeA: PubTypes.Scope, + scopeB: PubTypes.Scope +): "a" | "b" | "unknown" + -- If we can prove one scope is inside of the other scope, then the outer + -- scope must live longer than the inner scope (assuming idiomatic scopes). + -- So, we will search the scopes recursively until we find one of them, at + -- which point we know they must have been found inside the other scope. + local openSet, nextOpenSet = {scopeA, scopeB}, {} + local openSetSize, nextOpenSetSize = 2, 0 + local closedSet = {} + while openSetSize > 0 do + for _, scope in openSet do + closedSet[scope] = true + for _, inScope in scope do + if inScope == scopeA then + return "b" + elseif inScope == scopeB then + return "a" + elseif inScope[1] ~= nil and closedSet[scope] == nil then + nextOpenSetSize += 1 + nextOpenSet[nextOpenSetSize] = inScope + end + end + end + table.clear(openSet) + openSet, nextOpenSet = nextOpenSet, openSet + openSetSize, nextOpenSetSize = nextOpenSetSize, 0 + end + return "unknown" +end + +local function whichLivesLonger( + scopeA: PubTypes.Scope, + a: any, + scopeB: PubTypes.Scope, + b: any +): "a" | "b" | "unknown" + if scopeA == scopeB then + for index = #scopeA, 1, -1 do + local value = scopeA[index] + if value == a then + return "b" + elseif value == b then + return "a" + end + end + return "unknown" + else + return whichScopeLivesLonger(scopeA, scopeB) + end +end + +return whichLivesLonger \ No newline at end of file diff --git a/src/PubTypes.lua b/src/PubTypes.lua index 0ddf811a7..118122cf1 100644 --- a/src/PubTypes.lua +++ b/src/PubTypes.lua @@ -49,6 +49,11 @@ export type Task = -- A scope of tasks to clean up. export type Scope = {Task} & Constructors +-- An object which uses a scope to dictate how long it lives. +export type ScopeLifetime = { + scope: Scope +} + -- Script-readable version information. export type Version = { major: number, @@ -60,7 +65,7 @@ export type Version = { ]] -- A graph object which can have dependents. -export type Dependency = { +export type Dependency = ScopeLifetime & { dependentSet: Set } diff --git a/src/State/Computed.lua b/src/State/Computed.lua index 74b5aff21..dfdb1f4e2 100644 --- a/src/State/Computed.lua +++ b/src/State/Computed.lua @@ -20,7 +20,7 @@ local isState = require(Package.State.isState) -- Memory local doCleanup = require(Package.Memory.doCleanup) local deriveScope = require(Package.Memory.deriveScope) -local assertLifetime = require(Package.Memory.assertLifetime) +local whichLivesLonger = require(Package.Memory.whichLivesLonger) local class = {} @@ -44,11 +44,11 @@ function class:update(): boolean self._oldDependencySet, self.dependencySet = self.dependencySet, self._oldDependencySet table.clear(self.dependencySet) - local innerScope = deriveScope(self._outerScope) + local innerScope = deriveScope(self.scope) local function use(target: PubTypes.CanBeState): T if isState(target) then - if not assertLifetime(self._outerScope, self, target) then - logWarn("possiblyOutlives", "Computed", target.kind) + if whichLivesLonger(self.scope, self, target.scope, target) == "a" then + logWarn("possiblyOutlives", `The {target.kind} object`, "the Computed that is use()-ing it") end self.dependencySet[target] = true return (target :: Types.StateObject):_peek() @@ -120,12 +120,12 @@ local function Computed( local self = setmetatable({ type = "State", kind = "Computed", + scope = scope, dependencySet = {}, dependentSet = {}, _oldDependencySet = {}, _processor = processor, _value = nil, - _outerScope = scope, _innerScope = nil }, CLASS_METATABLE) table.insert(scope, self) diff --git a/src/State/For.lua b/src/State/For.lua index a4a1cacf2..f55343497 100644 --- a/src/State/For.lua +++ b/src/State/For.lua @@ -215,6 +215,7 @@ local function For( local self = setmetatable({ type = "State", kind = "For", + scope = scope, dependencySet = {}, -- if we held strong references to the dependents, then they wouldn't be -- able to get garbage collected when they fall out of scope diff --git a/src/State/Observer.lua b/src/State/Observer.lua index b03f09d77..f28691b3e 100644 --- a/src/State/Observer.lua +++ b/src/State/Observer.lua @@ -11,7 +11,7 @@ local Package = script.Parent.Parent local PubTypes = require(Package.PubTypes) local Types = require(Package.Types) local External = require(Package.External) -local assertLifetime = require(Package.Memory.assertLifetime) +local whichLivesLonger = require(Package.Memory.whichLivesLonger) local logWarn = require(Package.Logging.logWarn) type Set = {[T]: any} @@ -67,18 +67,19 @@ local function Observer( local self = setmetatable({ type = "State", kind = "Observer", + scope = scope, dependencySet = {[watchedState] = true}, dependentSet = {}, _changeListeners = {} }, CLASS_METATABLE) + table.insert(scope, self) - if not assertLifetime(scope, watchedState) then - logWarn("possiblyOutlives", "Observer", watchedState.kind) + if whichLivesLonger(scope, self, watchedState.scope, watchedState) == "a" then + logWarn("possiblyOutlives", `The {watchedState.kind} object`, `the Observer that is watching it`) end -- add this object to the watched state's dependent set watchedState.dependentSet[self] = true - table.insert(scope, self) return self end diff --git a/src/State/Value.lua b/src/State/Value.lua index 0b36e7ef1..79b701b14 100644 --- a/src/State/Value.lua +++ b/src/State/Value.lua @@ -55,6 +55,7 @@ local function Value( local self = setmetatable({ type = "State", kind = "Value", + scope = scope, dependentSet = {}, _value = initialValue }, CLASS_METATABLE) diff --git a/src/Types.lua b/src/Types.lua index a4c4d9bc2..57ee2ac2a 100644 --- a/src/Types.lua +++ b/src/Types.lua @@ -49,10 +49,10 @@ export type State = PubTypes.Value & { -- A state object whose value is derived from other objects using a callback. export type Computed = PubTypes.Computed & { + scope: PubTypes.Scope, _oldDependencySet: Set, _processor: (PubTypes.Scope, PubTypes.Use) -> T, _value: T, - _outerScope: PubTypes.Scope, _innerScope: PubTypes.Scope? } diff --git a/test/Instances/applyInstanceProps.spec.lua b/test/Instances/applyInstanceProps.spec.lua index e8569f9b5..6351fa15c 100644 --- a/test/Instances/applyInstanceProps.spec.lua +++ b/test/Instances/applyInstanceProps.spec.lua @@ -7,6 +7,7 @@ return function() it("should assign properties (constant)", function() local scope = {} local instance = Instance.new("Folder") + table.insert(scope, instance) applyInstanceProps( scope, { Name = "Bob" }, @@ -20,6 +21,7 @@ return function() local scope = {} local value = Value(scope, "Bob") local instance = Instance.new("Folder") + table.insert(scope, instance) applyInstanceProps( scope, { Name = value }, @@ -37,7 +39,9 @@ return function() it("should assign Parent (constant)", function() local scope = {} local parent = Instance.new("Folder") + table.insert(scope, parent) local instance = Instance.new("Folder") + table.insert(scope, instance) applyInstanceProps( scope, { Parent = parent }, @@ -50,9 +54,12 @@ return function() it("should assign Parent (state)", function() local scope = {} local parent1 = Instance.new("Folder") + table.insert(scope, parent1) local parent2 = Instance.new("Folder") + table.insert(scope, parent2) local value = Value(scope, parent1) local instance = Instance.new("Folder") + table.insert(scope, instance) applyInstanceProps( scope, { Parent = value }, @@ -71,6 +78,7 @@ return function() expect(function() local scope = {} local instance = Instance.new("Folder") + table.insert(scope, instance) applyInstanceProps( scope, { NotARealProperty = true }, @@ -83,10 +91,12 @@ return function() it("should throw for non-existent properties (state)", function() expect(function() local scope = {} + local value = Value(scope, true) local instance = Instance.new("Folder") + table.insert(scope, instance) applyInstanceProps( scope, - { NotARealProperty = Value(scope, true) }, + { NotARealProperty = value }, instance ) doCleanup(scope) @@ -97,6 +107,7 @@ return function() expect(function() local scope = {} local instance = Instance.new("Folder") + table.insert(scope, instance) applyInstanceProps( scope, { Name = Vector3.new() }, @@ -109,10 +120,12 @@ return function() it("should throw for invalid property types (state)", function() expect(function() local scope = {} + local value = Value(scope, Vector3.new()) local instance = Instance.new("Folder") + table.insert(scope, instance) applyInstanceProps( scope, - { Name = Value(scope, Vector3.new()) }, + { Name = value }, instance ) doCleanup(scope) @@ -123,6 +136,7 @@ return function() expect(function() local scope = {} local instance = Instance.new("Folder") + table.insert(scope, instance) applyInstanceProps( scope, { Parent = Vector3.new() }, @@ -135,10 +149,12 @@ return function() it("should throw for invalid Parent types (state)", function() expect(function() local scope = {} + local value = Value(scope, Vector3.new()) local instance = Instance.new("Folder") + table.insert(scope, instance) applyInstanceProps( scope, - { Parent = Value(scope, Vector3.new()) }, + { Parent = value }, instance ) doCleanup(scope) @@ -149,6 +165,7 @@ return function() expect(function() local scope = {} local instance = Instance.new("Folder") + table.insert(scope, instance) applyInstanceProps( scope, { [2] = true }, @@ -162,6 +179,7 @@ return function() local scope = {} local value = Value(scope, "Bob") local instance = Instance.new("Folder") + table.insert(scope, instance) applyInstanceProps( scope, { Name = value }, @@ -178,9 +196,12 @@ return function() it("should defer Parent changes", function() local scope = {} local parent1 = Instance.new("Folder") + table.insert(scope, parent1) local parent2 = Instance.new("Folder") + table.insert(scope, parent2) local value = Value(scope, parent1) local instance = Instance.new("Folder") + table.insert(scope, instance) applyInstanceProps( scope, { Parent = value }, diff --git a/test/Memory/assertLifetime.spec.lua b/test/Memory/assertLifetime.spec.lua deleted file mode 100644 index b675c3e1c..000000000 --- a/test/Memory/assertLifetime.spec.lua +++ /dev/null @@ -1,63 +0,0 @@ -local Package = game:GetService("ReplicatedStorage").Fusion -local assertLifetime = require(Package.Memory.assertLifetime) - -return function() - it("disallows absent children in flat arrays", function() - expect(assertLifetime({}, "c")).to.equal(false) - expect(assertLifetime({1, 2, 3, 4, 5, 6}, "c")).to.equal(false) - expect(assertLifetime({"p"}, "c", "p")).to.equal(false) - expect(assertLifetime({1, 2, 3, "p", 4, 5, 6}, "c", "p")).to.equal(false) - expect(assertLifetime({"p", 1, 2, 3, 4, 5, 6}, "c", "p")).to.equal(false) - expect(assertLifetime({1, 2, 3, 4, 5, 6, "p"}, "c", "p")).to.equal(false) - end) - it("allows present children in flat arrays", function() - expect(assertLifetime({"c"}, "c")).to.equal(true) - expect(assertLifetime({"c", 1, 2, 3, 4, 5, 6}, "c")).to.equal(true) - expect(assertLifetime({1, 2, 3, "c", 4, 5, 6}, "c")).to.equal(true) - expect(assertLifetime({1, 2, 3, 4, 5, 6, "c"}, "c")).to.equal(true) - end) - it("allows correct order in flat arrays", function() - expect(assertLifetime({"p", "c"}, "c", "p")).to.equal(true) - expect(assertLifetime({1, 2, 3, "p", "c", 4, 5, 6}, "c", "p")).to.equal(true) - expect(assertLifetime({1, "p", 2, 3, 4, "c", 5, 6}, "c", "p")).to.equal(true) - expect(assertLifetime({"p", 1, 2, 3, 4, 5, 6, "c"}, "c", "p")).to.equal(true) - expect(assertLifetime({"p", "c", 1, 2, 3, 4, 5, 6}, "c", "p")).to.equal(true) - expect(assertLifetime({1, 2, 3, 4, 5, 6, "p", "c"}, "c", "p")).to.equal(true) - end) - it("disallows incorrect order in flat arrays", function() - expect(assertLifetime({"c", "p"}, "c", "p")).to.equal(false) - expect(assertLifetime({1, 2, 3, "c", "p", 4, 5, 6}, "c", "p")).to.equal(false) - expect(assertLifetime({1, "c", 2, 3, 4, "p", 5, 6}, "c", "p")).to.equal(false) - expect(assertLifetime({"c", 1, 2, 3, 4, 5, 6, "p"}, "c", "p")).to.equal(false) - expect(assertLifetime({"c", "p", 1, 2, 3, 4, 5, 6}, "c", "p")).to.equal(false) - expect(assertLifetime({1, 2, 3, 4, 5, 6, "c", "p"}, "c", "p")).to.equal(false) - end) - it("disallows absent children in nested arrays", function() - expect(assertLifetime({{"p"}}, "c", "p")).to.equal(false) - expect(assertLifetime({1, {2, 3, "p"}, 4, 5, 6}, "c", "p")).to.equal(false) - expect(assertLifetime({"p", 1, 2, 3, {4, 5, 6}}, "c", "p")).to.equal(false) - expect(assertLifetime({1, {{2, 3}, 4, 5, 6}, "p"}, "c", "p")).to.equal(false) - end) - it("allows present children in nested arrays", function() - expect(assertLifetime({{"c"}}, "c")).to.equal(true) - expect(assertLifetime({"c", {1, 2, 3}, 4, 5, 6}, "c")).to.equal(true) - expect(assertLifetime({1, 2, 3, {"c", 4, 5, 6}}, "c")).to.equal(true) - expect(assertLifetime({1, 2, 3, 4, 5, {6, "c"}}, "c")).to.equal(true) - end) - it("allows correct order in nested arrays", function() - expect(assertLifetime({{"p"}, {"c"}}, "c", "p")).to.equal(true) - expect(assertLifetime({1, {2, 3, "p"}, "c", 4, 5, 6}, "c", "p")).to.equal(true) - expect(assertLifetime({{1, {"p"}}, 2, {3, 4}, {"c", 5, 6}}, "c", "p")).to.equal(true) - expect(assertLifetime({"p", 1, 2, 3, {4, 5, 6, "c"}}, "c", "p")).to.equal(true) - expect(assertLifetime({"p", {"c", 1, 2, 3, 4}, 5, 6}, "c", "p")).to.equal(true) - expect(assertLifetime({1, {{2, 3}, 4, 5, 6, "p"}, "c"}, "c", "p")).to.equal(true) - end) - it("disallows incorrect order in nested arrays", function() - expect(assertLifetime({{"c"}, {"p"}}, "c", "p")).to.equal(false) - expect(assertLifetime({1, {2, 3, "c"}, "p", 4, 5, 6}, "c", "p")).to.equal(false) - expect(assertLifetime({{1, {"c"}}, 2, {3, 4}, {"p", 5, 6}}, "c", "p")).to.equal(false) - expect(assertLifetime({"c", 1, 2, 3, {4, 5, 6, "p"}}, "c", "p")).to.equal(false) - expect(assertLifetime({"c", {"p", 1, 2, 3, 4}, 5, 6}, "c", "p")).to.equal(false) - expect(assertLifetime({1, {{2, 3}, 4, 5, 6, "c"}, "p"}, "c", "p")).to.equal(false) - end) -end \ No newline at end of file From 30ba6fd2030eed8d6bb673561c6ef46551c10663 Mon Sep 17 00:00:00 2001 From: Elttob Date: Wed, 29 Nov 2023 19:32:38 +0000 Subject: [PATCH 095/287] Fix Attribute lifetime analysis --- src/Instances/Attribute.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Instances/Attribute.lua b/src/Instances/Attribute.lua index 4c9a648d5..9591570da 100644 --- a/src/Instances/Attribute.lua +++ b/src/Instances/Attribute.lua @@ -31,7 +31,7 @@ local function Attribute(attributeName: string): PubTypes.SpecialKey applyTo: Instance ) if isState(value) then - if whichLivesLonger(scope, applyTo, value.scope, value) == "b" then + if whichLivesLonger(scope, applyTo, value.scope, value) == "a" then logWarn("possiblyOutlives", `The {value.kind} object, bound to [Attribute "{attributeName}"],`, `the {applyTo.ClassName} instance`) end local didDefer = false From 6c172628c5d38f5e741001490f2fe93946783ab4 Mon Sep 17 00:00:00 2001 From: Elttob Date: Thu, 30 Nov 2023 14:17:05 +0000 Subject: [PATCH 096/287] Spring and Tween now assert lifetimes --- src/Animation/Spring.lua | 15 ++++++++++----- src/Animation/Tween.lua | 10 ++++++++-- 2 files changed, 18 insertions(+), 7 deletions(-) diff --git a/src/Animation/Spring.lua b/src/Animation/Spring.lua index 1bb020e15..b7f988418 100644 --- a/src/Animation/Spring.lua +++ b/src/Animation/Spring.lua @@ -13,8 +13,10 @@ local logErrorNonFatal = require(Package.Logging.logErrorNonFatal) local unpackType = require(Package.Animation.unpackType) local SpringScheduler = require(Package.Animation.SpringScheduler) local updateAll = require(Package.State.updateAll) -local xtypeof = require(Package.Utility.xtypeof) +local isState = require(Package.State.isState) local peek = require(Package.State.peek) +local whichLivesLonger = require(Package.Memory.whichLivesLonger) +local logWarn = require(Package.Logging.logWarn) local class = {} @@ -168,7 +170,7 @@ end local function Spring( scope: PubTypes.Scope, - goalState: PubTypes.Value, + goalState: PubTypes.StateObject, speed: PubTypes.CanBeState?, damping: PubTypes.CanBeState? ): Types.Spring @@ -181,10 +183,10 @@ local function Spring( end local dependencySet = {[goalState] = true} - if xtypeof(speed) == "State" then + if isState(speed) then dependencySet[speed] = true end - if xtypeof(damping) == "State" then + if isState(damping) then dependencySet[damping] = true end @@ -211,11 +213,14 @@ local function Spring( _springGoals = nil, _springVelocities = nil }, CLASS_METATABLE) + table.insert(scope, self) + if whichLivesLonger(scope, self, goalState.scope, goalState) == "a" then + logWarn("possiblyOutlives", `The {goalState.kind} object`, `the Spring that is following it`) + end -- add this object to the goal state's dependent set goalState.dependentSet[self] = true self:update() - table.insert(self, scope) return self end diff --git a/src/Animation/Tween.lua b/src/Animation/Tween.lua index c6fe17603..ee51524fb 100644 --- a/src/Animation/Tween.lua +++ b/src/Animation/Tween.lua @@ -14,6 +14,8 @@ local logError = require(Package.Logging.logError) local logErrorNonFatal = require(Package.Logging.logErrorNonFatal) local xtypeof = require(Package.Utility.xtypeof) local peek = require(Package.State.peek) +local whichLivesLonger = require(Package.Memory.whichLivesLonger) +local logWarn = require(Package.Logging.logWarn) local class = {} @@ -79,7 +81,7 @@ end local function Tween( scope: PubTypes.Scope, - goalState: PubTypes.StateObject, + goalState: PubTypes.StateObject, tweenInfo: PubTypes.CanBeState? ): Types.Tween local currentValue = peek(goalState) @@ -125,9 +127,13 @@ local function Tween( _currentlyAnimating = false }, CLASS_METATABLE) + table.insert(scope, self) + if whichLivesLonger(scope, self, goalState.scope, goalState) == "a" then + logWarn("possiblyOutlives", `The {goalState.kind} object`, `the Tween that is following it`) + end + -- add this object to the goal state's dependent set goalState.dependentSet[self] = true - table.insert(scope, self) return self end From b82ce4e968ff5ff0d877b71b980c0c3dd43eb1c7 Mon Sep 17 00:00:00 2001 From: Elttob Date: Thu, 30 Nov 2023 14:42:14 +0000 Subject: [PATCH 097/287] Use after destroy detection --- src/Animation/Spring.lua | 5 ++++- src/Animation/Tween.lua | 5 ++++- src/Instances/Attribute.lua | 4 +++- src/Instances/AttributeOut.lua | 4 +++- src/Instances/Out.lua | 4 +++- src/Instances/Ref.lua | 4 +++- src/Instances/applyInstanceProps.lua | 4 +++- src/Logging/messages.lua | 3 ++- src/PubTypes.lua | 2 +- src/State/Computed.lua | 5 ++++- src/State/For.lua | 1 + src/State/Observer.lua | 6 +++++- src/State/Value.lua | 1 + 13 files changed, 37 insertions(+), 11 deletions(-) diff --git a/src/Animation/Spring.lua b/src/Animation/Spring.lua index b7f988418..5805ec6f9 100644 --- a/src/Animation/Spring.lua +++ b/src/Animation/Spring.lua @@ -163,6 +163,7 @@ function class:get() end function class:destroy() + self.scope = nil for dependency in pairs(self.dependencySet) do dependency.dependentSet[self] = nil end @@ -214,7 +215,9 @@ local function Spring( _springVelocities = nil }, CLASS_METATABLE) table.insert(scope, self) - if whichLivesLonger(scope, self, goalState.scope, goalState) == "a" then + if goalState.scope == nil then + logError("useAfterDestroy", `The {goalState.kind} object`, `the Spring that is following it`) + elseif whichLivesLonger(scope, self, goalState.scope, goalState) == "a" then logWarn("possiblyOutlives", `The {goalState.kind} object`, `the Spring that is following it`) end diff --git a/src/Animation/Tween.lua b/src/Animation/Tween.lua index ee51524fb..62cc5950e 100644 --- a/src/Animation/Tween.lua +++ b/src/Animation/Tween.lua @@ -74,6 +74,7 @@ function class:get() end function class:destroy() + self.scope = nil for dependency in pairs(self.dependencySet) do dependency.dependentSet[self] = nil end @@ -128,7 +129,9 @@ local function Tween( }, CLASS_METATABLE) table.insert(scope, self) - if whichLivesLonger(scope, self, goalState.scope, goalState) == "a" then + if goalState.scope == nil then + logError("useAfterDestroy", `The {goalState.kind} object`, `the Tween that is following it`) + elseif whichLivesLonger(scope, self, goalState.scope, goalState) == "a" then logWarn("possiblyOutlives", `The {goalState.kind} object`, `the Tween that is following it`) end diff --git a/src/Instances/Attribute.lua b/src/Instances/Attribute.lua index 9591570da..92ee5e97d 100644 --- a/src/Instances/Attribute.lua +++ b/src/Instances/Attribute.lua @@ -31,7 +31,9 @@ local function Attribute(attributeName: string): PubTypes.SpecialKey applyTo: Instance ) if isState(value) then - if whichLivesLonger(scope, applyTo, value.scope, value) == "a" then + if value.scope == nil then + logError("useAfterDestroy", `The {value.kind} object, bound to [Attribute "{attributeName}"],`, `the {applyTo.ClassName} instance`) + elseif whichLivesLonger(scope, applyTo, value.scope, value) == "a" then logWarn("possiblyOutlives", `The {value.kind} object, bound to [Attribute "{attributeName}"],`, `the {applyTo.ClassName} instance`) end local didDefer = false diff --git a/src/Instances/AttributeOut.lua b/src/Instances/AttributeOut.lua index c87dffaa7..d4d637444 100644 --- a/src/Instances/AttributeOut.lua +++ b/src/Instances/AttributeOut.lua @@ -33,7 +33,9 @@ local function AttributeOut(attributeName: string): PubTypes.SpecialKey if not ok then logError("invalidOutAttributeName", applyTo.ClassName, attributeName) else - if whichLivesLonger(scope, applyTo, stateObject.scope, stateObject) == "a" then + if stateObject.scope == nil then + logError("useAfterDestroy", `The Value object, which [AttributeOut "{attributeName}"] outputs to,`, `the {applyTo.ClassName} instance`) + elseif whichLivesLonger(scope, applyTo, stateObject.scope, stateObject) == "a" then logWarn("possiblyOutlives", `The Value object, which [AttributeOut "{attributeName}"] outputs to,`, `the {applyTo.ClassName} instance`) end stateObject:set((applyTo :: any):GetAttribute(attributeName)) diff --git a/src/Instances/Out.lua b/src/Instances/Out.lua index c04308710..ccd9fa8f3 100644 --- a/src/Instances/Out.lua +++ b/src/Instances/Out.lua @@ -29,7 +29,9 @@ local function Out(propertyName: string): PubTypes.SpecialKey elseif xtypeof(outState) ~= "State" or outState.kind ~= "Value" then logError("invalidOutType") else - if whichLivesLonger(scope, applyTo, outState.scope, outState) == "a" then + if outState.scope == nil then + logError("useAfterDestroy", `The Value, which [Out "{propertyName}"] outputs to,`, `the {applyTo.ClassName} instance`) + elseif whichLivesLonger(scope, applyTo, outState.scope, outState) == "a" then logWarn("possiblyOutlives", `The Value, which [Out "{propertyName}"] outputs to,`, `the {applyTo.ClassName} instance`) end outState:set((applyTo :: any)[propertyName]) diff --git a/src/Instances/Ref.lua b/src/Instances/Ref.lua index 18ad61376..1ec1d84f8 100644 --- a/src/Instances/Ref.lua +++ b/src/Instances/Ref.lua @@ -25,7 +25,9 @@ function Ref:apply( if xtypeof(refState) ~= "State" or refState.kind ~= "Value" then logError("invalidRefType") else - if whichLivesLonger(scope, applyTo, refState.scope, refState) == "a" then + if refState.scope == nil then + logError("useAfterDestroy", "The Value object, which [Ref] outputs to,", `the {applyTo} instance`) + elseif whichLivesLonger(scope, applyTo, refState.scope, refState) == "a" then logWarn("possiblyOutlives", "The Value object, which [Ref] outputs to,", `the {applyTo} instance`) end refState:set(applyTo) diff --git a/src/Instances/applyInstanceProps.lua b/src/Instances/applyInstanceProps.lua index b027cd359..7be9f72f0 100644 --- a/src/Instances/applyInstanceProps.lua +++ b/src/Instances/applyInstanceProps.lua @@ -65,7 +65,9 @@ local function bindProperty( value: PubTypes.CanBeState ) if isState(value) then - if whichLivesLonger(scope, instance, value.scope, value) ~= "b" then + if value.scope == nil then + logError("useAfterDestroy", `The {value.kind} object, bound to {property},`, `the {instance.ClassName} instance`) + elseif whichLivesLonger(scope, instance, value.scope, value) ~= "b" then logWarn("possiblyOutlives", `The {value.kind} object, bound to {property},`, `the {instance.ClassName} instance`) end diff --git a/src/Logging/messages.lua b/src/Logging/messages.lua index 0fe35449d..d2826a1b3 100644 --- a/src/Logging/messages.lua +++ b/src/Logging/messages.lua @@ -39,5 +39,6 @@ return { unknownMessage = "Unknown error: ERROR_MESSAGE", unrecognisedChildType = "'%s' type children aren't accepted by `[Children]`.", unrecognisedPropertyKey = "'%s' keys aren't accepted in property tables.", - unrecognisedPropertyStage = "'%s' isn't a valid stage for a special key to be applied at." + unrecognisedPropertyStage = "'%s' isn't a valid stage for a special key to be applied at.", + useAfterDestroy = "%s is no longer valid - it was destroyed before %s.", } \ No newline at end of file diff --git a/src/PubTypes.lua b/src/PubTypes.lua index 118122cf1..95391e8c4 100644 --- a/src/PubTypes.lua +++ b/src/PubTypes.lua @@ -51,7 +51,7 @@ export type Scope = {Task} & Constructors -- An object which uses a scope to dictate how long it lives. export type ScopeLifetime = { - scope: Scope + scope: Scope? } -- Script-readable version information. diff --git a/src/State/Computed.lua b/src/State/Computed.lua index dfdb1f4e2..4057acc78 100644 --- a/src/State/Computed.lua +++ b/src/State/Computed.lua @@ -47,7 +47,9 @@ function class:update(): boolean local innerScope = deriveScope(self.scope) local function use(target: PubTypes.CanBeState): T if isState(target) then - if whichLivesLonger(self.scope, self, target.scope, target) == "a" then + if target.scope == nil then + logError("useAfterDestroy", `The {target.kind} object`, "the Computed that is use()-ing it") + elseif whichLivesLonger(self.scope, self, target.scope, target) == "a" then logWarn("possiblyOutlives", `The {target.kind} object`, "the Computed that is use()-ing it") end self.dependencySet[target] = true @@ -104,6 +106,7 @@ function class:get() end function class:destroy() + self.scope = nil for dependency in pairs(self.dependencySet) do dependency.dependentSet[self] = nil end diff --git a/src/State/For.lua b/src/State/For.lua index f55343497..b3bf30ffc 100644 --- a/src/State/For.lua +++ b/src/State/For.lua @@ -195,6 +195,7 @@ function class:get() end function class:destroy() + self.scope = nil for dependency in pairs(self.dependencySet) do dependency.dependentSet[self] = nil end diff --git a/src/State/Observer.lua b/src/State/Observer.lua index f28691b3e..ceb684f3a 100644 --- a/src/State/Observer.lua +++ b/src/State/Observer.lua @@ -13,6 +13,7 @@ local Types = require(Package.Types) local External = require(Package.External) local whichLivesLonger = require(Package.Memory.whichLivesLonger) local logWarn = require(Package.Logging.logWarn) +local logError = require(Package.Logging.logError) type Set = {[T]: any} @@ -55,6 +56,7 @@ function class:onBind(callback: () -> ()): () -> () end function class:destroy() + self.scope = nil for dependency in pairs(self.dependencySet) do dependency.dependentSet[self] = nil end @@ -74,7 +76,9 @@ local function Observer( }, CLASS_METATABLE) table.insert(scope, self) - if whichLivesLonger(scope, self, watchedState.scope, watchedState) == "a" then + if watchedState.scope == nil then + logError("useAfterDestroy", `The {watchedState.kind} object`, `the Observer that is watching it`) + elseif whichLivesLonger(scope, self, watchedState.scope, watchedState) == "a" then logWarn("possiblyOutlives", `The {watchedState.kind} object`, `the Observer that is watching it`) end diff --git a/src/State/Value.lua b/src/State/Value.lua index 79b701b14..90f8692a4 100644 --- a/src/State/Value.lua +++ b/src/State/Value.lua @@ -46,6 +46,7 @@ function class:get() end function class:destroy() + self.scope = nil end local function Value( From 155bd55cb97d3f819bc2a4513e8ee6cd4e16e108 Mon Sep 17 00:00:00 2001 From: Elttob Date: Thu, 30 Nov 2023 14:46:07 +0000 Subject: [PATCH 098/287] Add destroyed twice error --- src/Animation/Spring.lua | 3 +++ src/Animation/Tween.lua | 3 +++ src/Logging/messages.lua | 1 + src/State/Computed.lua | 3 +++ src/State/For.lua | 3 +++ src/State/Observer.lua | 3 +++ src/State/Value.lua | 3 +++ 7 files changed, 19 insertions(+) diff --git a/src/Animation/Spring.lua b/src/Animation/Spring.lua index 5805ec6f9..6b8dc61bf 100644 --- a/src/Animation/Spring.lua +++ b/src/Animation/Spring.lua @@ -163,6 +163,9 @@ function class:get() end function class:destroy() + if self.scope == nil then + logError("destroyedTwice", "Spring") + end self.scope = nil for dependency in pairs(self.dependencySet) do dependency.dependentSet[self] = nil diff --git a/src/Animation/Tween.lua b/src/Animation/Tween.lua index 62cc5950e..cea8be715 100644 --- a/src/Animation/Tween.lua +++ b/src/Animation/Tween.lua @@ -74,6 +74,9 @@ function class:get() end function class:destroy() + if self.scope == nil then + logError("destroyedTwice", "Tween") + end self.scope = nil for dependency in pairs(self.dependencySet) do dependency.dependentSet[self] = nil diff --git a/src/Logging/messages.lua b/src/Logging/messages.lua index d2826a1b3..03f3b2360 100644 --- a/src/Logging/messages.lua +++ b/src/Logging/messages.lua @@ -13,6 +13,7 @@ return { cannotCreateClass = "Can't create a new instance of class '%s'.", cleanupWasRenamed = "`Fusion.cleanup` was renamed to `Fusion.doCleanup`. This will be an error in future versions of Fusion.", computedCallbackError = "Computed callback error: ERROR_MESSAGE", + destroyedTwice = "Attempted to destroy %s twice; if you meant to destroy this separately from the rest of the scope, put it in its own scope and call doCleanup() on that scope instead.", destructorNeededComputed = "To return instances from Computeds, provide a destructor function. This will be an error soon - see discussion #183 on GitHub.", multiReturnComputed = "Returning multiple values from Computeds is discouraged, as behaviour will change soon - see discussion #189 on GitHub.", forKeyCollision = "The key '%s' was returned multiple times simultaneously, which is not allowed in `For` objects.", diff --git a/src/State/Computed.lua b/src/State/Computed.lua index 4057acc78..8dd9590c4 100644 --- a/src/State/Computed.lua +++ b/src/State/Computed.lua @@ -106,6 +106,9 @@ function class:get() end function class:destroy() + if self.scope == nil then + logError("destroyedTwice", "Computed") + end self.scope = nil for dependency in pairs(self.dependencySet) do dependency.dependentSet[self] = nil diff --git a/src/State/For.lua b/src/State/For.lua index b3bf30ffc..97287778b 100644 --- a/src/State/For.lua +++ b/src/State/For.lua @@ -195,6 +195,9 @@ function class:get() end function class:destroy() + if self.scope == nil then + logError("destroyedTwice", "For") + end self.scope = nil for dependency in pairs(self.dependencySet) do dependency.dependentSet[self] = nil diff --git a/src/State/Observer.lua b/src/State/Observer.lua index ceb684f3a..aa9e37f7c 100644 --- a/src/State/Observer.lua +++ b/src/State/Observer.lua @@ -56,6 +56,9 @@ function class:onBind(callback: () -> ()): () -> () end function class:destroy() + if self.scope == nil then + logError("destroyedTwice", "Observer") + end self.scope = nil for dependency in pairs(self.dependencySet) do dependency.dependentSet[self] = nil diff --git a/src/State/Value.lua b/src/State/Value.lua index 90f8692a4..710fadf94 100644 --- a/src/State/Value.lua +++ b/src/State/Value.lua @@ -46,6 +46,9 @@ function class:get() end function class:destroy() + if self.scope == nil then + logError("destroyedTwice", "Value") + end self.scope = nil end From e8513f60e8e6c8655b4d802a550d85d0114cfe48 Mon Sep 17 00:00:00 2001 From: Elttob Date: Thu, 30 Nov 2023 15:18:32 +0000 Subject: [PATCH 099/287] Add developer-friendly scopeMissing error --- src/Animation/Spring.lua | 5 ++++- src/Animation/Tween.lua | 9 ++++++--- src/Instances/Hydrate.lua | 4 ++++ src/Instances/New.lua | 3 +++ src/Logging/messages.lua | 2 ++ src/State/Computed.lua | 9 +++++++-- src/State/For.lua | 2 +- src/State/ForKeys.lua | 9 +++++++-- src/State/ForPairs.lua | 9 +++++++-- src/State/ForValues.lua | 9 +++++++-- src/State/Observer.lua | 6 +++++- src/State/Value.lua | 6 +++++- src/Utility/restrictRead.lua | 28 -------------------------- src/init.lua | 5 ++--- test/Utility/restrictRead.spec.lua | 32 ------------------------------ test/init.spec.lua | 4 ++-- 16 files changed, 62 insertions(+), 80 deletions(-) delete mode 100644 src/Utility/restrictRead.lua delete mode 100644 test/Utility/restrictRead.spec.lua diff --git a/src/Animation/Spring.lua b/src/Animation/Spring.lua index 6b8dc61bf..cf02cba3b 100644 --- a/src/Animation/Spring.lua +++ b/src/Animation/Spring.lua @@ -164,7 +164,7 @@ end function class:destroy() if self.scope == nil then - logError("destroyedTwice", "Spring") + logError("destroyedTwice", nil, "Spring") end self.scope = nil for dependency in pairs(self.dependencySet) do @@ -178,6 +178,9 @@ local function Spring( speed: PubTypes.CanBeState?, damping: PubTypes.CanBeState? ): Types.Spring + if isState(scope) then + logError("scopeMissing", nil, "Springs", "myScope:Spring(goalState, speed, damping)") + end -- apply defaults for speed and damping if speed == nil then speed = 10 diff --git a/src/Animation/Tween.lua b/src/Animation/Tween.lua index cea8be715..b7e3868eb 100644 --- a/src/Animation/Tween.lua +++ b/src/Animation/Tween.lua @@ -12,7 +12,7 @@ local Types = require(Package.Types) local TweenScheduler = require(Package.Animation.TweenScheduler) local logError = require(Package.Logging.logError) local logErrorNonFatal = require(Package.Logging.logErrorNonFatal) -local xtypeof = require(Package.Utility.xtypeof) +local isState = require(Package.State.isState) local peek = require(Package.State.peek) local whichLivesLonger = require(Package.Memory.whichLivesLonger) local logWarn = require(Package.Logging.logWarn) @@ -75,7 +75,7 @@ end function class:destroy() if self.scope == nil then - logError("destroyedTwice", "Tween") + logError("destroyedTwice", nil, "Tween") end self.scope = nil for dependency in pairs(self.dependencySet) do @@ -88,6 +88,9 @@ local function Tween( goalState: PubTypes.StateObject, tweenInfo: PubTypes.CanBeState? ): Types.Tween + if isState(scope) then + logError("scopeMissing", nil, "Tweens", "myScope:Tween(goalState, tweenInfo)") + end local currentValue = peek(goalState) -- apply defaults for tween info @@ -96,7 +99,7 @@ local function Tween( end local dependencySet = {[goalState] = true} - local tweenInfoIsState = xtypeof(tweenInfo) == "State" + local tweenInfoIsState = isState(tweenInfo) if tweenInfoIsState then dependencySet[tweenInfo] = true end diff --git a/src/Instances/Hydrate.lua b/src/Instances/Hydrate.lua index 0ff618339..653327915 100644 --- a/src/Instances/Hydrate.lua +++ b/src/Instances/Hydrate.lua @@ -8,11 +8,15 @@ local Package = script.Parent.Parent local PubTypes = require(Package.PubTypes) local applyInstanceProps = require(Package.Instances.applyInstanceProps) +local logError = require(Package.Logging.logError) local function Hydrate( scope: PubTypes.Scope, target: Instance ) + if target == nil then + logError("scopeMissing", nil, "instances using Hydrate", "myScope:Hydrate (instance) { ... }") + end return function( props: PubTypes.PropertyTable ): Instance diff --git a/src/Instances/New.lua b/src/Instances/New.lua index f10625868..449f1fc52 100644 --- a/src/Instances/New.lua +++ b/src/Instances/New.lua @@ -15,6 +15,9 @@ local function New( scope: PubTypes.Scope, className: string ) + if className == nil then + logError("scopeMissing", nil, "instances using New", "myScope:New \"" .. scope .. "\" { ... }") + end return function( props: PubTypes.PropertyTable ): Instance diff --git a/src/Logging/messages.lua b/src/Logging/messages.lua index 03f3b2360..4be720637 100644 --- a/src/Logging/messages.lua +++ b/src/Logging/messages.lua @@ -15,6 +15,7 @@ return { computedCallbackError = "Computed callback error: ERROR_MESSAGE", destroyedTwice = "Attempted to destroy %s twice; if you meant to destroy this separately from the rest of the scope, put it in its own scope and call doCleanup() on that scope instead.", destructorNeededComputed = "To return instances from Computeds, provide a destructor function. This will be an error soon - see discussion #183 on GitHub.", + destructorRedundant = "%s destructors no longer do anything. If you wish to run code on destroy, `table.insert` a function into the `scope` argument. See #xxx on GitHub for advice.", multiReturnComputed = "Returning multiple values from Computeds is discouraged, as behaviour will change soon - see discussion #189 on GitHub.", forKeyCollision = "The key '%s' was returned multiple times simultaneously, which is not allowed in `For` objects.", forProcessorError = "Error while processing `For` object: ERROR_MESSAGE", @@ -34,6 +35,7 @@ return { mistypedTweenInfo = "The tween info of a tween must be a TweenInfo. (got a %s)", noTaskScheduler = "Fusion is not connected to an external task scheduler.", possiblyOutlives = "%s could be destroyed before %s. Ensure they're created in the correct scope & correct order.", + scopeMissing = "To create %s, provide a scope. (e.g. `%s`). See discussion#xxx on GitHub for advice.", springTypeMismatch = "The type '%s' doesn't match the spring's type '%s'.", stateGetWasRemoved = "`StateObject:get()` has been replaced by `use()` and `peek()` - see discussion #217 on GitHub.", strictReadError = "'%s' is not a valid member of '%s'.", diff --git a/src/State/Computed.lua b/src/State/Computed.lua index 8dd9590c4..4610ff5a5 100644 --- a/src/State/Computed.lua +++ b/src/State/Computed.lua @@ -107,7 +107,7 @@ end function class:destroy() if self.scope == nil then - logError("destroyedTwice", "Computed") + logError("destroyedTwice", nil, "Computed") end self.scope = nil for dependency in pairs(self.dependencySet) do @@ -121,8 +121,13 @@ end local function Computed( scope: PubTypes.Scope, processor: (PubTypes.Scope, PubTypes.Use) -> T, - destructor: any -- TODO: warn for this + destructor: any ): Types.Computed + if typeof(scope) == "function" then + logError("scopeMissing", nil, "Computeds", "myScope:Computed(function(scope, use) ... end)") + elseif destructor ~= nil then + logWarn("destructorRedundant", "Computed") + end local self = setmetatable({ type = "State", kind = "Computed", diff --git a/src/State/For.lua b/src/State/For.lua index 97287778b..e75c8ba00 100644 --- a/src/State/For.lua +++ b/src/State/For.lua @@ -196,7 +196,7 @@ end function class:destroy() if self.scope == nil then - logError("destroyedTwice", "For") + logError("destroyedTwice", nil, "For") end self.scope = nil for dependency in pairs(self.dependencySet) do diff --git a/src/State/ForKeys.lua b/src/State/ForKeys.lua index afdfdf230..251d555e3 100644 --- a/src/State/ForKeys.lua +++ b/src/State/ForKeys.lua @@ -26,9 +26,14 @@ local doCleanup = require(Package.Memory.doCleanup) local function ForKeys( scope: PubTypes.Scope, inputTable: PubTypes.CanBeState<{[KI]: V}>, - processor: (PubTypes.Scope, PubTypes.Use, KI) -> KO + processor: (PubTypes.Scope, PubTypes.Use, KI) -> KO, + destructor: any? ): Types.For - + if typeof(inputTable) == "function" then + logError("scopeMissing", nil, "ForKeys", "myScope:ForKeys(inputTable, function(scope, use, key) ... end)") + elseif destructor ~= nil then + logWarn("destructorRedundant", "ForKeys") + end return For( scope, inputTable, diff --git a/src/State/ForPairs.lua b/src/State/ForPairs.lua index e6671fa31..02f8f48aa 100644 --- a/src/State/ForPairs.lua +++ b/src/State/ForPairs.lua @@ -26,9 +26,14 @@ local doCleanup = require(Package.Memory.doCleanup) local function ForPairs( scope: PubTypes.Scope, inputTable: PubTypes.CanBeState<{[KI]: VI}>, - processor: (PubTypes.Scope, PubTypes.Use, KI, VI) -> (KO, VO) + processor: (PubTypes.Scope, PubTypes.Use, KI, VI) -> (KO, VO), + destructor: any? ): Types.For - + if typeof(inputTable) == "function" then + logError("scopeMissing", nil, "ForPairs", "myScope:ForPairs(inputTable, function(scope, use, key, value) ... end)") + elseif destructor ~= nil then + logWarn("destructorRedundant", "ForPairs") + end return For( scope, inputTable, diff --git a/src/State/ForValues.lua b/src/State/ForValues.lua index d30e22a11..c4efd4de9 100644 --- a/src/State/ForValues.lua +++ b/src/State/ForValues.lua @@ -26,9 +26,14 @@ local doCleanup = require(Package.Memory.doCleanup) local function ForValues( scope: PubTypes.Scope, inputTable: PubTypes.CanBeState<{[K]: VI}>, - processor: (PubTypes.Scope, PubTypes.Use, VI) -> VO + processor: (PubTypes.Scope, PubTypes.Use, VI) -> VO, + destructor: any? ): Types.For - + if typeof(inputTable) == "function" then + logError("scopeMissing", nil, "ForValues", "myScope:ForValues(inputTable, function(scope, use, value) ... end)") + elseif destructor ~= nil then + logWarn("destructorRedundant", "ForValues") + end return For( scope, inputTable, diff --git a/src/State/Observer.lua b/src/State/Observer.lua index aa9e37f7c..95a66e307 100644 --- a/src/State/Observer.lua +++ b/src/State/Observer.lua @@ -57,7 +57,7 @@ end function class:destroy() if self.scope == nil then - logError("destroyedTwice", "Observer") + logError("destroyedTwice", nil, "Observer") end self.scope = nil for dependency in pairs(self.dependencySet) do @@ -69,6 +69,10 @@ local function Observer( scope: PubTypes.Scope, watchedState: PubTypes.StateObject ): Types.Observer + if watchedState == nil then + logError("scopeMissing", nil, "Observer", "myScope:Observer(watchedState)") + end + local self = setmetatable({ type = "State", kind = "Observer", diff --git a/src/State/Value.lua b/src/State/Value.lua index 710fadf94..765255132 100644 --- a/src/State/Value.lua +++ b/src/State/Value.lua @@ -47,7 +47,7 @@ end function class:destroy() if self.scope == nil then - logError("destroyedTwice", "Value") + logError("destroyedTwice", nil, "Value") end self.scope = nil end @@ -56,6 +56,10 @@ local function Value( scope: PubTypes.Scope, initialValue: T ): Types.State + if initialValue == nil and (typeof(scope) ~= "table" or (scope[1] == nil and next(scope) ~= nil)) then + logError("scopeMissing", nil, "Value", "myScope:Value(initialValue)") + end + local self = setmetatable({ type = "State", kind = "Value", diff --git a/src/Utility/restrictRead.lua b/src/Utility/restrictRead.lua deleted file mode 100644 index c86b87758..000000000 --- a/src/Utility/restrictRead.lua +++ /dev/null @@ -1,28 +0,0 @@ ---!strict - ---[[ - Restricts the reading of missing members for a table. -]] - -local Package = script.Parent.Parent -local logError = require(Package.Logging.logError) - -type table = {[any]: any} - -local function restrictRead(tableName: string, strictTable: table): table - -- FIXME: Typed Luau doesn't recognise this correctly yet - local metatable = getmetatable(strictTable :: any) - - if metatable == nil then - metatable = {} - setmetatable(strictTable, metatable) - end - - function metatable:__index(memberName) - logError("strictReadError", nil, tostring(memberName), tableName) - end - - return strictTable -end - -return restrictRead \ No newline at end of file diff --git a/src/init.lua b/src/init.lua index 158081c04..3a5eaa715 100644 --- a/src/init.lua +++ b/src/init.lua @@ -6,7 +6,6 @@ local PubTypes = require(script.PubTypes) local External = require(script.External) -local restrictRead = require(script.Utility.restrictRead) export type Symbol = PubTypes.Symbol export type Animatable = PubTypes.Animatable @@ -35,7 +34,7 @@ do External.setExternalScheduler(RobloxExternal) end -local Fusion = restrictRead("Fusion", { +local Fusion: PubTypes.Fusion = { version = {major = 0, minor = 3, isRelease = false}, cleanup = require(script.Memory.legacyCleanup), @@ -66,6 +65,6 @@ local Fusion = restrictRead("Fusion", { Attribute = require(script.Instances.Attribute), AttributeChange = require(script.Instances.AttributeChange), AttributeOut = require(script.Instances.AttributeOut) -}) :: PubTypes.Fusion +} return Fusion diff --git a/test/Utility/restrictRead.spec.lua b/test/Utility/restrictRead.spec.lua deleted file mode 100644 index b89f1a35a..000000000 --- a/test/Utility/restrictRead.spec.lua +++ /dev/null @@ -1,32 +0,0 @@ -local Package = game:GetService("ReplicatedStorage").Fusion -local restrictRead = require(Package.Utility.restrictRead) - -return function() - it("should error for missing members", function() - local strictTable = restrictRead("", {}) - - expect(function() - local x = strictTable.thisDoesNotExist - end).to.throw("strictReadError") - end) - - it("should not error for present members", function() - local strictTable = restrictRead("", { - foo = 2, - bar = "blue" - }) - - expect(function() - local x = strictTable.foo - local y = strictTable.bar - end).never.to.throw() - end) - - it("should preserve metatables", function() - local metatable = {} - local strictTable = setmetatable({}, metatable) - restrictRead("", strictTable) - - expect(getmetatable(strictTable)).to.equal(metatable) - end) -end \ No newline at end of file diff --git a/test/init.spec.lua b/test/init.spec.lua index 6aa15bb4c..c0146bc9c 100644 --- a/test/init.spec.lua +++ b/test/init.spec.lua @@ -57,9 +57,9 @@ return function() end end) - it("should error when accessing non-existent APIs", function() + it("should not error when accessing non-existent APIs", function() expect(function() local foo = Fusion.thisIsNotARealAPI - end).to.throw("strictReadError") + end).never.to.throw() end) end \ No newline at end of file From 23d277ba5f0a251ce996a2d73b0fdeda012d7274 Mon Sep 17 00:00:00 2001 From: Elttob Date: Thu, 30 Nov 2023 15:24:31 +0000 Subject: [PATCH 100/287] Fix Observer unit test --- src/Logging/messages.lua | 2 +- src/Memory/doCleanup.lua | 1 + src/State/Observer.lua | 2 +- test/State/Observer.spec.lua | 8 ++++++-- 4 files changed, 9 insertions(+), 4 deletions(-) diff --git a/src/Logging/messages.lua b/src/Logging/messages.lua index 4be720637..8affbb17c 100644 --- a/src/Logging/messages.lua +++ b/src/Logging/messages.lua @@ -35,7 +35,7 @@ return { mistypedTweenInfo = "The tween info of a tween must be a TweenInfo. (got a %s)", noTaskScheduler = "Fusion is not connected to an external task scheduler.", possiblyOutlives = "%s could be destroyed before %s. Ensure they're created in the correct scope & correct order.", - scopeMissing = "To create %s, provide a scope. (e.g. `%s`). See discussion#xxx on GitHub for advice.", + scopeMissing = "To create %s, provide a scope. (e.g. `%s`). See discussion #xxx on GitHub for advice.", springTypeMismatch = "The type '%s' doesn't match the spring's type '%s'.", stateGetWasRemoved = "`StateObject:get()` has been replaced by `use()` and `peek()` - see discussion #217 on GitHub.", strictReadError = "'%s' is not a valid member of '%s'.", diff --git a/src/Memory/doCleanup.lua b/src/Memory/doCleanup.lua index 69870cff5..d1d57901c 100644 --- a/src/Memory/doCleanup.lua +++ b/src/Memory/doCleanup.lua @@ -41,6 +41,7 @@ local function doCleanupOne(task: any) -- objects are added in order of construction. for index = #task, 1, -1 do doCleanupOne(task[index]) + task[index] = nil end end end diff --git a/src/State/Observer.lua b/src/State/Observer.lua index 95a66e307..031ea1674 100644 --- a/src/State/Observer.lua +++ b/src/State/Observer.lua @@ -70,7 +70,7 @@ local function Observer( watchedState: PubTypes.StateObject ): Types.Observer if watchedState == nil then - logError("scopeMissing", nil, "Observer", "myScope:Observer(watchedState)") + logError("scopeMissing", nil, "Observers", "myScope:Observer(watchedState)") end local self = setmetatable({ diff --git a/test/State/Observer.spec.lua b/test/State/Observer.spec.lua index c97d7fa95..6e232308a 100644 --- a/test/State/Observer.spec.lua +++ b/test/State/Observer.spec.lua @@ -95,13 +95,17 @@ return function() it("disconnects on destroy", function() local scope = {} local dependency = Value(scope, 5) - local observer = Observer(scope, dependency) + + local subScope = {} + table.insert(scope, subScope) + local observer = Observer(subScope, dependency) + local numFires = 0 local _ = observer:onChange(function() numFires += 1 end) dependency:set(15) - observer:destroy() + doCleanup(subScope) dependency:set(2) expect(numFires).to.equal(1) From 54d670e00581d836db6640018ee7a76c1a1d78a2 Mon Sep 17 00:00:00 2001 From: Elttob Date: Thu, 30 Nov 2023 15:25:23 +0000 Subject: [PATCH 101/287] Expand doCleanup spec to cover array nil-ing --- test/Memory/doCleanup.spec.lua | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/test/Memory/doCleanup.spec.lua b/test/Memory/doCleanup.spec.lua index a6643cc2e..7f40dcba4 100644 --- a/test/Memory/doCleanup.spec.lua +++ b/test/Memory/doCleanup.spec.lua @@ -63,9 +63,14 @@ return function() numRuns += 1 end - doCleanup({doRun, doRun, doRun}) + local arr = {doRun, doRun, doRun} + + doCleanup(arr) expect(numRuns).to.equal(3) + expect(arr[3]).to.equal(nil) + expect(arr[2]).to.equal(nil) + expect(arr[1]).to.equal(nil) end) it("should clean up contents of nested arrays", function() From 1e2fe6a2f5539169f074d2d89f775879fa10c02b Mon Sep 17 00:00:00 2001 From: Elttob Date: Sat, 2 Dec 2023 22:26:24 +0000 Subject: [PATCH 102/287] Tweaks after testing in codebases --- src/Animation/Spring.lua | 8 +- src/Animation/SpringScheduler.lua | 10 +- src/Animation/Tween.lua | 2 +- src/Animation/lerpType.lua | 2 +- src/Animation/unpackType.lua | 2 +- src/Instances/Attribute.lua | 60 ++++--- src/Instances/AttributeChange.lua | 47 +++-- src/Instances/AttributeOut.lua | 59 ++++--- src/Instances/Children.lua | 251 +++++++++++++-------------- src/Instances/Hydrate.lua | 2 +- src/Instances/New.lua | 2 +- src/Instances/OnChange.lua | 43 +++-- src/Instances/OnEvent.lua | 39 ++--- src/Instances/Out.lua | 59 ++++--- src/Instances/Ref.lua | 43 +++-- src/Instances/applyInstanceProps.lua | 3 +- src/Memory/whichLivesLonger.lua | 15 +- src/PubTypes.lua | 22 +-- src/RobloxExternal.lua | 2 +- src/State/Computed.lua | 7 +- src/State/For.lua | 7 +- src/State/ForKeys.lua | 13 +- src/State/ForPairs.lua | 11 +- src/State/ForValues.lua | 13 +- src/State/Observer.lua | 2 +- src/State/peek.lua | 2 +- src/State/updateAll.lua | 8 +- src/Types.lua | 21 ++- src/Utility/None.lua | 13 -- src/init.lua | 1 - 30 files changed, 387 insertions(+), 382 deletions(-) delete mode 100644 src/Utility/None.lua diff --git a/src/Animation/Spring.lua b/src/Animation/Spring.lua index cf02cba3b..943e52f4e 100644 --- a/src/Animation/Spring.lua +++ b/src/Animation/Spring.lua @@ -218,11 +218,15 @@ local function Spring( _springPositions = nil, _springGoals = nil, - _springVelocities = nil + _springVelocities = nil, + + _lastSchedule = -math.huge, + _startDisplacements = {}, + _startVelocities = {} }, CLASS_METATABLE) table.insert(scope, self) if goalState.scope == nil then - logError("useAfterDestroy", `The {goalState.kind} object`, `the Spring that is following it`) + logError("useAfterDestroy", nil, `The {goalState.kind} object`, `the Spring that is following it`) elseif whichLivesLonger(scope, self, goalState.scope, goalState) == "a" then logWarn("possiblyOutlives", `The {goalState.kind} object`, `the Spring that is following it`) end diff --git a/src/Animation/SpringScheduler.lua b/src/Animation/SpringScheduler.lua index ae5ad8d3d..05a37ab54 100644 --- a/src/Animation/SpringScheduler.lua +++ b/src/Animation/SpringScheduler.lua @@ -25,8 +25,8 @@ function SpringScheduler.add(spring: Spring) -- the last update time so that springs started within the same frame have -- identical time steps spring._lastSchedule = lastUpdateTime - spring._startDisplacements = {} - spring._startVelocities = {} + table.clear(spring._startDisplacements) + table.clear(spring._startVelocities) for index, goal in ipairs(spring._springGoals) do spring._startDisplacements[index] = spring._springPositions[index] - goal spring._startVelocities[index] = spring._springVelocities[index] @@ -46,7 +46,11 @@ local function updateAllSprings( lastUpdateTime = now for spring in pairs(activeSprings) do - local posPos, posVel, velPos, velVel = springCoefficients(lastUpdateTime - spring._lastSchedule, spring._currentDamping, spring._currentSpeed) + local posPos, posVel, velPos, velVel = springCoefficients( + lastUpdateTime - spring._lastSchedule, + spring._currentDamping, + spring._currentSpeed + ) local positions = spring._springPositions local velocities = spring._springVelocities diff --git a/src/Animation/Tween.lua b/src/Animation/Tween.lua index b7e3868eb..e9738e1a1 100644 --- a/src/Animation/Tween.lua +++ b/src/Animation/Tween.lua @@ -136,7 +136,7 @@ local function Tween( table.insert(scope, self) if goalState.scope == nil then - logError("useAfterDestroy", `The {goalState.kind} object`, `the Tween that is following it`) + logError("useAfterDestroy", nil, `The {goalState.kind} object`, `the Tween that is following it`) elseif whichLivesLonger(scope, self, goalState.scope, goalState) == "a" then logWarn("possiblyOutlives", `The {goalState.kind} object`, `the Tween that is following it`) end diff --git a/src/Animation/lerpType.lua b/src/Animation/lerpType.lua index 3530dbec6..2db0e1ddc 100644 --- a/src/Animation/lerpType.lua +++ b/src/Animation/lerpType.lua @@ -1,4 +1,5 @@ --!strict +--!nolint LocalShadow --[[ Linearly interpolates the given animatable types by a ratio. @@ -10,7 +11,6 @@ ]] local Package = script.Parent.Parent -local PubTypes = require(Package.PubTypes) local Oklab = require(Package.Colour.Oklab) local function lerpType(from: any, to: any, ratio: number): any diff --git a/src/Animation/unpackType.lua b/src/Animation/unpackType.lua index 7eacfbe24..1b8239006 100644 --- a/src/Animation/unpackType.lua +++ b/src/Animation/unpackType.lua @@ -1,4 +1,5 @@ --!strict +--!nolint LocalShadow --[[ Unpacks an animatable type into an array of numbers. @@ -13,7 +14,6 @@ ]] local Package = script.Parent.Parent -local PubTypes = require(Package.PubTypes) local Oklab = require(Package.Colour.Oklab) local function unpackType(value: any, typeString: string): {number} diff --git a/src/Instances/Attribute.lua b/src/Instances/Attribute.lua index 92ee5e97d..66f1f20c5 100644 --- a/src/Instances/Attribute.lua +++ b/src/Instances/Attribute.lua @@ -16,43 +16,41 @@ local peek = require(Package.State.peek) local whichLivesLonger = require(Package.Memory.whichLivesLonger) local function Attribute(attributeName: string): PubTypes.SpecialKey - local AttributeKey = {} - AttributeKey.type = "SpecialKey" - AttributeKey.kind = "Attribute" - AttributeKey.stage = "self" - if attributeName == nil then logError("attributeNameNil") end - - function AttributeKey:apply( - scope: PubTypes.Scope, - value: any, - applyTo: Instance - ) - if isState(value) then - if value.scope == nil then - logError("useAfterDestroy", `The {value.kind} object, bound to [Attribute "{attributeName}"],`, `the {applyTo.ClassName} instance`) - elseif whichLivesLonger(scope, applyTo, value.scope, value) == "a" then - logWarn("possiblyOutlives", `The {value.kind} object, bound to [Attribute "{attributeName}"],`, `the {applyTo.ClassName} instance`) - end - local didDefer = false - local function update() - if not didDefer then - didDefer = true - External.doTaskDeferred(function() - didDefer = false - applyTo:SetAttribute(attributeName, peek(value)) - end) + return { + type = "SpecialKey", + kind = "Attribute", + stage = "self", + apply = function( + scope: PubTypes.Scope, + value: any, + applyTo: Instance + ) + if isState(value) then + if value.scope == nil then + logError("useAfterDestroy", nil, `The {value.kind} object, bound to [Attribute "{attributeName}"],`, `the {applyTo.ClassName} instance`) + elseif whichLivesLonger(scope, applyTo, value.scope, value) == "a" then + logWarn("possiblyOutlives", `The {value.kind} object, bound to [Attribute "{attributeName}"],`, `the {applyTo.ClassName} instance`) end + local didDefer = false + local function update() + if not didDefer then + didDefer = true + External.doTaskDeferred(function() + didDefer = false + applyTo:SetAttribute(attributeName, peek(value)) + end) + end + end + applyTo:SetAttribute(attributeName, peek(value)) + table.insert(scope, Observer(scope, value :: any):onChange(update)) + else + applyTo:SetAttribute(attributeName, value) end - applyTo:SetAttribute(attributeName, peek(value)) - table.insert(scope, Observer(scope, value :: any):onChange(update)) - else - applyTo:SetAttribute(attributeName, value) end - end - return AttributeKey + } end return Attribute \ No newline at end of file diff --git a/src/Instances/AttributeChange.lua b/src/Instances/AttributeChange.lua index bb5b615a9..5e9699246 100644 --- a/src/Instances/AttributeChange.lua +++ b/src/Instances/AttributeChange.lua @@ -8,37 +8,36 @@ local Package = script.Parent.Parent local PubTypes = require(Package.PubTypes) local logError = require(Package.Logging.logError) -local xtypeof = require(Package.Utility.xtypeof) local function AttributeChange(attributeName: string): PubTypes.SpecialKey - local attributeKey = {} - attributeKey.type = "SpecialKey" - attributeKey.kind = "AttributeChange" - attributeKey.stage = "observer" - if attributeName == nil then logError("attributeNameNil") end - - function attributeKey:apply( - scope: PubTypes.Scope, - value: any, - applyTo: Instance - ) - if typeof(value) ~= "function" then - logError("invalidAttributeChangeHandler", nil, attributeName) - end - local ok, event = pcall(applyTo.GetAttributeChangedSignal, applyTo, attributeName) - if not ok then - logError("cannotConnectAttributeChange", nil, applyTo.ClassName, attributeName) - else - value((applyTo :: any):GetAttribute(attributeName)) - table.insert(scope, event:Connect(function() + + return { + type = "SpecialKey", + kind = "AttributeChange", + stage = "observer", + apply = function( + self: PubTypes.SpecialKey, + scope: PubTypes.Scope, + value: any, + applyTo: Instance + ) + if typeof(value) ~= "function" then + logError("invalidAttributeChangeHandler", nil, attributeName) + end + local ok, event = pcall(applyTo.GetAttributeChangedSignal, applyTo, attributeName) + if not ok then + logError("cannotConnectAttributeChange", nil, applyTo.ClassName, attributeName) + else value((applyTo :: any):GetAttribute(attributeName)) - end)) + table.insert(scope, event:Connect(function() + value((applyTo :: any):GetAttribute(attributeName)) + end)) + end end - end - return attributeKey + } end return AttributeChange \ No newline at end of file diff --git a/src/Instances/AttributeOut.lua b/src/Instances/AttributeOut.lua index d4d637444..33771e394 100644 --- a/src/Instances/AttributeOut.lua +++ b/src/Instances/AttributeOut.lua @@ -13,39 +13,38 @@ local xtypeof = require(Package.Utility.xtypeof) local whichLivesLonger = require(Package.Memory.whichLivesLonger) local function AttributeOut(attributeName: string): PubTypes.SpecialKey - local attributeOutKey = {} - attributeOutKey.type = "SpecialKey" - attributeOutKey.kind = "AttributeOut" - attributeOutKey.stage = "observer" - - function attributeOutKey:apply( - scope: PubTypes.Scope, - stateObject: any, - applyTo: Instance - ) - if xtypeof(stateObject) ~= "State" or stateObject.kind ~= "Value" then - logError("invalidAttributeOutType") - end - if attributeName == nil then - logError("attributeNameNil") - end - local ok, event = pcall(applyTo.GetAttributeChangedSignal, applyTo, attributeName) - if not ok then - logError("invalidOutAttributeName", applyTo.ClassName, attributeName) - else - if stateObject.scope == nil then - logError("useAfterDestroy", `The Value object, which [AttributeOut "{attributeName}"] outputs to,`, `the {applyTo.ClassName} instance`) - elseif whichLivesLonger(scope, applyTo, stateObject.scope, stateObject) == "a" then - logWarn("possiblyOutlives", `The Value object, which [AttributeOut "{attributeName}"] outputs to,`, `the {applyTo.ClassName} instance`) + return { + type = "SpecialKey", + kind = "AttributeOut", + stage = "observer", + apply = function( + self: PubTypes.SpecialKey, + scope: PubTypes.Scope, + stateObject: any, + applyTo: Instance + ) + if xtypeof(stateObject) ~= "State" or stateObject.kind ~= "Value" then + logError("invalidAttributeOutType") end - stateObject:set((applyTo :: any):GetAttribute(attributeName)) - table.insert(scope, event:Connect(function() + if attributeName == nil then + logError("attributeNameNil") + end + local ok, event = pcall(applyTo.GetAttributeChangedSignal, applyTo, attributeName) + if not ok then + logError("invalidOutAttributeName", nil, applyTo.ClassName, attributeName) + else + if stateObject.scope == nil then + logError("useAfterDestroy", nil, `The Value object, which [AttributeOut "{attributeName}"] outputs to,`, `the {applyTo.ClassName} instance`) + elseif whichLivesLonger(scope, applyTo, stateObject.scope, stateObject) == "a" then + logWarn("possiblyOutlives", `The Value object, which [AttributeOut "{attributeName}"] outputs to,`, `the {applyTo.ClassName} instance`) + end stateObject:set((applyTo :: any):GetAttribute(attributeName)) - end)) + table.insert(scope, event:Connect(function() + stateObject:set((applyTo :: any):GetAttribute(attributeName)) + end)) + end end - end - - return attributeOutKey + } end return AttributeOut diff --git a/src/Instances/Children.lua b/src/Instances/Children.lua index 57141360a..9f3f1f812 100644 --- a/src/Instances/Children.lua +++ b/src/Instances/Children.lua @@ -18,137 +18,136 @@ type Set = {[T]: boolean} -- Experimental flag: name children based on the key used in the [Children] table local EXPERIMENTAL_AUTO_NAMING = false -local Children = {} -Children.type = "SpecialKey" -Children.kind = "Children" -Children.stage = "descendants" - -function Children:apply( - scope: PubTypes.Scope, - propValue: any, - applyTo: Instance -) - local newParented: Set = {} - local oldParented: Set = {} - - -- save disconnection functions for state object observers - local newDisconnects: {[PubTypes.StateObject]: () -> ()} = {} - local oldDisconnects: {[PubTypes.StateObject]: () -> ()} = {} - - local updateQueued = false - local queueUpdate: () -> () - - -- Rescans this key's value to find new instances to parent and state objects - -- to observe for changes; then unparents instances no longer found and - -- disconnects observers for state objects no longer present. - local function updateChildren() - if not updateQueued then - return -- this update may have been canceled by destruction, etc. - end - updateQueued = false - - oldParented, newParented = newParented, oldParented - oldDisconnects, newDisconnects = newDisconnects, oldDisconnects - table.clear(newParented) - table.clear(newDisconnects) - - local function processChild(child: any, autoName: string?) - local childType = typeof(child) - - if childType == "Instance" then - -- case 1; single instance - - newParented[child] = true - if oldParented[child] == nil then - -- wasn't previously present - - -- TODO: check for ancestry conflicts here - child.Parent = applyTo - else - -- previously here; we want to reuse, so remove from old - -- set so we don't encounter it during unparenting - oldParented[child] = nil - end - - if EXPERIMENTAL_AUTO_NAMING and autoName ~= nil then - child.Name = autoName - end - - elseif isState(child) then - -- case 2; state object - - local value = peek(child) - -- allow nil to represent the absence of a child - if value ~= nil then - processChild(value, autoName) - end - - local disconnect = oldDisconnects[child] - if disconnect == nil then - -- wasn't previously present - disconnect = Observer(scope, child):onChange(queueUpdate) - else - -- previously here; we want to reuse, so remove from old - -- set so we don't encounter it during unparenting - oldDisconnects[child] = nil - end - - newDisconnects[child] = disconnect - - elseif childType == "table" then - -- case 3; table of objects - - for key, subChild in pairs(child) do - local keyType = typeof(key) - local subAutoName: string? = nil - - if keyType == "string" then - subAutoName = key - elseif keyType == "number" and autoName ~= nil then - subAutoName = autoName .. "_" .. key +return { + type = "SpecialKey", + kind = "Children", + stage = "descendants", + apply = function( + self: PubTypes.SpecialKey, + scope: PubTypes.Scope, + propValue: any, + applyTo: Instance + ) + local newParented: Set = {} + local oldParented: Set = {} + + -- save disconnection functions for state object observers + local newDisconnects: {[PubTypes.StateObject]: () -> ()} = {} + local oldDisconnects: {[PubTypes.StateObject]: () -> ()} = {} + + local updateQueued = false + local queueUpdate: () -> () + + -- Rescans this key's value to find new instances to parent and state objects + -- to observe for changes; then unparents instances no longer found and + -- disconnects observers for state objects no longer present. + local function updateChildren() + if not updateQueued then + return -- this update may have been canceled by destruction, etc. + end + updateQueued = false + + oldParented, newParented = newParented, oldParented + oldDisconnects, newDisconnects = newDisconnects, oldDisconnects + table.clear(newParented) + table.clear(newDisconnects) + + local function processChild(child: any, autoName: string?) + local childType = typeof(child) + + if childType == "Instance" then + -- case 1; single instance + + newParented[child] = true + if oldParented[child] == nil then + -- wasn't previously present + + -- TODO: check for ancestry conflicts here + child.Parent = applyTo + else + -- previously here; we want to reuse, so remove from old + -- set so we don't encounter it during unparenting + oldParented[child] = nil end - - processChild(subChild, subAutoName) + + if EXPERIMENTAL_AUTO_NAMING and autoName ~= nil then + child.Name = autoName + end + + elseif isState(child) then + -- case 2; state object + + local value = peek(child) + -- allow nil to represent the absence of a child + if value ~= nil then + processChild(value, autoName) + end + + local disconnect = oldDisconnects[child] + if disconnect == nil then + -- wasn't previously present + disconnect = Observer(scope, child):onChange(queueUpdate) + else + -- previously here; we want to reuse, so remove from old + -- set so we don't encounter it during unparenting + oldDisconnects[child] = nil + end + + newDisconnects[child] = disconnect + + elseif childType == "table" then + -- case 3; table of objects + + for key, subChild in pairs(child) do + local keyType = typeof(key) + local subAutoName: string? = nil + + if keyType == "string" then + subAutoName = key + elseif keyType == "number" and autoName ~= nil then + subAutoName = autoName .. "_" .. key + end + + processChild(subChild, subAutoName) + end + + else + logWarn("unrecognisedChildType", childType) end - - else - logWarn("unrecognisedChildType", childType) + end + + if propValue ~= nil then + -- `propValue` is set to nil on cleanup, so we don't process children + -- in that case + processChild(propValue) + end + + -- unparent any children that are no longer present + for oldInstance in pairs(oldParented) do + oldInstance.Parent = nil + end + + -- disconnect observers which weren't reused + for oldState, disconnect in pairs(oldDisconnects) do + disconnect() end end - - if propValue ~= nil then - -- `propValue` is set to nil on cleanup, so we don't process children - -- in that case - processChild(propValue) - end - - -- unparent any children that are no longer present - for oldInstance in pairs(oldParented) do - oldInstance.Parent = nil - end - - -- disconnect observers which weren't reused - for oldState, disconnect in pairs(oldDisconnects) do - disconnect() + + queueUpdate = function() + if not updateQueued then + updateQueued = true + External.doTaskDeferred(updateChildren) + end end - end - - queueUpdate = function() - if not updateQueued then + + table.insert(scope, function() + propValue = nil updateQueued = true - External.doTaskDeferred(updateChildren) - end - end - - table.insert(scope, function() - propValue = nil + updateChildren() + end) + + -- perform initial child parenting updateQueued = true updateChildren() - end) - - -- perform initial child parenting - updateQueued = true - updateChildren() -end - -return Children :: PubTypes.SpecialKey \ No newline at end of file + end +} :: PubTypes.SpecialKey \ No newline at end of file diff --git a/src/Instances/Hydrate.lua b/src/Instances/Hydrate.lua index 653327915..3f3976f5c 100644 --- a/src/Instances/Hydrate.lua +++ b/src/Instances/Hydrate.lua @@ -14,7 +14,7 @@ local function Hydrate( scope: PubTypes.Scope, target: Instance ) - if target == nil then + if target :: any == nil then logError("scopeMissing", nil, "instances using Hydrate", "myScope:Hydrate (instance) { ... }") end return function( diff --git a/src/Instances/New.lua b/src/Instances/New.lua index 449f1fc52..a7b4276c5 100644 --- a/src/Instances/New.lua +++ b/src/Instances/New.lua @@ -29,7 +29,7 @@ local function New( local classDefaults = defaultProps[className] if classDefaults ~= nil then for defaultProp, defaultValue in pairs(classDefaults) do - instance[defaultProp] = defaultValue + (instance :: any)[defaultProp] = defaultValue end end diff --git a/src/Instances/OnChange.lua b/src/Instances/OnChange.lua index e7c0fdae8..c9f59be2c 100644 --- a/src/Instances/OnChange.lua +++ b/src/Instances/OnChange.lua @@ -10,29 +10,28 @@ local PubTypes = require(Package.PubTypes) local logError = require(Package.Logging.logError) local function OnChange(propertyName: string): PubTypes.SpecialKey - local changeKey = {} - changeKey.type = "SpecialKey" - changeKey.kind = "OnChange" - changeKey.stage = "observer" - - function changeKey:apply( - scope: PubTypes.Scope, - callback: any, - applyTo: Instance - ) - local ok, event = pcall(applyTo.GetPropertyChangedSignal, applyTo, propertyName) - if not ok then - logError("cannotConnectChange", nil, applyTo.ClassName, propertyName) - elseif typeof(callback) ~= "function" then - logError("invalidChangeHandler", nil, propertyName) - else - table.insert(scope, event:Connect(function() - callback((applyTo :: any)[propertyName]) - end)) + return { + type = "SpecialKey", + kind = "OnChange", + stage = "observer", + apply = function( + self: PubTypes.SpecialKey, + scope: PubTypes.Scope, + callback: any, + applyTo: Instance + ) + local ok, event = pcall(applyTo.GetPropertyChangedSignal, applyTo, propertyName) + if not ok then + logError("cannotConnectChange", nil, applyTo.ClassName, propertyName) + elseif typeof(callback) ~= "function" then + logError("invalidChangeHandler", nil, propertyName) + else + table.insert(scope, event:Connect(function() + callback((applyTo :: any)[propertyName]) + end)) + end end - end - - return changeKey + } end return OnChange \ No newline at end of file diff --git a/src/Instances/OnEvent.lua b/src/Instances/OnEvent.lua index b6054be57..812a86da7 100644 --- a/src/Instances/OnEvent.lua +++ b/src/Instances/OnEvent.lua @@ -14,27 +14,26 @@ local function getProperty_unsafe(instance: Instance, property: string) end local function OnEvent(eventName: string): PubTypes.SpecialKey - local eventKey = {} - eventKey.type = "SpecialKey" - eventKey.kind = "OnEvent" - eventKey.stage = "observer" - - function eventKey:apply( - scope: PubTypes.Scope, - callback: any, - applyTo: Instance - ) - local ok, event = pcall(getProperty_unsafe, applyTo, eventName) - if not ok or typeof(event) ~= "RBXScriptSignal" then - logError("cannotConnectEvent", nil, applyTo.ClassName, eventName) - elseif typeof(callback) ~= "function" then - logError("invalidEventHandler", nil, eventName) - else - table.insert(scope, event:Connect(callback)) + return { + type = "SpecialKey", + kind = "OnEvent", + stage = "observer", + apply = function( + self: PubTypes.SpecialKey, + scope: PubTypes.Scope, + callback: any, + applyTo: Instance + ) + local ok, event = pcall(getProperty_unsafe, applyTo, eventName) + if not ok or typeof(event) ~= "RBXScriptSignal" then + logError("cannotConnectEvent", nil, applyTo.ClassName, eventName) + elseif typeof(callback) ~= "function" then + logError("invalidEventHandler", nil, eventName) + else + table.insert(scope, event:Connect(callback)) + end end - end - - return eventKey + } end return OnEvent \ No newline at end of file diff --git a/src/Instances/Out.lua b/src/Instances/Out.lua index ccd9fa8f3..72a7bde41 100644 --- a/src/Instances/Out.lua +++ b/src/Instances/Out.lua @@ -13,38 +13,37 @@ local xtypeof = require(Package.Utility.xtypeof) local whichLivesLonger = require(Package.Memory.whichLivesLonger) local function Out(propertyName: string): PubTypes.SpecialKey - local outKey = {} - outKey.type = "SpecialKey" - outKey.kind = "Out" - outKey.stage = "observer" - - function outKey:apply( - scope: PubTypes.Scope, - outState: any, - applyTo: Instance - ) - local ok, event = pcall(applyTo.GetPropertyChangedSignal, applyTo, propertyName) - if not ok then - logError("invalidOutProperty", nil, applyTo.ClassName, propertyName) - elseif xtypeof(outState) ~= "State" or outState.kind ~= "Value" then - logError("invalidOutType") - else - if outState.scope == nil then - logError("useAfterDestroy", `The Value, which [Out "{propertyName}"] outputs to,`, `the {applyTo.ClassName} instance`) - elseif whichLivesLonger(scope, applyTo, outState.scope, outState) == "a" then - logWarn("possiblyOutlives", `The Value, which [Out "{propertyName}"] outputs to,`, `the {applyTo.ClassName} instance`) + return { + type = "SpecialKey", + kind = "Out", + stage = "observer", + apply = function( + self: PubTypes.SpecialKey, + scope: PubTypes.Scope, + outState: any, + applyTo: Instance + ) + local ok, event = pcall(applyTo.GetPropertyChangedSignal, applyTo, propertyName) + if not ok then + logError("invalidOutProperty", nil, applyTo.ClassName, propertyName) + elseif xtypeof(outState) ~= "State" or outState.kind ~= "Value" then + logError("invalidOutType") + else + if outState.scope == nil then + logError("useAfterDestroy", nil, `The Value, which [Out "{propertyName}"] outputs to,`, `the {applyTo.ClassName} instance`) + elseif whichLivesLonger(scope, applyTo, outState.scope, outState) == "a" then + logWarn("possiblyOutlives", `The Value, which [Out "{propertyName}"] outputs to,`, `the {applyTo.ClassName} instance`) + end + outState:set((applyTo :: any)[propertyName]) + table.insert( + scope, + event:Connect(function() + outState:set((applyTo :: any)[propertyName]) + end) + ) end - outState:set((applyTo :: any)[propertyName]) - table.insert( - scope, - event:Connect(function() - outState:set((applyTo :: any)[propertyName]) - end) - ) end - end - - return outKey + } end return Out diff --git a/src/Instances/Ref.lua b/src/Instances/Ref.lua index 1ec1d84f8..a67c1de30 100644 --- a/src/Instances/Ref.lua +++ b/src/Instances/Ref.lua @@ -9,29 +9,28 @@ local Package = script.Parent.Parent local PubTypes = require(Package.PubTypes) local logWarn = require(Package.Logging.logWarn) local logError = require(Package.Logging.logError) -local xtypeof = require(Package.Utility.xtypeof) +local isState = require(Package.State.isState) local whichLivesLonger = require(Package.Memory.whichLivesLonger) -local Ref = {} -Ref.type = "SpecialKey" -Ref.kind = "Ref" -Ref.stage = "observer" - -function Ref:apply( - scope: PubTypes.Scope, - refState: any, - applyTo: Instance -) - if xtypeof(refState) ~= "State" or refState.kind ~= "Value" then - logError("invalidRefType") - else - if refState.scope == nil then - logError("useAfterDestroy", "The Value object, which [Ref] outputs to,", `the {applyTo} instance`) - elseif whichLivesLonger(scope, applyTo, refState.scope, refState) == "a" then - logWarn("possiblyOutlives", "The Value object, which [Ref] outputs to,", `the {applyTo} instance`) +return { + type = "SpecialKey", + kind = "Ref", + stage = "observer", + apply = function( + self: PubTypes.SpecialKey, + scope: PubTypes.Scope, + refState: any, + applyTo: Instance + ) + if not isState(refState) or refState.kind ~= "Value" then + logError("invalidRefType") + else + if refState.scope == nil then + logError("useAfterDestroy", nil, "The Value object, which [Ref] outputs to,", `the {applyTo} instance`) + elseif whichLivesLonger(scope, applyTo, refState.scope, refState) == "a" then + logWarn("possiblyOutlives", "The Value object, which [Ref] outputs to,", `the {applyTo} instance`) + end + refState:set(applyTo) end - refState:set(applyTo) end -end - -return Ref \ No newline at end of file +} :: PubTypes.SpecialKey \ No newline at end of file diff --git a/src/Instances/applyInstanceProps.lua b/src/Instances/applyInstanceProps.lua index 7be9f72f0..58b61375d 100644 --- a/src/Instances/applyInstanceProps.lua +++ b/src/Instances/applyInstanceProps.lua @@ -15,7 +15,6 @@ local Package = script.Parent.Parent local PubTypes = require(Package.PubTypes) -local doCleanup = require(Package.Memory.doCleanup) local External = require(Package.External) local isState = require(Package.State.isState) local logError = require(Package.Logging.logError) @@ -66,7 +65,7 @@ local function bindProperty( ) if isState(value) then if value.scope == nil then - logError("useAfterDestroy", `The {value.kind} object, bound to {property},`, `the {instance.ClassName} instance`) + logError("useAfterDestroy", nil, `The {value.kind} object, bound to {property},`, `the {instance.ClassName} instance`) elseif whichLivesLonger(scope, instance, value.scope, value) ~= "b" then logWarn("possiblyOutlives", `The {value.kind} object, bound to {property},`, `the {instance.ClassName} instance`) end diff --git a/src/Memory/whichLivesLonger.lua b/src/Memory/whichLivesLonger.lua index 8550f3364..b7bf68ded 100644 --- a/src/Memory/whichLivesLonger.lua +++ b/src/Memory/whichLivesLonger.lua @@ -1,4 +1,5 @@ --!strict +--!nolint LocalShadow --[[ Calculates how the lifetimes of the two values relate. Specifically, it @@ -22,15 +23,18 @@ local function whichScopeLivesLonger( while openSetSize > 0 do for _, scope in openSet do closedSet[scope] = true - for _, inScope in scope do + for _, inScope in ipairs(scope) do if inScope == scopeA then return "b" elseif inScope == scopeB then return "a" - elseif inScope[1] ~= nil and closedSet[scope] == nil then - nextOpenSetSize += 1 - nextOpenSet[nextOpenSetSize] = inScope - end + elseif typeof(inScope) == "table" then + local inScope: {any} = inScope + if inScope[1] ~= nil and closedSet[scope] == nil then + nextOpenSetSize += 1 + nextOpenSet[nextOpenSetSize] = inScope + end + end end end table.clear(openSet) @@ -47,6 +51,7 @@ local function whichLivesLonger( b: any ): "a" | "b" | "unknown" if scopeA == scopeB then + local scopeA: {any} = scopeA for index = #scopeA, 1, -1 do local value = scopeA[index] if value == a then diff --git a/src/PubTypes.lua b/src/PubTypes.lua index 95391e8c4..7ce5e581f 100644 --- a/src/PubTypes.lua +++ b/src/PubTypes.lua @@ -10,12 +10,6 @@ type Set = {[T]: any} General use types ]] --- A unique symbolic value. -export type Symbol = { - type: "Symbol", - name: string -} - -- Types that can be expressed as vectors of numbers, and so can be animated. export type Animatable = number | @@ -47,7 +41,7 @@ export type Task = {Task} -- A scope of tasks to clean up. -export type Scope = {Task} & Constructors +export type Scope = {any} & Constructors -- An object which uses a scope to dictate how long it lives. export type ScopeLifetime = { @@ -70,7 +64,7 @@ export type Dependency = ScopeLifetime & { } -- A graph object which can have dependencies. -export type Dependent = { +export type Dependent = ScopeLifetime & { update: (Dependent) -> boolean, dependencySet: Set } @@ -118,26 +112,27 @@ export type For = StateObject<{[KO]: VO}> & Dependent & { kind: "For", destroy: () -> () } -type ForPairsConstructor = ( +export type ForPairsConstructor = ( scope: Scope, inputTable: CanBeState<{[KI]: VI}>, processor: (Scope, Use, KI, VI) -> (KO, VO) ) -> For -type ForKeysConstructor = ( +export type ForKeysConstructor = ( scope: Scope, inputTable: CanBeState<{[KI]: V}>, - processor: (Scope, Use, KI) -> (KO, M?) + processor: (Scope, Use, KI) -> KO ) -> For -type ForValuesConstructor = ( +export type ForValuesConstructor = ( scope: Scope, inputTable: CanBeState<{[K]: VI}>, - processor: (Scope, Use, VI) -> (VO, M?) + processor: (Scope, Use, VI) -> VO ) -> For -- An object which can listen for updates on another state object. export type Observer = Dependent & { kind: "Observer", onChange: (Observer, callback: () -> ()) -> (() -> ()), + onBind: (Observer, callback: () -> ()) -> (() -> ()), destroy: () -> () } type ObserverConstructor = ( @@ -227,7 +222,6 @@ export type Fusion = { Hydrate: HydrateConstructor, Ref: SpecialKey, - Cleanup: SpecialKey, Children: SpecialKey, Out: (propertyName: string) -> SpecialKey, OnEvent: (eventName: string) -> SpecialKey, diff --git a/src/RobloxExternal.lua b/src/RobloxExternal.lua index d2370b2f4..779664365 100644 --- a/src/RobloxExternal.lua +++ b/src/RobloxExternal.lua @@ -39,7 +39,7 @@ end --[[ Binds Fusion's update step to RunService step events. ]] -local stopSchedulerFunc = nil +local stopSchedulerFunc = nil :: (() -> ())? function RobloxExternal.startScheduler() if stopSchedulerFunc ~= nil then return diff --git a/src/State/Computed.lua b/src/State/Computed.lua index 4610ff5a5..ee6951b44 100644 --- a/src/State/Computed.lua +++ b/src/State/Computed.lua @@ -1,5 +1,5 @@ --!nonstrict - +--!nolint LocalShadow --[[ Constructs and returns objects which can be used to model derived reactive state. @@ -47,15 +47,16 @@ function class:update(): boolean local innerScope = deriveScope(self.scope) local function use(target: PubTypes.CanBeState): T if isState(target) then + local target = target :: PubTypes.StateObject if target.scope == nil then - logError("useAfterDestroy", `The {target.kind} object`, "the Computed that is use()-ing it") + logError("useAfterDestroy", nil, `The {target.kind} object`, "the Computed that is use()-ing it") elseif whichLivesLonger(self.scope, self, target.scope, target) == "a" then logWarn("possiblyOutlives", `The {target.kind} object`, "the Computed that is use()-ing it") end self.dependencySet[target] = true return (target :: Types.StateObject):_peek() else - return target + return target :: T end end local ok, newValue = xpcall(self._processor, parseError, innerScope, use) diff --git a/src/State/For.lua b/src/State/For.lua index e75c8ba00..ea850c767 100644 --- a/src/State/For.lua +++ b/src/State/For.lua @@ -17,6 +17,7 @@ local isState = require(Package.State.isState) local Value = require(Package.State.Value) -- Memory local doCleanup = require(Package.Memory.doCleanup) +local deriveScope = require(Package.Memory.deriveScope) local class = {} @@ -61,7 +62,6 @@ function class:update(): boolean -- NOTE: we also reuse processors with nil output keys here, so long as -- they match values. This ensures they don't get recomputed either. for tryReuseProcessor in existingProcessors do - local key = peek(tryReuseProcessor.inputPair).key local value = peek(tryReuseProcessor.inputPair).value if peek(tryReuseProcessor.outputPair).key == nil then for key, remainingValues in remainingPairs do @@ -74,6 +74,7 @@ function class:update(): boolean end end else + local key = peek(tryReuseProcessor.inputPair).key local remainingValues = remainingPairs[key] if remainingValues ~= nil and remainingValues[value] ~= nil then remainingValues[value] = nil @@ -138,7 +139,7 @@ function class:update(): boolean for key, remainingValues in remainingPairs do for value in remainingValues do - local scope = {} + local scope = deriveScope(self.scope) local inputPair = Value(scope, {key = key, value = value}) local processOK, outputPair = xpcall(self._processor, parseError, scope, inputPair) if processOK then @@ -213,7 +214,7 @@ local function For( processor: ( {any}, PubTypes.StateObject<{key: KI, value: VI}> - ) -> (PubTypes.StateObject<{key: KO?, value: VO}>) + ) -> (PubTypes.StateObject<{key: KO?, value: VO?}>) ): Types.For local self = setmetatable({ diff --git a/src/State/ForKeys.lua b/src/State/ForKeys.lua index 251d555e3..4095df116 100644 --- a/src/State/ForKeys.lua +++ b/src/State/ForKeys.lua @@ -20,6 +20,8 @@ local Computed = require(Package.State.Computed) -- Logging local parseError = require(Package.Logging.parseError) local logErrorNonFatal = require(Package.Logging.logErrorNonFatal) +local logError = require(Package.Logging.logError) +local logWarn = require(Package.Logging.logWarn) -- Memory local doCleanup = require(Package.Memory.doCleanup) @@ -37,16 +39,19 @@ local function ForKeys( return For( scope, inputTable, - function(scope, inputPair) - local inputKey = Computed(scope, function(scope, use) + function( + scope: PubTypes.Scope, + inputPair: PubTypes.StateObject<{key: KI, value: V}> + ) + local inputKey = Computed(scope, function(scope, use): KI return use(inputPair).key end) - local outputKey = Computed(scope, function(scope, use) + local outputKey = Computed(scope, function(scope, use): KO? local ok, key = xpcall(processor, parseError, scope, use, use(inputKey)) if ok then return key else - logErrorNonFatal("forProcessorError", parseError) + logErrorNonFatal("forProcessorError", key :: any) doCleanup(scope) table.clear(scope) return nil diff --git a/src/State/ForPairs.lua b/src/State/ForPairs.lua index 02f8f48aa..9b06a2320 100644 --- a/src/State/ForPairs.lua +++ b/src/State/ForPairs.lua @@ -20,6 +20,8 @@ local Computed = require(Package.State.Computed) -- Logging local parseError = require(Package.Logging.parseError) local logErrorNonFatal = require(Package.Logging.logErrorNonFatal) +local logError = require(Package.Logging.logError) +local logWarn = require(Package.Logging.logWarn) -- Memory local doCleanup = require(Package.Memory.doCleanup) @@ -37,13 +39,16 @@ local function ForPairs( return For( scope, inputTable, - function(scope, inputPair) - return Computed(scope, function(scope, use) + function( + scope: PubTypes.Scope, + inputPair: PubTypes.StateObject<{key: KI, value: VI}> + ) + return Computed(scope, function(scope, use): {key: KO?, value: VO?} local ok, key, value = xpcall(processor, parseError, scope, use, use(inputPair).key, use(inputPair).value) if ok then return {key = key, value = value} else - logErrorNonFatal("forProcessorError", parseError) + logErrorNonFatal("forProcessorError", key :: any) doCleanup(scope) table.clear(scope) return {key = nil, value = nil} diff --git a/src/State/ForValues.lua b/src/State/ForValues.lua index c4efd4de9..a9cbfca2f 100644 --- a/src/State/ForValues.lua +++ b/src/State/ForValues.lua @@ -20,6 +20,8 @@ local Computed = require(Package.State.Computed) -- Logging local parseError = require(Package.Logging.parseError) local logErrorNonFatal = require(Package.Logging.logErrorNonFatal) +local logError = require(Package.Logging.logError) +local logWarn = require(Package.Logging.logWarn) -- Memory local doCleanup = require(Package.Memory.doCleanup) @@ -37,16 +39,19 @@ local function ForValues( return For( scope, inputTable, - function(scope, inputPair) - local inputValue = Computed(scope, function(scope, use) + function( + scope: PubTypes.Scope, + inputPair: PubTypes.StateObject<{key: K, value: VI}> + ) + local inputValue = Computed(scope, function(scope, use): VI return use(inputPair).value end) - return Computed(scope, function(scope, use) + return Computed(scope, function(scope, use): {key: nil, value: VO?} local ok, value = xpcall(processor, parseError, scope, use, use(inputValue)) if ok then return {key = nil, value = value} else - logErrorNonFatal("forProcessorError", parseError) + logErrorNonFatal("forProcessorError", value :: any) doCleanup(scope) table.clear(scope) return {key = nil, value = nil} diff --git a/src/State/Observer.lua b/src/State/Observer.lua index 031ea1674..ca1f81d86 100644 --- a/src/State/Observer.lua +++ b/src/State/Observer.lua @@ -84,7 +84,7 @@ local function Observer( table.insert(scope, self) if watchedState.scope == nil then - logError("useAfterDestroy", `The {watchedState.kind} object`, `the Observer that is watching it`) + logError("useAfterDestroy", nil, `The {watchedState.kind} object`, `the Observer that is watching it`) elseif whichLivesLonger(scope, self, watchedState.scope, watchedState) == "a" then logWarn("possiblyOutlives", `The {watchedState.kind} object`, `the Observer that is watching it`) end diff --git a/src/State/peek.lua b/src/State/peek.lua index fd6970461..123741caa 100644 --- a/src/State/peek.lua +++ b/src/State/peek.lua @@ -14,7 +14,7 @@ local function peek(target: PubTypes.CanBeState): T if isState(target) then return (target :: Types.StateObject):_peek() else - return target + return target :: T end end diff --git a/src/State/updateAll.lua b/src/State/updateAll.lua index d6500da93..adba1b84d 100644 --- a/src/State/updateAll.lua +++ b/src/State/updateAll.lua @@ -47,7 +47,13 @@ local function updateAll(root: PubTypes.Dependency) local next = queue[queuePos] local counter = counters[next] - 1 counters[next] = counter - if counter == 0 and flags[next] and next:update() and (next :: any).dependentSet ~= nil then + if + counter == 0 + and flags[next] + and next.scope ~= nil + and next:update() + and (next :: any).dependentSet ~= nil + then for object in (next :: any).dependentSet do flags[object] = true end diff --git a/src/Types.lua b/src/Types.lua index 57ee2ac2a..5dfc43218 100644 --- a/src/Types.lua +++ b/src/Types.lua @@ -17,11 +17,6 @@ type Set = {[T]: any} General use types ]] --- A symbol that represents the absence of a value. -export type None = PubTypes.Symbol & { - -- name: "None" (add this when Luau supports singleton types) -} - -- Stores useful information about Luau errors. export type Error = { type: string, -- replace with "Error" when Luau supports singleton types @@ -61,7 +56,7 @@ export type For = PubTypes.For & { _processor: ( PubTypes.Scope, PubTypes.StateObject<{key: KI, value: VI}> - ) -> (PubTypes.StateObject<{key: KO?, value: VO}>), + ) -> (PubTypes.StateObject<{key: KO?, value: VO?}>), _inputTable: PubTypes.CanBeState<{[KI]: VI}>, _existingInputTable: {[KI]: VI}?, _existingOutputTable: {[KO]: VO}, @@ -92,18 +87,22 @@ export type Tween = PubTypes.Tween & { -- A state object which follows another state object using spring simulation. export type Spring = PubTypes.Spring & { _speed: PubTypes.CanBeState, - _speedIsState: boolean, - _lastSpeed: number, _damping: PubTypes.CanBeState, - _dampingIsState: boolean, - _lastDamping: number, _goalState: State, _goalValue: T, + _currentType: string, _currentValue: T, + _currentSpeed: number, + _currentDamping: number, + _springPositions: {number}, _springGoals: {number}, - _springVelocities: {number} + _springVelocities: {number}, + + _lastSchedule: number, + _startDisplacements: {number}, + _startVelocities: {number} } -- An object which can listen for updates on another state object. diff --git a/src/Utility/None.lua b/src/Utility/None.lua deleted file mode 100644 index 6089aa326..000000000 --- a/src/Utility/None.lua +++ /dev/null @@ -1,13 +0,0 @@ ---!strict - ---[[ - A symbol for representing nil values in contexts where nil is not usable. -]] - -local Package = script.Parent.Parent -local Types = require(Package.Types) - -return { - type = "Symbol", - name = "None" -} :: Types.None \ No newline at end of file diff --git a/src/init.lua b/src/init.lua index 3a5eaa715..b16e39cd9 100644 --- a/src/init.lua +++ b/src/init.lua @@ -7,7 +7,6 @@ local PubTypes = require(script.PubTypes) local External = require(script.External) -export type Symbol = PubTypes.Symbol export type Animatable = PubTypes.Animatable export type Task = PubTypes.Task export type Scope = PubTypes.Scope From 0fe44f150ad2c24041651b30c14cb35e391c04b1 Mon Sep 17 00:00:00 2001 From: Elttob Date: Tue, 5 Dec 2023 20:21:40 +0000 Subject: [PATCH 103/287] Remove outdated broken benchmarking code --- benchmark/Colour/Oklab.bench.lua | 36 ----- benchmark/Dependencies/updateAll.bench.lua | 104 --------------- benchmark/Instances/New.bench.lua | 147 --------------------- benchmark/State/ForKeys.bench.lua | 107 --------------- benchmark/State/ForPairs.bench.lua | 105 --------------- benchmark/State/ForValues.bench.lua | 107 --------------- test-runner/Run.client.lua | 86 ------------ 7 files changed, 692 deletions(-) delete mode 100644 benchmark/Colour/Oklab.bench.lua delete mode 100644 benchmark/Dependencies/updateAll.bench.lua delete mode 100644 benchmark/Instances/New.bench.lua delete mode 100644 benchmark/State/ForKeys.bench.lua delete mode 100644 benchmark/State/ForPairs.bench.lua delete mode 100644 benchmark/State/ForValues.bench.lua diff --git a/benchmark/Colour/Oklab.bench.lua b/benchmark/Colour/Oklab.bench.lua deleted file mode 100644 index 3ed2cc0d0..000000000 --- a/benchmark/Colour/Oklab.bench.lua +++ /dev/null @@ -1,36 +0,0 @@ -local Package = game:GetService("ReplicatedStorage").Fusion -local Oklab = require(Package.Colour.Oklab) - -return { - - { - name = "Convert to Oklab", - calls = 50000, - - preRun = function() - return { - colour = Color3.new(0.25, 0.5, 0.75) - } - end, - - run = function(state) - Oklab.to(state.colour) - end - }, - - { - name = "Convert from Oklab", - calls = 50000, - - preRun = function() - return { - colour = Vector3.new(0.25, 0.5, 0.75) - } - end, - - run = function(state) - Oklab.from(state.colour) - end - } - -} \ No newline at end of file diff --git a/benchmark/Dependencies/updateAll.bench.lua b/benchmark/Dependencies/updateAll.bench.lua deleted file mode 100644 index 0105d1242..000000000 --- a/benchmark/Dependencies/updateAll.bench.lua +++ /dev/null @@ -1,104 +0,0 @@ -local Package = game:GetService("ReplicatedStorage").Fusion -local updateAll = require(Package.Dependencies.updateAll) - -local function update() - return true -end - -local function makeDependency(dependency, dependent) - dependent.dependencySet[dependency] = true - dependency.dependentSet[dependent] = true -end - -return { - - { - name = "Update empty tree", - calls = 50000, - - preRun = function() - local root = { dependentSet = {} } - return root - end, - - run = function(state) - updateAll(state) - end - }, - - { - name = "Update shallow tree", - calls = 50000, - - preRun = function() - local root = { dependentSet = {} } - local A = { dependentSet = {}, dependencySet = {}, update = update} - local B = { dependentSet = {}, dependencySet = {}, update = update} - local C = { dependentSet = {}, dependencySet = {}, update = update} - local D = { dependentSet = {}, dependencySet = {}, update = update} - - makeDependency(root, A) - makeDependency(root, B) - makeDependency(root, C) - makeDependency(root, D) - - return root - end, - - run = function(state) - updateAll(state) - end - }, - - { - name = "Update deep tree", - calls = 50000, - - preRun = function() - local root = { dependentSet = {} } - local A = { dependentSet = {}, dependencySet = {}, update = update} - local B = { dependentSet = {}, dependencySet = {}, update = update} - local C = { dependentSet = {}, dependencySet = {}, update = update} - local D = { dependentSet = {}, dependencySet = {}, update = update} - - makeDependency(root, A) - makeDependency(A, B) - makeDependency(B, C) - makeDependency(C, D) - - return root - end, - - run = function(state) - updateAll(state) - end - }, - - { - name = "Update tree with complex dependencies", - calls = 50000, - - preRun = function() - local root = { dependentSet = {} } - local A = { dependentSet = {}, dependencySet = {}, update = update} - local B = { dependentSet = {}, dependencySet = {}, update = update} - local C = { dependentSet = {}, dependencySet = {}, update = update} - local D = { dependentSet = {}, dependencySet = {}, update = update} - - makeDependency(root, A) - makeDependency(A, B) - makeDependency(B, C) - makeDependency(C, D) - makeDependency(A, C) - makeDependency(A, D) - makeDependency(B, D) - - return root - end, - - run = function(state) - updateAll(state) - end - } - -} \ No newline at end of file diff --git a/benchmark/Instances/New.bench.lua b/benchmark/Instances/New.bench.lua deleted file mode 100644 index 2261376fa..000000000 --- a/benchmark/Instances/New.bench.lua +++ /dev/null @@ -1,147 +0,0 @@ -local Package = game:GetService("ReplicatedStorage").Fusion -local New = require(Package.Instances.New) -local Children = require(Package.Instances.Children) -local OnEvent = require(Package.Instances.OnEvent) -local OnChange = require(Package.Instances.OnChange) -local Value = require(Package.State.Value) - -local function callback() - -end - -return { - - { - name = "New without properties", - calls = 50000, - - run = function() - New "Frame" {} - end - }, - - { - name = "New with properties - constant", - calls = 50000, - - run = function() - New "Frame" { - Name = "Foo" - } - end - }, - - { - name = "New with properties - state", - calls = 50000, - - run = function() - New "Frame" { - Name = Value("Foo") - } - end - }, - - { - name = "New with Parent - constant", - calls = 10000, - - preRun = function() - end, - - run = function() - New "Folder" { - Parent = Instance.new("Folder") - } - end - }, - - { - name = "New with Parent - state", - calls = 10000, - - preRun = function() - return Instance.new("Folder") - end, - - run = function(parent) - New "Folder" { - Parent = Value(parent) - } - end, - - postRun = function(parent) - parent:Destroy() - end - }, - - { - name = "New with Children - single", - calls = 50000, - - preRun = function() - return New "Folder" {} - end, - - run = function(child) - New "Frame" { - [Children] = child - } - end - }, - - { - name = "New with Children - array", - calls = 50000, - - preRun = function() - return { - New "Folder" {} - } - end, - - run = function(children) - New "Frame" { - [Children] = children - } - end - }, - - { - name = "New with Children - state", - calls = 50000, - - preRun = function() - return New "Folder" {} - end, - - run = function(child) - New "Frame" { - [Children] = Value(child) - } - end - }, - - { - name = "New with OnEvent", - calls = 50000, - - run = function() - New "Frame" { - [OnEvent "MouseEnter"] = callback - } - end - }, - - { - name = "New with OnChange", - calls = 50000, - - run = function() - New "Frame" { - [OnChange "Name"] = callback - } - end - } - -} \ No newline at end of file diff --git a/benchmark/State/ForKeys.bench.lua b/benchmark/State/ForKeys.bench.lua deleted file mode 100644 index 0d777d3d1..000000000 --- a/benchmark/State/ForKeys.bench.lua +++ /dev/null @@ -1,107 +0,0 @@ -local Package = game:GetService("ReplicatedStorage").Fusion -local Value = require(Package.State.Value) -local ForKeys = require(Package.State.ForValues) - -local function callback() - return true -end - -local function constantOutput(key) - return key -end - -local function dynamicOutput(key) - return {key} -end - -return { - { - name = "ForKeys with blank input table", - calls = 20000, - - run = function() - ForKeys({}, callback) - end, - }, - - { - name = "ForKeys with input table - constant output", - calls = 20000, - - run = function() - ForKeys({ ["Foo"] = "bar" }, constantOutput) - end, - }, - - { - name = "ForKeys with input table - dynamic output", - calls = 20000, - - run = function() - ForKeys({ ["Foo"] = "bar" }, dynamicOutput) - end, - }, - - - { - name = "ForKeys with input state - constant output", - calls = 20000, - - run = function() - ForKeys(Value({ ["Foo"] = "bar" }), constantOutput) - end, - }, - - { - name = "ForKeys with input state - dynamic output", - calls = 20000, - - run = function() - ForKeys(Value({ ["Foo"] = "bar" }), dynamicOutput) - end, - }, - - { - name = "ForKeys with changed input table - constant output", - calls = 20000, - - run = function() - local computed = ForKeys({ ["Foo"] = "bar" }, constantOutput) - computed.__inputTable = { ["Bar"] = "foo" } - computed:update() - end, - }, - - { - name = "ForKeys with changed input table - dynamic output", - calls = 20000, - - run = function() - local computed = ForKeys({ ["Foo"] = "bar" }, dynamicOutput) - computed.__inputTable = { ["Bar"] = "foo" } - computed:update() - end, - }, - - { - name = "ForKeys with changed input state - constant output", - calls = 20000, - - run = function() - local state = Value({ ["Foo"] = "bar" }) - ForKeys(state, constantOutput) - state:set({ ["Bar"] = "foo" }) - end, - }, - - { - name = "ForKeys with changed input state - dynamic output", - calls = 20000, - - run = function() - local state = Value({ ["Foo"] = "bar" }) - ForKeys(state, dynamicOutput) - state:set({ ["Bar"] = "foo" }) - end, - }, -} diff --git a/benchmark/State/ForPairs.bench.lua b/benchmark/State/ForPairs.bench.lua deleted file mode 100644 index 4dc03613f..000000000 --- a/benchmark/State/ForPairs.bench.lua +++ /dev/null @@ -1,105 +0,0 @@ -local Package = game:GetService("ReplicatedStorage").Fusion -local Value = require(Package.State.Value) -local ForPairs = require(Package.State.ForValues) - -local function callback() - return true -end - -local function constantOutput(key, value) - return key, value -end - -local function dynamicOutput(key, value) - return {key}, {value} -end - -return { - { - name = "ForPairs with blank input table", - calls = 20000, - - run = function() - ForPairs({}, callback) - end, - }, - - { - name = "ForPairs with input table - constant output", - calls = 20000, - run = function() - ForPairs({ ["Foo"] = "bar" }, constantOutput) - end, - }, - - { - name = "ForPairs with input table - dynamic output", - calls = 20000, - - run = function() - ForPairs({ ["Foo"] = "bar" }, dynamicOutput) - end, - }, - - { - name = "ForPairs with input state - constant output", - calls = 20000, - - run = function() - ForPairs(Value({ ["Foo"] = "bar" }), constantOutput) - end, - }, - - { - name = "ForPairs with input state - dynamic output", - calls = 20000, - - run = function() - ForPairs(Value({ ["Foo"] = "bar" }), dynamicOutput) - end, - }, - - { - name = "ForPairs with changed input table - constant output", - calls = 20000, - - run = function() - local computed = ForPairs({ ["Foo"] = "bar" }, constantOutput) - computed.__inputTable = { ["Bar"] = "foo" } - computed:update() - end, - }, - - { - name = "ForPairs with changed input table - dynamic output", - calls = 20000, - - run = function() - local computed = ForPairs({ ["Foo"] = "bar" }, dynamicOutput) - computed.__inputTable = { ["Bar"] = "foo" } - computed:update() - end, - }, - - { - name = "ForPairs with changed input state - constant output", - calls = 20000, - - run = function() - local state = Value({ ["Foo"] = "bar" }) - ForPairs(state, constantOutput) - state:set({ ["Bar"] = "foo" }) - end, - }, - - { - name = "ForPairs with changed input state - dynamic output", - calls = 20000, - - run = function() - local state = Value({ ["Foo"] = "bar" }) - ForPairs(state, dynamicOutput) - state:set({ ["Bar"] = "foo" }) - end, - }, -} diff --git a/benchmark/State/ForValues.bench.lua b/benchmark/State/ForValues.bench.lua deleted file mode 100644 index 82424cd4c..000000000 --- a/benchmark/State/ForValues.bench.lua +++ /dev/null @@ -1,107 +0,0 @@ -local Package = game:GetService("ReplicatedStorage").Fusion -local Value = require(Package.State.Value) -local ForValues = require(Package.State.ForValues) - -local function callback() - return true -end - -local function constantOutput(val) - return val -end - -local function dynamicOutput(val) - return {val} -end - -return { - { - name = "ForValues with blank input table", - calls = 20000, - - run = function() - ForValues({}, callback) - end, - }, - - { - name = "ForValues with input table - constant output", - calls = 20000, - - run = function() - ForValues({ 1 }, constantOutput) - end, - }, - - { - name = "ForValues with input table - dynamic output", - calls = 20000, - - run = function() - ForValues({ 1 }, dynamicOutput) - end, - }, - - - { - name = "ForValues with input state - constant output", - calls = 20000, - - run = function() - ForValues(Value({ 1 }), constantOutput) - end, - }, - - { - name = "ForValues with input state - dynamic output", - calls = 20000, - - run = function() - ForValues(Value({ 1 }), dynamicOutput) - end, - }, - - { - name = "ForValues with changed input table - constant output", - calls = 20000, - - run = function() - local computed = ForValues({ 1 }, constantOutput) - computed.__inputTable = { 2 } - computed:update() - end, - }, - - { - name = "ForValues with changed input table - dynamic output", - calls = 20000, - - run = function() - local computed = ForValues({ 1 }, dynamicOutput) - computed.__inputTable = { 2 } - computed:update() - end, - }, - - { - name = "ForValues with changed input state - constant output", - calls = 20000, - - run = function() - local state = Value({ 1 }) - ForValues(state, constantOutput) - state:set({ 2 }) - end, - }, - - { - name = "ForValues with changed input state - dynamic output", - calls = 20000, - - run = function() - local state = Value({ 1 }) - ForValues(state, dynamicOutput) - state:set({ 2 }) - end, - }, -} diff --git a/test-runner/Run.client.lua b/test-runner/Run.client.lua index f8282d15f..a46b43601 100644 --- a/test-runner/Run.client.lua +++ b/test-runner/Run.client.lua @@ -4,7 +4,6 @@ local StarterPlayerScripts = game:GetService("StarterPlayer").StarterPlayerScrip local TestEZ = require(StarterPlayerScripts.TestEZ) local RUN_TESTS = true -local RUN_BENCHMARKS = false -- run unit tests if RUN_TESTS then @@ -19,89 +18,4 @@ if RUN_TESTS then if data.failureCount > 0 then return end -end - --- run benchmarks -if RUN_BENCHMARKS then - print("Running benchmarks...") - - -- wait for a bit to allow initial load to pass - this means the lag from a ton - -- of things starting up shouldn't impact the benchmarks (as much) - wait(5) - - local results = {} - local maxNameLength = 0 - - for _, instance in pairs(ReplicatedStorage.FusionBench:GetDescendants()) do - if instance:IsA("ModuleScript") and instance.Name:match("%.bench$") then - -- yield between benchmarks so we don't freeze Studio - wait() - local benchmarks = require(instance) - - local fileName = instance.Name:gsub("%.bench$", "") - local fileResults = {} - - for index, benchmarkInfo in ipairs(benchmarks) do - local name = benchmarkInfo.name - local calls = benchmarkInfo.calls - - local preRun = benchmarkInfo.preRun - local run = benchmarkInfo.run - local postRun = benchmarkInfo.postRun - - maxNameLength = math.max(maxNameLength, #name) - - local state - - if preRun ~= nil then - state = preRun() - end - - local start = os.clock() - for n=1, calls do - run(state) - end - local fin = os.clock() - - if postRun ~= nil then - postRun(state) - end - - local timeMicros = (fin - start) * 1000000 / calls - - fileResults[index] = {name = name, time = timeMicros} - end - - table.sort(fileResults, function(a, b) - return a.name < b.name - end) - - table.insert(results, {fileName = fileName, results = fileResults}) - end - end - - table.sort(results, function(a, b) - return a.fileName < b.fileName - end) - - local resultsString = "Benchmark results:" - - for _, fileInfo in ipairs(results) do - resultsString ..= "\n[+] " .. fileInfo.fileName - - for _, testInfo in ipairs(fileInfo.results) do - resultsString ..= "\n [+] " - resultsString ..= testInfo.name .. " " - resultsString ..= ("."):rep(maxNameLength - #testInfo.name + 4) .. " " - resultsString ..= ("%.2f μs"):format(testInfo.time) - end - end - - print(resultsString) -end - --- run test main script -if StarterPlayerScripts:FindFirstChild("TestMain") then - print("Running test main script...") - require(StarterPlayerScripts.TestMain) end \ No newline at end of file From 5adbd8f7b53e5aa64162b5b623610f8688b107da Mon Sep 17 00:00:00 2001 From: Elttob Date: Tue, 5 Dec 2023 20:21:56 +0000 Subject: [PATCH 104/287] Apply fixes according to Script Analysis panel --- test/Instances/Attribute.spec.lua | 4 ++-- test/Instances/AttributeOut.spec.lua | 2 +- test/Instances/applyInstanceProps.spec.lua | 2 +- test/State/Value.spec.lua | 4 ++-- test/init.spec.lua | 2 +- 5 files changed, 7 insertions(+), 7 deletions(-) diff --git a/test/Instances/Attribute.spec.lua b/test/Instances/Attribute.spec.lua index e32bcf6fc..51fe0486c 100644 --- a/test/Instances/Attribute.spec.lua +++ b/test/Instances/Attribute.spec.lua @@ -41,7 +41,7 @@ return function() expect(function() local scope = {} local child = New(scope, "Folder") { - [Attribute(nil)] = "foo" + [Attribute(nil :: any)] = "foo" } doCleanup(scope) end).to.throw("attributeNameNil") @@ -52,7 +52,7 @@ return function() local scope = {} local attributeValue = Value(scope, "foo") local child = New(scope, "Folder") { - [Attribute(nil)] = attributeValue + [Attribute(nil :: any)] = attributeValue } doCleanup(scope) end).to.throw("attributeNameNil") diff --git a/test/Instances/AttributeOut.spec.lua b/test/Instances/AttributeOut.spec.lua index 9da5b30f5..73cc1ea7a 100644 --- a/test/Instances/AttributeOut.spec.lua +++ b/test/Instances/AttributeOut.spec.lua @@ -24,7 +24,7 @@ return function() it("should update when state objects linked update", function() local scope = {} local attributeValue = Value(scope, "Foo") - local attributeOutValue = Value(scope) + local attributeOutValue = Value(scope, nil) local child = New(scope, "Folder") { [Attribute "Foo"] = attributeValue, [AttributeOut "Foo"] = attributeOutValue diff --git a/test/Instances/applyInstanceProps.spec.lua b/test/Instances/applyInstanceProps.spec.lua index 6351fa15c..c977b4d5c 100644 --- a/test/Instances/applyInstanceProps.spec.lua +++ b/test/Instances/applyInstanceProps.spec.lua @@ -168,7 +168,7 @@ return function() table.insert(scope, instance) applyInstanceProps( scope, - { [2] = true }, + { [2 :: any] = true }, instance ) doCleanup(scope) diff --git a/test/State/Value.spec.lua b/test/State/Value.spec.lua index f191fe8af..872a19371 100644 --- a/test/State/Value.spec.lua +++ b/test/State/Value.spec.lua @@ -6,7 +6,7 @@ local doCleanup = require(Package.Memory.doCleanup) return function() it("constructs in scopes", function() local scope = {} - local value = Value(scope) + local value = Value(scope, nil) expect(value).to.be.a("table") expect(value.type).to.equal("State") @@ -17,7 +17,7 @@ return function() end) it("is destroyable", function() - local value = Value({}) + local value = Value({}, nil) expect(value.destroy).to.be.a("function") expect(function() value:destroy() diff --git a/test/init.spec.lua b/test/init.spec.lua index c0146bc9c..c16b23bd6 100644 --- a/test/init.spec.lua +++ b/test/init.spec.lua @@ -59,7 +59,7 @@ return function() it("should not error when accessing non-existent APIs", function() expect(function() - local foo = Fusion.thisIsNotARealAPI + local foo = Fusion["thisIsNotARealAPI" :: any] end).never.to.throw() end) end \ No newline at end of file From 35b2837d3ddffe2bdc9f254c1e650b6541a10e21 Mon Sep 17 00:00:00 2001 From: Elttob Date: Tue, 5 Dec 2023 20:59:05 +0000 Subject: [PATCH 105/287] Fix Attribute missing self --- src/Instances/Attribute.lua | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Instances/Attribute.lua b/src/Instances/Attribute.lua index 66f1f20c5..d66677aa7 100644 --- a/src/Instances/Attribute.lua +++ b/src/Instances/Attribute.lua @@ -24,6 +24,7 @@ local function Attribute(attributeName: string): PubTypes.SpecialKey kind = "Attribute", stage = "self", apply = function( + self: PubTypes.SpecialKey, scope: PubTypes.Scope, value: any, applyTo: Instance From a35c359541f52ae12109eed3f2a014b91cd8e863 Mon Sep 17 00:00:00 2001 From: Elttob Date: Tue, 5 Dec 2023 21:01:37 +0000 Subject: [PATCH 106/287] Update updateAll spec to account for destruction --- test/State/updateAll.spec.lua | 28 +++++++++++++++++++++++++++- 1 file changed, 27 insertions(+), 1 deletion(-) diff --git a/test/State/updateAll.spec.lua b/test/State/updateAll.spec.lua index 9762667e2..f01f7ba81 100644 --- a/test/State/updateAll.spec.lua +++ b/test/State/updateAll.spec.lua @@ -17,7 +17,8 @@ local function buildReactiveGraph(ancestorsToDescendants, handler) dependentSet = {}, update = handler, updates = 0, - name = named + name = named, + scope = "not nil" } objects[named] = object @@ -75,6 +76,31 @@ return function() expect(objects.D.updates).to.equal(1) end) + it("should not update destroyed objects", function() + local objects = buildReactiveGraph({ + edge("A", "B"), + edge("B", "C"), + edge("C", "D"), + }, function(self) + self.updates += 1 + return true + end) + + objects.D.scope = nil + updateAll(objects.A) + expect(objects.A.updates).to.equal(0) + expect(objects.B.updates).to.equal(1) + expect(objects.C.updates).to.equal(1) + expect(objects.D.updates).to.equal(0) + + objects.B.scope = nil + updateAll(objects.A) + expect(objects.A.updates).to.equal(0) + expect(objects.B.updates).to.equal(1) + expect(objects.C.updates).to.equal(1) + expect(objects.D.updates).to.equal(0) + end) + it("should not update unchanged subgraphs", function() local objects = buildReactiveGraph({ edge("A", "B"), From 2e2045bf864b636f0ef4b44c4d77bad51c884487 Mon Sep 17 00:00:00 2001 From: Elttob Date: Wed, 6 Dec 2023 00:07:19 +0000 Subject: [PATCH 107/287] Switch order of `scope` and `use` parameters --- src/Logging/messages.lua | 6 +++--- src/PubTypes.lua | 2 +- src/State/Computed.lua | 6 +++--- src/State/For.lua | 2 +- src/State/ForKeys.lua | 10 +++++----- src/State/ForPairs.lua | 6 +++--- src/State/ForValues.lua | 8 ++++---- src/Types.lua | 6 +++--- test/State/Computed.spec.lua | 12 ++++++------ test/State/For.spec.lua | 8 ++++---- test/State/ForKeys.spec.lua | 16 ++++++++-------- test/State/ForPairs.spec.lua | 14 +++++++------- test/State/ForValues.spec.lua | 14 +++++++------- 13 files changed, 55 insertions(+), 55 deletions(-) diff --git a/src/Logging/messages.lua b/src/Logging/messages.lua index 8affbb17c..60caede20 100644 --- a/src/Logging/messages.lua +++ b/src/Logging/messages.lua @@ -13,7 +13,7 @@ return { cannotCreateClass = "Can't create a new instance of class '%s'.", cleanupWasRenamed = "`Fusion.cleanup` was renamed to `Fusion.doCleanup`. This will be an error in future versions of Fusion.", computedCallbackError = "Computed callback error: ERROR_MESSAGE", - destroyedTwice = "Attempted to destroy %s twice; if you meant to destroy this separately from the rest of the scope, put it in its own scope and call doCleanup() on that scope instead.", + destroyedTwice = "Attempted to destroy %s twice; ensure you're not manually calling `:destroy()` while using scopes. See discussion #xxx on GitHub for advice.", destructorNeededComputed = "To return instances from Computeds, provide a destructor function. This will be an error soon - see discussion #183 on GitHub.", destructorRedundant = "%s destructors no longer do anything. If you wish to run code on destroy, `table.insert` a function into the `scope` argument. See #xxx on GitHub for advice.", multiReturnComputed = "Returning multiple values from Computeds is discouraged, as behaviour will change soon - see discussion #189 on GitHub.", @@ -34,7 +34,7 @@ return { mistypedSpringSpeed = "The speed of a spring must be a number. (got a %s)", mistypedTweenInfo = "The tween info of a tween must be a TweenInfo. (got a %s)", noTaskScheduler = "Fusion is not connected to an external task scheduler.", - possiblyOutlives = "%s could be destroyed before %s. Ensure they're created in the correct scope & correct order.", + possiblyOutlives = "%s will be destroyed before %s; try swapping the order of creation. See discussion #xxx on GitHub for advice.", scopeMissing = "To create %s, provide a scope. (e.g. `%s`). See discussion #xxx on GitHub for advice.", springTypeMismatch = "The type '%s' doesn't match the spring's type '%s'.", stateGetWasRemoved = "`StateObject:get()` has been replaced by `use()` and `peek()` - see discussion #217 on GitHub.", @@ -43,5 +43,5 @@ return { unrecognisedChildType = "'%s' type children aren't accepted by `[Children]`.", unrecognisedPropertyKey = "'%s' keys aren't accepted in property tables.", unrecognisedPropertyStage = "'%s' isn't a valid stage for a special key to be applied at.", - useAfterDestroy = "%s is no longer valid - it was destroyed before %s.", + useAfterDestroy = "%s is no longer valid - it was destroyed before %s. See discussion #xxx on GitHub for advice.", } \ No newline at end of file diff --git a/src/PubTypes.lua b/src/PubTypes.lua index 7ce5e581f..a2845a96c 100644 --- a/src/PubTypes.lua +++ b/src/PubTypes.lua @@ -104,7 +104,7 @@ export type Computed = StateObject & Dependent & { } type ComputedConstructor = ( scope: Scope, - callback: (Scope, Use) -> T + callback: (Use, Scope) -> T ) -> Computed -- A state object which maps over keys and/or values in another table. diff --git a/src/State/Computed.lua b/src/State/Computed.lua index ee6951b44..d58f082eb 100644 --- a/src/State/Computed.lua +++ b/src/State/Computed.lua @@ -59,7 +59,7 @@ function class:update(): boolean return target :: T end end - local ok, newValue = xpcall(self._processor, parseError, innerScope, use) + local ok, newValue = xpcall(self._processor, parseError, use, innerScope) if ok then local oldValue = self._value @@ -121,11 +121,11 @@ end local function Computed( scope: PubTypes.Scope, - processor: (PubTypes.Scope, PubTypes.Use) -> T, + processor: (PubTypes.Use, PubTypes.Scope) -> T, destructor: any ): Types.Computed if typeof(scope) == "function" then - logError("scopeMissing", nil, "Computeds", "myScope:Computed(function(scope, use) ... end)") + logError("scopeMissing", nil, "Computeds", "myScope:Computed(function(use, scope) ... end)") elseif destructor ~= nil then logWarn("destructorRedundant", "Computed") end diff --git a/src/State/For.lua b/src/State/For.lua index ea850c767..c210ee27c 100644 --- a/src/State/For.lua +++ b/src/State/For.lua @@ -212,7 +212,7 @@ local function For( scope: PubTypes.Scope, inputTable: PubTypes.CanBeState<{ [KI]: VI }>, processor: ( - {any}, + PubTypes.Scope, PubTypes.StateObject<{key: KI, value: VI}> ) -> (PubTypes.StateObject<{key: KO?, value: VO?}>) ): Types.For diff --git a/src/State/ForKeys.lua b/src/State/ForKeys.lua index 4095df116..d410894b3 100644 --- a/src/State/ForKeys.lua +++ b/src/State/ForKeys.lua @@ -28,7 +28,7 @@ local doCleanup = require(Package.Memory.doCleanup) local function ForKeys( scope: PubTypes.Scope, inputTable: PubTypes.CanBeState<{[KI]: V}>, - processor: (PubTypes.Scope, PubTypes.Use, KI) -> KO, + processor: (PubTypes.Use, PubTypes.Scope, KI) -> KO, destructor: any? ): Types.For if typeof(inputTable) == "function" then @@ -43,11 +43,11 @@ local function ForKeys( scope: PubTypes.Scope, inputPair: PubTypes.StateObject<{key: KI, value: V}> ) - local inputKey = Computed(scope, function(scope, use): KI + local inputKey = Computed(scope, function(use, scope): KI return use(inputPair).key end) - local outputKey = Computed(scope, function(scope, use): KO? - local ok, key = xpcall(processor, parseError, scope, use, use(inputKey)) + local outputKey = Computed(scope, function(use, scope): KO? + local ok, key = xpcall(processor, parseError, use, scope, use(inputKey)) if ok then return key else @@ -57,7 +57,7 @@ local function ForKeys( return nil end end) - return Computed(scope, function(scope, use) + return Computed(scope, function(use, scope) return {key = use(outputKey), value = use(inputPair).value} end) end diff --git a/src/State/ForPairs.lua b/src/State/ForPairs.lua index 9b06a2320..9ff9b7094 100644 --- a/src/State/ForPairs.lua +++ b/src/State/ForPairs.lua @@ -28,7 +28,7 @@ local doCleanup = require(Package.Memory.doCleanup) local function ForPairs( scope: PubTypes.Scope, inputTable: PubTypes.CanBeState<{[KI]: VI}>, - processor: (PubTypes.Scope, PubTypes.Use, KI, VI) -> (KO, VO), + processor: (PubTypes.Use, PubTypes.Scope, KI, VI) -> (KO, VO), destructor: any? ): Types.For if typeof(inputTable) == "function" then @@ -43,8 +43,8 @@ local function ForPairs( scope: PubTypes.Scope, inputPair: PubTypes.StateObject<{key: KI, value: VI}> ) - return Computed(scope, function(scope, use): {key: KO?, value: VO?} - local ok, key, value = xpcall(processor, parseError, scope, use, use(inputPair).key, use(inputPair).value) + return Computed(scope, function(use, scope): {key: KO?, value: VO?} + local ok, key, value = xpcall(processor, parseError, use, scope, use(inputPair).key, use(inputPair).value) if ok then return {key = key, value = value} else diff --git a/src/State/ForValues.lua b/src/State/ForValues.lua index a9cbfca2f..fd149803d 100644 --- a/src/State/ForValues.lua +++ b/src/State/ForValues.lua @@ -28,7 +28,7 @@ local doCleanup = require(Package.Memory.doCleanup) local function ForValues( scope: PubTypes.Scope, inputTable: PubTypes.CanBeState<{[K]: VI}>, - processor: (PubTypes.Scope, PubTypes.Use, VI) -> VO, + processor: (PubTypes.Use, PubTypes.Scope, VI) -> VO, destructor: any? ): Types.For if typeof(inputTable) == "function" then @@ -43,11 +43,11 @@ local function ForValues( scope: PubTypes.Scope, inputPair: PubTypes.StateObject<{key: K, value: VI}> ) - local inputValue = Computed(scope, function(scope, use): VI + local inputValue = Computed(scope, function(use, scope): VI return use(inputPair).value end) - return Computed(scope, function(scope, use): {key: nil, value: VO?} - local ok, value = xpcall(processor, parseError, scope, use, use(inputValue)) + return Computed(scope, function(use, scope): {key: nil, value: VO?} + local ok, value = xpcall(processor, parseError, use, scope, use(inputValue)) if ok then return {key = nil, value = value} else diff --git a/src/Types.lua b/src/Types.lua index 5dfc43218..3eb124f25 100644 --- a/src/Types.lua +++ b/src/Types.lua @@ -46,7 +46,7 @@ export type State = PubTypes.Value & { export type Computed = PubTypes.Computed & { scope: PubTypes.Scope, _oldDependencySet: Set, - _processor: (PubTypes.Scope, PubTypes.Use) -> T, + _processor: (PubTypes.Use, PubTypes.Scope) -> T, _value: T, _innerScope: PubTypes.Scope? } @@ -54,8 +54,8 @@ export type Computed = PubTypes.Computed & { -- A state object which maps over keys and/or values in another table. export type For = PubTypes.For & { _processor: ( - PubTypes.Scope, - PubTypes.StateObject<{key: KI, value: VI}> + PubTypes.StateObject<{key: KI, value: VI}>, + PubTypes.Scope ) -> (PubTypes.StateObject<{key: KO?, value: VO?}>), _inputTable: PubTypes.CanBeState<{[KI]: VI}>, _existingInputTable: {[KI]: VI}?, diff --git a/test/State/Computed.spec.lua b/test/State/Computed.spec.lua index 8075a9ab0..783f525b8 100644 --- a/test/State/Computed.spec.lua +++ b/test/State/Computed.spec.lua @@ -31,7 +31,7 @@ return function() it("computes with constants", function() local scope = {} - local computed = Computed(scope, function(_, use) + local computed = Computed(scope, function(use) return use(5) end) expect(peek(computed)).to.equal(5) @@ -41,7 +41,7 @@ return function() it("computes with state objects", function() local scope = {} local dependency = Value(scope, 5) - local computed = Computed(scope, function(_, use) + local computed = Computed(scope, function(use) return use(dependency) end) expect(peek(computed)).to.equal(5) @@ -53,7 +53,7 @@ return function() it("preserves value on error", function() local scope = {} local dependency = Value(scope, 5) - local computed = Computed(scope, function(_, use) + local computed = Computed(scope, function(use) assert(use(dependency) ~= 13, "This is an intentional error from a unit test") return use(dependency) end) @@ -82,7 +82,7 @@ return function() local scope = {} local destructed = {} local dependency = Value(scope, 1) - local _ = Computed(scope, function(innerScope, use) + local _ = Computed(scope, function(use, innerScope) local value = use(dependency) table.insert(innerScope, function() destructed[value] = true @@ -103,7 +103,7 @@ return function() local scope = {} local numDestructions = {} local dependency = Value(scope, 1) - local _ = Computed(scope, function(innerScope, use) + local _ = Computed(scope, function(use, innerScope) local value = use(dependency) table.insert(innerScope, function() numDestructions[value] = (numDestructions[value] or 0) + 1 @@ -127,7 +127,7 @@ return function() it("destroys inner scope on destroy", function() local scope = {} local destructed = false - local _ = Computed(scope, function(innerScope, use) + local _ = Computed(scope, function(use, innerScope) table.insert(innerScope, function() destructed = true end) diff --git a/test/State/For.spec.lua b/test/State/For.spec.lua index 7c792f495..9ac4821c8 100644 --- a/test/State/For.spec.lua +++ b/test/State/For.spec.lua @@ -40,7 +40,7 @@ return function() numCalls += 1 local k, v = peek(inputPair).key, peek(inputPair).value seen[k] = v - return Computed(scope, function(_, use) + return Computed(scope, function(use) return {key = string.upper(use(inputPair).key), value = use(inputPair).value * 10} end) end) @@ -60,7 +60,7 @@ return function() local numCalls = 0 local forObject = For(scope, data, function(scope, inputPair) numCalls += 1 - return Computed(scope, function(_, use) + return Computed(scope, function(use) return {key = string.upper(use(inputPair).key), value = use(inputPair).value * 10} end) end) @@ -119,7 +119,7 @@ return function() local data = {first = 1, second = 2, third = 3} local omitThird = Value(scope, false) local forObject = For(scope, data, function(scope, inputPair) - return Computed(scope, function(_, use) + return Computed(scope, function(use) if use(inputPair).key == "second" then return {key = use(inputPair).key, value = nil} elseif use(inputPair).key == "third" and use(omitThird) then @@ -149,7 +149,7 @@ return function() local numCalls = 0 local forObject = For(scope, data, function(scope, inputPair) numCalls += 1 - return Computed(scope, function(_, use) + return Computed(scope, function(use) return {key = nil, value = use(inputPair).value} end) end) diff --git a/test/State/ForKeys.spec.lua b/test/State/ForKeys.spec.lua index 99608ad93..d597bd0ca 100644 --- a/test/State/ForKeys.spec.lua +++ b/test/State/ForKeys.spec.lua @@ -61,7 +61,7 @@ return function() it("computes with constants", function() local scope = {} local data = {foo = 1, bar = 2} - local forObject = ForKeys(scope, data, function(_, use, key) + local forObject = ForKeys(scope, data, function(use, _, key) return key .. use("baz") end) expect(peek(forObject).foobaz).to.equal(1) @@ -73,7 +73,7 @@ return function() local scope = {} local data = {foo = 1, bar = 2} local suffix = Value(scope, "first") - local forObject = ForKeys(scope, data, function(_, use, key) + local forObject = ForKeys(scope, data, function(use, _, key) return key .. use(suffix) end) expect(peek(forObject).foofirst).to.equal(1) @@ -91,7 +91,7 @@ return function() local data = {foo = 1, bar = 2, baz = 3} local suffix = Value(scope, "first") local destroyed = {} - local forObject = ForKeys(scope, data, function(innerScope, use, key) + local forObject = ForKeys(scope, data, function(use, innerScope, key) local generated = key .. use(suffix) table.insert(innerScope, function() destroyed[generated] = true @@ -134,7 +134,7 @@ return function() local scope = {} local data = {foo = 1, bar = 2, baz = 3} local omitThird = Value(scope, false) - local forObject = ForKeys(scope, data, function(_, use, key) + local forObject = ForKeys(scope, data, function(use, _, key) if key == "bar" then return nil end @@ -163,7 +163,7 @@ return function() local scope = {} local destructed = {} local data = Value(scope, {foo = 1, bar = 2}) - local _ = ForKeys(scope, data, function(innerScope, _, key) + local _ = ForKeys(scope, data, function(_, innerScope, key) table.insert(innerScope, function() destructed[key] = true end) @@ -182,7 +182,7 @@ return function() local scope = {} local destructed = {} local data = Value(scope, {foo = 1, bar = 2}) - local _ = ForKeys(scope, data, function(innerScope, _, key) + local _ = ForKeys(scope, data, function(_, innerScope, key) table.insert(innerScope, function() destructed[key] = true end) @@ -201,7 +201,7 @@ return function() local scope = {} local destructed = {} local data = Value(scope, {foo = 1, bar = 2}) - local _ = ForKeys(scope, data, function(innerScope, _, key) + local _ = ForKeys(scope, data, function(_, innerScope, key) table.insert(innerScope, function() destructed[key] = true end) @@ -218,7 +218,7 @@ return function() local scope = {} local data = Value(scope, {foo = 1, bar = 2}) local computations = 0 - local _ = ForKeys(scope, data, function(innerScope, _, key) + local _ = ForKeys(scope, data, function(_, _, key) computations += 1 return string.upper(key) end) diff --git a/test/State/ForPairs.spec.lua b/test/State/ForPairs.spec.lua index ae33a4ff9..68e38c29f 100644 --- a/test/State/ForPairs.spec.lua +++ b/test/State/ForPairs.spec.lua @@ -63,7 +63,7 @@ return function() it("computes with constants", function() local scope = {} local data = {foo = "oof", bar = "rab"} - local forObject = ForPairs(scope, data, function(_, use, key, value) + local forObject = ForPairs(scope, data, function(use, _, key, value) return value .. use("baz"), key .. use("baz") end) expect(peek(forObject).oofbaz).to.equal("foobaz") @@ -75,7 +75,7 @@ return function() local scope = {} local data = {foo = "oof", bar = "rab"} local suffix = Value(scope, "first") - local forObject = ForPairs(scope, data, function(_, use, key, value) + local forObject = ForPairs(scope, data, function(use, _, key, value) return value .. use(suffix), key .. use(suffix) end) expect(peek(forObject).ooffirst).to.equal("foofirst") @@ -93,7 +93,7 @@ return function() local data = {foo = "oof", bar = "rab", baz = "zab"} local suffix = Value(scope, "first") local destroyed = {} - local forObject = ForPairs(scope, data, function(innerScope, use, key, value) + local forObject = ForPairs(scope, data, function(use, innerScope, key, value) local generatedKey = value .. use(suffix) local generatedValue = key .. use(suffix) table.insert(innerScope, function() @@ -137,7 +137,7 @@ return function() local scope = {} local data = {foo = "oof", bar = "rab", baz = "zab"} local omitThird = Value(scope, false) - local forObject = ForPairs(scope, data, function(_, use, key, value) + local forObject = ForPairs(scope, data, function(use, _, key, value) if key == "bar" then return nil end @@ -166,7 +166,7 @@ return function() local scope = {} local destructed = {} local data = Value(scope, {foo = "oof", bar = "rab", baz = "zab"}) - local _ = ForPairs(scope, data, function(innerScope, _, key, value) + local _ = ForPairs(scope, data, function(_, innerScope, key, value) table.insert(innerScope, function() destructed[key] = true end) @@ -185,7 +185,7 @@ return function() local scope = {} local destructed = {} local data = Value(scope, {foo = "oof", bar = "rab"}) - local _ = ForPairs(scope, data, function(innerScope, _, key, value) + local _ = ForPairs(scope, data, function(_, innerScope, key, value) table.insert(innerScope, function() destructed[key] = true end) @@ -204,7 +204,7 @@ return function() local scope = {} local destructed = {} local data = Value(scope, {foo = "oof", bar = "rab"}) - local _ = ForPairs(scope, data, function(innerScope, _, key, value) + local _ = ForPairs(scope, data, function(_, innerScope, key, value) table.insert(innerScope, function() destructed[key] = true end) diff --git a/test/State/ForValues.spec.lua b/test/State/ForValues.spec.lua index 533142e36..f7ffd43f5 100644 --- a/test/State/ForValues.spec.lua +++ b/test/State/ForValues.spec.lua @@ -63,7 +63,7 @@ return function() it("computes with constants", function() local scope = {} local data = {"foo", "bar"} - local forObject = ForValues(scope, data, function(_, use, value) + local forObject = ForValues(scope, data, function(use, _, value) return value .. use("baz") end) expect(table.find(peek(forObject), "foobaz")).to.be.ok() @@ -75,7 +75,7 @@ return function() local scope = {} local data = {"foo", "bar"} local suffix = Value(scope, "first") - local forObject = ForValues(scope, data, function(_, use, value) + local forObject = ForValues(scope, data, function(use, _, value) return value .. use(suffix) end) expect(table.find(peek(forObject), "foofirst")).to.be.ok() @@ -91,7 +91,7 @@ return function() local data = {"foo", "bar", "baz"} local suffix = Value(scope, "first") local destroyed = {} - local forObject = ForValues(scope, data, function(innerScope, use, value) + local forObject = ForValues(scope, data, function(use, innerScope, value) local generated = value .. use(suffix) table.insert(innerScope, function() destroyed[generated] = true @@ -134,7 +134,7 @@ return function() local scope = {} local data = {"foo", "bar", "baz"} local omitThird = Value(scope, false) - local forObject = ForValues(scope, data, function(_, use, value) + local forObject = ForValues(scope, data, function(use, _, value) if value == "bar" then return nil end @@ -163,7 +163,7 @@ return function() local scope = {} local destructed = {} local data = Value(scope, {"foo", "bar"}) - local _ = ForValues(scope, data, function(innerScope, _, value) + local _ = ForValues(scope, data, function(_, innerScope, value) table.insert(innerScope, function() destructed[value] = true end) @@ -182,7 +182,7 @@ return function() local scope = {} local destructed = {} local data = Value(scope, {"foo", "bar"}) - local _ = ForValues(scope, data, function(innerScope, _, value) + local _ = ForValues(scope, data, function(_, innerScope, value) table.insert(innerScope, function() destructed[value] = true end) @@ -201,7 +201,7 @@ return function() local scope = {} local destructed = {} local data = Value(scope, {"foo", "bar"}) - local _ = ForValues(scope, data, function(innerScope, _, value) + local _ = ForValues(scope, data, function(_, innerScope, value) table.insert(innerScope, function() destructed[value] = true end) From b2240bb68090f57db8e584a6483c79f87728b7bb Mon Sep 17 00:00:00 2001 From: Elttob Date: Wed, 6 Dec 2023 01:33:55 +0000 Subject: [PATCH 108/287] Update messages with discussion (+ wording) --- src/Logging/messages.lua | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/Logging/messages.lua b/src/Logging/messages.lua index 60caede20..5dd9ec4bf 100644 --- a/src/Logging/messages.lua +++ b/src/Logging/messages.lua @@ -13,9 +13,9 @@ return { cannotCreateClass = "Can't create a new instance of class '%s'.", cleanupWasRenamed = "`Fusion.cleanup` was renamed to `Fusion.doCleanup`. This will be an error in future versions of Fusion.", computedCallbackError = "Computed callback error: ERROR_MESSAGE", - destroyedTwice = "Attempted to destroy %s twice; ensure you're not manually calling `:destroy()` while using scopes. See discussion #xxx on GitHub for advice.", + destroyedTwice = "Attempted to destroy %s twice; ensure you're not manually calling `:destroy()` while using scopes. See discussion #292 on GitHub for advice.", destructorNeededComputed = "To return instances from Computeds, provide a destructor function. This will be an error soon - see discussion #183 on GitHub.", - destructorRedundant = "%s destructors no longer do anything. If you wish to run code on destroy, `table.insert` a function into the `scope` argument. See #xxx on GitHub for advice.", + destructorRedundant = "%s destructors no longer do anything. If you wish to run code on destroy, `table.insert` a function into the `scope` argument. See #292 on GitHub for advice.", multiReturnComputed = "Returning multiple values from Computeds is discouraged, as behaviour will change soon - see discussion #189 on GitHub.", forKeyCollision = "The key '%s' was returned multiple times simultaneously, which is not allowed in `For` objects.", forProcessorError = "Error while processing `For` object: ERROR_MESSAGE", @@ -34,8 +34,8 @@ return { mistypedSpringSpeed = "The speed of a spring must be a number. (got a %s)", mistypedTweenInfo = "The tween info of a tween must be a TweenInfo. (got a %s)", noTaskScheduler = "Fusion is not connected to an external task scheduler.", - possiblyOutlives = "%s will be destroyed before %s; try swapping the order of creation. See discussion #xxx on GitHub for advice.", - scopeMissing = "To create %s, provide a scope. (e.g. `%s`). See discussion #xxx on GitHub for advice.", + possiblyOutlives = "%s could be destroyed before %s; review the order they're created in, and what scopes they belong to. See discussion #292 on GitHub for advice.", + scopeMissing = "To create %s, provide a scope. (e.g. `%s`). See discussion #292 on GitHub for advice.", springTypeMismatch = "The type '%s' doesn't match the spring's type '%s'.", stateGetWasRemoved = "`StateObject:get()` has been replaced by `use()` and `peek()` - see discussion #217 on GitHub.", strictReadError = "'%s' is not a valid member of '%s'.", @@ -43,5 +43,5 @@ return { unrecognisedChildType = "'%s' type children aren't accepted by `[Children]`.", unrecognisedPropertyKey = "'%s' keys aren't accepted in property tables.", unrecognisedPropertyStage = "'%s' isn't a valid stage for a special key to be applied at.", - useAfterDestroy = "%s is no longer valid - it was destroyed before %s. See discussion #xxx on GitHub for advice.", + useAfterDestroy = "%s is no longer valid - it was destroyed before %s. See discussion #292 on GitHub for advice.", } \ No newline at end of file From b61c7a812c8244b553d939f96928a95c9efc1d2c Mon Sep 17 00:00:00 2001 From: Elttob Date: Wed, 6 Dec 2023 01:34:22 +0000 Subject: [PATCH 109/287] Wording tweak for `destructorRedundant` --- src/Logging/messages.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Logging/messages.lua b/src/Logging/messages.lua index 5dd9ec4bf..f4c2892af 100644 --- a/src/Logging/messages.lua +++ b/src/Logging/messages.lua @@ -15,7 +15,7 @@ return { computedCallbackError = "Computed callback error: ERROR_MESSAGE", destroyedTwice = "Attempted to destroy %s twice; ensure you're not manually calling `:destroy()` while using scopes. See discussion #292 on GitHub for advice.", destructorNeededComputed = "To return instances from Computeds, provide a destructor function. This will be an error soon - see discussion #183 on GitHub.", - destructorRedundant = "%s destructors no longer do anything. If you wish to run code on destroy, `table.insert` a function into the `scope` argument. See #292 on GitHub for advice.", + destructorRedundant = "%s destructors no longer do anything. If you wish to run code on destroy, `table.insert` a function into the `scope` argument. See discussion #292 on GitHub for advice.", multiReturnComputed = "Returning multiple values from Computeds is discouraged, as behaviour will change soon - see discussion #189 on GitHub.", forKeyCollision = "The key '%s' was returned multiple times simultaneously, which is not allowed in `For` objects.", forProcessorError = "Error while processing `For` object: ERROR_MESSAGE", From 1a00b1a7a2e3d10e3f8108148279972a5866b62c Mon Sep 17 00:00:00 2001 From: Elttob Date: Wed, 6 Dec 2023 23:25:14 +0000 Subject: [PATCH 110/287] Objects introduction tutorial --- docs/tutorials/fundamentals/destructors.md | 134 -------------- docs/tutorials/fundamentals/objects.md | 165 ++++++++++++++++++ .../values/Game-UI-Variables-Dark.svg | 34 ---- .../values/Game-UI-Variables-Light.svg | 38 ---- mkdocs.yml | 4 +- 5 files changed, 167 insertions(+), 208 deletions(-) delete mode 100644 docs/tutorials/fundamentals/destructors.md create mode 100644 docs/tutorials/fundamentals/objects.md delete mode 100644 docs/tutorials/fundamentals/values/Game-UI-Variables-Dark.svg delete mode 100644 docs/tutorials/fundamentals/values/Game-UI-Variables-Light.svg diff --git a/docs/tutorials/fundamentals/destructors.md b/docs/tutorials/fundamentals/destructors.md deleted file mode 100644 index 68705e48f..000000000 --- a/docs/tutorials/fundamentals/destructors.md +++ /dev/null @@ -1,134 +0,0 @@ -Destructors are functions that clean up values passed to them. Computed objects -use them to clean up old values when they're no longer needed. - -```Lua -local function callDestroy(x) - x:Destroy() -end - -local brick = Computed(function(use) - return Instance.new("Part") -end, callDestroy) -``` - ------ - -## Memory Management - -In Luau, most values clean themselves up automatically, because they're managed -by the garbage collector: - -```Lua --- This will create a new table in memory: -local x = { - hello = "world" -} -task.wait(5) --- The table is destroyed automatically when you stop using it. -x = nil -``` - -However, not all values clean themselves up. Some common 'unmanaged' types are: - -1. Instances - need to be `:Destroy()`ed -2. Event connections - need to be `:Disconnect()`ed -3. Custom objects - might provide their own `:Destroy()` methods. - -The garbage collector doesn't manage these for you, so if you don't clean them -up, they could stick around forever: - -```Lua --- We're creating an event connection here. -local event = workspace.Changed:Connect(function() - print("Hello!") -end) - --- Even if we stop using the event connection in our code, it will continue to --- receive events. It will not be disconnected for you. -event = nil -``` - ------ - -## State Objects - -Those types of values are a problem for Computed objects. For example, if they -generate fresh instances, they need to destroy those instances too: - -```Lua -local className = Value("Frame") --- `instance` will generate a Frame at first -local instance = Computed(function(use) - return Instance.new(use(className)) -end) --- This will cause it to generate a TextLabel - but we didn't destroy the Frame! -className:set("TextLabel") -``` - -This is where destructors help out. You can provide a second function, which -Fusion will call to destroy the values we generate: - -```Lua -local function callDestroy(x) - x:Destroy() -end - -local instance = Computed(function(use) - return Instance.new(use(className)) -end, callDestroy) -``` - -Destructors aren't limited to typical cleanup behaviour! You can customise what -happens during cleanup, or do no cleanup at all: - -```Lua -local function moveToServerStorage(x) - x.Parent = game:GetService("ServerStorage") -end - -local function doNothing(x) - -- intentionally left blank -end -``` - ------ - -## Shorthand - -Most of the time, you'll want to either: - -1. destroy/disconnect/clean up the values you generate... -2. ...or leave them alone and do nothing. - -Fusion provides default destructors for both of these situations. - -### Cleanup - -`Fusion.cleanup` is a function which tries to cleans up whatever you pass to it: - -- given an instance, it is `:Destroy()`ed -- given an event connection, it is `:Disconnect()`ed -- given an object, any `:destroy()` or `:Destroy()` methods are run -- given a function, the function is run -- given an array, it cleans up everything inside - -You can use this when generating unmanaged values: - -```Lua -local instance = Computed(function(use) - return Instance.new(use(className)) -end, Fusion.cleanup) -``` - -### Do Nothing - -`Fusion.doNothing` is an empty function. It does nothing. - -You can use this when passing 'through' unmanaged values that you don't control. -It makes it clear that your code is supposed to leave the values alone: - -```Lua -local instance = Computed(function(use) - return workspace:FindFirstChild(use(className)) -end, Fusion.doNothing) -``` \ No newline at end of file diff --git a/docs/tutorials/fundamentals/objects.md b/docs/tutorials/fundamentals/objects.md new file mode 100644 index 000000000..fe790c116 --- /dev/null +++ b/docs/tutorials/fundamentals/objects.md @@ -0,0 +1,165 @@ +In Fusion, you will create a lot of objects. These objects will need to be +destroyed when you are done with them. + +Fusion introduces a few coding conventions that makes working with a large +quantity of objects a lot easier. You're learning these coding conventions now, +before you learn about Fusion's features, because it's used throughout Fusion +and is very important. + +----- + +## Scopes + +When you create a bunch of objects at the same time, you will often want to +`:destroy()` them later at the same time, too. To make this easier, you can add +your objects to an array. Arrays that group together objects like this are given +a special name: *scopes*. + +To create a new scope, create an empty array. This array will hold your objects. + +```Lua linenums="1" hl_lines="3" +local Fusion = require(ReplicatedStorage.Fusion) + +local scope = {} +``` + +Later, when you create objects, they will ask you to provide a scope as the +first argument. + +```Lua linenums="1" hl_lines="4" +local Fusion = require(ReplicatedStorage.Fusion) + +local scope = {} +local thing = Fusion.Value(scope, "i am a thing") +``` + +When you create an object in Fusion like this, it will add itself to the scope. + +```Lua linenums="1" hl_lines="6" +local Fusion = require(ReplicatedStorage.Fusion) + +local scope = {} +local thing = Fusion.Value(scope, "i am a thing") + +print(scope[1] == thing) --> true +``` + +You can repeat this as many times as you like. Objects are added in the order +they are created. + +```Lua linenums="1" hl_lines="4-10" +local Fusion = require(ReplicatedStorage.Fusion) + +local scope = {} +local thing1 = Fusion.Value(scope, "i am thing 1") +local thing2 = Fusion.Value(scope, "i am thing 2") +local thing3 = Fusion.Value(scope, "i am thing 3") + +print(scope[1] == thing1) --> true +print(scope[2] == thing2) --> true +print(scope[3] == thing3) --> true +``` + +Later on, you can destroy the scope by using the `doCleanup()` function. That +will destroy all the objects you added to it. + +```Lua linenums="1" hl_lines="2 9" +local Fusion = require(ReplicatedStorage.Fusion) +local doCleanup = Fusion.doCleanup + +local scope = {} +local thing1 = Fusion.Value(scope, "i am thing 1") +local thing2 = Fusion.Value(scope, "i am thing 2") +local thing3 = Fusion.Value(scope, "i am thing 3") + +doCleanup(scope) +-- Using `doCleanup` is the same as: +-- thing3:destroy() +-- thing2:destroy() +-- thing1:destroy() +``` + +That's all there is to scopes. They are nothing more than arrays of objects. + +----- + +## Improved Syntax + +While the above way of writing scopes is good for learning, Fusion also provides +more convenient shorthand that makes writing them out easier. + +The first change is to use `scoped()` to make our scope, instead of creating an +array normally. It asks for a table argument, which we'll leave empty for now. + +```Lua linenums="1" hl_lines="2 4" +local Fusion = require(ReplicatedStorage.Fusion) +local doCleanup, scoped = Fusion.doCleanup, Fusion.scoped + +local scope = scoped({}) +local thing1 = Fusion.Value(scope, "i am thing 1") +local thing2 = Fusion.Value(scope, "i am thing 2") +local thing3 = Fusion.Value(scope, "i am thing 3") + +doCleanup(scope) +``` + +`scoped()` will return an array, just like the one we created ourselves. +However, we can now upgrade it with Fusion's syntax. + +Whenever we have a function that takes `scope` as the first argument, we can add +it to the table argument of `scoped()`. + +```Lua linenums="1" hl_lines="4-6" +local Fusion = require(ReplicatedStorage.Fusion) +local doCleanup, scoped = Fusion.doCleanup, Fusion.scoped + +local scope = scoped({ + Value = Fusion.Value +}) +local thing1 = Fusion.Value(scope, "i am thing 1") +local thing2 = Fusion.Value(scope, "i am thing 2") +local thing3 = Fusion.Value(scope, "i am thing 3") + +doCleanup(scope) +``` + +Now, we can use that function as a method on `scope`, like this: + +```Lua linenums="1" hl_lines="7-9" +local Fusion = require(ReplicatedStorage.Fusion) +local doCleanup, scoped = Fusion.doCleanup, Fusion.scoped + +local scope = scoped({ + Value = Fusion.Value +}) +local thing1 = scope:Value("i am thing 1") +local thing2 = scope:Value("i am thing 2") +local thing3 = scope:Value("i am thing 3") + +doCleanup(scope) +``` + +This makes your code shorter, cleaner and more consistent. When you write code +in this way, you no longer have to refer to the full function name, and it's +easier to copy and paste around without making mistakes. + +As a nice shorthand, you can pass in `Fusion` directly to `scoped()` rather than +listing out all the functions you want manually. Because `Fusion` is already a +table full of functions for creating things, it works too. + +```Lua linenums="1" hl_lines="4" +local Fusion = require(ReplicatedStorage.Fusion) +local doCleanup, scoped = Fusion.doCleanup, Fusion.scoped + +local scope = scoped(Fusion) +local thing1 = scope:Value("i am thing 1") +local thing2 = scope:Value("i am thing 2") +local thing3 = scope:Value("i am thing 3") + +doCleanup(scope) +``` + +Remember - this is just a nicer syntax for exactly the same thing we were doing +before. + +From now on, you'll see this `scoped()` syntax used throughout the tutorials. \ No newline at end of file diff --git a/docs/tutorials/fundamentals/values/Game-UI-Variables-Dark.svg b/docs/tutorials/fundamentals/values/Game-UI-Variables-Dark.svg deleted file mode 100644 index f166397de..000000000 --- a/docs/tutorials/fundamentals/values/Game-UI-Variables-Dark.svg +++ /dev/null @@ -1,34 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/docs/tutorials/fundamentals/values/Game-UI-Variables-Light.svg b/docs/tutorials/fundamentals/values/Game-UI-Variables-Light.svg deleted file mode 100644 index c35f98bbd..000000000 --- a/docs/tutorials/fundamentals/values/Game-UI-Variables-Light.svg +++ /dev/null @@ -1,38 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/mkdocs.yml b/mkdocs.yml index 81da6fdb0..3ea38eecf 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -54,11 +54,11 @@ nav: - Home: index.md - Tutorials: - Get Started: tutorials/index.md - - State Objects: + - Fundamentals: + - Objects: tutorials/fundamentals/objects.md - Values: tutorials/fundamentals/values.md - Observers: tutorials/fundamentals/observers.md - Computeds: tutorials/fundamentals/computeds.md - - Destructors: tutorials/fundamentals/destructors.md - Instances: - Hydration: tutorials/instances/hydration.md - New Instances: tutorials/instances/new-instances.md From 2f75e067d2eab39706cd9a8fe3e96e63ae63afa8 Mon Sep 17 00:00:00 2001 From: Elttob Date: Wed, 6 Dec 2023 23:25:20 +0000 Subject: [PATCH 111/287] Values tutorial update --- docs/tutorials/fundamentals/values.md | 149 ++++++-------------------- 1 file changed, 35 insertions(+), 114 deletions(-) diff --git a/docs/tutorials/fundamentals/values.md b/docs/tutorials/fundamentals/values.md index ea5fb5be7..611d0fa7c 100644 --- a/docs/tutorials/fundamentals/values.md +++ b/docs/tutorials/fundamentals/values.md @@ -1,8 +1,11 @@ +Now that you understand how Fusion works with objects, you can create Fusion's +simplest object. + Values are objects which store single values. You can write to them with their `:set()` method, and read from them with the `peek()` function. ```Lua -local health = Value(100) +local health = scope:Value(100) print(peek(health)) --> 100 health:set(25) @@ -13,132 +16,50 @@ print(peek(health)) --> 25 ## Usage -To use `Value` in your code, you first need to import it from the Fusion module, -so that you can refer to it by name. You should also import `peek` for later: +To create a new value object, call `scope:Value()` and give it a value you want +to store. -```Lua linenums="1" hl_lines="2-3" +```Lua linenums="1" hl_lines="5" local Fusion = require(ReplicatedStorage.Fusion) -local Value = Fusion.Value -local peek = Fusion.peek -``` +local doCleanup, scoped = Fusion.doCleanup, Fusion.scoped -To create a new value, call the `Value` function: - -```Lua -local health = Value() -- this will create and return a new Value object +local scope = scoped(Fusion) +local health = scope:Value(5) ``` -By default, new `Value` objects store `nil`. If you want the `Value` object to -start with a different value, you can provide one: +Fusion provides a global `peek()` function. It will read the value of whatever +you give it. You'll use `peek()` to read the value of lots of things; for now, +it's useful for printing `health` back out. -```Lua -local health = Value(100) -- the Value will initially store a value of 100 +```Lua linenums="1" hl_lines="3 7" +local Fusion = require(ReplicatedStorage.Fusion) +local doCleanup, scoped = Fusion.doCleanup, Fusion.scoped +local peek = Fusion.peek + +local scope = scoped(Fusion) +local health = scope:Value(5) +print(peek(health)) --> 5 ``` -Fusion provides a global `peek()` function which returns the value of whatever -you give it. For example, it will read the value of our `health` object: +You can change the value using the `:set()` method. Unlike `peek()`, this is +specific to value objects, so it's done on the object itself. -```Lua -print(peek(health)) --> 100 -``` +```Lua linenums="1" hl_lines="9-10" +local Fusion = require(ReplicatedStorage.Fusion) +local doCleanup, scoped = Fusion.doCleanup, Fusion.scoped +local peek = Fusion.peek -We can change the value using the `:set()` method on the object itself: +local scope = scoped(Fusion) +local health = scope:Value(5) +print(peek(health)) --> 5 -```Lua health:set(25) print(peek(health)) --> 25 ``` ------ - -## Why Objects? - -### The Problem - -Imagine some UI in your head. Think about what it looks like, and think about -the different variables it's showing to you. - -
-![An example of a game's UI, with some variables labelled and linked to parts of the UI.](Game-UI-Variables-Light.svg#only-light) -![An example of a game's UI, with some variables labelled and linked to parts of the UI.](Game-UI-Variables-Dark.svg#only-dark) -
Screenshot: GameUIDatabase (Halo Infinite)
-
- -Your UIs are usually driven by a few internal variables. When those variables -change, you want your UI to reflect those changes. - -Unfortunately, there's no way to listen for those changes in Lua. When you -change those variables, it's normally *your* responsibility to figure out what -needs to update, and to send out those updates. - -Over time, we've come up with many methods of dealing with this inconvenience. -Perhaps the simplest are 'setter functions', like these: - -```Lua -local ammo = 100 - -local function setAmmo(newAmmo) - ammo = newAmmo - -- you need to send out updates to every piece of code using `ammo` here - updateHUD() - updateGunModel() - sendAmmoToServer() -end -``` - -But this is clunky and unreliable; what if there's another piece of code using -`ammo` that we've forgotten to update here? How can you guarantee we've covered -everything? Moreover, why is the code setting the `ammo` even concerned with who -uses it? - -### Building Better Variables - -In an ideal world, anyone using `ammo` should be able to listen for changes, and -get notified when someone sets it to a new value. - -To make this work, we need to fundamentally extend what variables can do. In -particular, we need two additional features: - -- We need to save a list of *dependents* - other places currently using our -variable. This is so we know who to notify when the value changes. -- We need to run some code when the variable is set to a new value. If we can -do that, then we can go through the list and notify everyone. - -To solve this, Fusion introduces the idea of a 'state object'. These are objects -that represent a single value, which you can `peek()` at any time. They also -keep a list of dependents; when the object's value changes, it can notify -everyone so they can respond to the change. - -`Value` is one such state object. It's specifically designed to act like a -variable, so it has an extra `:set()` method. Using that method, you can change -the object's value manually. If you set it to a different value than before, -it'll notify anyone using the object. - -This means you can use `Value` objects like variables, with the added benefit of -being able to listen to changes like we wanted! - -### Sharing Variables - -There is another benefit to using objects too; you can easily share your objects -directly with other code. Every usage of that object will refer to the -same underlying value: - -```Lua --- someObject is a `Value` object -local function printValue(someObject) - print(peek(someObject)) -end - -local health = Value(100) -printValue(health) --> 100 - -local myDogsName = Value("Dan") -printValue(myDogsName) --> Dan -``` - -This is something that normal variables can't do by default, and is a benefit -exclusive to state objects. +Value objects are Fusion's simplest 'state object'. State objects contain a +single value - their *state*, you might say - and that single value can be read +out at any time using `peek()`. -In the above code, `printValue` can operate on *any* arbitrary variable without -knowing what it is, or where it comes from. This is very useful for writing -generic, reusable code, and you'll see it used a lot throughout Fusion. \ No newline at end of file +Later on, you'll discover more advanced state objects that can calculate their +value in more interesting ways. \ No newline at end of file From 32b79ae2424c9b75e068fc9c3449e0bd5df1095b Mon Sep 17 00:00:00 2001 From: Elttob Date: Wed, 6 Dec 2023 23:50:02 +0000 Subject: [PATCH 112/287] Trim code blocks in Value tutorial --- docs/tutorials/fundamentals/values.md | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/docs/tutorials/fundamentals/values.md b/docs/tutorials/fundamentals/values.md index 611d0fa7c..7c2177dc8 100644 --- a/docs/tutorials/fundamentals/values.md +++ b/docs/tutorials/fundamentals/values.md @@ -44,11 +44,7 @@ print(peek(health)) --> 5 You can change the value using the `:set()` method. Unlike `peek()`, this is specific to value objects, so it's done on the object itself. -```Lua linenums="1" hl_lines="9-10" -local Fusion = require(ReplicatedStorage.Fusion) -local doCleanup, scoped = Fusion.doCleanup, Fusion.scoped -local peek = Fusion.peek - +```Lua linenums="5" hl_lines="5-6" local scope = scoped(Fusion) local health = scope:Value(5) print(peek(health)) --> 5 From 36ad9fb689f2814cc3eeb48aba72ead92fb3a9eb Mon Sep 17 00:00:00 2001 From: Elttob Date: Wed, 6 Dec 2023 23:50:18 +0000 Subject: [PATCH 113/287] Observer tutorial updated skeleton --- docs/tutorials/instances/cleanup.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/tutorials/instances/cleanup.md b/docs/tutorials/instances/cleanup.md index a596752ea..5849b79dc 100644 --- a/docs/tutorials/instances/cleanup.md +++ b/docs/tutorials/instances/cleanup.md @@ -153,7 +153,7 @@ method is called, meaning other kinds of destruction are ignored. For example, notice only one of these parts runs their cleanup code: -=== "Script code" +=== "Luau code" ```Lua linenums="1" local part1 = New "Part" { @@ -186,7 +186,7 @@ For example, notice only one of these parts runs their cleanup code: Meanwhile, Fusion's `[Cleanup]` will work regardless of how your instances were destroyed, meaning you can avoid serious memory leaks: -=== "Script code" +=== "Luau code" ```Lua linenums="1" local part1 = New "Part" { From 0150b6a5f6f3f168a4e7ab6142e64ad98619fa1b Mon Sep 17 00:00:00 2001 From: Elttob Date: Wed, 6 Dec 2023 23:50:42 +0000 Subject: [PATCH 114/287] Replace "Script code" with generic "Luau code" --- docs/tutorials/components/callbacks.md | 2 +- docs/tutorials/fundamentals/observers.md | 104 +++++++++-------------- 2 files changed, 40 insertions(+), 66 deletions(-) diff --git a/docs/tutorials/components/callbacks.md b/docs/tutorials/components/callbacks.md index c04e88e15..15f32de7f 100644 --- a/docs/tutorials/components/callbacks.md +++ b/docs/tutorials/components/callbacks.md @@ -38,7 +38,7 @@ function. Luau will execute the function, then return to your code. In this example, the `fiveTimes` function calls a callback five times: -=== "Script code" +=== "Luau code" ```Lua local function fiveTimes(callback) diff --git a/docs/tutorials/fundamentals/observers.md b/docs/tutorials/fundamentals/observers.md index 41807e0c7..017aa4ef1 100644 --- a/docs/tutorials/fundamentals/observers.md +++ b/docs/tutorials/fundamentals/observers.md @@ -1,14 +1,14 @@ -Observers allow you to detect when any state object changes value. You can -connect handlers using `:onChange()`, which returns a function you can call to -disconnect it later. +When you're working with state objects, it can be useful to detect various +changes that happen to them. -```Lua -local observer = Observer(health) +Observers allow you to detect those changes. Create one with a state object to +'watch', then connect code to run using `:onChange()` or `:onBind()`. +```Lua +local observer = scope:Observer(health) local disconnect = observer:onChange(function() print("The new value is: ", peek(health)) end) - task.wait(5) disconnect() ``` @@ -17,37 +17,48 @@ disconnect() ## Usage -To use `Observer` in your code, you first need to import it from the Fusion -module, so that you can refer to it by name: +To create a new observer object, call `scope:Observer()` and give it a state +object you want to detect changes on. -```Lua linenums="1" hl_lines="2" -local Fusion = require(ReplicatedStorage.Fusion) -local Observer = Fusion.Observer +```Lua linenums="5" hl_lines="3" +local scope = scoped(Fusion) +local health = scope:Value(5) +local observer = scope:Observer(health) ``` -To create a new observer, call the `Observer` function with an object to watch: +The observer will watch the state object for changes until it's destroyed. You +can take advantage of this by connecting your own code using the observer's +different methods. -```Lua -local health = Value(100) +The first method is `:onChange()`, which runs your code when the state object +changes value. --- This observer will watch `health` for changes: -local observer = Observer(health) +```Lua +observer:onChange(function() + print("The new value is: ", peek(health)) +end) ``` -When the watched object changes value, the observer will run all of its handlers. -To add a handler, you can use `:onChange()`: +By default, the `:onChange()` connection is disconnected when the observer +object is destroyed. However, if you want to disconnect it earlier, the +`:onChange()` method returns an optional disconnect function. Calling it will +disconnect that specific `:onChange()` handler early. ```Lua local disconnect = observer:onChange(function() print("The new value is: ", peek(health)) end) + +-- disconnect the above handler after 5 seconds +task.wait(5) +disconnect() ``` -When you're done with the handler, it's very important to disconnect it. The -`:onChange()` method returns a function you can call to disconnect your handler: +The second method is `:onBind()`. It works identically to `:onChange()`, but it +also runs your code right away, which can often be useful. ```Lua -local disconnect = observer:onChange(function() +local disconnect = observer:onBind(function() print("The new value is: ", peek(health)) end) @@ -56,22 +67,14 @@ task.wait(5) disconnect() ``` -??? question "Why is disconnecting so important?" - While an observer has at least one active handler, it will hold the watched - object in memory forcibly. This is done to make sure that changes aren't - missed. - - Disconnecting your handlers tells Fusion you don't need to track changes - any more, which allows it to clean up the observer and the watched object. - ----- ## What Counts As A Change? -You might notice that not all calls to `Value:set()` will cause your observer to -run: +If you set the `health` to the same value multiple times in a row, you might +notice your observer only runs the first time. -=== "Script code" +=== "Luau code" ```Lua local thing = Value("Hello") @@ -97,40 +100,11 @@ run: Setting thing thrice... ``` -When you set the value, if it's the same as the existing value, an update won't -be sent out. This means observers won't re-run when you set the -value multiple times in a row. +This is because the `health` object sees that it isn't actually changing value, +so it doesn't broadcast any updates. Therefore, our observer doesn't run. ![A diagram showing how value objects only send updates if the new value and previous value aren't equal.](Value-Equality-Dark.svg#only-dark) ![A diagram showing how value objects only send updates if the new value and previous value aren't equal.](Value-Equality-Light.svg#only-light) -In most cases, this leads to improved performance because your code runs less -often. However, if you need to override this behaviour, `Value:set()` accepts a -second argument - if you set it to `true`, an update will be forced: - -=== "Script code" - - ```Lua hl_lines="11-12" - local thing = Value("Hello") - - Observer(thing):onChange(function() - print("=> Thing changed to", peek(thing)) - end) - - print("Setting thing once...") - thing:set("World") - print("Setting thing twice...") - thing:set("World") - print("Setting thing thrice (update forced)...") - thing:set("World", true) - ``` - -=== "Output" - - ``` hl_lines="4-5" - Setting thing once... - => Thing changed to World - Setting thing twice... - Setting thing thrice (update forced)... - => Thing changed to World - ``` \ No newline at end of file +This leads to improved performance because your code runs less often. Fusion +applies these kinds of optimisations generously throughout your program. \ No newline at end of file From 9655e2c8d5f8a805567f0923c8a8d232a174cf19 Mon Sep 17 00:00:00 2001 From: Elttob Date: Thu, 7 Dec 2023 00:39:51 +0000 Subject: [PATCH 115/287] Finish observer tutorial --- docs/tutorials/fundamentals/observers.md | 84 ++++++++++++------- .../observers/Value-Equality-Dark.svg | 17 ---- .../observers/Value-Equality-Light.svg | 17 ---- 3 files changed, 53 insertions(+), 65 deletions(-) delete mode 100644 docs/tutorials/fundamentals/observers/Value-Equality-Dark.svg delete mode 100644 docs/tutorials/fundamentals/observers/Value-Equality-Light.svg diff --git a/docs/tutorials/fundamentals/observers.md b/docs/tutorials/fundamentals/observers.md index 017aa4ef1..7d319a1e3 100644 --- a/docs/tutorials/fundamentals/observers.md +++ b/docs/tutorials/fundamentals/observers.md @@ -33,18 +33,34 @@ different methods. The first method is `:onChange()`, which runs your code when the state object changes value. -```Lua -observer:onChange(function() - print("The new value is: ", peek(health)) -end) -``` +=== "Luau code" + + ```Lua linenums="7" hl_lines="4-6" + local observer = scope:Observer(health) + + print("...connecting...") + observer:onChange(function() + print("Observed a change to: ", peek(health)) + end) + + print("...setting health to 25...") + health:set(25) + ``` + +=== "Output" + + ``` + ...connecting... + ...setting health to 25... + Observed a change to: 25 + ``` By default, the `:onChange()` connection is disconnected when the observer object is destroyed. However, if you want to disconnect it earlier, the `:onChange()` method returns an optional disconnect function. Calling it will disconnect that specific `:onChange()` handler early. -```Lua +```Lua linenums="7" hl_lines="1 5-7" local disconnect = observer:onChange(function() print("The new value is: ", peek(health)) end) @@ -57,15 +73,28 @@ disconnect() The second method is `:onBind()`. It works identically to `:onChange()`, but it also runs your code right away, which can often be useful. -```Lua -local disconnect = observer:onBind(function() - print("The new value is: ", peek(health)) -end) +=== "Luau code" --- disconnect the above handler after 5 seconds -task.wait(5) -disconnect() -``` + ```Lua linenums="7" hl_lines="4" + local observer = scope:Observer(health) + + print("...connecting...") + observer:onBind(function() + print("Observed a change to: ", peek(health)) + end) + + print("...setting health to 25...") + health:set(25) + ``` + +=== "Output" + + ``` + ...connecting... + Observed a change to: 5 + ...setting health to 25... + Observed a change to: 25 + ``` ----- @@ -76,35 +105,28 @@ notice your observer only runs the first time. === "Luau code" - ```Lua - local thing = Value("Hello") + ```Lua linenums="7" + local observer = scope:Observer(health) - Observer(thing):onChange(function() - print("=> Thing changed to", peek(thing)) + observer:onChange(function() + print("Observed a change to: ", peek(health)) end) - print("Setting thing once...") - thing:set("World") - print("Setting thing twice...") - thing:set("World") - print("Setting thing thrice...") - thing:set("World") + print("...setting health to 25 three times...") + health:set(25) + health:set(25) + health:set(25) ``` === "Output" ``` - Setting thing once... - => Thing changed to World - Setting thing twice... - Setting thing thrice... + ...setting health to 25 three times... + Observed a change to: 25 ``` This is because the `health` object sees that it isn't actually changing value, so it doesn't broadcast any updates. Therefore, our observer doesn't run. -![A diagram showing how value objects only send updates if the new value and previous value aren't equal.](Value-Equality-Dark.svg#only-dark) -![A diagram showing how value objects only send updates if the new value and previous value aren't equal.](Value-Equality-Light.svg#only-light) - This leads to improved performance because your code runs less often. Fusion applies these kinds of optimisations generously throughout your program. \ No newline at end of file diff --git a/docs/tutorials/fundamentals/observers/Value-Equality-Dark.svg b/docs/tutorials/fundamentals/observers/Value-Equality-Dark.svg deleted file mode 100644 index 1d9c4c00f..000000000 --- a/docs/tutorials/fundamentals/observers/Value-Equality-Dark.svg +++ /dev/null @@ -1,17 +0,0 @@ - - - - - - - - - - - - - - - - - diff --git a/docs/tutorials/fundamentals/observers/Value-Equality-Light.svg b/docs/tutorials/fundamentals/observers/Value-Equality-Light.svg deleted file mode 100644 index c17ff4a0a..000000000 --- a/docs/tutorials/fundamentals/observers/Value-Equality-Light.svg +++ /dev/null @@ -1,17 +0,0 @@ - - - - - - - - - - - - - - - - - From e25f955a1740527e4ca611dc1be4a6fb4f3d9cac Mon Sep 17 00:00:00 2001 From: Elttob Date: Thu, 7 Dec 2023 01:06:54 +0000 Subject: [PATCH 116/287] Rename Instances section to Roblox --- mkdocs.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mkdocs.yml b/mkdocs.yml index 3ea38eecf..79c504ee2 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -59,7 +59,7 @@ nav: - Values: tutorials/fundamentals/values.md - Observers: tutorials/fundamentals/observers.md - Computeds: tutorials/fundamentals/computeds.md - - Instances: + - Roblox: - Hydration: tutorials/instances/hydration.md - New Instances: tutorials/instances/new-instances.md - Parenting: tutorials/instances/parenting.md From ac55dcdfa957f2805498515c2f95dfcbc71c350b Mon Sep 17 00:00:00 2001 From: Elttob Date: Thu, 7 Dec 2023 04:05:35 +0000 Subject: [PATCH 117/287] Updated Computed tutorial --- docs/tutorials/fundamentals/computeds.md | 275 ++++++++++------------- docs/tutorials/fundamentals/objects.md | 11 +- 2 files changed, 125 insertions(+), 161 deletions(-) diff --git a/docs/tutorials/fundamentals/computeds.md b/docs/tutorials/fundamentals/computeds.md index e7bfc4da2..2521e4c8e 100644 --- a/docs/tutorials/fundamentals/computeds.md +++ b/docs/tutorials/fundamentals/computeds.md @@ -1,12 +1,14 @@ -Computeds are state objects that can process values from other state objects. +Computeds are state objects that immediately process values from other state +objects. + You pass in a callback to define a calculation. Then, you can use `peek()` to read the result of the calculation at any time. ```Lua -local numCoins = Value(50) -local itemPrice = Value(10) +local numCoins = scope:Value(50) +local itemPrice = scope:Value(10) -local finalCoins = Computed(function(use) +local finalCoins = scope:Computed(function(use, scope) return use(numCoins) - use(itemPrice) end) @@ -21,19 +23,12 @@ print(peek(finalCoins)) --> 10 ## Usage -To use `Computed` in your code, you first need to import it from the Fusion -module, so that you can refer to it by name: - -```Lua linenums="1" hl_lines="2" -local Fusion = require(ReplicatedStorage.Fusion) -local Computed = Fusion.Computed -``` - -To create a new computed object, call the `Computed` function. You need to give -it a callback representing the calculation - for now, we'll add two numbers: +To create a new computed object, call `scope:Computed()` and give it a function +that performs your calculation. -```Lua -local hardMaths = Computed(function(use) +```Lua linenums="5" hl_lines="2-4" +local scope = scoped(Fusion) +local hardMaths = scope:Computed(function(_, _) return 1 + 1 end) ``` @@ -41,20 +36,33 @@ end) The value the callback returns will be stored as the computed's value. You can get the computed's current value using `peek()`: -```Lua +```Lua linenums="5" hl_lines="6" +local scope = scoped(Fusion) +local hardMaths = scope:Computed(function(_, _) + return 1 + 1 +end) + print(peek(hardMaths)) --> 2 ``` -The calculation is only run once by default. If you try and use `peek()` inside -the calculation, your code won't work: +The calculation should be *immediate* - that is, it should never delay. That +means you should not use computed objects when you need to wait for something to +occur (e.g. waiting for a server to respond to a request). -```Lua -local number = Value(2) -local double = Computed(function(use) +----- + +## Using State Objects + +The calculation is only run once by default. If you try to `peek()` at state +objects inside the calculation, your code breaks quickly: + +```Lua linenums="5" +local scope = scoped(Fusion) +local number = scope:Value(2) +local double = scope:Computed(function(_, _) return peek(number) * 2 end) --- The calculation runs once by default. print(peek(number), peek(double)) --> 2 4 -- The calculation won't re-run! Oh no! @@ -62,12 +70,14 @@ number:set(10) print(peek(number), peek(double)) --> 10 4 ``` -This is where the `use` parameter comes in (see line 2 above). If you want your -calculation to re-run when your objects change value, pass the object to `use()`: +Instead, the computed object provides a `use` function as the first argument. +As your logic runs, you can call this function with different state objects. If +any of them changes, then the computed throws everything away and recalculates. -```Lua -local number = Value(2) -local double = Computed(function(use) +```Lua linenums="5" hl_lines="4" +local scope = scoped(Fusion) +local number = scope:Value(2) +local double = scope:Computed(function(use, _) use(number) -- the calculation will re-run when `number` changes value return peek(number) * 2 end) @@ -80,156 +90,101 @@ print(peek(number), peek(double)) --> 10 20 ``` For convenience, `use()` will also read the value, just like `peek()`, so you -can easily replace `peek()` calls with `use()` calls: +can easily replace `peek()` calls with `use()` calls. This keeps your logic +concise, readable and easily copyable. + +```Lua linenums="5" hl_lines="4" +local scope = scoped(Fusion) +local number = scope:Value(2) +local double = scope:Computed(function(use, _) + return use(number) * 2 +end) + +print(peek(number), peek(double)) --> 2 4 + +number:set(10) +print(peek(number), peek(double)) --> 10 20 +``` + +It's recommended you always give the first parameter the name `use`, even if it +already exists. This helps prevent you from using the wrong parameter if you +have multiple computed objects at the same time. ```Lua -local number = Value(2) -local double = Computed(function(use) - return use(number) * 2 -- works identically to before +scope:Computed(function(use, _) + -- ... + scope:Computed(function(use, _) + -- ... + scope:Computed(function(use, _) + return use(number) * 2 + end) + -- ... + end) + -- ... end) ``` ------ +??? question "Help! Using the same name gives me a warning." -## When To Use This + Depending on your setup, Luau might be configured to warn when you use the + same variable name multiple times. -Computeds are more specialist than regular values and observers. They're -designed for a single purpose: they make it easier and more efficient to derive -new values from existing state objects. + In many cases, using the same variable name can be a mistake, but in this + case we actually find it useful. So, to turn off the warning, try adding + `--!nolint LocalShadow` to the top of your file. -Derived values show up in a lot of places throughout UIs. For example, you might -want to insert a death counter into a string. Therefore, the contents of the -string are derived from the death counter: +Keep in mind that Fusion might apply its own optimisations to these calculations. +It might choose to delay the recalculation if the computed isn't actively being +used, or it might never recalculate at all. It shouldn't affect normal +calculations, but if you need to do things like playing sound effects, you +should put those things inside observer objects instead. -![Diagram showing how the message depends on the death counter.](Derived-Value-Dark.svg#only-dark) -![Diagram showing how the message depends on the death counter.](Derived-Value-Light.svg#only-light) +----- -While you can do this with values and observers alone, your code could get messy. +## Inner Scopes -Consider the following code that doesn't use computeds - the intent is to create -a derived value, `finalCoins`, which equals `numCoins - itemPrice` at all times: +Sometimes, you'll need to create things inside computed objects temporarily. In +these cases, you want the temporary things to be destroyed when you're done. -```Lua linenums="1" -local numCoins = Value(50) -local itemPrice = Value(10) +You might try and reuse the scope you already have, but that scope doesn't get +destroyed when the computed object recalculates, so it won't work: -local finalCoins = Value(peek(numCoins) - peek(itemPrice)) -local function updateFinalCoins() - finalCoins:set(peek(numCoins) - peek(itemPrice)) -end -Observer(numCoins):onChange(updateFinalCoins) -Observer(itemPrice):onChange(updateFinalCoins) +```Lua linenums="5" +local scope = scoped(Fusion) +local valueMaker = scope:Computed(function(use, _) + -- this `innerValue` never gets destroyed, ever + local innerValue = scope:Value(5) +end) ``` -There are a few problems with this code currently: +That's why the second argument is a freshly created scope for you to use while +inside the computed object. This freshly created scope is automatically cleaned +up for you when the computed object recalculates. -- It's not immediately clear what's happening at a glance; there's lots of -boilerplate code obscuring what the *intent* of the code is. -- The logic for calculating `finalCoins` is duplicated twice - on lines 4 and 6. -- You have to manage updating the value yourself using observers. This is an -easy place for desynchronisation bugs to slip in. -- Another part of the code base could call `finalCoins:set()` and mess with the -value. - -When written with computeds, the above problems are largely solved: +```Lua linenums="5" hl_lines="2" +local scope = scoped(Fusion) +local valueMaker = scope:Computed(function(use, brandNewScope) + -- now, `innerValue` is destroyed at the correct time + local innerValue = brandNewScope:Value(5) +end) +``` -```Lua linenums="1" -local numCoins = Value(50) -local itemPrice = Value(10) +It can help to give this parameter the same name as the original scope. This +stops you from accidentally using the original scope inside the computed, and +makes your code more easily copyable and movable. -local finalCoins = Computed(function(use) - return use(numCoins) - use(itemPrice) +```Lua linenums="5" +local scope = scoped(Fusion) +local valueMaker = scope:Computed(function(use, scope) + local innerValue = scope:Value(5) end) ``` -- The intent is immediately clear - this is a derived value. -- The logic is only specified once, in one callback. -- The computed updates itself when a state object you `use()` changes value. -- The callback is the only thing that can change the value - there is no `:set()` -method. - -??? warning "A warning about delays in computed callbacks" - - One small caveat of computeds is that you must return the value immediately. - If you need to send a request to the server or perform a long-running - calculation, you shouldn't use computeds. - - The reason for this is consistency between variables. When all computeds run - immediately (i.e. without yielding), all of your variables will behave - consistently: - - ```Lua - local numCoins = Value(50) - local isEnoughCoins = Computed(function(use) - return use(numCoins) > 25 - end) - - local message = Computed(function(use) - if use(isEnoughCoins) then - return use(numCoins) .. " is enough coins." - else - return use(numCoins) .. " is NOT enough coins." - end - end) - - print(peek(message)) --> 50 is enough coins. - numCoins:set(2) - print(peek(message)) --> 2 is NOT enough coins. - ``` - - If a delay is introduced, then inconsistencies and nonsense values could - quickly appear: - - ```Lua hl_lines="3 17" - local numCoins = Value(50) - local isEnoughCoins = Computed(function(use) - task.wait(5) -- Don't do this! This is just for the example - return use(numCoins) > 25 - end) - - local message = Computed(function(use) - if use(isEnoughCoins) then - return use(numCoins) .. " is enough coins." - else - return use(numCoins) .. " is NOT enough coins." - end - end) - - print(peek(message)) --> 50 is enough coins. - numCoins:set(2) - print(peek(message)) --> 2 is enough coins. - ``` - - For this reason, yielding in computed callbacks is disallowed. - - If you have to introduce a delay, for example when invoking a - RemoteFunction, consider using values and observers. - - ```Lua hl_lines="3-10 13-14 24-26" - local numCoins = Value(50) - - local isEnoughCoins = Value(nil) - local function updateIsEnoughCoins() - isEnoughCoins:set(nil) -- indicate that we're calculating the value - task.wait(5) -- this is now ok - isEnoughCoins:set(peek(numCoins) > 25) - end - task.spawn(updateIsEnoughCoins) - Observer(numCoins):onChange(updateIsEnoughCoins) - - local message = Computed(function() - if peek(isEnoughCoins) == nil then - return "Loading..." - elseif peek(isEnoughCoins) then - return peek(numCoins) .. " is enough coins." - else - return peek(numCoins) .. " is NOT enough coins." - end - end) - - print(peek(message)) --> 50 is enough coins. - numCoins:set(2) - print(peek(message)) --> Loading... - task.wait(5) - print(peek(message)) --> 2 is NOT enough coins. - ``` \ No newline at end of file +??? question "Help! Using the same name gives me a warning." + + Depending on your setup, Luau might be configured to warn when you use the + same variable name multiple times. + + In many cases, using the same variable name can be a mistake, but in this + case we actually find it useful. So, to turn off the warning, try adding + `--!nolint LocalShadow` to the top of your file. \ No newline at end of file diff --git a/docs/tutorials/fundamentals/objects.md b/docs/tutorials/fundamentals/objects.md index fe790c116..c7568a1c5 100644 --- a/docs/tutorials/fundamentals/objects.md +++ b/docs/tutorials/fundamentals/objects.md @@ -79,7 +79,16 @@ doCleanup(scope) -- thing1:destroy() ``` -That's all there is to scopes. They are nothing more than arrays of objects. +Scopes passed to `doCleanup` can have any of the following: + +- Objects with `:destroy()` or `:Destroy()` methods to be called +- Functions to be run +- Roblox instances to destroy +- Roblox event connections to disconnect +- Other nested scopes to be cleaned up + +That's all there is to scopes. They are nothing more than arrays of objects +which eventually get passed to a cleanup function. ----- From a36602d5525b8496f9a966a7b13ffa4515328b35 Mon Sep 17 00:00:00 2001 From: Elttob Date: Thu, 7 Dec 2023 16:56:07 +0000 Subject: [PATCH 118/287] Tutorial cleanup --- docs/tutorials/fundamentals/computeds.md | 22 ++--- .../computeds/Derived-Value-Dark.svg | 11 --- .../computeds/Derived-Value-Light.svg | 11 --- docs/tutorials/fundamentals/objects.md | 18 ++--- docs/tutorials/fundamentals/observers.md | 10 +-- docs/tutorials/fundamentals/values.md | 6 +- docs/tutorials/index.md | 81 ++++++++++--------- 7 files changed, 73 insertions(+), 86 deletions(-) delete mode 100644 docs/tutorials/fundamentals/computeds/Derived-Value-Dark.svg delete mode 100644 docs/tutorials/fundamentals/computeds/Derived-Value-Light.svg diff --git a/docs/tutorials/fundamentals/computeds.md b/docs/tutorials/fundamentals/computeds.md index 2521e4c8e..9eaf4d54e 100644 --- a/docs/tutorials/fundamentals/computeds.md +++ b/docs/tutorials/fundamentals/computeds.md @@ -26,7 +26,7 @@ print(peek(finalCoins)) --> 10 To create a new computed object, call `scope:Computed()` and give it a function that performs your calculation. -```Lua linenums="5" hl_lines="2-4" +```Lua linenums="6" hl_lines="2-4" local scope = scoped(Fusion) local hardMaths = scope:Computed(function(_, _) return 1 + 1 @@ -36,7 +36,7 @@ end) The value the callback returns will be stored as the computed's value. You can get the computed's current value using `peek()`: -```Lua linenums="5" hl_lines="6" +```Lua linenums="6" hl_lines="6" local scope = scoped(Fusion) local hardMaths = scope:Computed(function(_, _) return 1 + 1 @@ -56,7 +56,7 @@ occur (e.g. waiting for a server to respond to a request). The calculation is only run once by default. If you try to `peek()` at state objects inside the calculation, your code breaks quickly: -```Lua linenums="5" +```Lua linenums="6" local scope = scoped(Fusion) local number = scope:Value(2) local double = scope:Computed(function(_, _) @@ -74,7 +74,7 @@ Instead, the computed object provides a `use` function as the first argument. As your logic runs, you can call this function with different state objects. If any of them changes, then the computed throws everything away and recalculates. -```Lua linenums="5" hl_lines="4" +```Lua linenums="6" hl_lines="4" local scope = scoped(Fusion) local number = scope:Value(2) local double = scope:Computed(function(use, _) @@ -93,7 +93,7 @@ For convenience, `use()` will also read the value, just like `peek()`, so you can easily replace `peek()` calls with `use()` calls. This keeps your logic concise, readable and easily copyable. -```Lua linenums="5" hl_lines="4" +```Lua linenums="6" hl_lines="4" local scope = scoped(Fusion) local number = scope:Value(2) local double = scope:Computed(function(use, _) @@ -124,7 +124,7 @@ scope:Computed(function(use, _) end) ``` -??? question "Help! Using the same name gives me a warning." +??? warning "Help! Using the same name gives me a warning." Depending on your setup, Luau might be configured to warn when you use the same variable name multiple times. @@ -149,10 +149,10 @@ these cases, you want the temporary things to be destroyed when you're done. You might try and reuse the scope you already have, but that scope doesn't get destroyed when the computed object recalculates, so it won't work: -```Lua linenums="5" +```Lua linenums="6" local scope = scoped(Fusion) local valueMaker = scope:Computed(function(use, _) - -- this `innerValue` never gets destroyed, ever + -- this `innerValue` never gets destroyed by the computed object local innerValue = scope:Value(5) end) ``` @@ -161,7 +161,7 @@ That's why the second argument is a freshly created scope for you to use while inside the computed object. This freshly created scope is automatically cleaned up for you when the computed object recalculates. -```Lua linenums="5" hl_lines="2" +```Lua linenums="6" hl_lines="2" local scope = scoped(Fusion) local valueMaker = scope:Computed(function(use, brandNewScope) -- now, `innerValue` is destroyed at the correct time @@ -173,14 +173,14 @@ It can help to give this parameter the same name as the original scope. This stops you from accidentally using the original scope inside the computed, and makes your code more easily copyable and movable. -```Lua linenums="5" +```Lua linenums="6" local scope = scoped(Fusion) local valueMaker = scope:Computed(function(use, scope) local innerValue = scope:Value(5) end) ``` -??? question "Help! Using the same name gives me a warning." +??? warning "Help! Using the same name gives me a warning." Depending on your setup, Luau might be configured to warn when you use the same variable name multiple times. diff --git a/docs/tutorials/fundamentals/computeds/Derived-Value-Dark.svg b/docs/tutorials/fundamentals/computeds/Derived-Value-Dark.svg deleted file mode 100644 index 747707506..000000000 --- a/docs/tutorials/fundamentals/computeds/Derived-Value-Dark.svg +++ /dev/null @@ -1,11 +0,0 @@ - - - - - - - - - - - diff --git a/docs/tutorials/fundamentals/computeds/Derived-Value-Light.svg b/docs/tutorials/fundamentals/computeds/Derived-Value-Light.svg deleted file mode 100644 index 996f72587..000000000 --- a/docs/tutorials/fundamentals/computeds/Derived-Value-Light.svg +++ /dev/null @@ -1,11 +0,0 @@ - - - - - - - - - - - diff --git a/docs/tutorials/fundamentals/objects.md b/docs/tutorials/fundamentals/objects.md index c7568a1c5..2e60e440b 100644 --- a/docs/tutorials/fundamentals/objects.md +++ b/docs/tutorials/fundamentals/objects.md @@ -17,7 +17,7 @@ a special name: *scopes*. To create a new scope, create an empty array. This array will hold your objects. -```Lua linenums="1" hl_lines="3" +```Lua linenums="2" hl_lines="3" local Fusion = require(ReplicatedStorage.Fusion) local scope = {} @@ -26,7 +26,7 @@ local scope = {} Later, when you create objects, they will ask you to provide a scope as the first argument. -```Lua linenums="1" hl_lines="4" +```Lua linenums="2" hl_lines="4" local Fusion = require(ReplicatedStorage.Fusion) local scope = {} @@ -35,7 +35,7 @@ local thing = Fusion.Value(scope, "i am a thing") When you create an object in Fusion like this, it will add itself to the scope. -```Lua linenums="1" hl_lines="6" +```Lua linenums="2" hl_lines="6" local Fusion = require(ReplicatedStorage.Fusion) local scope = {} @@ -47,7 +47,7 @@ print(scope[1] == thing) --> true You can repeat this as many times as you like. Objects are added in the order they are created. -```Lua linenums="1" hl_lines="4-10" +```Lua linenums="2" hl_lines="4-10" local Fusion = require(ReplicatedStorage.Fusion) local scope = {} @@ -63,7 +63,7 @@ print(scope[3] == thing3) --> true Later on, you can destroy the scope by using the `doCleanup()` function. That will destroy all the objects you added to it. -```Lua linenums="1" hl_lines="2 9" +```Lua linenums="2" hl_lines="2 9" local Fusion = require(ReplicatedStorage.Fusion) local doCleanup = Fusion.doCleanup @@ -100,7 +100,7 @@ more convenient shorthand that makes writing them out easier. The first change is to use `scoped()` to make our scope, instead of creating an array normally. It asks for a table argument, which we'll leave empty for now. -```Lua linenums="1" hl_lines="2 4" +```Lua linenums="2" hl_lines="2 4" local Fusion = require(ReplicatedStorage.Fusion) local doCleanup, scoped = Fusion.doCleanup, Fusion.scoped @@ -118,7 +118,7 @@ However, we can now upgrade it with Fusion's syntax. Whenever we have a function that takes `scope` as the first argument, we can add it to the table argument of `scoped()`. -```Lua linenums="1" hl_lines="4-6" +```Lua linenums="2" hl_lines="4-6" local Fusion = require(ReplicatedStorage.Fusion) local doCleanup, scoped = Fusion.doCleanup, Fusion.scoped @@ -134,7 +134,7 @@ doCleanup(scope) Now, we can use that function as a method on `scope`, like this: -```Lua linenums="1" hl_lines="7-9" +```Lua linenums="2" hl_lines="7-9" local Fusion = require(ReplicatedStorage.Fusion) local doCleanup, scoped = Fusion.doCleanup, Fusion.scoped @@ -156,7 +156,7 @@ As a nice shorthand, you can pass in `Fusion` directly to `scoped()` rather than listing out all the functions you want manually. Because `Fusion` is already a table full of functions for creating things, it works too. -```Lua linenums="1" hl_lines="4" +```Lua linenums="2" hl_lines="4" local Fusion = require(ReplicatedStorage.Fusion) local doCleanup, scoped = Fusion.doCleanup, Fusion.scoped diff --git a/docs/tutorials/fundamentals/observers.md b/docs/tutorials/fundamentals/observers.md index 7d319a1e3..b86f0289d 100644 --- a/docs/tutorials/fundamentals/observers.md +++ b/docs/tutorials/fundamentals/observers.md @@ -20,7 +20,7 @@ disconnect() To create a new observer object, call `scope:Observer()` and give it a state object you want to detect changes on. -```Lua linenums="5" hl_lines="3" +```Lua linenums="6" hl_lines="3" local scope = scoped(Fusion) local health = scope:Value(5) local observer = scope:Observer(health) @@ -35,7 +35,7 @@ changes value. === "Luau code" - ```Lua linenums="7" hl_lines="4-6" + ```Lua linenums="8" hl_lines="4-6" local observer = scope:Observer(health) print("...connecting...") @@ -60,7 +60,7 @@ object is destroyed. However, if you want to disconnect it earlier, the `:onChange()` method returns an optional disconnect function. Calling it will disconnect that specific `:onChange()` handler early. -```Lua linenums="7" hl_lines="1 5-7" +```Lua linenums="8" hl_lines="1 5-7" local disconnect = observer:onChange(function() print("The new value is: ", peek(health)) end) @@ -75,7 +75,7 @@ also runs your code right away, which can often be useful. === "Luau code" - ```Lua linenums="7" hl_lines="4" + ```Lua linenums="8" hl_lines="4" local observer = scope:Observer(health) print("...connecting...") @@ -105,7 +105,7 @@ notice your observer only runs the first time. === "Luau code" - ```Lua linenums="7" + ```Lua linenums="8" local observer = scope:Observer(health) observer:onChange(function() diff --git a/docs/tutorials/fundamentals/values.md b/docs/tutorials/fundamentals/values.md index 7c2177dc8..1b3b1323a 100644 --- a/docs/tutorials/fundamentals/values.md +++ b/docs/tutorials/fundamentals/values.md @@ -19,7 +19,7 @@ print(peek(health)) --> 25 To create a new value object, call `scope:Value()` and give it a value you want to store. -```Lua linenums="1" hl_lines="5" +```Lua linenums="2" hl_lines="5" local Fusion = require(ReplicatedStorage.Fusion) local doCleanup, scoped = Fusion.doCleanup, Fusion.scoped @@ -31,7 +31,7 @@ Fusion provides a global `peek()` function. It will read the value of whatever you give it. You'll use `peek()` to read the value of lots of things; for now, it's useful for printing `health` back out. -```Lua linenums="1" hl_lines="3 7" +```Lua linenums="2" hl_lines="3 7" local Fusion = require(ReplicatedStorage.Fusion) local doCleanup, scoped = Fusion.doCleanup, Fusion.scoped local peek = Fusion.peek @@ -44,7 +44,7 @@ print(peek(health)) --> 5 You can change the value using the `:set()` method. Unlike `peek()`, this is specific to value objects, so it's done on the object itself. -```Lua linenums="5" hl_lines="5-6" +```Lua linenums="6" hl_lines="5-6" local scope = scoped(Fusion) local health = scope:Value(5) print(peek(health)) --> 5 diff --git a/docs/tutorials/index.md b/docs/tutorials/index.md index 49df9e6ff..2409993cd 100644 --- a/docs/tutorials/index.md +++ b/docs/tutorials/index.md @@ -28,25 +28,24 @@ things with Fusion, even if you're a complete newcomer to the library. These tutorials assume: -- You're comfortable with Roblox and the Luau scripting language. - - These tutorials aren't an introduction to scripting! If you'd like to - learn, check out the [Roblox DevHub](https://developer.roblox.com/). -- You're familiar with how UI works on Roblox. - - You don't have to be a designer - knowing about UI instances, events - and data types like `UDim2` and `Color3` will be good enough. +- That you're comfortable with the Luau scripting language. + - These tutorials aren't an introduction to Luau! If you'd like to learn, + check out the [Roblox documentation](https://create.roblox.com/docs). +- That - if you're using Roblox features - you're familiar with how Roblox works. + - You don't have to be an expert! Knowing about basic instances, events + and data types will be good enough. Of course, based on your existing knowledge, you may find some tutorials easier or harder. Fusion's built to be easy to learn, but it may still take a bit of -time to absorb some concepts, so don't be discouraged :slightly_smiling_face: +time to absorb some concepts, so don't be discouraged! ----- -## Installing Fusion +## Install Fusion inside Roblox Studio -Fusion is distributed as a single module script. Before starting, you'll need -to add this module script to your game. Here's how: - -### If you edit scripts in Roblox Studio... +If you are creating Luau experiences in Roblox Studio, then you can use a +version of Fusion packaged up as a Roblox model. Before starting, you'll need +to add this model to your game. Head over to [Fusion's 'Releases' page](https://github.com/Elttob/Fusion/releases). Click the 'Assets' dropdown to view the downloadable files: @@ -54,8 +53,7 @@ Click the 'Assets' dropdown to view the downloadable files: ![Picture of Fusion's GitHub Releases page, with the Assets dropdown highlighted.](index/Github-Releases-Guide-1-Light.png#only-light) ![Picture of Fusion's GitHub Releases page, with the Assets dropdown highlighted.](index/Github-Releases-Guide-1-Dark.png#only-dark) -Now, click on the `Fusion.rbxm` file to download it. This contains the module as -a Roblox model: +Now, click on the `Fusion.rbxm` file to download it. This model contains Fusion. ![The Assets dropdown opened to reveal downloads, with Fusion.rbxm highlighted.](index/Github-Releases-Guide-2-Light.png#only-light) ![The Assets dropdown opened to reveal downloads, with Fusion.rbxm highlighted.](index/Github-Releases-Guide-2-Dark.png#only-dark) @@ -68,29 +66,10 @@ Right-click on `ReplicatedStorage`, and select 'Insert from File': ![ReplicatedStorage is right-clicked, showing a context menu of items. Insert from File is highlighted.](index/Github-Releases-Guide-3-Light.png#only-light) ![ReplicatedStorage is right-clicked, showing a context menu of items. Insert from File is highlighted.](index/Github-Releases-Guide-3-Dark.png#only-dark) -Select the `Fusion.rbxm` file you just downloaded. You should see the module -script appear in `ReplicatedStorage` - you're ready to go! - -### If you edit scripts externally... (advanced) - -If you use an external editor to write scripts, and synchronise them into Roblox -using a plugin, you can use these alternate steps instead: - -??? example "Steps (click to expand)" - 1. Head over to [Fusion's 'Releases' page](https://github.com/Elttob/Fusion/releases). - 2. Under 'Assets', download `Source code (zip)`. Inside is a copy - of the Fusion GitHub repository. - 3. Inside the zip, copy the `src` folder - it may be inside another folder. - 4. Paste `src` into your local project, preferably in your `shared` folder - if you have one. - 5. Rename the folder from `src` to `Fusion`. - - Once everything is set up, you should see Fusion appear in Studio when you - next synchronise your project. +Select the `Fusion.rbxm` file you just downloaded. You should see a 'Fusion' +module script appear in `ReplicatedStorage`! ------ - -## Setting Up A Test Script +### Setting Up A Test Script Now that you've installed Fusion, you can set up a local script for testing. Here's how: @@ -121,6 +100,36 @@ local Fusion = require(ReplicatedStorage.Fusion) ----- +## Install Fusion to your filesystem + +If you're using pure Luau, or if you're synchronising into Roblox Studio from +the filesystem or an external editor, you can use these alternate steps instead: + +??? example "Steps (click to expand)" + 1. Head over to [Fusion's 'Releases' page](https://github.com/Elttob/Fusion/releases). + 2. Under 'Assets', download `Source code (zip)`. Inside is a copy + of the Fusion GitHub repository. + 3. Inside the zip, copy the `src` folder - it may be inside another folder. + 4. Paste the `src` folder into your local project, wherever you keep your + libraries (e.g. inside a `lib` or `shared` folder) + 5. Rename the pasted folder from `src` to `Fusion`. + + Once everything is set up, you should be able to `require()` Fusion in one + of the following ways: + + ```Lua + -- Rojo + local Fusion = require(ReplicatedStorage.Fusion) + + -- darklua + local Fusion = require("../shared/Fusion") + + -- vanilla Luau + local Fusion = require("../shared/Fusion/init.lua") + ``` + +----- + ## Where To Get Help Fusion is built to be easy to use, and we want these tutorials to be as useful From 384bc027eac370ecc5e463397421761f8dc48109 Mon Sep 17 00:00:00 2001 From: Elttob Date: Thu, 7 Dec 2023 17:30:03 +0000 Subject: [PATCH 119/287] Foreword changes --- docs/tutorials/index.md | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/docs/tutorials/index.md b/docs/tutorials/index.md index 2409993cd..ecc404635 100644 --- a/docs/tutorials/index.md +++ b/docs/tutorials/index.md @@ -1,15 +1,18 @@ Welcome to the Fusion tutorial section! Here, you'll learn how to build great things with Fusion, even if you're a complete newcomer to the library. +You'll not only learn how Fusion's features work, but you'll also be presented +with wisdom from those who've worked with some of the largest Fusion codebases +today. + !!! caution "But first, something important..." ** - Do not use Fusion for real-world production work unless you're 100,000% - willing and able to withstand large breaking changes. + Do not use Fusion for real-world work unless you're 100,000% able to deal + with a changing environment! ** - Fusion is in beta right now! You *will* encounter: + Fusion is not version 1.0 yet! You *will* encounter: - - bugs in core features - updates that completely remove existing features - changes in behaviour between versions - changing advice on coding conventions and how to structure your project @@ -18,9 +21,8 @@ things with Fusion, even if you're a complete newcomer to the library. we can quickly abandon counterproductive ideas and features, and discover much more solid foundations to build upon. - Don't be discouraged from Fusion though; feel free to follow along with our - development and try using the library in your own time! More stable, - long-term Fusion versions will be available once Fusion exits beta testing. + Don't be discouraged from Fusion though! Once features have settled down, + releases will be more stable. ----- From 728207868ebea2073031222d90af2e310ab6d5c8 Mon Sep 17 00:00:00 2001 From: Elttob Date: Thu, 7 Dec 2023 17:30:10 +0000 Subject: [PATCH 120/287] Code clarity --- docs/tutorials/fundamentals/computeds.md | 120 ++++++++++++++++++----- docs/tutorials/fundamentals/objects.md | 2 +- docs/tutorials/fundamentals/observers.md | 2 +- 3 files changed, 97 insertions(+), 27 deletions(-) diff --git a/docs/tutorials/fundamentals/computeds.md b/docs/tutorials/fundamentals/computeds.md index 9eaf4d54e..2064cae09 100644 --- a/docs/tutorials/fundamentals/computeds.md +++ b/docs/tutorials/fundamentals/computeds.md @@ -56,7 +56,7 @@ occur (e.g. waiting for a server to respond to a request). The calculation is only run once by default. If you try to `peek()` at state objects inside the calculation, your code breaks quickly: -```Lua linenums="6" +```Lua linenums="6" hl_lines="10-11" local scope = scoped(Fusion) local number = scope:Value(2) local double = scope:Computed(function(_, _) @@ -133,11 +133,9 @@ end) case we actually find it useful. So, to turn off the warning, try adding `--!nolint LocalShadow` to the top of your file. -Keep in mind that Fusion might apply its own optimisations to these calculations. -It might choose to delay the recalculation if the computed isn't actively being -used, or it might never recalculate at all. It shouldn't affect normal -calculations, but if you need to do things like playing sound effects, you -should put those things inside observer objects instead. +Keep in mind that Fusion applies optimisations; recalculations might be +postponed or cancelled if the value of the computed isn't being used. This is +why you should not use computed objects for things like playing sound effects. ----- @@ -146,37 +144,109 @@ should put those things inside observer objects instead. Sometimes, you'll need to create things inside computed objects temporarily. In these cases, you want the temporary things to be destroyed when you're done. -You might try and reuse the scope you already have, but that scope doesn't get -destroyed when the computed object recalculates, so it won't work: +You might try and reuse the scope you already have, to construct objects and +add cleanup tasks. -```Lua linenums="6" -local scope = scoped(Fusion) -local valueMaker = scope:Computed(function(use, _) - -- this `innerValue` never gets destroyed by the computed object - local innerValue = scope:Value(5) -end) -``` +=== "Luau code" + + ```Lua linenums="6" hl_lines="7" + local scope = scoped(Fusion) + local number = scope:Value(5) + local double = scope:Computed(function(use, _) + local current = use(number) + print("Creating", current) + -- suppose we want to run some cleanup code for stuff in here + table.insert(scope, function() + print("Destroying", current) + end) + return current * 2 + end) + + print("...setting to 25...") + number:set(25) + print("...setting to 2...") + number:set(2) + print("...cleaning up...") + doCleanup(scope) + ``` + +=== "Output" + + ```Lua + Creating 5 + ...setting to 25... + Creating 25 + ...setting to 2... + Creating 2 + ...cleaning up... + Destroying 2 + Destroying 25 + Destroying 5 + ``` + +However, this doesn't work the way you'd want it to. All of the tasks pile up at +the end of the program, so they aren't being destroyed quickly enough. That's why the second argument is a freshly created scope for you to use while inside the computed object. This freshly created scope is automatically cleaned up for you when the computed object recalculates. -```Lua linenums="6" hl_lines="2" -local scope = scoped(Fusion) -local valueMaker = scope:Computed(function(use, brandNewScope) - -- now, `innerValue` is destroyed at the correct time - local innerValue = brandNewScope:Value(5) -end) -``` +=== "Luau code" + + ```Lua linenums="6" hl_lines="3 6" + local scope = scoped(Fusion) + local number = scope:Value(5) + local double = scope:Computed(function(use, myBrandNewScope) + local current = use(number) + print("Creating", current) + table.insert(myBrandNewScope, function() + print("Destroying", current) + end) + return current * 2 + end) + + print("...setting to 25...") + number:set(25) + print("...setting to 2...") + number:set(2) + print("...cleaning up...") + doCleanup(scope) + ``` + +=== "Output" + + ```Lua + Creating 5 + ...setting to 25... + Creating 25 + Destroying 5 + ...setting to 2... + Creating 2 + Destroying 25 + ...cleaning up... + Destroying 2 + ``` + +When using this new 'inner' scope, the tasks no longer pile up at the end of the +program. Instead, they're cleaned up as soon as possible, when the computed +object throws away the old calculation. It can help to give this parameter the same name as the original scope. This stops you from accidentally using the original scope inside the computed, and makes your code more easily copyable and movable. -```Lua linenums="6" +```Lua local scope = scoped(Fusion) -local valueMaker = scope:Computed(function(use, scope) - local innerValue = scope:Value(5) +scope:Computed(function(use, scope) + -- ... + scope:Computed(function(use, scope) + -- ... + scope:Computed(function(use, scope) + local innerValue = scope:Value(5) + end) + -- ... + end) + -- ... end) ``` diff --git a/docs/tutorials/fundamentals/objects.md b/docs/tutorials/fundamentals/objects.md index 2e60e440b..9120c16ab 100644 --- a/docs/tutorials/fundamentals/objects.md +++ b/docs/tutorials/fundamentals/objects.md @@ -47,7 +47,7 @@ print(scope[1] == thing) --> true You can repeat this as many times as you like. Objects are added in the order they are created. -```Lua linenums="2" hl_lines="4-10" +```Lua linenums="2" local Fusion = require(ReplicatedStorage.Fusion) local scope = {} diff --git a/docs/tutorials/fundamentals/observers.md b/docs/tutorials/fundamentals/observers.md index b86f0289d..bf2b96868 100644 --- a/docs/tutorials/fundamentals/observers.md +++ b/docs/tutorials/fundamentals/observers.md @@ -60,7 +60,7 @@ object is destroyed. However, if you want to disconnect it earlier, the `:onChange()` method returns an optional disconnect function. Calling it will disconnect that specific `:onChange()` handler early. -```Lua linenums="8" hl_lines="1 5-7" +```Lua linenums="8" hl_lines="1 7" local disconnect = observer:onChange(function() print("The new value is: ", peek(health)) end) From 66c726c9437e6514213b8597525a1f73b321adcb Mon Sep 17 00:00:00 2001 From: Elttob Date: Thu, 7 Dec 2023 17:43:11 +0000 Subject: [PATCH 121/287] Computed tutorial parameter clarification in intro --- docs/tutorials/fundamentals/computeds.md | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/docs/tutorials/fundamentals/computeds.md b/docs/tutorials/fundamentals/computeds.md index 2064cae09..12914a81d 100644 --- a/docs/tutorials/fundamentals/computeds.md +++ b/docs/tutorials/fundamentals/computeds.md @@ -24,7 +24,8 @@ print(peek(finalCoins)) --> 10 ## Usage To create a new computed object, call `scope:Computed()` and give it a function -that performs your calculation. +that performs your calculation. It takes two parameters which will be explained +later; for the first part of this tutorial, they'll be left unnamed. ```Lua linenums="6" hl_lines="2-4" local scope = scoped(Fusion) @@ -133,8 +134,8 @@ end) case we actually find it useful. So, to turn off the warning, try adding `--!nolint LocalShadow` to the top of your file. -Keep in mind that Fusion applies optimisations; recalculations might be -postponed or cancelled if the value of the computed isn't being used. This is +Keep in mind that Fusion sometimes applies optimisations; recalculations might +be postponed or cancelled if the value of the computed isn't being used. This is why you should not use computed objects for things like playing sound effects. ----- From 2c6946f60140b5706c6d2ec7a3fd73ebc04bbf37 Mon Sep 17 00:00:00 2001 From: Elttob Date: Thu, 7 Dec 2023 17:45:00 +0000 Subject: [PATCH 122/287] Final words on Computed tutorial --- docs/tutorials/fundamentals/computeds.md | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/docs/tutorials/fundamentals/computeds.md b/docs/tutorials/fundamentals/computeds.md index 12914a81d..8e582f136 100644 --- a/docs/tutorials/fundamentals/computeds.md +++ b/docs/tutorials/fundamentals/computeds.md @@ -258,4 +258,7 @@ end) In many cases, using the same variable name can be a mistake, but in this case we actually find it useful. So, to turn off the warning, try adding - `--!nolint LocalShadow` to the top of your file. \ No newline at end of file + `--!nolint LocalShadow` to the top of your file. + +Once you understand computeds, as well as the previously discussed scopes, +values and observers, you're well positioned to explore the rest of Fusion. \ No newline at end of file From 890336cb2acc98769529f41ce0cc7e4d7ca16527 Mon Sep 17 00:00:00 2001 From: Elttob Date: Thu, 7 Dec 2023 17:49:09 +0000 Subject: [PATCH 123/287] Updated spring tutorial to use scopes --- docs/tutorials/animation/springs.md | 23 +++++++++++++++-------- 1 file changed, 15 insertions(+), 8 deletions(-) diff --git a/docs/tutorials/animation/springs.md b/docs/tutorials/animation/springs.md index e6422008c..acec77f49 100644 --- a/docs/tutorials/animation/springs.md +++ b/docs/tutorials/animation/springs.md @@ -17,12 +17,12 @@ local Fusion = require(ReplicatedStorage.Fusion) local Spring = Fusion.Spring ``` -To create a new spring object, call the `Spring` function and pass it a state +To create a new spring object, call `scope:Spring()` and pass it a state object to move towards: ```Lua -local goal = Value(0) -local animated = Spring(target) +local goal = scope:Value(0) +local animated = scope:Spring(target) ``` The spring will smoothly follow the 'goal' state object over time. As with other @@ -36,10 +36,17 @@ To configure how the spring moves, you can provide a speed and damping ratio to use. Both are optional, and both can be state objects if desired: ```Lua -local goal = Value(0) +local goal = scope:Value(0) local speed = 25 -local damping = Value(0.5) -local animated = Spring(target, speed, damping) +local damping = scope:Value(0.5) +local animated = scope:Spring(target, speed, damping) +``` + +You can also set the position and velocity of the spring at any time. + +```Lua +animated:setPosition(5) -- teleport the spring to 5 +animated:setVelocity(2) -- from here, move 2 units/second ``` You can use many different kinds of values with springs, not just numbers. @@ -47,8 +54,8 @@ Vectors, CFrames, Color3s, UDim2s and other number-based types are supported; each number inside the type is animated individually. ```Lua -local goalPosition = Value(UDim2.new(0.5, 0, 0, 0)) -local animated = Spring(target, 25, 0.5) +local goalPosition = scope:Value(UDim2.new(0.5, 0, 0, 0)) +local animated = scope:Spring(target, 25, 0.5) ``` ----- From 3cbb7fc63fd9dbab2711e5c1e54eb4d1f83f620f Mon Sep 17 00:00:00 2001 From: Elttob Date: Thu, 7 Dec 2023 20:48:16 +0000 Subject: [PATCH 124/287] Clarity tweaks for COmputed tutorial --- docs/tutorials/fundamentals/computeds.md | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/docs/tutorials/fundamentals/computeds.md b/docs/tutorials/fundamentals/computeds.md index 8e582f136..a415ce15f 100644 --- a/docs/tutorials/fundamentals/computeds.md +++ b/docs/tutorials/fundamentals/computeds.md @@ -173,7 +173,7 @@ add cleanup tasks. === "Output" - ```Lua + ``` Creating 5 ...setting to 25... Creating 25 @@ -186,11 +186,12 @@ add cleanup tasks. ``` However, this doesn't work the way you'd want it to. All of the tasks pile up at -the end of the program, so they aren't being destroyed quickly enough. +the end of the program, instead of being thrown away with the rest of the +calculation. -That's why the second argument is a freshly created scope for you to use while -inside the computed object. This freshly created scope is automatically cleaned -up for you when the computed object recalculates. +That's why the second argument is a different scope for you to use while inside +the computed object. This scope argument is automatically cleaned up for you +when the computed object recalculates. === "Luau code" @@ -216,7 +217,7 @@ up for you when the computed object recalculates. === "Output" - ```Lua + ``` Creating 5 ...setting to 25... Creating 25 From c8856821f33ce560d176eb01732dfca9ffe7346e Mon Sep 17 00:00:00 2001 From: Elttob Date: Sat, 9 Dec 2023 00:07:13 +0000 Subject: [PATCH 125/287] Update tween tutorial to use scopes --- docs/tutorials/animation/springs.md | 17 +++++------------ docs/tutorials/animation/tweens.md | 29 +++++++++++------------------ 2 files changed, 16 insertions(+), 30 deletions(-) diff --git a/docs/tutorials/animation/springs.md b/docs/tutorials/animation/springs.md index acec77f49..34665f1df 100644 --- a/docs/tutorials/animation/springs.md +++ b/docs/tutorials/animation/springs.md @@ -9,24 +9,17 @@ movement naturally without abrupt changes in direction. ## Usage -To use `Spring` in your code, you first need to import it from the Fusion -module, so that you can refer to it by name: - -```Lua linenums="1" hl_lines="2" -local Fusion = require(ReplicatedStorage.Fusion) -local Spring = Fusion.Spring -``` - -To create a new spring object, call `scope:Spring()` and pass it a state -object to move towards: +To create a new spring object, call `scope:Spring()` and pass it a state object +to move towards: ```Lua local goal = scope:Value(0) local animated = scope:Spring(target) ``` -The spring will smoothly follow the 'goal' state object over time. As with other -state objects, you can `peek()` at its value at any time: +The spring will smoothly follow the 'goal' state object over time. + +As with other state objects, you can `peek()` at its value at any time: ```Lua print(peek(animated)) --> 0.26425... diff --git a/docs/tutorials/animation/tweens.md b/docs/tutorials/animation/tweens.md index d7a430423..b9c0a1e56 100644 --- a/docs/tutorials/animation/tweens.md +++ b/docs/tutorials/animation/tweens.md @@ -8,24 +8,17 @@ This can be used for basic, predictable animations. ## Usage -To use `Tween` in your code, you first need to import it from the Fusion -module, so that you can refer to it by name: - -```Lua linenums="1" hl_lines="2" -local Fusion = require(ReplicatedStorage.Fusion) -local Tween = Fusion.Tween -``` - -To create a new tween object, call the `Tween` function and pass it a state -object to move towards: +To create a new tween object, call `scope:Tween()` and pass it a state object to +move towards: ```Lua -local goal = Value(0) -local animated = Tween(target) +local goal = scope:Value(0) +local animated = scope:Tween(target) ``` -The tween will smoothly follow the 'goal' state object over time. As with other -state objects, you can `peek()` at its value at any time: +The tween will smoothly follow the 'goal' state object over time. + +As with other state objects, you can `peek()` at its value at any time: ```Lua print(peek(animated)) --> 0.26425... @@ -36,9 +29,9 @@ shape of the animation curve. It's optional, and it can be a state object if desired: ```Lua -local goal = Value(0) +local goal = scope:Value(0) local style = TweenInfo.new(0.5, Enum.EasingStyle.Quad) -local animated = Tween(target, style) +local animated = scope:Tween(target, style) ``` You can use many different kinds of values with tweens, not just numbers. @@ -46,8 +39,8 @@ Vectors, CFrames, Color3s, UDim2s and other number-based types are supported; each number inside the type is animated individually. ```Lua -local goalPosition = Value(UDim2.new(0.5, 0, 0, 0)) -local animated = Tween(target, TweenInfo.new(0.5, Enum.EasingStyle.Quad)) +local goalPosition = scope:Value(UDim2.new(0.5, 0, 0, 0)) +local animated = scope:Tween(target, TweenInfo.new(0.5, Enum.EasingStyle.Quad)) ``` ----- From 4200da2a9645c6febcce2d762f6aa5180440e4f2 Mon Sep 17 00:00:00 2001 From: Elttob Date: Sat, 9 Dec 2023 00:22:13 +0000 Subject: [PATCH 126/287] Component callback tutorial updated to use scopes --- docs/tutorials/components/callbacks.md | 42 +++++++++++++++++++++----- 1 file changed, 34 insertions(+), 8 deletions(-) diff --git a/docs/tutorials/components/callbacks.md b/docs/tutorials/components/callbacks.md index 15f32de7f..9ea520772 100644 --- a/docs/tutorials/components/callbacks.md +++ b/docs/tutorials/components/callbacks.md @@ -70,8 +70,16 @@ Components can use callbacks the same way. Consider this button component; when the button is clicked, the button needs to run some external code: ```Lua -local function Button(props) - return New "TextButton" { +local function Button( + props: { + Scope: Fusion.Scope, + Position: Fusion.CanBeState?, + Size: Fusion.CanBeState?, + Text: Fusion.CanBeState? + } +) + local scope = props.Scope + return scope:New "TextButton" { BackgroundColor3 = Color3.new(0.25, 0.5, 1), Position = props.Position, Size = props.Size, @@ -98,9 +106,18 @@ local button = Button { Assuming that callback is passed in, the callback can be passed directly into `[OnEvent]`, because `[OnEvent]` accepts functions: -```Lua hl_lines="10" -local function Button(props) - return New "TextButton" { +```Lua hl_lines="7 19" +local function Button( + props: { + Scope: Fusion.Scope, + Position: Fusion.CanBeState?, + Size: Fusion.CanBeState?, + Text: Fusion.CanBeState?, + OnClick: (() -> ())? + } +) + local scope = props.Scope + return scope:New "TextButton" { BackgroundColor3 = Color3.new(0.25, 0.5, 1), Position = props.Position, Size = props.Size, @@ -116,9 +133,18 @@ end Alternatively, we can call `props.OnClick` manually, which is useful if you want to do your own processing first: -```Lua hl_lines="10-15" -local function Button(props) - return New "TextButton" { +```Lua hl_lines="19-24" +local function Button( + props: { + Scope: Fusion.Scope, + Position: Fusion.CanBeState?, + Size: Fusion.CanBeState?, + Text: Fusion.CanBeState?, + OnClick: (() -> ())? + } +) + local scope = props.Scope + return scope:New "TextButton" { BackgroundColor3 = Color3.new(0.25, 0.5, 1), Position = props.Position, Size = props.Size, From 55347982d45c097716fd65d4fefc5d31426ac2ff Mon Sep 17 00:00:00 2001 From: Elttob Date: Sat, 9 Dec 2023 00:23:15 +0000 Subject: [PATCH 127/287] Add stricter typing for component callback tutorial --- docs/tutorials/components/callbacks.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/tutorials/components/callbacks.md b/docs/tutorials/components/callbacks.md index 9ea520772..96d10ac19 100644 --- a/docs/tutorials/components/callbacks.md +++ b/docs/tutorials/components/callbacks.md @@ -41,7 +41,9 @@ In this example, the `fiveTimes` function calls a callback five times: === "Luau code" ```Lua - local function fiveTimes(callback) + local function fiveTimes( + callback: (number) -> () + ) for x=1, 5 do callback(x) end From 901eaf5618a5b1813732026ff979c876ae9db7be Mon Sep 17 00:00:00 2001 From: Elttob Date: Sat, 9 Dec 2023 00:24:07 +0000 Subject: [PATCH 128/287] Fix correctness issue in component callback tutorial --- docs/tutorials/components/callbacks.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/docs/tutorials/components/callbacks.md b/docs/tutorials/components/callbacks.md index 96d10ac19..df144b887 100644 --- a/docs/tutorials/components/callbacks.md +++ b/docs/tutorials/components/callbacks.md @@ -135,14 +135,15 @@ end Alternatively, we can call `props.OnClick` manually, which is useful if you want to do your own processing first: -```Lua hl_lines="19-24" +```Lua hl_lines="7-8 20-25" local function Button( props: { Scope: Fusion.Scope, Position: Fusion.CanBeState?, Size: Fusion.CanBeState?, Text: Fusion.CanBeState?, - OnClick: (() -> ())? + Disabled: Fusion.CanBeState?, + OnClick: () -> () } ) local scope = props.Scope From 1b7c8616eff971ee662b2cfb7883590815ffe902 Mon Sep 17 00:00:00 2001 From: Elttob Date: Sat, 9 Dec 2023 00:27:54 +0000 Subject: [PATCH 129/287] Clarifying text --- docs/tutorials/components/callbacks.md | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/docs/tutorials/components/callbacks.md b/docs/tutorials/components/callbacks.md index df144b887..c5ce8379f 100644 --- a/docs/tutorials/components/callbacks.md +++ b/docs/tutorials/components/callbacks.md @@ -71,7 +71,7 @@ In this example, the `fiveTimes` function calls a callback five times: Components can use callbacks the same way. Consider this button component; when the button is clicked, the button needs to run some external code: -```Lua +```Lua hl_lines="18" local function Button( props: { Scope: Fusion.Scope, @@ -106,7 +106,8 @@ local button = Button { ``` Assuming that callback is passed in, the callback can be passed directly into -`[OnEvent]`, because `[OnEvent]` accepts functions: +`[OnEvent]`, because `[OnEvent]` accepts functions. It can even be optional - +Luau won't add the key to the table if the value is `nil`. ```Lua hl_lines="7 19" local function Button( @@ -135,7 +136,7 @@ end Alternatively, we can call `props.OnClick` manually, which is useful if you want to do your own processing first: -```Lua hl_lines="7-8 20-25" +```Lua hl_lines="7 20-24" local function Button( props: { Scope: Fusion.Scope, @@ -143,7 +144,7 @@ local function Button( Size: Fusion.CanBeState?, Text: Fusion.CanBeState?, Disabled: Fusion.CanBeState?, - OnClick: () -> () + OnClick: (() -> ())? } ) local scope = props.Scope @@ -156,8 +157,7 @@ local function Button( TextColor3 = Color3.new(1, 1, 1), [OnEvent "Activated"] = function() - -- don't send clicks if the button is disabled - if not peek(props.Disabled) then + if props.OnClick ~= nil and not peek(props.Disabled) then props.OnClick() end end From 63aa50e1314194f344e2fb7b7607ee4f597cf113 Mon Sep 17 00:00:00 2001 From: Elttob Date: Sun, 10 Dec 2023 08:13:19 +0000 Subject: [PATCH 130/287] Conciseness pass for Objects tutorial --- docs/tutorials/fundamentals/objects.md | 73 ++++++++++++-------------- 1 file changed, 35 insertions(+), 38 deletions(-) diff --git a/docs/tutorials/fundamentals/objects.md b/docs/tutorials/fundamentals/objects.md index 9120c16ab..853772f55 100644 --- a/docs/tutorials/fundamentals/objects.md +++ b/docs/tutorials/fundamentals/objects.md @@ -1,21 +1,20 @@ -In Fusion, you will create a lot of objects. These objects will need to be -destroyed when you are done with them. +In Fusion, you create a lot of objects. These objects need to be destroyed when +you're done with them. -Fusion introduces a few coding conventions that makes working with a large -quantity of objects a lot easier. You're learning these coding conventions now, -before you learn about Fusion's features, because it's used throughout Fusion -and is very important. +Fusion has some coding conventions to make large quantities of objects easier to +manage. ----- ## Scopes -When you create a bunch of objects at the same time, you will often want to -`:destroy()` them later at the same time, too. To make this easier, you can add -your objects to an array. Arrays that group together objects like this are given -a special name: *scopes*. +When you create many objects at once, you often want to `:destroy()` them +together later. -To create a new scope, create an empty array. This array will hold your objects. +To make this easier, some people add their objects to an array. Arrays that +group together objects like this are given a special name: *scopes*. + +To create a new scope, create an empty array. ```Lua linenums="2" hl_lines="3" local Fusion = require(ReplicatedStorage.Fusion) @@ -23,8 +22,7 @@ local Fusion = require(ReplicatedStorage.Fusion) local scope = {} ``` -Later, when you create objects, they will ask you to provide a scope as the -first argument. +Later, when you create objects, they will ask for a scope as the first argument. ```Lua linenums="2" hl_lines="4" local Fusion = require(ReplicatedStorage.Fusion) @@ -33,7 +31,7 @@ local scope = {} local thing = Fusion.Value(scope, "i am a thing") ``` -When you create an object in Fusion like this, it will add itself to the scope. +That object will add itself to the scope. ```Lua linenums="2" hl_lines="6" local Fusion = require(ReplicatedStorage.Fusion) @@ -44,8 +42,7 @@ local thing = Fusion.Value(scope, "i am a thing") print(scope[1] == thing) --> true ``` -You can repeat this as many times as you like. Objects are added in the order -they are created. +Repeat as many times as you like. Objects appear in order of creation. ```Lua linenums="2" local Fusion = require(ReplicatedStorage.Fusion) @@ -60,8 +57,8 @@ print(scope[2] == thing2) --> true print(scope[3] == thing3) --> true ``` -Later on, you can destroy the scope by using the `doCleanup()` function. That -will destroy all the objects you added to it. +Later, destroy the scope by using the `doCleanup()` function. The contents are +destroyed in reverse order. ```Lua linenums="2" hl_lines="2 9" local Fusion = require(ReplicatedStorage.Fusion) @@ -79,7 +76,7 @@ doCleanup(scope) -- thing1:destroy() ``` -Scopes passed to `doCleanup` can have any of the following: +Scopes passed to `doCleanup` can contain: - Objects with `:destroy()` or `:Destroy()` methods to be called - Functions to be run @@ -87,18 +84,21 @@ Scopes passed to `doCleanup` can have any of the following: - Roblox event connections to disconnect - Other nested scopes to be cleaned up -That's all there is to scopes. They are nothing more than arrays of objects -which eventually get passed to a cleanup function. +You can add these manually using `table.insert` if you need custom behaviour, +or if you are working with objects that don't add themselves to scopes. + +That's all there is to scopes. They are arrays of objects which later get passed +to a cleanup function. ----- ## Improved Syntax -While the above way of writing scopes is good for learning, Fusion also provides -more convenient shorthand that makes writing them out easier. +Fusion provides better shorthand for scopes to improve readability and +maintainability. -The first change is to use `scoped()` to make our scope, instead of creating an -array normally. It asks for a table argument, which we'll leave empty for now. +To use shorthand, use `scoped({})` to create your scopes. By default this still +creates a normal empty array. ```Lua linenums="2" hl_lines="2 4" local Fusion = require(ReplicatedStorage.Fusion) @@ -112,11 +112,10 @@ local thing3 = Fusion.Value(scope, "i am thing 3") doCleanup(scope) ``` -`scoped()` will return an array, just like the one we created ourselves. -However, we can now upgrade it with Fusion's syntax. +`scoped` can extend the array with custom methods. This is used primarily for +constructors. -Whenever we have a function that takes `scope` as the first argument, we can add -it to the table argument of `scoped()`. +Specify them in the table argument: ```Lua linenums="2" hl_lines="4-6" local Fusion = require(ReplicatedStorage.Fusion) @@ -132,7 +131,7 @@ local thing3 = Fusion.Value(scope, "i am thing 3") doCleanup(scope) ``` -Now, we can use that function as a method on `scope`, like this: +You can now rewrite those constructors as method calls. ```Lua linenums="2" hl_lines="7-9" local Fusion = require(ReplicatedStorage.Fusion) @@ -148,13 +147,12 @@ local thing3 = scope:Value("i am thing 3") doCleanup(scope) ``` -This makes your code shorter, cleaner and more consistent. When you write code -in this way, you no longer have to refer to the full function name, and it's -easier to copy and paste around without making mistakes. +This makes code shorter, cleaner and consistent. You import fewer things, names +are consistently positioned and more visually parseable, and it is easier to +move and copy code. -As a nice shorthand, you can pass in `Fusion` directly to `scoped()` rather than -listing out all the functions you want manually. Because `Fusion` is already a -table full of functions for creating things, it works too. +Try passing `Fusion` to `scoped()` rather than listing functions manually. +Because `Fusion` already contains those functions, it works too. ```Lua linenums="2" hl_lines="4" local Fusion = require(ReplicatedStorage.Fusion) @@ -168,7 +166,6 @@ local thing3 = scope:Value("i am thing 3") doCleanup(scope) ``` -Remember - this is just a nicer syntax for exactly the same thing we were doing -before. +Remember: this is only nicer syntax for exactly the same thing you did before. From now on, you'll see this `scoped()` syntax used throughout the tutorials. \ No newline at end of file From 96df21c5b82d7796a80296ec5e06e608c3c9905a Mon Sep 17 00:00:00 2001 From: Elttob Date: Tue, 26 Dec 2023 22:56:35 +0000 Subject: [PATCH 131/287] Rewrite tutorial introduction --- docs/tutorials/index.md | 134 +++++++++++++++++++--------------------- 1 file changed, 62 insertions(+), 72 deletions(-) diff --git a/docs/tutorials/index.md b/docs/tutorials/index.md index ecc404635..19b078081 100644 --- a/docs/tutorials/index.md +++ b/docs/tutorials/index.md @@ -5,24 +5,33 @@ You'll not only learn how Fusion's features work, but you'll also be presented with wisdom from those who've worked with some of the largest Fusion codebases today. -!!! caution "But first, something important..." +!!! caution "But first, some advice from the maintainers..." ** - Do not use Fusion for real-world work unless you're 100,000% able to deal - with a changing environment! + Fusion is pre-1.0 software. ** - Fusion is not version 1.0 yet! You *will* encounter: + We *(the maintainers and contributors)* work hard to keep releases bug-free + and relatively complete, so it should be safe to use in production. Many + people already do, and report fantastic results! - - updates that completely remove existing features - - changes in behaviour between versions - - changing advice on coding conventions and how to structure your project + However, we mark Fusion as pre-1.0 because we are working on the design of + the library itself. We strive for the best library design we can deliver, + which means breaking changes are common and sweeping. - This is not a bad thing! Moving fast with Fusion at this early stage means - we can quickly abandon counterproductive ideas and features, and discover - much more solid foundations to build upon. + With Fusion, you should expect: - Don't be discouraged from Fusion though! Once features have settled down, - releases will be more stable. + - upgrades to be frictionful, requiring code to be rethought + - features to be superseded or removed across versions + - advice or best practices to change over time + + You should *also* expect: + + - careful consideration around breakage, even though we reserve the right to + do it + - clear communication ahead of any major changes + - helpful advice to answer your questions and ease your porting process + + We hope you enjoy using Fusion! ----- @@ -37,75 +46,55 @@ These tutorials assume: - You don't have to be an expert! Knowing about basic instances, events and data types will be good enough. -Of course, based on your existing knowledge, you may find some tutorials easier -or harder. Fusion's built to be easy to learn, but it may still take a bit of -time to absorb some concepts, so don't be discouraged! +Based on your existing knowledge, you may find some tutorials easier or harder. +Don't be discouraged - Fusion's built to be easy to learn, but it may still take +a bit of time to absorb some concepts. Learn at a pace which is right for you. ----- -## Install Fusion inside Roblox Studio +## Installing Fusion -If you are creating Luau experiences in Roblox Studio, then you can use a -version of Fusion packaged up as a Roblox model. Before starting, you'll need -to add this model to your game. +There are two ways of installing Fusion, dependent on your use case. -Head over to [Fusion's 'Releases' page](https://github.com/Elttob/Fusion/releases). -Click the 'Assets' dropdown to view the downloadable files: +If you are creating Luau experiences in Roblox Studio, then you can import a +Roblox model file containing Fusion. -![Picture of Fusion's GitHub Releases page, with the Assets dropdown highlighted.](index/Github-Releases-Guide-1-Light.png#only-light) -![Picture of Fusion's GitHub Releases page, with the Assets dropdown highlighted.](index/Github-Releases-Guide-1-Dark.png#only-dark) +??? example "Steps (click to expand)" -Now, click on the `Fusion.rbxm` file to download it. This model contains Fusion. + Head over to [Fusion's 'Releases' page](https://github.com/Elttob/Fusion/releases). + Click the 'Assets' dropdown to view the downloadable files: -![The Assets dropdown opened to reveal downloads, with Fusion.rbxm highlighted.](index/Github-Releases-Guide-2-Light.png#only-light) -![The Assets dropdown opened to reveal downloads, with Fusion.rbxm highlighted.](index/Github-Releases-Guide-2-Dark.png#only-dark) + ![Picture of Fusion's GitHub Releases page, with the Assets dropdown highlighted.](index/Github-Releases-Guide-1-Light.png#only-light) + ![Picture of Fusion's GitHub Releases page, with the Assets dropdown highlighted.](index/Github-Releases-Guide-1-Dark.png#only-dark) -Head into Roblox Studio to import the model; if you're just following the -tutorials, an empty baseplate will do. + Now, click on the `Fusion.rbxm` file to download it. This model contains Fusion. -Right-click on `ReplicatedStorage`, and select 'Insert from File': + ![The Assets dropdown opened to reveal downloads, with Fusion.rbxm highlighted.](index/Github-Releases-Guide-2-Light.png#only-light) + ![The Assets dropdown opened to reveal downloads, with Fusion.rbxm highlighted.](index/Github-Releases-Guide-2-Dark.png#only-dark) -![ReplicatedStorage is right-clicked, showing a context menu of items. Insert from File is highlighted.](index/Github-Releases-Guide-3-Light.png#only-light) -![ReplicatedStorage is right-clicked, showing a context menu of items. Insert from File is highlighted.](index/Github-Releases-Guide-3-Dark.png#only-dark) + Head into Roblox Studio to import the model; if you're just following the + tutorials, an empty baseplate will do. -Select the `Fusion.rbxm` file you just downloaded. You should see a 'Fusion' -module script appear in `ReplicatedStorage`! + Right-click on `ReplicatedStorage`, and select 'Insert from File': -### Setting Up A Test Script + ![ReplicatedStorage is right-clicked, showing a context menu of items. Insert from File is highlighted.](index/Github-Releases-Guide-3-Light.png#only-light) + ![ReplicatedStorage is right-clicked, showing a context menu of items. Insert from File is highlighted.](index/Github-Releases-Guide-3-Dark.png#only-dark) -Now that you've installed Fusion, you can set up a local script for testing. -Here's how: + Select the `Fusion.rbxm` file you just downloaded. You should see a 'Fusion' + module script appear in `ReplicatedStorage`! -1. Create a `LocalScript` in `StarterGui` or `StarterPlayerScripts`. -2. Remove the default code, and paste the following code in: -```Lua linenums="1" -local ReplicatedStorage = game:GetService("ReplicatedStorage") -local Fusion = require(ReplicatedStorage.Fusion) -``` -3. Press 'Play' - if there are no errors, everything was set up correctly! + Now, you can create a script for testing: -??? fail "My script didn't work! (click to expand)" - ``` - Fusion is not a valid member of ReplicatedStorage "ReplicatedStorage" + 1. Create a `LocalScript` in `StarterGui` or `StarterPlayerScripts`. + 2. Remove the default code, and paste the following code in: + ```Lua linenums="1" + local ReplicatedStorage = game:GetService("ReplicatedStorage") + local Fusion = require(ReplicatedStorage.Fusion) ``` + 3. Press 'Play' - if there are no errors, everything was set up correctly!. - If you're seeing this error, then your script can't find Fusion. - - This code assumes you've installed Fusion into `ReplicatedStorage`. If - you've installed Fusion elsewhere, you'll need to tweak the `require()` on - line 2 to point to the correct location. - - If line 2 looks like it points to the correct location, refer back to - [the previous section](#installing-fusion) and double-check you've set - everything up properly. Make sure you have a `ModuleScript` inside - `ReplicatedStorage` called "Fusion". - ------ - -## Install Fusion to your filesystem - -If you're using pure Luau, or if you're synchronising into Roblox Studio from -the filesystem or an external editor, you can use these alternate steps instead: +If you're using pure Luau, or if you're synchronising external files into Roblox +Studio, then you can use Fusion's source code directly. ??? example "Steps (click to expand)" 1. Head over to [Fusion's 'Releases' page](https://github.com/Elttob/Fusion/releases). @@ -132,15 +121,16 @@ the filesystem or an external editor, you can use these alternate steps instead: ----- -## Where To Get Help +## Getting Help -Fusion is built to be easy to use, and we want these tutorials to be as useful -and comprehensive as possible. However, maybe you're stuck on a cursed issue -and really need some help; or perhaps you're looking to get a better overall -understanding of Fusion! +Fusion is built to be easy to use, and this website strives to be as useful and +comprehensive as possible. However, you might need targeted help on a specific +issue, or you might want to grow your understanding of Fusion in other ways. -Whatever you're looking for, here are some resources for you to get help: +The best place to get help is [the #fusion channel](https://discord.com/channels/385151591524597761/895437663040077834) +over on [the Roblox OSS Discord server](https://discord.gg/h2NV8PqhAD). +Maintainers and contributors drop in frequently, alongside many eager Fusion +users. -- [The Roblox OSS Discord](https://discord.gg/h2NV8PqhAD) has a [#fusion](https://discord.com/channels/385151591524597761/895437663040077834) channel -- Check out [our Discussions page](https://github.com/Elttob/Fusion/discussions) on GitHub -- [Open an issue](https://github.com/Elttob/Fusion/issues) if you run into bugs or have feature requests +For bugs and feature requests, [open an issue](https://github.com/Elttob/Fusion/issues) +on GitHub. \ No newline at end of file From 4b7522c0b091462b58ffbc4daf9f20b28d1c3892 Mon Sep 17 00:00:00 2001 From: Elttob Date: Tue, 26 Dec 2023 23:12:03 +0000 Subject: [PATCH 132/287] Update Hydrate tutorial --- docs/tutorials/instances/hydration.md | 28 +++++++++++++-------------- 1 file changed, 13 insertions(+), 15 deletions(-) diff --git a/docs/tutorials/instances/hydration.md b/docs/tutorials/instances/hydration.md index 3c6798b1b..1ce4ce550 100644 --- a/docs/tutorials/instances/hydration.md +++ b/docs/tutorials/instances/hydration.md @@ -1,3 +1,9 @@ +!!! warning "Intent to replace" + While the contents of this page still apply (and are useful for explaining + other features), `Hydrate` itself will be replaced by other primitives in + the near future. + [See this issue on GitHub for further details.](https://github.com/dphfox/Fusion/issues/206) + The process of connecting your scripts to a pre-made UI template is known as *hydration*. This is where logic in your scripts translate into UI effects, for example setting a message inside a TextLabel, moving menus around, or showing @@ -15,9 +21,9 @@ of properties. If you pass in Fusion objects, changes will be applied on the next frame: ```Lua -local showUI = Value(false) +local showUI = scope:Value(false) -local ui = Hydrate(StarterGui.Template:Clone()) { +local ui = scope:Hydrate(StarterGui.Template:Clone()) { Name = "MainGui", Enabled = showUI } @@ -34,21 +40,13 @@ print(ui.Enabled) --> true ## Usage -To use `Hydrate` in your code, you first need to import it from the Fusion -module, so that you can refer to it by name: - -```Lua linenums="1" hl_lines="2" -local Fusion = require(ReplicatedStorage.Fusion) -local Hydrate = Fusion.Hydrate -``` - The `Hydrate` function is called in two parts. First, call the function with the instance you want to hydrate, then pass in the property table: ```Lua local instance = workspace.Part -Hydrate(instance)({ +scope:Hydrate(instance)({ Color = Color3.new(1, 0, 0) }) ``` @@ -60,7 +58,7 @@ parentheses `()` are optional: local instance = workspace.Part -- This only works when you're using curly braces {}! -Hydrate(instance) { +scope:Hydrate(instance) { Color = Color3.new(1, 0, 0) } ``` @@ -68,7 +66,7 @@ Hydrate(instance) { `Hydrate` returns the instance you give it, so you can use it in declarations: ```Lua -local instance = Hydrate(workspace.Part) { +local instance = scope:Hydrate(workspace.Part) { Color = Color3.new(1, 0, 0) } ``` @@ -78,9 +76,9 @@ instance directly. However, if you pass in a Fusion object (like `Value`), then changes will be applied on the next frame: ```Lua -local message = Value("Loading...") +local message = scope:Value("Loading...") -Hydrate(PlayerGui.LoadingText) { +scope:Hydrate(PlayerGui.LoadingText) { Text = message } From 2a3a36b5a3a115578fad520b89d129a8149009b7 Mon Sep 17 00:00:00 2001 From: Elttob Date: Wed, 27 Dec 2023 00:01:47 +0000 Subject: [PATCH 133/287] New tutorial updated for scopes --- docs/tutorials/instances/new-instances.md | 16 ++++------------ 1 file changed, 4 insertions(+), 12 deletions(-) diff --git a/docs/tutorials/instances/new-instances.md b/docs/tutorials/instances/new-instances.md index bb58cda85..109a5f1dc 100644 --- a/docs/tutorials/instances/new-instances.md +++ b/docs/tutorials/instances/new-instances.md @@ -3,9 +3,9 @@ creates a new instance, applies some default properties, then hydrates it with a property table. ```Lua -local message = Value("Hello there!") +local message = scope:Value("Hello there!") -local ui = New "TextLabel" { +local ui = scope:New "TextLabel" { Name = "Greeting", Parent = PlayerGui.ScreenGui, @@ -24,19 +24,11 @@ print(ui.Text) --> Goodbye friend! ## Usage -To use `New` in your code, you first need to import it from the Fusion module, -so that you can refer to it by name: - -```Lua linenums="1" hl_lines="2" -local Fusion = require(ReplicatedStorage.Fusion) -local New = Fusion.New -``` - The `New` function is called in two parts. First, call the function with the type of instance, then pass in the property table: ```Lua -local instance = New("Part")({ +local instance = scope:New("Part")({ Parent = workspace, Color = Color3.new(1, 0, 0) }) @@ -47,7 +39,7 @@ your class type, the extra parentheses `()` are optional: ```Lua -- This only works when you're using curly braces {} and quotes '' ""! -local instance = New "Part" { +local instance = scope:New "Part" { Parent = workspace, Color = Color3.new(1, 0, 0) } From 4b2a1355eca70375517e952b40edf3bb43f9bf16 Mon Sep 17 00:00:00 2001 From: Elttob Date: Wed, 27 Dec 2023 05:23:24 +0000 Subject: [PATCH 134/287] Update parenting tutorial to use scopes --- docs/tutorials/instances/parenting.md | 45 ++++++++++++++------------- 1 file changed, 23 insertions(+), 22 deletions(-) diff --git a/docs/tutorials/instances/parenting.md b/docs/tutorials/instances/parenting.md index 84890d40e..74c35c93a 100644 --- a/docs/tutorials/instances/parenting.md +++ b/docs/tutorials/instances/parenting.md @@ -1,10 +1,11 @@ The `[Children]` key allows you to add children when hydrating or creating an instance. -It accepts instances, arrays of children and state objects containing children. +It accepts instances, arrays of children, and state objects containing children +or `nil`. ```Lua -local folder = New "Folder" { +local folder = scope:New "Folder" { [Children] = { New "Part" { Name = "Gregory", @@ -22,10 +23,10 @@ local folder = New "Folder" { ## Usage -To use `Children` in your code, you first need to import it from the Fusion -module, so that you can refer to it by name: +`Children` doesn't need a scope - import it into your code from Fusion +directly. -```Lua linenums="1" hl_lines="2" +```Lua hl_lines="2" local Fusion = require(ReplicatedStorage.Fusion) local Children = Fusion.Children ``` @@ -34,7 +35,7 @@ When using `New` or `Hydrate`, you can use `[Children]` as a key in the property table. Any instance you pass in will be parented: ```Lua -local folder = New "Folder" { +local folder = scope:New "Folder" { -- The part will be moved inside of the folder [Children] = workspace.Part } @@ -44,8 +45,8 @@ Since `New` and `Hydrate` both return their instances, you can nest them: ```Lua -- Makes a Folder, containing a part called Gregory -local folder = New "Folder" { - [Children] = New "Part" { +local folder = scope:New "Folder" { + [Children] = scope:New "Part" { Name = "Gregory", Color = Color3.new(1, 0, 0) } @@ -56,13 +57,13 @@ If you need to parent multiple children, arrays of children are accepted: ```Lua -- Makes a Folder, containing parts called Gregory and Sammy -local folder = New "Folder" { +local folder = scope:New "Folder" { [Children] = { - New "Part" { + scope:New "Part" { Name = "Gregory", Color = Color3.new(1, 0, 0) }, - New "Part" { + scope:New "Part" { Name = "Sammy", Material = "Glass" } @@ -73,12 +74,12 @@ local folder = New "Folder" { Arrays can be nested to any depth; all children will still be parented: ```Lua -local folder = New "Folder" { +local folder = scope:New "Folder" { [Children] = { { { { - New "Part" { + scope:New "Part" { Name = "Gregory", Color = Color3.new(1, 0, 0) } @@ -89,17 +90,17 @@ local folder = New "Folder" { } ``` -Similarly, state objects containing children (or `nil`) are also allowed: +State objects containing children or `nil` are also allowed: ```Lua -local value = Value() +local value = scope:Value() -local folder = New "Folder" { +local folder = scope:New "Folder" { [Children] = value } value:set( - New "Part" { + scope:New "Part" { Name = "Clyde", Transparency = 0.5 } @@ -110,22 +111,22 @@ You may use any combination of these to parent whichever children you need: ```Lua local modelChildren = workspace.Model:GetChildren() -local includeModel = Value(true) +local includeModel = scope:Value(true) -local folder = New "Folder" { +local folder = scope:New "Folder" { -- array of children [Children] = { -- single instance - New "Part" { + scope:New "Part" { Name = "Gregory", Color = Color3.new(1, 0, 0) }, -- state object containing children (or nil) - Computed(function(use) + scope:Computed(function(use) return if use(includeModel) then modelChildren:GetChildren() -- array of children else nil - end, Fusion.doNothing) + end) } } ``` \ No newline at end of file From 2d1e6e18cd9a5f058f4a74c875198dd9c257c61f Mon Sep 17 00:00:00 2001 From: Elttob Date: Wed, 27 Dec 2023 05:38:04 +0000 Subject: [PATCH 135/287] Change Events updated to use scopes --- docs/tutorials/instances/change-events.md | 58 ++++++----------------- docs/tutorials/instances/events.md | 12 ++--- docs/tutorials/instances/parenting.md | 3 +- 3 files changed, 20 insertions(+), 53 deletions(-) diff --git a/docs/tutorials/instances/change-events.md b/docs/tutorials/instances/change-events.md index 6f4d9ede0..b7403cda5 100644 --- a/docs/tutorials/instances/change-events.md +++ b/docs/tutorials/instances/change-events.md @@ -3,7 +3,7 @@ instance. Those keys let you connect functions to property changed events on the instance. ```Lua -local input = New "TextBox" { +local input = scope:New "TextBox" { [OnChange "Text"] = function(newText) print("You typed:", newText) end @@ -14,25 +14,30 @@ local input = New "TextBox" { ## Usage -To use `OnChange` in your code, you first need to import it from the Fusion -module, so that you can refer to it by name: +`OnChange` doesn't need a scope - import it into your code from Fusion directly. -```Lua linenums="1" hl_lines="2" -local Fusion = require(ReplicatedStorage.Fusion) +```Lua local OnChange = Fusion.OnChange ``` When you call `OnChange` with a property name, it will return a special key: ```Lua -local key = OnEvent("Activated") +local key = OnChange("Text") ``` When used in a property table, you can pass in a handler and it will be run when -that property changes. The new value of the property is passed in: +that property changes. + +!!! info "Arguments are different to Roblox API" + Normally in the Roblox API, when using `:GetPropertyChangedSignal()` on an + instance, the callback will not receive any arguments. + + To make working with change events easier, `OnChange` will pass the new value of + the property to the callback. ```Lua -local input = New "TextBox" { +local input = scope:New "TextBox" { [OnChange("Text")] = function(newText) print("You typed:", newText) end @@ -43,45 +48,10 @@ If you're using quotes `'' ""` for the event name, the extra parentheses `()` are optional: ```Lua -local input = New "TextBox" { +local input = scope:New "TextBox" { [OnChange "Text"] = function(newText) print("You typed:", newText) end } ``` -??? warning "A warning about using OnChange with state objects" - - When passing a state object as a property, changes will only affect the property - on the next frame: - - ```Lua - local text = Value("Hello") - - local message = New "Message" { - Text = text - } - - print(message.Text) --> Hello - - text:set("World") - print(message.Text) --> Hello - task.wait() -- wait for next frame - print(message.Text) --> World - ``` - - This means `OnChange` for that property will run your handlers *one frame after* - the state object is changed. This could introduce off-by-one-frame errors. - For this case, prefer to use an observer on the state object directly for - zero latency. - ------ - -## Differences from Roblox API - -Normally in the Roblox API, when using `:GetPropertyChangedSignal()` on an -instance, the handler callback will not receive any arguments. - -It's worth noting that `OnChange` is not identical in that respect. To make -working with change events easier, `OnChange` will pass the new value of the -property to the handler callback. \ No newline at end of file diff --git a/docs/tutorials/instances/events.md b/docs/tutorials/instances/events.md index b97939549..51f05a82d 100644 --- a/docs/tutorials/instances/events.md +++ b/docs/tutorials/instances/events.md @@ -2,7 +2,7 @@ instance. Those keys let you connect functions to events on the instance. ```Lua -local button = New "TextButton" { +local button = scope:New "TextButton" { [OnEvent "Activated"] = function(_, numClicks) print("The button was pressed", numClicks, "time(s)!") end @@ -13,11 +13,9 @@ local button = New "TextButton" { ## Usage -To use `OnEvent` in your code, you first need to import it from the Fusion -module, so that you can refer to it by name: +`OnEvent` doesn't need a scope - import it into your code from Fusion directly. -```Lua linenums="1" hl_lines="2" -local Fusion = require(ReplicatedStorage.Fusion) +```Lua local OnEvent = Fusion.OnEvent ``` @@ -31,7 +29,7 @@ When that key is used in a property table, you can pass in a handler and it will be connected to the event for you: ```Lua -local button = New "TextButton" { +local button = scope:New "TextButton" { [OnEvent("Activated")] = function(_, numClicks) print("The button was pressed", numClicks, "time(s)!") end @@ -42,7 +40,7 @@ If you're using quotes `'' ""` for the event name, the extra parentheses `()` are optional: ```Lua -local button = New "TextButton" { +local button = scope:New "TextButton" { [OnEvent "Activated"] = function(_, numClicks) print("The button was pressed", numClicks, "time(s)!") end diff --git a/docs/tutorials/instances/parenting.md b/docs/tutorials/instances/parenting.md index 74c35c93a..e848dfa60 100644 --- a/docs/tutorials/instances/parenting.md +++ b/docs/tutorials/instances/parenting.md @@ -26,8 +26,7 @@ local folder = scope:New "Folder" { `Children` doesn't need a scope - import it into your code from Fusion directly. -```Lua hl_lines="2" -local Fusion = require(ReplicatedStorage.Fusion) +```Lua local Children = Fusion.Children ``` From cb2e05d467c9f35aa6796384eac667e5fb340730 Mon Sep 17 00:00:00 2001 From: Elttob Date: Wed, 27 Dec 2023 05:39:29 +0000 Subject: [PATCH 136/287] Remove Cleanup tutorial --- docs/tutorials/instances/cleanup.md | 218 ---------------------------- mkdocs.yml | 1 - 2 files changed, 219 deletions(-) delete mode 100644 docs/tutorials/instances/cleanup.md diff --git a/docs/tutorials/instances/cleanup.md b/docs/tutorials/instances/cleanup.md deleted file mode 100644 index 5849b79dc..000000000 --- a/docs/tutorials/instances/cleanup.md +++ /dev/null @@ -1,218 +0,0 @@ -The `[Cleanup]` key allows you to add cleanup code to an instance you're -hydrating or creating. You can also pass in instances or event connections to -destroy. - -```Lua -local connection = RunService.Heartbeat:Connect(function() - print("Blah blah...") -end) - -local part = New "Part" { - [Cleanup] = connection -} -``` - -```Lua -local box = New "SelectionBox" { - Parent = PlayerGui -} - -local part = New "Part" { - [Cleanup] = box -} -``` - -```Lua -local part = New "Part" { - [Cleanup] = { - function() - print("Oh no, I got destroyed. Ouch :(") - end, - connection, - box - } -} -``` - ------ - -## Usage - -To use `Cleanup` in your code, you first need to import it from the Fusion -module, so that you can refer to it by name: - -```Lua linenums="1" hl_lines="2" -local Fusion = require(ReplicatedStorage.Fusion) -local Cleanup = Fusion.Cleanup -``` - -When using `New` or `Hydrate`, you can use `[Cleanup]` as a key in the property -table. Any function you pass in will be run when the instance is destroyed: - -```Lua -local folder = New "Folder" { - [Cleanup] = function() - print("This folder was destroyed") - end -} -``` - -Arrays are supported - their contents will be cleaned up in order: - -```Lua -local folder = New "Folder" { - [Cleanup] = { - function() - print("This will run first") - end, - function() - print("This will run next") - end, - function() - print("This will run last") - end - } -} -``` - -You may also nest arrays up to any depth - sub-arrays are also run in order: - -```Lua -local folder = New "Folder" { - [Cleanup] = { - function() - print("This will run first") - end, - { - function() - print("This will run next") - end, - function() - print("This will run second-to-last") - end, - }, - function() - print("This will run last") - end - } -} -``` - -For convenience, `Cleanup` allows you to pass some types of value in directly. -Passing in an instance will destroy it: - -```Lua -local box = New "SelectionBox" { - Parent = PlayerGui -} - -local part = New "Part" { - -- `box` will be destroyed when the part is destroyed - [Cleanup] = box -} -``` - -Passing in an event connection will disconnect it: - -```Lua -local connection = RunService.Heartbeat:Connect(function() - print("Blah blah...") -end) - -local part = New "Part" { - -- `connection` will be disconnected when the part is destroyed - [Cleanup] = connection -} -``` - -Finally, passing in anything with a `:destroy()` or `:Destroy()` method will -have that method called: - -```Lua --- you might receive an object like this from a third party library -local object = {} -function object:destroy() - print("I was destroyed!") -end - -local part = New "Part" { - -- `object:destroy()` will be called when the part is destroyed - [Cleanup] = object -} -``` - -Any other kind of value will do nothing by default. - ------ - -## Don't Use Destroyed - -While Roblox does provide it's own `Destroyed` event, it should *not* be relied -upon for cleaning up correctly in all cases. It only fires when the `Destroy` -method is called, meaning other kinds of destruction are ignored. - -For example, notice only one of these parts runs their cleanup code: - -=== "Luau code" - - ```Lua linenums="1" - local part1 = New "Part" { - [OnEvent "Destroyed"] = function() - print("=> Part 1 cleaned up") - end - } - - local part2 = New "Part" { - [OnEvent "Destroyed"] = function() - print("=> Part 2 cleaned up") - end - } - - print("Destroying part 1...") - part1:Destroy() - - print("Setting part 2 to nil...") - part2 = nil - ``` - -=== "Output" - - ``` - Destroying part 1... - => Part 1 cleaned up - Setting part 2 to nil... - ``` - -Meanwhile, Fusion's `[Cleanup]` will work regardless of how your instances were -destroyed, meaning you can avoid serious memory leaks: - -=== "Luau code" - - ```Lua linenums="1" - local part1 = New "Part" { - [Cleanup] = function() - print("=> Part 1 cleaned up") - end - } - - local part2 = New "Part" { - [Cleanup] = function() - print("=> Part 2 cleaned up") - end - } - - print("Destroying part 1...") - part1:Destroy() - - print("Setting part 2 to nil...") - part2 = nil - ``` - -=== "Output" - - ``` - Destroying part 1... - => Part 1 cleaned up - Setting part 2 to nil... - => Part 2 cleaned up - ``` \ No newline at end of file diff --git a/mkdocs.yml b/mkdocs.yml index 79c504ee2..28a7a2a36 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -66,7 +66,6 @@ nav: - Events: tutorials/instances/events.md - Change Events: tutorials/instances/change-events.md - Outputs: tutorials/instances/outputs.md - - Cleanup: tutorials/instances/cleanup.md - References: tutorials/instances/references.md - Lists & Tables: - The For Objects: tutorials/lists-and-tables/the-for-objects.md From 7c009fa51775ffd1b252b3a4837cf9e0da5a893d Mon Sep 17 00:00:00 2001 From: Elttob Date: Wed, 27 Dec 2023 05:45:25 +0000 Subject: [PATCH 137/287] Tutorial restructuring --- .../lists-and-tables/the-for-objects.md | 108 ------------------ .../{instances => roblox}/change-events.md | 0 .../tutorials/{instances => roblox}/events.md | 0 .../{instances => roblox}/hydration.md | 0 .../hydration/Hydration-Basic-Dark.svg | 0 .../hydration/Hydration-Basic-Light.svg | 0 .../{instances => roblox}/new-instances.md | 0 .../new-instances/Default-Props-Dark.svg | 0 .../new-instances/Default-Props-Light.svg | 0 .../{instances => roblox}/outputs.md | 0 .../{instances => roblox}/parenting.md | 0 .../{instances => roblox}/references.md | 0 .../{lists-and-tables => tables}/forkeys.md | 0 .../{lists-and-tables => tables}/forpairs.md | 0 .../Optimisation-KeyValueChange-Dark.svg | 0 .../Optimisation-KeyValueChange-Light.svg | 0 .../Optimisation-KeyValuePreserve-Dark.svg | 0 .../Optimisation-KeyValuePreserve-Light.svg | 0 .../{lists-and-tables => tables}/forvalues.md | 0 .../Optimisation-Duplicates-Dark.svg | 0 .../Optimisation-Duplicates-Light.svg | 0 .../Optimisation-Reordering-Dark.svg | 0 .../Optimisation-Reordering-Light.svg | 0 mkdocs.yml | 30 ++--- 24 files changed, 15 insertions(+), 123 deletions(-) delete mode 100644 docs/tutorials/lists-and-tables/the-for-objects.md rename docs/tutorials/{instances => roblox}/change-events.md (100%) rename docs/tutorials/{instances => roblox}/events.md (100%) rename docs/tutorials/{instances => roblox}/hydration.md (100%) rename docs/tutorials/{instances => roblox}/hydration/Hydration-Basic-Dark.svg (100%) rename docs/tutorials/{instances => roblox}/hydration/Hydration-Basic-Light.svg (100%) rename docs/tutorials/{instances => roblox}/new-instances.md (100%) rename docs/tutorials/{instances => roblox}/new-instances/Default-Props-Dark.svg (100%) rename docs/tutorials/{instances => roblox}/new-instances/Default-Props-Light.svg (100%) rename docs/tutorials/{instances => roblox}/outputs.md (100%) rename docs/tutorials/{instances => roblox}/parenting.md (100%) rename docs/tutorials/{instances => roblox}/references.md (100%) rename docs/tutorials/{lists-and-tables => tables}/forkeys.md (100%) rename docs/tutorials/{lists-and-tables => tables}/forpairs.md (100%) rename docs/tutorials/{lists-and-tables => tables}/forpairs/Optimisation-KeyValueChange-Dark.svg (100%) rename docs/tutorials/{lists-and-tables => tables}/forpairs/Optimisation-KeyValueChange-Light.svg (100%) rename docs/tutorials/{lists-and-tables => tables}/forpairs/Optimisation-KeyValuePreserve-Dark.svg (100%) rename docs/tutorials/{lists-and-tables => tables}/forpairs/Optimisation-KeyValuePreserve-Light.svg (100%) rename docs/tutorials/{lists-and-tables => tables}/forvalues.md (100%) rename docs/tutorials/{lists-and-tables => tables}/forvalues/Optimisation-Duplicates-Dark.svg (100%) rename docs/tutorials/{lists-and-tables => tables}/forvalues/Optimisation-Duplicates-Light.svg (100%) rename docs/tutorials/{lists-and-tables => tables}/forvalues/Optimisation-Reordering-Dark.svg (100%) rename docs/tutorials/{lists-and-tables => tables}/forvalues/Optimisation-Reordering-Light.svg (100%) diff --git a/docs/tutorials/lists-and-tables/the-for-objects.md b/docs/tutorials/lists-and-tables/the-for-objects.md deleted file mode 100644 index 053d6f7b8..000000000 --- a/docs/tutorials/lists-and-tables/the-for-objects.md +++ /dev/null @@ -1,108 +0,0 @@ -Often when building UI, you need to deal with lists, arrays and tables. For -example: - -- creating an array of TextLabels for a player list -- generating a settings page from pairs of keys and values in a configuration -- filling a grid with inventory slots and items - -Most of these use cases involve processing one table into another: - -- converting an array of player names into an array of TextLabels -- converting a table of settings into a table of UI controls -- converting an array of inventory items into an array of slot UIs - -So, to assist with these use cases, Fusion has a few state objects which are -specially designed for working with arrays and tables. These are known as the -`For` objects. - ------ - -## The Problem - -To start, let's try making a player list using the basic state objects from -before. Let's define a changeable list of player names and some basic UI: - -```Lua linenums="1" -local playerNames = Value({"Elttob", "boatbomber", "thisfall", "AxisAngles"}) - -local textLabels = {} -- TODO: implement this - -local ui = New "ScreenGui" { - Parent = Players.LocalPlayer.PlayerGui, - - [Children] = New "Frame" { - Name = "PlayerList", - - Position = UDim2.fromScale(1, 1), - AnchorPoint = Vector2.new(1, 0), - Size = UDim2.fromOffset(200, 0), - AutomaticSize = "Y", - - [Children] = { - New "UIListLayout" { - SortOrder = "Name" - }, - textLabels - } - } -} -``` - -Now, let's make a `Computed` which generates that list of text labels for us: - -```Lua linenums="1" hl_lines="3-13" -local playerNames = Value({"Elttob", "boatbomber", "thisfall", "AxisAngles"}) - -local textLabels = Computed(function(use) - local out = {} - for index, playerName in use(playerNames) do - out[index] = New "TextLabel" { - Name = playerName, - Size = UDim2.new(1, 0, 0, 50), - Text = playerName - } - end - return out -end, Fusion.cleanup) - -local ui = New "ScreenGui" { - Parent = Players.LocalPlayer.PlayerGui, -``` - -This is alright, but there are a few problems: - -- Firstly, there's a fair amount of boilerplate - in order to generate the list -of text labels, you have to create a `Computed`, initialise a new table, write a -for-loop to populate the table, then return it. - - Boilerplate is generally annoying, and especially so for a task as common - as dealing with lists and tables. It's less clear to read and more tedious - to write. -- Secondly, whenever `playerNames` is changed, you reconstruct the entire list, -destroying all of your instances and any data associated with them. This is both -inefficient and also causes issues with data loss. - - Ideally, you should only modify the text labels for players that have - joined or left, leaving the rest of the text labels alone. - -To address this shortcoming, the `For` objects provide a cleaner way to do the -same thing, except with less boilerplate and leaving unchanged values alone: - -```Lua linenums="1" hl_lines="3-9" -local playerNames = Value({"Elttob", "boatbomber", "thisfall", "AxisAngles"}) - -local textLabels = ForValues(playerNames, function(use, playerName) - return New "TextLabel" { - Name = playerName, - Size = UDim2.new(1, 0, 0, 50), - Text = playerName - } -end, Fusion.cleanup) - -local ui = New "ScreenGui" { - Parent = Players.LocalPlayer.PlayerGui, -``` - -Over the next few pages, we'll take a look at three state objects: - -- `ForValues`, which lets you process just the values in a table. -- `ForKeys`, which lets you process just the keys in a table. -- `ForPairs`, which lets you do both at the same time. \ No newline at end of file diff --git a/docs/tutorials/instances/change-events.md b/docs/tutorials/roblox/change-events.md similarity index 100% rename from docs/tutorials/instances/change-events.md rename to docs/tutorials/roblox/change-events.md diff --git a/docs/tutorials/instances/events.md b/docs/tutorials/roblox/events.md similarity index 100% rename from docs/tutorials/instances/events.md rename to docs/tutorials/roblox/events.md diff --git a/docs/tutorials/instances/hydration.md b/docs/tutorials/roblox/hydration.md similarity index 100% rename from docs/tutorials/instances/hydration.md rename to docs/tutorials/roblox/hydration.md diff --git a/docs/tutorials/instances/hydration/Hydration-Basic-Dark.svg b/docs/tutorials/roblox/hydration/Hydration-Basic-Dark.svg similarity index 100% rename from docs/tutorials/instances/hydration/Hydration-Basic-Dark.svg rename to docs/tutorials/roblox/hydration/Hydration-Basic-Dark.svg diff --git a/docs/tutorials/instances/hydration/Hydration-Basic-Light.svg b/docs/tutorials/roblox/hydration/Hydration-Basic-Light.svg similarity index 100% rename from docs/tutorials/instances/hydration/Hydration-Basic-Light.svg rename to docs/tutorials/roblox/hydration/Hydration-Basic-Light.svg diff --git a/docs/tutorials/instances/new-instances.md b/docs/tutorials/roblox/new-instances.md similarity index 100% rename from docs/tutorials/instances/new-instances.md rename to docs/tutorials/roblox/new-instances.md diff --git a/docs/tutorials/instances/new-instances/Default-Props-Dark.svg b/docs/tutorials/roblox/new-instances/Default-Props-Dark.svg similarity index 100% rename from docs/tutorials/instances/new-instances/Default-Props-Dark.svg rename to docs/tutorials/roblox/new-instances/Default-Props-Dark.svg diff --git a/docs/tutorials/instances/new-instances/Default-Props-Light.svg b/docs/tutorials/roblox/new-instances/Default-Props-Light.svg similarity index 100% rename from docs/tutorials/instances/new-instances/Default-Props-Light.svg rename to docs/tutorials/roblox/new-instances/Default-Props-Light.svg diff --git a/docs/tutorials/instances/outputs.md b/docs/tutorials/roblox/outputs.md similarity index 100% rename from docs/tutorials/instances/outputs.md rename to docs/tutorials/roblox/outputs.md diff --git a/docs/tutorials/instances/parenting.md b/docs/tutorials/roblox/parenting.md similarity index 100% rename from docs/tutorials/instances/parenting.md rename to docs/tutorials/roblox/parenting.md diff --git a/docs/tutorials/instances/references.md b/docs/tutorials/roblox/references.md similarity index 100% rename from docs/tutorials/instances/references.md rename to docs/tutorials/roblox/references.md diff --git a/docs/tutorials/lists-and-tables/forkeys.md b/docs/tutorials/tables/forkeys.md similarity index 100% rename from docs/tutorials/lists-and-tables/forkeys.md rename to docs/tutorials/tables/forkeys.md diff --git a/docs/tutorials/lists-and-tables/forpairs.md b/docs/tutorials/tables/forpairs.md similarity index 100% rename from docs/tutorials/lists-and-tables/forpairs.md rename to docs/tutorials/tables/forpairs.md diff --git a/docs/tutorials/lists-and-tables/forpairs/Optimisation-KeyValueChange-Dark.svg b/docs/tutorials/tables/forpairs/Optimisation-KeyValueChange-Dark.svg similarity index 100% rename from docs/tutorials/lists-and-tables/forpairs/Optimisation-KeyValueChange-Dark.svg rename to docs/tutorials/tables/forpairs/Optimisation-KeyValueChange-Dark.svg diff --git a/docs/tutorials/lists-and-tables/forpairs/Optimisation-KeyValueChange-Light.svg b/docs/tutorials/tables/forpairs/Optimisation-KeyValueChange-Light.svg similarity index 100% rename from docs/tutorials/lists-and-tables/forpairs/Optimisation-KeyValueChange-Light.svg rename to docs/tutorials/tables/forpairs/Optimisation-KeyValueChange-Light.svg diff --git a/docs/tutorials/lists-and-tables/forpairs/Optimisation-KeyValuePreserve-Dark.svg b/docs/tutorials/tables/forpairs/Optimisation-KeyValuePreserve-Dark.svg similarity index 100% rename from docs/tutorials/lists-and-tables/forpairs/Optimisation-KeyValuePreserve-Dark.svg rename to docs/tutorials/tables/forpairs/Optimisation-KeyValuePreserve-Dark.svg diff --git a/docs/tutorials/lists-and-tables/forpairs/Optimisation-KeyValuePreserve-Light.svg b/docs/tutorials/tables/forpairs/Optimisation-KeyValuePreserve-Light.svg similarity index 100% rename from docs/tutorials/lists-and-tables/forpairs/Optimisation-KeyValuePreserve-Light.svg rename to docs/tutorials/tables/forpairs/Optimisation-KeyValuePreserve-Light.svg diff --git a/docs/tutorials/lists-and-tables/forvalues.md b/docs/tutorials/tables/forvalues.md similarity index 100% rename from docs/tutorials/lists-and-tables/forvalues.md rename to docs/tutorials/tables/forvalues.md diff --git a/docs/tutorials/lists-and-tables/forvalues/Optimisation-Duplicates-Dark.svg b/docs/tutorials/tables/forvalues/Optimisation-Duplicates-Dark.svg similarity index 100% rename from docs/tutorials/lists-and-tables/forvalues/Optimisation-Duplicates-Dark.svg rename to docs/tutorials/tables/forvalues/Optimisation-Duplicates-Dark.svg diff --git a/docs/tutorials/lists-and-tables/forvalues/Optimisation-Duplicates-Light.svg b/docs/tutorials/tables/forvalues/Optimisation-Duplicates-Light.svg similarity index 100% rename from docs/tutorials/lists-and-tables/forvalues/Optimisation-Duplicates-Light.svg rename to docs/tutorials/tables/forvalues/Optimisation-Duplicates-Light.svg diff --git a/docs/tutorials/lists-and-tables/forvalues/Optimisation-Reordering-Dark.svg b/docs/tutorials/tables/forvalues/Optimisation-Reordering-Dark.svg similarity index 100% rename from docs/tutorials/lists-and-tables/forvalues/Optimisation-Reordering-Dark.svg rename to docs/tutorials/tables/forvalues/Optimisation-Reordering-Dark.svg diff --git a/docs/tutorials/lists-and-tables/forvalues/Optimisation-Reordering-Light.svg b/docs/tutorials/tables/forvalues/Optimisation-Reordering-Light.svg similarity index 100% rename from docs/tutorials/lists-and-tables/forvalues/Optimisation-Reordering-Light.svg rename to docs/tutorials/tables/forvalues/Optimisation-Reordering-Light.svg diff --git a/mkdocs.yml b/mkdocs.yml index 28a7a2a36..47de82a2d 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -59,27 +59,27 @@ nav: - Values: tutorials/fundamentals/values.md - Observers: tutorials/fundamentals/observers.md - Computeds: tutorials/fundamentals/computeds.md + - Tables: + - ForValues: tutorials/tables/forvalues.md + - ForKeys: tutorials/tables/forkeys.md + - ForPairs: tutorials/tables/forpairs.md + - Animation: + - Tweens: tutorials/animation/tweens.md + - Springs: tutorials/animation/springs.md - Roblox: - - Hydration: tutorials/instances/hydration.md - - New Instances: tutorials/instances/new-instances.md - - Parenting: tutorials/instances/parenting.md - - Events: tutorials/instances/events.md - - Change Events: tutorials/instances/change-events.md - - Outputs: tutorials/instances/outputs.md - - References: tutorials/instances/references.md - - Lists & Tables: - - The For Objects: tutorials/lists-and-tables/the-for-objects.md - - ForValues: tutorials/lists-and-tables/forvalues.md - - ForKeys: tutorials/lists-and-tables/forkeys.md - - ForPairs: tutorials/lists-and-tables/forpairs.md + - Hydration: tutorials/roblox/hydration.md + - New Instances: tutorials/roblox/new-instances.md + - Parenting: tutorials/roblox/parenting.md + - Events: tutorials/roblox/events.md + - Change Events: tutorials/roblox/change-events.md + - Outputs: tutorials/roblox/outputs.md + - References: tutorials/roblox/references.md - Components: - Reusing UI: tutorials/components/reusing-ui.md - Children: tutorials/components/children.md - Callbacks: tutorials/components/callbacks.md - State: tutorials/components/state.md - - Animation: - - Tweens: tutorials/animation/tweens.md - - Springs: tutorials/animation/springs.md + - Examples: - Home: examples/index.md - Cookbook: From eb9c15c64338af4c625794f10242401baf632ab1 Mon Sep 17 00:00:00 2001 From: Elttob Date: Wed, 27 Dec 2023 15:21:27 +0000 Subject: [PATCH 138/287] Update Outputs tutorial to use scopes --- docs/tutorials/roblox/outputs.md | 24 +++++++++++------------- 1 file changed, 11 insertions(+), 13 deletions(-) diff --git a/docs/tutorials/roblox/outputs.md b/docs/tutorials/roblox/outputs.md index 723eb70ca..7f3691d8c 100644 --- a/docs/tutorials/roblox/outputs.md +++ b/docs/tutorials/roblox/outputs.md @@ -2,9 +2,9 @@ instance. Those keys let you output a property's value to a `Value` object. ```Lua -local name = Value() +local name = scope:Value() -local thing = New "Part" { +local thing = scope:New "Part" { [Out "Name"] = name } @@ -18,11 +18,9 @@ print(peek(name)) --> Jimmy ## Usage -To use `Out` in your code, you first need to import it from the Fusion module, -so that you can refer to it by name: +`Out` doesn't need a scope - import it into your code from Fusion directly. -```Lua linenums="1" hl_lines="2" -local Fusion = require(ReplicatedStorage.Fusion) +```Lua local Out = Fusion.Out ``` @@ -37,9 +35,9 @@ to the value of the property, and when the property changes, it will be set to the new value: ```Lua -local name = Value() +local name = scope:Value() -local thing = New "Part" { +local thing = scope:New "Part" { [Out("Name")] = name } @@ -53,7 +51,7 @@ If you're using quotes `'' ""` for the event name, the extra parentheses `()` are optional: ```Lua -local thing = New "Part" { +local thing = scope:New "Part" { [Out "Name"] = name } ``` @@ -66,9 +64,9 @@ By default, `Out` only *outputs* changes to the property. If you set the value to something else, the property remains the same: ```Lua -local name = Value() +local name = scope:Value() -local thing = New "Part" { +local thing = scope:New "Part" { [Out "Name"] = name -- When `thing.Name` changes, set `name` } @@ -82,9 +80,9 @@ If you want the value to both *change* and *be changed* by the property, you need to explicitly say so: ```Lua hl_lines="4 11" -local name = Value() +local name = scope:Value() -local thing = New "Part" { +local thing = scope:New "Part" { Name = name -- When `name` changes, set `thing.Name` [Out "Name"] = name -- When `thing.Name` changes, set `name` } From b40048ed04f4b9b9570142d4ff6ab46fea098f4f Mon Sep 17 00:00:00 2001 From: Elttob Date: Wed, 27 Dec 2023 15:37:28 +0000 Subject: [PATCH 139/287] Ref tutorial updated to use scopes --- docs/tutorials/roblox/references.md | 132 +++++++++------------------- 1 file changed, 40 insertions(+), 92 deletions(-) diff --git a/docs/tutorials/roblox/references.md b/docs/tutorials/roblox/references.md index a400ea67b..047307264 100644 --- a/docs/tutorials/roblox/references.md +++ b/docs/tutorials/roblox/references.md @@ -2,9 +2,9 @@ The `[Ref]` key allows you to save a reference to an instance you're hydrating or creating. ```Lua -local myRef = Value() +local myRef = scope:Value() -local thing = New "Part" { +local thing = scope:New "Part" { [Ref] = myRef } @@ -16,102 +16,51 @@ print(peek(myRef) == thing) --> true ## Usage -To use `Ref` in your code, you first need to import it from the Fusion module, -so that you can refer to it by name: +`Ref` doesn't need a scope - import it into your code from Fusion directly. ```Lua linenums="1" hl_lines="2" local Fusion = require(ReplicatedStorage.Fusion) local Ref = Fusion.Ref ``` -When using `New` or `Hydrate`, you can use `[Ref]` as a key in the property -table. It expects a value object to be passed in, and it will save a reference -to the instance in that object: +When creating an instance with `New`, `[Ref]` will save that instance to a value +object. ```Lua -local myRef = Value() +local myRef = scope:Value() -New "Part" { +scope:New "Part" { [Ref] = myRef } print(peek(myRef)) --> Part ``` -When the instance is cleaned up, the value object is set to nil to avoid memory -leaks: +Among other things, this allows you to refer to instances from other instances. ```Lua -local myPart = Value() - -New "Part" { - [Ref] = myPart -} - -print(peek(myRef)) --> Part - -peek(myRef):Destroy() - -print(peek(myRef)) --> nil -``` - ------ +local myPart = scope:Value() -## When To Use This - -You may have noticed that `New` and `Hydrate` already return their instances. -You might wonder why there's two ways to get the same instance reference: - -```Lua -local fromRef = Value() -local returned = New "Part" { - [Ref] = fromRef -} - -print(returned) --> Part -print(peek(fromRef)) --> Part - -print(returned == peek(fromRef)) --> true -``` - -There are two main use cases. Firstly, when you're using `[Children]` to nest -instances inside each other, it's hard to access the instance reference: - -```Lua -local folders = New "Folder" { - [Children] = New "Folder" { - -- the instance reference gets passed straight into [Children] - -- so... how do you save this somewhere else? - [Children] = New "Part" {} - } +New "SelectionBox" { + -- the selection box should adorn to the part + Adornee = myPart } -``` - -One solution is to extract the `New` call out to a separate variable. This is -the simplest solution, but because the part is separated from the folders, it's -harder to see they're related at a glance: - -```Lua --- build the part elsewhere, so it can be saved to a variable -local myPart = New "Part" {} -local folders = New "Folder" { - [Children] = New "Folder" { - -- use the saved reference - [Children] = myPart - } +New "Part" { + -- sets `myPart` to this part, which sets the adornee to this part + [Ref] = myPart } ``` -`Ref` allows you to save the reference without moving the `New` call: +You can also get references to instances from deep inside function calls. ```Lua --- use a Value instead of a plain variable, so it can be passed to `Ref` -local myPart = Value() +-- this will refer to the part, once we create it +local myPart = scope:Value() -local folders = New "Folder" { - [Children] = New "Folder" { - [Children] = New "Part" { +scope:New "Folder" { + [Children] = scope:New "Folder" { + [Children] = scope:New "Part" { -- save a reference into the value object [Ref] = myPart } @@ -119,23 +68,22 @@ local folders = New "Folder" { } ``` -The second use case arises when one instance needs to refer to another. Since -`Ref` saves to a value object, you can pass the object directly into another -`New` or `Hydrate` call: - -```Lua -local myPart = Value() - -New "SelectionBox" { - -- the selection box should adorn to the part - Adornee = myPart -} - -New "Part" { - -- saving a reference to `myPart`, which will change the Adornee prop above - [Ref] = myPart -} -``` - -These aren't the only use cases for `Ref`, but they're the most common patterns -which are worth covering. \ No newline at end of file +!!! warning "Nil hazard" + Before the part is created, the `myPart` value object will be `nil`. Be + careful not to use it before it's created. + + If you need to know about the instance ahead of time, you should create the + instance early, and parent it in later, when you create the rest of the + instances. + + ```Lua + -- build the part elsewhere, so it can be saved to a variable + local myPart = scope:New "Part" {} + + local folders = scope:New "Folder" { + [Children] = scope:New "Folder" { + -- parent the part into the folder here + [Children] = myPart + } + } + ``` \ No newline at end of file From 139aa4aff032f27bab7b8a686be22616f777c026 Mon Sep 17 00:00:00 2001 From: Elttob Date: Wed, 27 Dec 2023 15:43:38 +0000 Subject: [PATCH 140/287] Update objects tutorial with tip admonition --- docs/tutorials/fundamentals/objects.md | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/docs/tutorials/fundamentals/objects.md b/docs/tutorials/fundamentals/objects.md index 853772f55..5d7eb5e8c 100644 --- a/docs/tutorials/fundamentals/objects.md +++ b/docs/tutorials/fundamentals/objects.md @@ -166,6 +166,9 @@ local thing3 = scope:Value("i am thing 3") doCleanup(scope) ``` -Remember: this is only nicer syntax for exactly the same thing you did before. +!!! tip "This syntax is recommended" + It is recommended to use `scoped()` syntax. However, it is technically + optional; if it does not work for your codebase requirements, the barebones + syntax will always be available. -From now on, you'll see this `scoped()` syntax used throughout the tutorials. \ No newline at end of file + From now on, you'll see this `scoped()` syntax used throughout the tutorials. \ No newline at end of file From ac5922faa758e9a45a2f959bf96161fecaf426c0 Mon Sep 17 00:00:00 2001 From: Elttob Date: Wed, 27 Dec 2023 16:04:37 +0000 Subject: [PATCH 141/287] Add advice on merging libraries with scopes --- docs/tutorials/fundamentals/objects.md | 57 +++++++++++++++++++++----- 1 file changed, 47 insertions(+), 10 deletions(-) diff --git a/docs/tutorials/fundamentals/objects.md b/docs/tutorials/fundamentals/objects.md index 5d7eb5e8c..45a3db05d 100644 --- a/docs/tutorials/fundamentals/objects.md +++ b/docs/tutorials/fundamentals/objects.md @@ -97,8 +97,8 @@ to a cleanup function. Fusion provides better shorthand for scopes to improve readability and maintainability. -To use shorthand, use `scoped({})` to create your scopes. By default this still -creates a normal empty array. +To use shorthand, use `scoped({})` to create your scopes. This creates a normal +empty array. ```Lua linenums="2" hl_lines="2 4" local Fusion = require(ReplicatedStorage.Fusion) @@ -112,10 +112,10 @@ local thing3 = Fusion.Value(scope, "i am thing 3") doCleanup(scope) ``` -`scoped` can extend the array with custom methods. This is used primarily for -constructors. +`scoped` can add methods to that array for you. This is most useful for +constructor functions. -Specify them in the table argument: +Name some constructors in the table argument of `scoped`: ```Lua linenums="2" hl_lines="4-6" local Fusion = require(ReplicatedStorage.Fusion) @@ -131,7 +131,8 @@ local thing3 = Fusion.Value(scope, "i am thing 3") doCleanup(scope) ``` -You can now rewrite those constructors as method calls. +You can now rewrite those constructors as method calls. The `scope` argument is +inferred for you. ```Lua linenums="2" hl_lines="7-9" local Fusion = require(ReplicatedStorage.Fusion) @@ -147,9 +148,8 @@ local thing3 = scope:Value("i am thing 3") doCleanup(scope) ``` -This makes code shorter, cleaner and consistent. You import fewer things, names -are consistently positioned and more visually parseable, and it is easier to -move and copy code. +This strongly ties your constructors to your scopes, which makes it much harder +to mess up or circumvent them. It also makes code read much more naturally. Try passing `Fusion` to `scoped()` rather than listing functions manually. Because `Fusion` already contains those functions, it works too. @@ -166,7 +166,44 @@ local thing3 = scope:Value("i am thing 3") doCleanup(scope) ``` -!!! tip "This syntax is recommended" +This gives you access to all of Fusion's constructors without having to import +each one manually. + +??? tip "Merging libraries together" + If you use multiple libraries supporting scopes, you can mix functions from + both in your `scoped` call. + + ```Lua + local scope = scoped({ + Foo = LibraryA.Foo, + Bar = LibraryB.Bar + }) + + local foo = scope:Foo() + local bar = scope:Bar() + ``` + + You can automatically generate this merged table if desired. However, be + aware that libraries might use the same name for different things. + + The following code is *one way* of dealing with this: + + ```Lua + local everything = {} + for name, member in {LibraryA, LibraryB} do + if everything[name] ~= nil then + error("Two libraries contain '" .. name .. "' - they can't be merged.") + else + everything[name] = member + end + end + + -- later... + local scope = scope(everything) + ``` + + +!!! success "This syntax is recommended" It is recommended to use `scoped()` syntax. However, it is technically optional; if it does not work for your codebase requirements, the barebones syntax will always be available. From c9283f055c3aec53074af32716931acb6c082708 Mon Sep 17 00:00:00 2001 From: Elttob Date: Wed, 27 Dec 2023 16:17:44 +0000 Subject: [PATCH 142/287] Swap order of admonitions --- docs/tutorials/fundamentals/objects.md | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/docs/tutorials/fundamentals/objects.md b/docs/tutorials/fundamentals/objects.md index 45a3db05d..0958e29a6 100644 --- a/docs/tutorials/fundamentals/objects.md +++ b/docs/tutorials/fundamentals/objects.md @@ -169,6 +169,13 @@ doCleanup(scope) This gives you access to all of Fusion's constructors without having to import each one manually. +!!! success "This syntax is recommended" + It is recommended to use `scoped()` syntax. However, it is technically + optional; if it does not work for your codebase requirements, the barebones + syntax will always be available. + + From now on, you'll see this `scoped()` syntax used throughout the tutorials. + ??? tip "Merging libraries together" If you use multiple libraries supporting scopes, you can mix functions from both in your `scoped` call. @@ -200,12 +207,4 @@ each one manually. -- later... local scope = scope(everything) - ``` - - -!!! success "This syntax is recommended" - It is recommended to use `scoped()` syntax. However, it is technically - optional; if it does not work for your codebase requirements, the barebones - syntax will always be available. - - From now on, you'll see this `scoped()` syntax used throughout the tutorials. \ No newline at end of file + ``` \ No newline at end of file From dbbf8e6491d9871242baeb77c28d2c66a1b043b9 Mon Sep 17 00:00:00 2001 From: Elttob Date: Wed, 27 Dec 2023 16:35:09 +0000 Subject: [PATCH 143/287] Update ForValues tutorial to use scopes --- docs/tutorials/tables/forvalues.md | 160 +++++++++-------------------- 1 file changed, 50 insertions(+), 110 deletions(-) diff --git a/docs/tutorials/tables/forvalues.md b/docs/tutorials/tables/forvalues.md index 469adac24..9777b94c2 100644 --- a/docs/tutorials/tables/forvalues.md +++ b/docs/tutorials/tables/forvalues.md @@ -1,5 +1,4 @@ -`ForValues` is a state object that creates a new table by processing values from -another table. +`ForValues` is a state object that processes values read from another table. The input table can be a state object, and the output values can use state objects. @@ -22,49 +21,35 @@ print(multiplied:get()) --> {10, 20, 30, 40, 50} ## Usage -To use `ForValues` in your code, you first need to import it from the Fusion -module, so that you can refer to it by name: - -```Lua linenums="1" hl_lines="2" -local Fusion = require(ReplicatedStorage.Fusion) -local ForValues = Fusion.ForValues -``` - -### Basic Usage - To create a new `ForValues` object, call the constructor with an input table and -a processor function: +a processor function. The first two arguments are `use` and `scope`, just like +[computed objects](../fundamentals/computeds). The third argument is one of the +values read from the input table. ```Lua local numbers = {1, 2, 3, 4, 5} -local doubled = ForValues(numbers, function(use, num) +local doubled = scope:ForValues(numbers, function(use, scope, num) return num * 2 end) ``` -This will generate a new table of values, where each value is passed through the -processor function. The first argument is `use`, similar to a computed, and the -second argument is one of the values from the input table. - -You can read the processed table using `peek()`: +You can read the table of processed values using `peek()`: ```Lua hl_lines="6" local numbers = {1, 2, 3, 4, 5} -local doubled = ForValues(numbers, function(use, num) +local doubled = scope:ForValues(numbers, function(use, scope, num) return num * 2 end) print(peek(doubled)) --> {2, 4, 6, 8, 10} ``` -### State Objects - -The input table can be provided as a state object instead, and the output table -will update as the input table is changed: +The input table can be a state object. When the input table changes, the output +will update. ```Lua -local numbers = Value({}) -local doubled = ForValues(numbers, function(use, num) +local numbers = scope:Value({}) +local doubled = scope:ForValues(numbers, function(use, scope, num) return num * 2 end) @@ -75,13 +60,12 @@ numbers:set({5, 15, 25}) print(peek(doubled)) --> {10, 30, 50} ``` -Additionally, you can `use()` state objects in your calculations, just like a -computed: +You can also `use()` state objects in your calculations, just like a computed. ```Lua local numbers = {1, 2, 3, 4, 5} -local factor = Value(2) -local multiplied = ForValues(numbers, function(use, num) +local factor = scope:Value(2) +local multiplied = scope:ForValues(numbers, function(use, scope, num) return num * use(factor) end) @@ -91,94 +75,50 @@ factor:set(10) print(peek(multiplied)) --> {10, 20, 30, 40, 50} ``` -### Cleanup Behaviour - -Similar to computeds, if you want to run your own code when values are removed, -you can pass in a second 'destructor' function: - -```Lua hl_lines="9-13" -local names = Value({"Jodi", "Amber", "Umair"}) -local textLabels = ForValues(names, - -- processor - function(use, name) - return New "TextLabel" { - Text = name - } - end, - -- destructor - function(textLabel) - print("Destructor got text label:", textLabel.Text) - textLabel:Destroy() -- don't forget we're overriding the default cleanup - end -) - --- remove Jodi from the names list --- this will run the destructor with Jodi's TextLabel -names:set({"Amber", "Umair"}) --> Destructor got text label: Jodi -``` - -When using a custom destructor, you can send one extra return value to your -destructor without including it in the output table: - -```Lua hl_lines="11 14" -local names = Value({"Jodi", "Amber", "Umair"}) -local textLabels = ForValues(names, - -- processor - function(use, name) - local textLabel = New "TextLabel" { - Text = name - } - local uppercased = name:upper() - -- `textLabel` will be included in the output table - -- `uppercased` is not included, but still passed to the destructor - return textLabel, uppercased - end, - -- destructor - function(textLabel, uppercased) - print("Destructor got uppercased:", uppercased) - textLabel:Destroy() - end -) - -names:set({"Amber", "Umair"}) --> Destructor got uppercased: JODI -``` - ------ +Anything added to the `scope` is cleaned up for you when the processed value is +removed. -## Optimisations +```Lua +local names = scope:Value({"Jodi", "Amber", "Umair"}) +local shoutingNames = scope:ForValues(names, function(use, scope, name) + table.insert(scope, function() + print("Goodbye, " .. name .. "!") + end) + return string.upper(name) +end) -!!! help "Optional" - You don't have to memorise these optimisations to use `ForValues`, but it - can be helpful if you have a performance problem. +names:set({"Amber", "Umair"}) --> Goodbye, Jodi! +``` -Rather than creating a new output table from scratch every time the input table -is changed, `ForValues` will try and reuse as much as possible to improve -performance. +??? tip "How ForValues optimises your code" + Rather than creating a new output table from scratch every time the input table + is changed, `ForValues` will try and reuse as much as possible to improve + performance. -For example, let's say we're measuring the lengths of an array of words: + Say you're measuring the lengths of an array of words: -```Lua -local words = Value({"Orange", "Red", "Magenta"}) -local lengths = ForValues(words, function(use, word) - return #word -end) + ```Lua + local words = scope:Value({"Orange", "Red", "Magenta"}) + local lengths = scope:ForValues(words, function(use, scope, word) + return #word + end) -print(peek(lengths)) --> {6, 3, 7} -``` + print(peek(lengths)) --> {6, 3, 7} + ``` -The word lengths don't depend on the position of the word in the array. This -means that rearranging the words in the input array will just rearrange the -lengths in the output array: + The word lengths don't depend on the position of the word in the array. This + means that rearranging the words in the input array will just rearrange the + lengths in the output array: -![A diagram visualising how the values move around.](Optimisation-Reordering-Dark.svg#only-dark) -![A diagram visualising how the values move around.](Optimisation-Reordering-Light.svg#only-light) + ![A diagram visualising how the values move around.](Optimisation-Reordering-Dark.svg#only-dark) + ![A diagram visualising how the values move around.](Optimisation-Reordering-Light.svg#only-light) -`ForValues` takes advantage of this - when input values move around, the output -values will move around too, instead of being recalculated. + `ForValues` takes advantage of this - when input values move around, the output + values will move around too, instead of being recalculated. -Note that values are only reused once. For example, if you added another -occurence of 'Orange', your calculation would have to run again for the second -'Orange': + Note that values are only reused once. For example, if you added another + occurence of 'Orange', your calculation would have to run again for the second + 'Orange': -![A diagram visualising how values aren't reused when duplicates appear.](Optimisation-Duplicates-Dark.svg#only-dark) -![A diagram visualising how values aren't reused when duplicates appear.](Optimisation-Duplicates-Light.svg#only-light) \ No newline at end of file + ![A diagram visualising how values aren't reused when duplicates appear.](Optimisation-Duplicates-Dark.svg#only-dark) + ![A diagram visualising how values aren't reused when duplicates appear.](Optimisation-Duplicates-Light.svg#only-light) \ No newline at end of file From a94902484bd0d032db105ecf61ecab1784e8f8c5 Mon Sep 17 00:00:00 2001 From: Elttob Date: Wed, 27 Dec 2023 16:37:06 +0000 Subject: [PATCH 144/287] Improve wording of ForValues intro --- docs/tutorials/tables/forvalues.md | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/docs/tutorials/tables/forvalues.md b/docs/tutorials/tables/forvalues.md index 9777b94c2..931825aef 100644 --- a/docs/tutorials/tables/forvalues.md +++ b/docs/tutorials/tables/forvalues.md @@ -1,7 +1,6 @@ -`ForValues` is a state object that processes values read from another table. +`ForValues` is a state object that processes values from another table. -The input table can be a state object, and the output values can use state -objects. +It supports both constants and state objects. ```Lua local numbers = {1, 2, 3, 4, 5} From 8f223928a368c395b75d3aee375d441ce44458b1 Mon Sep 17 00:00:00 2001 From: Elttob Date: Wed, 27 Dec 2023 16:52:37 +0000 Subject: [PATCH 145/287] Fix outdated syntax in ForPairs tutorial --- docs/tutorials/tables/forvalues.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/tutorials/tables/forvalues.md b/docs/tutorials/tables/forvalues.md index 931825aef..c7239822d 100644 --- a/docs/tutorials/tables/forvalues.md +++ b/docs/tutorials/tables/forvalues.md @@ -10,10 +10,10 @@ local multiplied = ForValues(numbers, function(use, num) return num * use(multiplier) end) -print(multiplied:get()) --> {2, 4, 6, 8, 10} +print(peek(multiplied)) --> {2, 4, 6, 8, 10} multiplier:set(10) -print(multiplied:get()) --> {10, 20, 30, 40, 50} +print(peek(multiplied)) --> {10, 20, 30, 40, 50} ``` ----- From 2f964614bb95800c1962fbd1f53e86d4225c1609 Mon Sep 17 00:00:00 2001 From: Elttob Date: Wed, 27 Dec 2023 17:17:48 +0000 Subject: [PATCH 146/287] Update ForKeys tutorial with scopes --- docs/tutorials/tables/forkeys.md | 196 +++++++++++-------------------- 1 file changed, 66 insertions(+), 130 deletions(-) diff --git a/docs/tutorials/tables/forkeys.md b/docs/tutorials/tables/forkeys.md index 0184e2007..e69a3ac88 100644 --- a/docs/tutorials/tables/forkeys.md +++ b/docs/tutorials/tables/forkeys.md @@ -1,7 +1,6 @@ -`ForKeys` is a state object that creates a new table by processing keys from -another table. +`ForKeys` is a state object that processes keys from another table. -The input table can be a state object, and the output keys can use state objects. +It supports both constants and state objects. ```Lua local data = {Red = "foo", Blue = "bar"} @@ -21,173 +20,110 @@ print(peek(renamed)) --> {colourRed = "foo", colourBlue = "bar"} ## Usage -To use `ForKeys` in your code, you first need to import it from the Fusion -module, so that you can refer to it by name: - -```Lua linenums="1" hl_lines="2" -local Fusion = require(ReplicatedStorage.Fusion) -local ForKeys = Fusion.ForKeys -``` - -### Basic Usage - To create a new `ForKeys` object, call the constructor with an input table and -a processor function: +a processor function. The first two arguments are `use` and `scope`, just like +[computed objects](../fundamentals/computeds.md). The third argument is one of +the keys read from the input table. ```Lua local data = {red = "foo", blue = "bar"} -local renamed = ForKeys(data, function(use, key) +local renamed = scope:ForKeys(data, function(use, scope, key) return string.upper(key) end) ``` -This will generate a new table, where each key is replaced using the processor -function. The first argument is `use`, similar to a computed, and the -second argument is one of the keys from the input table. - -You can read the processed table using `peek()`: +You can read the table of processed keys using `peek()`: ```Lua hl_lines="6" local data = {red = "foo", blue = "bar"} -local renamed = ForKeys(data, function(use, key) +local renamed = scope:ForKeys(data, function(use, scope, key) return string.upper(key) end) print(peek(renamed)) --> {RED = "foo", BLUE = "bar"} ``` -### State Objects - -The input table can be provided as a state object instead, and the output table -will update as the input table is changed: +The input table can be a state object. When the input table changes, the output +will update. ```Lua -local playerSet = Value({}) -local userIdSet = ForKeys(playerSet, function(use, player) - return player.UserId +local foodSet = scope:Value({}) + +local prefixes = { pie = "tasty", chocolate = "yummy", broccoli = "gross" } +local renamedFoodSet = scope:ForKeys(foodSet, function(use, scope, food) + return prefixes[food] .. food end) -playerSet:set({ [Players.Elttob] = true }) -print(peek(userIdSet)) --> {[1670764] = true} +foodSet:set({ pie = true }) +print(peek(renamedFoodSet)) --> { tasty_pie = true } -playerSet:set({ [Players.boatbomber] = true, [Players.EgoMoose] = true }) -print(peek(userIdSet)) --> {[33655127] = true, [2155311] = true} +foodSet:set({ broccoli = true, chocolate = true }) +print(peek(renamedFoodSet)) --> { gross_broccoli = true, yummy_chocolate = true } ``` -Additionally, you can `use()` state objects in your calculations, just like a -computed: +You can also `use()` state objects in your calculations, just like a computed. ```Lua -local playerSet = { [Players.boatbomber] = true, [Players.EgoMoose] = true } -local prefix = Value("User_") -local userIdSet = ForKeys(playerSet, function(use, player) - return use(prefix) .. player.UserId +local foodSet = scope:Value({ broccoli = true, chocolate = true }) + +local prefixes = { chocolate = "yummy", broccoli = scope:Value("gross") } +local renamedFoodSet = scope:ForKeys(foodSet, function(use, scope, food) + return use(prefixes[food]) .. food end) -print(peek(userIdSet)) --> {User_33655127 = true, User_2155311 = true} +print(peek(renamedFoodSet)) --> { gross_broccoli = true, yummy_chocolate = true } -prefix:set("player") -print(peek(userIdSet)) --> {player33655127 = true, player2155311 = true} +prefixes.broccoli:set("scrumptious") +print(peek(renamedFoodSet)) --> { scrumptious_broccoli = true, yummy_chocolate = true } ``` -### Cleanup Behaviour - -Similar to computeds, if you want to run your own code when values are removed, -you can pass in a second 'destructor' function: - -```Lua hl_lines="15-19" -local eventSet = Value({ - [RunService.RenderStepped] = true, - [RunService.Heartbeat] = true -}) - -local connectionSet = ForKeys(eventSet, - -- processor - function(use, event) - local eventName = tostring(event) - local connection = event:Connect(function(...) - print(eventName, "fired with arguments:", ...) - end) - return connection - end, - -- destructor - function(connection) - print("Disconnecting the event!") - connection:Disconnect() -- don't forget we're overriding the default cleanup - end -) - --- remove Heartbeat from the event set --- this will run the destructor with the Heartbeat connection -eventSet:set({ [RunService.RenderStepped] = true }) --> Disconnecting the event! -``` +Anything added to the `scope` is cleaned up for you when the processed key is +removed. -When using a custom destructor, you can send one extra return value to your -destructor without including it in the output table: - -```Lua hl_lines="13 16" -local eventSet = Value({ - [RunService.RenderStepped] = true, - [RunService.Heartbeat] = true -}) - -local connectionSet = ForKeys(eventSet, - -- processor - function(use, event) - local eventName = tostring(event) - local connection = event:Connect(function(...) - print(eventName, "fired with arguments:", ...) - end) - return connection, eventName - end, - -- destructor - function(connection, eventName) - print("Disconnecting " .. eventName .. "!") - connection:Disconnect() - end -) - -eventSet:set({ [RunService.RenderStepped] = true }) --> Disconnecting Signal Heartbeat! -``` +```Lua +local foodSet = scope:Value({ broccoli = true, chocolate = true }) ------ +local shoutingFoodSet = scope:ForKeys(names, function(use, scope, food) + table.insert(scope, function() + print("I ate the " .. food .. "!") + end) + return string.upper(food) +end) -## Optimisations +names:set({ chocolate = true }) --> I ate the broccoli! +``` -!!! help "Optional" - You don't have to memorise these optimisations to use `ForKeys`, but it - can be helpful if you have a performance problem. +??? tip "How ForKeys optimises your code" + Rather than creating a new output table from scratch every time the input table + is changed, `ForKeys` will try and reuse as much as possible to improve + performance. -Rather than creating a new output table from scratch every time the input table -is changed, `ForKeys` will try and reuse as much as possible to improve -performance. + Say you're converting an array to a dictionary: -For example, let's say we're converting an array to a dictionary: + ```Lua + local array = Value({"Fusion", "Knit", "Matter"}) + local dict = scope:ForKeys(array, function(use, scope, index) + return "Value" .. index + end) -```Lua -local array = Value({"Fusion", "Knit", "Matter"}) -local dict = ForKeys(array, function(use, index) - return "Value" .. index -end) + print(peek(dict)) --> {Value1 = "Fusion", Value2 = "Knit", Value3 = "Matter"} + ``` -print(peek(dict)) --> {Value1 = "Fusion", Value2 = "Knit", Value3 = "Matter"} -``` + Because `ForKeys` only operates on the keys, changing the values in the array + doesn't affect the keys. Keys are only added or removed as needed: -Because `ForKeys` only operates on the keys, changing the values in the array -doesn't affect the keys. Keys are only added or removed as needed: + ```Lua + local array = Value({"Fusion", "Knit", "Matter"}) + local dict = scope:ForKeys(array, function(use, scope, index) + return "Value" .. index + end) -```Lua -local array = Value({"Fusion", "Knit", "Matter"}) -local dict = ForKeys(array, function(use, index) - return "Value" .. index -end) + print(peek(dict)) --> {Value1 = "Fusion", Value2 = "Knit", Value3 = "Matter"} -print(peek(dict)) --> {Value1 = "Fusion", Value2 = "Knit", Value3 = "Matter"} - -array:set({"Roact", "Rodux"}) -print(peek(dict)) --> {Value1 = "Roact", Value2 = "Rodux"} -``` + array:set({"Roact", "Rodux", "Promise"}) + print(peek(dict)) --> {Value1 = "Roact", Value2 = "Rodux", Value3 = "Promise"} + ``` -`ForKeys` takes advantage of this - when a value changes, it's copied into the -output table without recalculating the key. Keys are only calculated when a -value is assigned to a new key. \ No newline at end of file + `ForKeys` takes advantage of this - when a value changes, it's copied into the + output table without recalculating the key. Keys are only calculated when a + value is assigned to a new key. \ No newline at end of file From cb60e65fb0611836eb293980daa1d78bcf440abb Mon Sep 17 00:00:00 2001 From: Elttob Date: Wed, 27 Dec 2023 17:25:35 +0000 Subject: [PATCH 147/287] Fix missing scopes in ForKeys tutorial --- docs/tutorials/tables/forkeys.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/tutorials/tables/forkeys.md b/docs/tutorials/tables/forkeys.md index e69a3ac88..52321662b 100644 --- a/docs/tutorials/tables/forkeys.md +++ b/docs/tutorials/tables/forkeys.md @@ -4,9 +4,9 @@ It supports both constants and state objects. ```Lua local data = {Red = "foo", Blue = "bar"} -local prefix = Value("Key_") +local prefix = scope:Value("Key_") -local renamed = ForKeys(data, function(use, key) +local renamed = scope:ForKeys(data, function(use, key) return use(prefix) .. key end) @@ -101,7 +101,7 @@ names:set({ chocolate = true }) --> I ate the broccoli! Say you're converting an array to a dictionary: ```Lua - local array = Value({"Fusion", "Knit", "Matter"}) + local array = scope:Value({"Fusion", "Knit", "Matter"}) local dict = scope:ForKeys(array, function(use, scope, index) return "Value" .. index end) @@ -113,7 +113,7 @@ names:set({ chocolate = true }) --> I ate the broccoli! doesn't affect the keys. Keys are only added or removed as needed: ```Lua - local array = Value({"Fusion", "Knit", "Matter"}) + local array = scope:Value({"Fusion", "Knit", "Matter"}) local dict = scope:ForKeys(array, function(use, scope, index) return "Value" .. index end) From 4fe1eb71dac59035187f19c275bdb47831790221 Mon Sep 17 00:00:00 2001 From: Elttob Date: Wed, 27 Dec 2023 18:03:53 +0000 Subject: [PATCH 148/287] Update ForPairs tutorial to use scopes --- docs/tutorials/tables/forpairs.md | 159 ++++++++++++++---------------- 1 file changed, 73 insertions(+), 86 deletions(-) diff --git a/docs/tutorials/tables/forpairs.md b/docs/tutorials/tables/forpairs.md index 6983eb158..839938553 100644 --- a/docs/tutorials/tables/forpairs.md +++ b/docs/tutorials/tables/forpairs.md @@ -1,135 +1,122 @@ -`ForPairs` combines the functions of `ForValues` and `ForKeys` into one object. -It can process pairs of keys and values at the same time. +`ForPairs` is like `ForValues` and `ForKeys` in one object. It can process pairs +of keys and values at the same time. -The input table can be a state object, and the output values can use state -objects. +It supports both constants and state objects. ```Lua local itemColours = { shoes = "red", socks = "blue" } -local owner = Value("Elttob") +local owner = scope:Value("Janet") -local manipulated = ForPairs(itemColours, function(use, thing, colour) +local manipulated = scope:ForPairs(itemColours, function(use, scope, thing, colour) local newKey = colour local newValue = use(owner) .. "'s " .. thing return newKey, newValue end) -print(peek(manipulated)) --> {red = "Elttob's shoes", blue = "Elttob's socks"} +print(peek(manipulated)) --> {red = "Janet's shoes", blue = "Janet's socks"} -owner:set("Quenty") -print(peek(manipulated)) --> {red = "Quenty's shoes", blue = "Quenty's socks"} +owner:set("April") +print(peek(manipulated)) --> {red = "April's shoes", blue = "April's socks"} ``` ----- ## Usage -To use `ForPairs` in your code, you first need to import it from the Fusion -module, so that you can refer to it by name: - -```Lua linenums="1" hl_lines="2" -local Fusion = require(ReplicatedStorage.Fusion) -local ForPairs = Fusion.ForPairs -``` - -### Basic Usage - To create a new `ForPairs` object, call the constructor with an input table and -a processor function: +a processor function. The first two arguments are `use` and `scope`, just like +[computed objects](../fundamentals/computeds). The third and fourth arguments +are one of the key-value pairs read from the input table. ```Lua local itemColours = { shoes = "red", socks = "blue" } -local swapped = ForPairs(data, function(use, key, value) - return value, key +local swapped = scope:ForPairs(data, function(use, scope, item, colour) + return colour, item end) ``` -This will generate a new table, where each key-value pair is replaced using the -processor function. The first argument is `use`, similar to a computed, and the -second/third arguments are a key/value pair from the input table. - You can read the processed table using `peek()`: ```Lua hl_lines="6" local itemColours = { shoes = "red", socks = "blue" } -local swapped = ForPairs(data, function(use, key, value) - return value, key +local swapped = scope:ForPairs(data, function(use, scope, item, colour) + return colour, item end) -print(peek(swapped)) --> {red = "shoes", blue = "socks"} +print(peek(swapped)) --> { red = "shoes", blue = "socks" } ``` -### State Objects +The input table can be a state object. When the input table changes, the output +will update. + +```Lua +local itemColours = scope:Value({ shoes = "red", socks = "blue" }) +local swapped = scope:ForPairs(data, function(use, scope, item, colour) + return colour, item +end) -As with `ForKeys` and `ForValues`, the input table can be provided as a state -object, and the processor function can `use()` other state objects in its -calculations. [See the ForValues page for examples.](./forvalues.md#state-objects) +print(peek(swapped)) --> { red = "shoes", blue = "socks" } -### Cleanup Behaviour +itemColours:set({ sandals = "red", socks = "green" }) +print(peek(swapped)) --> { red = "sandals", green = "socks" } +``` -Similar to `ForValues` and `ForKeys`, you may pass in a 'destructor' function to -add cleanup behaviour, and send your own metadata to it: +You can also `use()` state objects in your calculations, just like a computed. ```Lua -local watchedInstances = Value({ - [workspace.Part1] = "One", - [workspace.Part2] = "Two", - [workspace.Part3] = "Three" -}) - -local connectionSet = ForPairs(eventSet, - -- processor - function(use, instance, displayName) - local metadata = { displayName = displayName, numChanges = 0 } - local connection = instance.Changed:Connect(function() - print("Instance", displayName, "was changed!") - metadata.numChanges += 1 - end) - return instance, connection, metadata - end, - -- destructor - function(instance, connection, metadata) - print("Removing", metadata.displayName, "after", metadata.numChanges, "changes") - connection:Disconnect() -- don't forget we're overriding the default cleanup +local itemColours = { shoes = "red", socks = "blue" } + +local shouldSwap = scope:Value(false) +local swapped = scope:ForPairs(data, function(use, scope, item, colour) + if use(shouldSwap) then + return colour, item + else + return item, colour end -) - --- remove Part3 from the input table --- this will run the destructor with Part3, its Changed event, and its metadata -watchedInstances:set({ - [workspace.Part1] = "One", - [workspace.Part2] = "Two" -}) +end) + +print(peek(swapped)) --> { shoes = "red", socks = "blue" } + +shouldSwap:set(true) +print(peek(swapped)) --> { red = "shoes", blue = "socks" } ``` ------ +Anything added to the `scope` is cleaned up for you when either the processed +key or the processed value is removed. -## Optimisations +```Lua +local itemColours = scope:Value({ shoes = "red", socks = "blue" }) +local swapped = scope:ForPairs(data, function(use, scope, item, colour) + table.insert(scope, function() + print("No longer wearing " .. colour .. " " .. item) + end) + return colour, item +end) -!!! help "Optional" - You don't have to memorise these optimisations to use `ForPairs`, but it - can be helpful if you have a performance problem. +itemColours:set({ shoes = "red", socks = "green" }) --> No longer wearing blue socks +``` -Rather than creating a new output table from scratch every time the input table -is changed, `ForPairs` will try and reuse as much as possible to improve -performance. +??? tip "How ForPairs optimises your code" + Rather than creating a new output table from scratch every time the input table + is changed, `ForPairs` will try and reuse as much as possible to improve + performance. -Since `ForPairs` has to depend on both keys and values, changing any value in -the input table will cause a recalculation for that key-value pair. + Since `ForPairs` has to depend on both keys and values, changing any value in + the input table will cause a recalculation for that key-value pair. -![A diagram showing values of keys changing in a table.](Optimisation-KeyValueChange-Dark.svg#only-dark) -![A diagram showing values of keys changing in a table.](Optimisation-KeyValueChange-Light.svg#only-light) + ![A diagram showing values of keys changing in a table.](Optimisation-KeyValueChange-Dark.svg#only-dark) + ![A diagram showing values of keys changing in a table.](Optimisation-KeyValueChange-Light.svg#only-light) -Inversely, `ForPairs` won't recalculate any key-value pairs that stay the same. -Instead, these will be preserved in the output table. + Inversely, `ForPairs` won't recalculate any key-value pairs that stay the same. + Instead, these will be preserved in the output table. -![A diagram showing values of keys staying the same in a table.](Optimisation-KeyValuePreserve-Dark.svg#only-dark) -![A diagram showing values of keys staying the same in a table.](Optimisation-KeyValuePreserve-Light.svg#only-light) + ![A diagram showing values of keys staying the same in a table.](Optimisation-KeyValuePreserve-Dark.svg#only-dark) + ![A diagram showing values of keys staying the same in a table.](Optimisation-KeyValuePreserve-Light.svg#only-light) -If you don't need the keys or the values, Fusion can offer better optimisations. -For example, if you're working with an array of values where position doesn't -matter, [ForValues can move values between keys.](./forvalues.md#optimisations) + If you don't need the keys or the values, Fusion can offer better optimisations. + For example, if you're working with an array of values where position doesn't + matter, [ForValues can move values between keys.](./forvalues.md#optimisations) -Alternatively, if you're working with a set of objects stored in keys, and don't -need the values in the table, -[ForKeys will ignore the values for optimal performance.](./forkeys.md#optimisations) \ No newline at end of file + Alternatively, if you're working with a set of objects stored in keys, and don't + need the values in the table, + [ForKeys will ignore the values for optimal performance.](./forkeys.md#optimisations) \ No newline at end of file From 93788b242e8637614354f828fe227d29b19e718c Mon Sep 17 00:00:00 2001 From: Elttob Date: Wed, 27 Dec 2023 18:08:48 +0000 Subject: [PATCH 149/287] Fix links in For tutorials --- docs/tutorials/tables/forkeys.md | 2 +- docs/tutorials/tables/forpairs.md | 6 +++--- docs/tutorials/tables/forvalues.md | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/docs/tutorials/tables/forkeys.md b/docs/tutorials/tables/forkeys.md index 52321662b..af73778af 100644 --- a/docs/tutorials/tables/forkeys.md +++ b/docs/tutorials/tables/forkeys.md @@ -22,7 +22,7 @@ print(peek(renamed)) --> {colourRed = "foo", colourBlue = "bar"} To create a new `ForKeys` object, call the constructor with an input table and a processor function. The first two arguments are `use` and `scope`, just like -[computed objects](../fundamentals/computeds.md). The third argument is one of +[computed objects](../../fundamentals/computeds). The third argument is one of the keys read from the input table. ```Lua diff --git a/docs/tutorials/tables/forpairs.md b/docs/tutorials/tables/forpairs.md index 839938553..e9786ffc9 100644 --- a/docs/tutorials/tables/forpairs.md +++ b/docs/tutorials/tables/forpairs.md @@ -25,7 +25,7 @@ print(peek(manipulated)) --> {red = "April's shoes", blue = "April's socks"} To create a new `ForPairs` object, call the constructor with an input table and a processor function. The first two arguments are `use` and `scope`, just like -[computed objects](../fundamentals/computeds). The third and fourth arguments +[computed objects](../../fundamentals/computeds). The third and fourth arguments are one of the key-value pairs read from the input table. ```Lua @@ -115,8 +115,8 @@ itemColours:set({ shoes = "red", socks = "green" }) --> No longer wearing blue s If you don't need the keys or the values, Fusion can offer better optimisations. For example, if you're working with an array of values where position doesn't - matter, [ForValues can move values between keys.](./forvalues.md#optimisations) + matter, [ForValues can move values between keys.](./forvalues.md) Alternatively, if you're working with a set of objects stored in keys, and don't need the values in the table, - [ForKeys will ignore the values for optimal performance.](./forkeys.md#optimisations) \ No newline at end of file + [ForKeys will ignore the values for optimal performance.](./forkeys.md) \ No newline at end of file diff --git a/docs/tutorials/tables/forvalues.md b/docs/tutorials/tables/forvalues.md index c7239822d..e6fe9bcd9 100644 --- a/docs/tutorials/tables/forvalues.md +++ b/docs/tutorials/tables/forvalues.md @@ -22,7 +22,7 @@ print(peek(multiplied)) --> {10, 20, 30, 40, 50} To create a new `ForValues` object, call the constructor with an input table and a processor function. The first two arguments are `use` and `scope`, just like -[computed objects](../fundamentals/computeds). The third argument is one of the +[computed objects](../../fundamentals/computeds). The third argument is one of the values read from the input table. ```Lua From 8039ff20b313098436aaa4803646ca31e06ab8d7 Mon Sep 17 00:00:00 2001 From: Elttob Date: Wed, 27 Dec 2023 18:10:13 +0000 Subject: [PATCH 150/287] Fix logo alignment when not using mike --- docs/assets/theme/page.css | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/assets/theme/page.css b/docs/assets/theme/page.css index 0659e02f3..885d06918 100644 --- a/docs/assets/theme/page.css +++ b/docs/assets/theme/page.css @@ -169,6 +169,10 @@ margin-right: 0rem; } +.md-header__title .md-header__topic:first-child { + height: 2.4rem; +} + .md-header__title .md-header__topic:first-child::before { content: ""; display: inline-block; From daa06a12fd004a9f4d22b158e2eb2e952f81e34d Mon Sep 17 00:00:00 2001 From: Elttob Date: Wed, 27 Dec 2023 22:28:51 +0000 Subject: [PATCH 151/287] Fix project no longer using benchmark --- test-runner.project.json | 3 --- 1 file changed, 3 deletions(-) diff --git a/test-runner.project.json b/test-runner.project.json index 7786390c0..7c51d64ad 100644 --- a/test-runner.project.json +++ b/test-runner.project.json @@ -8,9 +8,6 @@ "Fusion": { "$path": "src" }, - "FusionBench": { - "$path": "benchmark" - }, "FusionTest": { "$path": "test" } From ec06ca7a65cad8452bd35ba7597c0af79bef5fdf Mon Sep 17 00:00:00 2001 From: Elttob Date: Wed, 27 Dec 2023 22:51:57 +0000 Subject: [PATCH 152/287] This code will surely land me in therapy --- src/Logging/messages.lua | 1 + src/Memory/scoped.lua | 46 +++++++++++++++++++++--- test/Memory/scoped.spec.lua | 72 +++++++++++++++++++++++++++++++++++++ 3 files changed, 114 insertions(+), 5 deletions(-) create mode 100644 test/Memory/scoped.spec.lua diff --git a/src/Logging/messages.lua b/src/Logging/messages.lua index f4c2892af..830c690d3 100644 --- a/src/Logging/messages.lua +++ b/src/Logging/messages.lua @@ -33,6 +33,7 @@ return { mistypedSpringDamping = "The damping ratio for a spring must be a number. (got a %s)", mistypedSpringSpeed = "The speed of a spring must be a number. (got a %s)", mistypedTweenInfo = "The tween info of a tween must be a TweenInfo. (got a %s)", + mergeConflict = "Multiple definitions for '%s' found while merging.", noTaskScheduler = "Fusion is not connected to an external task scheduler.", possiblyOutlives = "%s could be destroyed before %s; review the order they're created in, and what scopes they belong to. See discussion #292 on GitHub for advice.", scopeMissing = "To create %s, provide a scope. (e.g. `%s`). See discussion #292 on GitHub for advice.", diff --git a/src/Memory/scoped.lua b/src/Memory/scoped.lua index 635ca12f6..779096a6d 100644 --- a/src/Memory/scoped.lua +++ b/src/Memory/scoped.lua @@ -6,11 +6,47 @@ local Package = script.Parent.Parent local PubTypes = require(Package.PubTypes) +local logError = require(Package.Logging.logError) --- This return type is technically a lie, but it's required for useful type --- checking behaviour. -local function scoped(constructors: T): PubTypes.Scope - return setmetatable({}, {__index = constructors}) :: any +local function merge( + into: any, + from: any?, + ...: any +): any + if from == nil then + return into + else + for key, value in from do + if into[key] == nil then + into[key] = value + else + logError("mergeConflict", nil, tostring(key)) + end + end + return merge(into, ...) + end end -return scoped \ No newline at end of file +local function scoped( + ...: any +): any + return setmetatable({}, {__index = merge({}, ...)}) :: any +end + +-- Is there a sane way to write out this type? +-- ... I sure hope so. + +return (scoped :: any) :: + (() -> PubTypes.Scope<{}>) & + (
(A & {}) -> PubTypes.Scope) & + ((A & {}, B & {}) -> PubTypes.Scope) & + ((A & {}, B & {}, C & {}) -> PubTypes.Scope) & + ((A & {}, B & {}, C & {}, D & {}) -> PubTypes.Scope) & + ((A & {}, B & {}, C & {}, D & {}, E & {}) -> PubTypes.Scope) & + ((A & {}, B & {}, C & {}, D & {}, E & {}, F & {}) -> PubTypes.Scope) & + ((A & {}, B & {}, C & {}, D & {}, E & {}, F & {}, G & {}) -> PubTypes.Scope) & + ((A & {}, B & {}, C & {}, D & {}, E & {}, F & {}, G & {}, H & {}) -> PubTypes.Scope) & + ((A & {}, B & {}, C & {}, D & {}, E & {}, F & {}, G & {}, H & {}, I & {}) -> PubTypes.Scope) & + ((A & {}, B & {}, C & {}, D & {}, E & {}, F & {}, G & {}, H & {}, I & {}, J & {}) -> PubTypes.Scope) & + ((A & {}, B & {}, C & {}, D & {}, E & {}, F & {}, G & {}, H & {}, I & {}, J & {}, K & {}) -> PubTypes.Scope) & + ((A & {}, B & {}, C & {}, D & {}, E & {}, F & {}, G & {}, H & {}, I & {}, J & {}, K & {}, L & {}) -> PubTypes.Scope) \ No newline at end of file diff --git a/test/Memory/scoped.spec.lua b/test/Memory/scoped.spec.lua new file mode 100644 index 000000000..8b7a5472f --- /dev/null +++ b/test/Memory/scoped.spec.lua @@ -0,0 +1,72 @@ +local Package = game:GetService("ReplicatedStorage").Fusion +local scoped = require(Package.Memory.scoped) + +return function() + -- it("should accept zero arguments", function() + -- local merged = merge() + + -- expect(merged).to.be.a("table") + -- expect(#merged).to.equal(0) + -- end) + + -- it("should clone single arguments", function() + -- local original = {foo = "FOO", bar = "BAR", baz = "BAZ"} + -- local merged = merge(original) + + -- expect(merged).to.be.a("table") + -- expect(merged).to.never.equal(original) + -- for key, value in original do + -- expect(merged[key]).to.equal(value) + -- end + -- end) + + -- it("should merge two arguments", function() + -- local originalA = {foo = "FOO", bar = "BAR", baz = "BAZ"} + -- local originalB = {frob = "FROB", garb = "GARB", grok = "GROK"} + -- local merged = merge(originalA, originalB) + + -- expect(merged).to.be.a("table") + -- for _, original in {originalA, originalB} do + -- expect(merged).to.never.equal(original) + -- for key, value in original do + -- expect(merged[key]).to.equal(value) + -- end + -- for key, value in original do + -- expect(merged[key]).to.equal(value) + -- end + -- for key, value in original do + -- expect(merged[key]).to.equal(value) + -- end + -- end + -- end) + + -- it("should merge three arguments", function() + -- local originalA = {foo = "FOO", bar = "BAR", baz = "BAZ"} + -- local originalB = {frob = "FROB", garb = "GARB", grok = "GROK"} + -- local originalC = {grep = "GREP", bork = "BORK", grum = "GRUM"} + -- local merged = merge(originalA, originalB, originalC) + + -- expect(merged).to.be.a("table") + -- for _, original in {originalA, originalB, originalC} do + -- expect(merged).to.never.equal(original) + -- for key, value in original do + -- expect(merged[key]).to.equal(value) + -- end + -- for key, value in original do + -- expect(merged[key]).to.equal(value) + -- end + -- for key, value in original do + -- expect(merged[key]).to.equal(value) + -- end + -- end + -- end) + + -- it("should error on collision", function() + -- expect(function() + -- local originalA = {foo = "FOO", bar = "BAR", baz = "BAZ"} + -- local originalB = {frob = "FROB", garb = "GARB", grok = "GROK"} + -- local originalC = {grep = "GREP", grok = "GROK", grum = "GRUM"} + -- merge(originalA, originalB, originalC) + -- end).to.throw("mergeConflict") + -- end) +end \ No newline at end of file From e3f405b9c669bd5190cfece6a498c1623b0290a2 Mon Sep 17 00:00:00 2001 From: Elttob Date: Wed, 27 Dec 2023 23:28:07 +0000 Subject: [PATCH 153/287] Update footer attribution --- docs/assets/theme/page.css | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/assets/theme/page.css b/docs/assets/theme/page.css index 885d06918..703bd121c 100644 --- a/docs/assets/theme/page.css +++ b/docs/assets/theme/page.css @@ -242,7 +242,7 @@ } .md-copyright::after { - content: "∙ Theme by Elttob"; + content: "∙ Theme by Daniel P H Fox"; } @media screen and (max-width: 76.1875em) { From 665a79b5225d8e7655e87b7e91eaae2b437688cb Mon Sep 17 00:00:00 2001 From: Elttob Date: Thu, 28 Dec 2023 03:04:07 +0000 Subject: [PATCH 154/287] Update components guidance --- docs/tutorials/components/reusing-ui.md | 226 +++++++++++++++++++++--- docs/tutorials/fundamentals/objects.md | 138 ++++++++++----- 2 files changed, 295 insertions(+), 69 deletions(-) diff --git a/docs/tutorials/components/reusing-ui.md b/docs/tutorials/components/reusing-ui.md index e44d15810..154f4530a 100644 --- a/docs/tutorials/components/reusing-ui.md +++ b/docs/tutorials/components/reusing-ui.md @@ -21,8 +21,19 @@ For example, consider this function, which generates a button based on some `props` the user passes in: ```Lua -local function Button(props) - return New "TextButton" { +type Dependencies = typeof(Fusion) + +local function Button( + scope: Fusion.Scope, + props: { + Position: Fusion.CanBeState?, + AnchorPoint: Fusion.CanBeState?, + Size: Fusion.CanBeState?, + LayoutOrder: Fusion.CanBeState?, + ButtonText: Fusion.CanBeState + } +) + return scope:New "TextButton" { BackgroundColor3 = Color3.new(0, 0.25, 1), Position = props.Position, AnchorPoint = props.AnchorPoint, @@ -41,11 +52,10 @@ end You can call this function later to generate as many buttons as you need: ```Lua --- this is just a regular Lua function call! -local helloBtn = Button { +local helloBtn = Button(scope, { ButtonText = "Hello", Size = UDim2.fromOffset(200, 50) -} +}) helloBtn.Parent = Players.LocalPlayer.PlayerGui.ScreenGui ``` @@ -57,6 +67,38 @@ components are functions which return a child. In the above example, `Button` is a component, because it's a function that returns a TextButton. +!!! tip "Components can be scoped, too" + You may remember that `scoped()` lets you add functions as methods, so long + as the function takes a scope as its first parameter. + + If you define your components with `(scope, props)` as its arguments - like + above - then you can add it to `scoped()` too. + + ```Lua + local scope = scoped(Fusion, { + Button = Button + }) + ``` + + This gives you the same clean syntax as all other objects in Fusion. + + ```Lua + local helloBtn = scope:Button({ + ButtonText = "Hello", + Size = UDim2.fromOffset(200, 50) + }) + ``` + + In addition, much like `New`, you can remove the parentheses for clean and + visually consistent code. + + ```Lua + local helloBtn = scope:Button { + ButtonText = "Hello", + Size = UDim2.fromOffset(200, 50) + } + ``` + ----- ## Modules @@ -74,33 +116,51 @@ Here's an example of how you could split up some components into modules: === "Main script" ```Lua linenums="1" - local PopUp = require(script.Parent.PopUp) + local Fusion = require(game:GetService("ReplicatedStorage").Fusion) + local scoped, doCleanup = Fusion.scoped, Fusion.doCleanup - local ui = New "ScreenGui" { + local scope = scoped(Fusion, { + PopUp = require(script.Parent.PopUp), + Message = require(script.Parent.Message), + Button = require(script.Parent.Button) + }) + + local ui = scope:New "ScreenGui" { -- ...some properties... - [Children] = PopUp { + [Children] = scope:PopUp { Message = "Hello, world!", DismissText = "Close" } } ``` - -=== "PopUp.lua" +=== "PopUp" ```Lua linenums="1" - local Message = require(script.Parent.Message) - local Button = require(script.Parent.Button) - - local function PopUp(props) - return New "Frame" { + local Fusion = require(game:GetService("ReplicatedStorage").Fusion) + + type Dependencies = typeof(Fusion) & { + Message: typeof(require(script.Parent.Message)), + Button: typeof(require(script.Parent.Button)), + } + + local function PopUp( + scope: Fusion.Scope, + props: { + Message: Fusion.CanBeState, + DismissText: Fusion.CanBeState + } + ) + return scope:New "Frame" { -- ...some properties... [Children] = { - Message { + scope:Message { + Scope = scope, Text = props.Message } - Button { + scope:Button { + Scope = scope, Text = props.DismissText } } @@ -110,11 +170,20 @@ Here's an example of how you could split up some components into modules: return PopUp ``` -=== "Message.lua" +=== "Message" ```Lua linenums="1" - local function Message(props) - return New "TextLabel" { + local Fusion = require(game:GetService("ReplicatedStorage").Fusion) + + type Dependencies = typeof(Fusion) + + local function Message( + scope: Fusion.Scope, + props: { + Text: Fusion.CanBeState + } + ) + return scope:New "TextLabel" { AutomaticSize = "XY", BackgroundTransparency = 1, @@ -127,11 +196,20 @@ Here's an example of how you could split up some components into modules: return Message ``` -=== "Button.lua" +=== "Button" ```Lua linenums="1" - local function Button(props) - return New "TextButton" { + local Fusion = require(game:GetService("ReplicatedStorage").Fusion) + + type Dependencies = typeof(Fusion) + + local function Button( + scope: Fusion.Scope, + props: { + Text: Fusion.CanBeState + } + ) + return scope:New "TextButton" { BackgroundColor3 = Color3.new(0.25, 0.5, 1), AutoButtonColor = true, @@ -144,7 +222,107 @@ Here's an example of how you could split up some components into modules: return Button ``` -You can further group your modules using folders if you need more organisation. +??? tip "Type checking with components & scopes" + You might notice the large type definition at the top of `PopUp`. + + ```Lua + type Dependencies = typeof(Fusion) & { + Message: typeof(require(script.Parent.Message)), + Button: typeof(require(script.Parent.Button)), + } + ``` + + This type merges together the `Fusion` table with a second table containing + `Message` and `Button`. + + It'd be a bit like writing this out: + + ```Lua + type Dependencies = { + Message: (scope, props) -> Instance, + Button: (scope, props) -> Instance, + Value: (scope, initialValue) -> Value, + Computed: (scope, processor) -> Computed, + -- etc... + } + ``` + + Later on, this is passed to Fusion's `Scope` type. `T` is the table of + methods you want to access with `scoped()` syntax. + + ```Lua hl_lines="2" + local function PopUp( + scope: Fusion.Scope, + props: { + Message: Fusion.CanBeState, + DismissText: Fusion.CanBeState + } + ) + ``` + + Because the `Dependencies` type contains all of Fusion & the `Message` and + `Button` components, it tells Luau: + + - to reject scopes that don't have those methods + - to show you autocomplete information for those methods while working on + your code + + The scope defined in the main script contains all of those methods, so it + passes type checking: + + ```Lua + local scope = scoped(Fusion, { + PopUp = require(script.Parent.PopUp), + Message = require(script.Parent.Message), + Button = require(script.Parent.Button) + }) + + -- this is ok + scope:PopUp { + Message = "Hello, world!", + DismissText = "Close" + } + ``` + + However, removing one of the methods emits a type checking error, because + the scope can no longer support the `PopUp` component. + + ```Lua hl_lines="3" + local scope = scoped(Fusion, { + PopUp = require(script.Parent.PopUp), + Message = nil, + Button = require(script.Parent.Button) + }) + + -- the type checker will flag this up! + scope:PopUp { + Message = "Hello, world!", + DismissText = "Close" + } + ``` + + A nice benefit of this system is that components only specify the type of + the component, rather than actually loading the specific component they use. + This means you can substitute in other components if they provide the same + API. + + ```Lua hl_lines="3-4" + local scope = scoped(Fusion, { + PopUp = require(script.Parent.PopUp), + Message = require(script.Parent.Test.Message), + Button = require(script.Parent.Test.Button) + }) + + -- works as long as `Test.Message` and `Test.Button` match the real counterparts + scope:PopUp { + Message = "Hello, world!", + DismissText = "Close" + } + ``` + + This is particularly valuable for testing code in fictional environments or + for writing reusable code that can use custom implementations provided by + the developers using it. It might be scary at first to see a large list of modules, but because you can browse visually by names and folders, it's almost always better than having one diff --git a/docs/tutorials/fundamentals/objects.md b/docs/tutorials/fundamentals/objects.md index 0958e29a6..41742c418 100644 --- a/docs/tutorials/fundamentals/objects.md +++ b/docs/tutorials/fundamentals/objects.md @@ -94,17 +94,24 @@ to a cleanup function. ## Improved Syntax -Fusion provides better shorthand for scopes to improve readability and +!!! success "This syntax is recommended" + It is recommended to use this syntax. However, it is technically optional; + if it does not fit your technical requirements, the barebones syntax will + always be available. + + From now on, you'll see this syntax used throughout the tutorials. + +Fusion provides alternate syntax for scopes, which improves readability and maintainability. -To use shorthand, use `scoped({})` to create your scopes. This creates a normal +To use this syntax, call `scoped()` to create your scopes. This creates a normal empty array. ```Lua linenums="2" hl_lines="2 4" local Fusion = require(ReplicatedStorage.Fusion) local doCleanup, scoped = Fusion.doCleanup, Fusion.scoped -local scope = scoped({}) +local scope = scoped() local thing1 = Fusion.Value(scope, "i am thing 1") local thing2 = Fusion.Value(scope, "i am thing 2") local thing3 = Fusion.Value(scope, "i am thing 3") @@ -112,10 +119,10 @@ local thing3 = Fusion.Value(scope, "i am thing 3") doCleanup(scope) ``` -`scoped` can add methods to that array for you. This is most useful for +`scoped` can then add methods to that array for you. This is most useful for constructor functions. -Name some constructors in the table argument of `scoped`: +Name some constructors in a table, and pass it to `scoped()`. ```Lua linenums="2" hl_lines="4-6" local Fusion = require(ReplicatedStorage.Fusion) @@ -151,8 +158,10 @@ doCleanup(scope) This strongly ties your constructors to your scopes, which makes it much harder to mess up or circumvent them. It also makes code read much more naturally. +### Adding Methods In Bulk + Try passing `Fusion` to `scoped()` rather than listing functions manually. -Because `Fusion` already contains those functions, it works too. +Because `Fusion` is a table containing functions, it works too. ```Lua linenums="2" hl_lines="4" local Fusion = require(ReplicatedStorage.Fusion) @@ -169,42 +178,81 @@ doCleanup(scope) This gives you access to all of Fusion's constructors without having to import each one manually. -!!! success "This syntax is recommended" - It is recommended to use `scoped()` syntax. However, it is technically - optional; if it does not work for your codebase requirements, the barebones - syntax will always be available. - - From now on, you'll see this `scoped()` syntax used throughout the tutorials. - -??? tip "Merging libraries together" - If you use multiple libraries supporting scopes, you can mix functions from - both in your `scoped` call. - - ```Lua - local scope = scoped({ - Foo = LibraryA.Foo, - Bar = LibraryB.Bar - }) - - local foo = scope:Foo() - local bar = scope:Bar() - ``` - - You can automatically generate this merged table if desired. However, be - aware that libraries might use the same name for different things. - - The following code is *one way* of dealing with this: - - ```Lua - local everything = {} - for name, member in {LibraryA, LibraryB} do - if everything[name] ~= nil then - error("Two libraries contain '" .. name .. "' - they can't be merged.") - else - everything[name] = member - end - end - - -- later... - local scope = scope(everything) - ``` \ No newline at end of file +You can merge in as many extras as you'd like by adding them as arguments. + +```Lua hl_lines="4" +local LibraryA = { foo = ..., bar = ... } +local LibraryB = { frob = ..., garb = ... } + +local scope = scoped(Fusion, LibraryA, LibraryB) + +print(scope.Value == Fusion.Value) --> true +print(scope.foo == LibraryA.foo) --> true +print(scope.garb == LibraryB.garb) --> true + +``` + +!!! fail "Conflicting names" + If you pass in two tables that contain things with the same name, `scoped()` + will error. + +### Reusing Methods From Other Scopes + +Sometimes, you'll want to make a new scope with the same methods as an existing +scope. + +```Lua +local foo = scoped({ + Foo = Foo, + Bar = Bar, + Baz = Baz +}) + +-- it'd be nice to define this once only... +local bar = scoped({ + Foo = Foo, + Bar = Bar, + Baz = Baz +}) +``` + +To do this, Fusion provides a `deriveScope` function. It behaves like `scoped` +but lets you skip defining the methods. Instead, you give it an example of what +the scope should look like. + +```Lua linenums="2" hl_lines="2 11" +local Fusion = require(ReplicatedStorage.Fusion) +local scoped, deriveScope = Fusion.scoped, Fusion.deriveScope +local doCleanup = Fusion.doCleanup + +local foo = scoped({ + Foo = Foo, + Bar = Bar, + Baz = Baz +}) + +local bar = deriveScope(foo) + +doCleanup(bar) +doCleanup(foo) +``` + +*Deriving* scopes like this is highly efficient because Fusion can re-use the +same information for both scopes. It also helps keep your definitions all in +one place. + +----- + +## When You'll Use This + +Scopes might sound like a lot of upfront work. However, you'll find in practice +that Fusion manages most of this for you. + +You'll only ever have to manage scopes when you're creating and destroying them +directly. For example, you'll likely deal with them in your main code file, +where you need to create a scope directly in order to start using Fusion. + +However, Fusion manages most of your scopes for you. As you'll see, parts of +Fusion will often give you an automatically-created scope. In those cases, +Fusion takes responsibility for managing them, so you can use them without +thinking about how they work. \ No newline at end of file From f53cb7937b3a52fddd57f95221fde3b975d54d06 Mon Sep 17 00:00:00 2001 From: Elttob Date: Thu, 28 Dec 2023 20:55:54 +0000 Subject: [PATCH 155/287] Proliferate better strict typing --- src/Animation/Spring.lua | 45 +++-- src/Animation/SpringScheduler.lua | 18 +- src/Animation/Tween.lua | 28 +-- src/Animation/TweenScheduler.lua | 22 +-- src/Animation/getTweenRatio.lua | 6 +- src/Animation/lerpType.lua | 9 +- src/Animation/packType.lua | 12 +- src/Animation/springCoefficients.lua | 7 +- src/Animation/unpackType.lua | 29 ++- src/Colour/Oklab.lua | 1 + src/External.lua | 122 ++++++------ src/Instances/Attribute.lua | 14 +- src/Instances/AttributeChange.lua | 14 +- src/Instances/AttributeOut.lua | 56 +++--- src/Instances/Children.lua | 33 ++-- src/Instances/Hydrate.lua | 7 +- src/Instances/New.lua | 10 +- src/Instances/OnChange.lua | 14 +- src/Instances/OnEvent.lua | 18 +- src/Instances/Out.lua | 52 +++-- src/Instances/Ref.lua | 32 +-- src/Instances/applyInstanceProps.lua | 26 +-- src/Instances/defaultProps.lua | 1 + src/InternalTypes.lua | 116 +++++++++++ src/Logging/logError.lua | 9 +- src/Logging/logErrorNonFatal.lua | 9 +- src/Logging/logWarn.lua | 6 +- src/Logging/messages.lua | 1 + src/Logging/parseError.lua | 7 +- src/Memory/deriveScope.lua | 5 +- src/Memory/doCleanup.lua | 17 +- src/Memory/doNothing.lua | 10 - src/Memory/legacyCleanup.lua | 5 +- src/Memory/needsDestruction.lua | 11 +- src/Memory/scoped.lua | 33 +--- src/Memory/whichLivesLonger.lua | 18 +- src/PubTypes.lua | 235 ---------------------- src/RobloxExternal.lua | 70 +++---- src/State/Computed.lua | 38 ++-- src/State/For.lua | 52 ++--- src/State/ForKeys.lua | 20 +- src/State/ForPairs.lua | 20 +- src/State/ForValues.lua | 20 +- src/State/Observer.lua | 27 ++- src/State/Value.lua | 20 +- src/State/isState.lua | 11 +- src/State/peek.lua | 9 +- src/State/updateAll.lua | 16 +- src/Types.lua | 286 ++++++++++++++++++++------- src/Utility/isSimilar.lua | 27 +-- src/Utility/xtypeof.lua | 16 +- src/init.lua | 48 ++--- 52 files changed, 971 insertions(+), 767 deletions(-) create mode 100644 src/InternalTypes.lua delete mode 100644 src/Memory/doNothing.lua delete mode 100644 src/PubTypes.lua diff --git a/src/Animation/Spring.lua b/src/Animation/Spring.lua index 943e52f4e..c4a81c833 100644 --- a/src/Animation/Spring.lua +++ b/src/Animation/Spring.lua @@ -1,4 +1,5 @@ ---!nonstrict +--!strict +--!nolint LocalShadow --[[ Constructs a new computed state object, which follows the value of another @@ -6,8 +7,8 @@ ]] local Package = script.Parent.Parent -local PubTypes = require(Package.PubTypes) local Types = require(Package.Types) +local InternalTypes = require(Package.InternalTypes) local logError = require(Package.Logging.logError) local logErrorNonFatal = require(Package.Logging.logErrorNonFatal) local unpackType = require(Package.Animation.unpackType) @@ -21,7 +22,6 @@ local logWarn = require(Package.Logging.logWarn) local class = {} local CLASS_METATABLE = {__index = class} -local WEAK_KEYS_METATABLE = {__mode = "k"} --[[ Sets the position of the internal springs, meaning the value of this @@ -30,7 +30,10 @@ local WEAK_KEYS_METATABLE = {__mode = "k"} If the type doesn't match the current type of the spring, an error will be thrown. ]] -function class:setPosition(newValue: PubTypes.Animatable) +function class:setPosition( + newValue: Types.Animatable +) + local self = self :: InternalTypes.Spring local newType = typeof(newValue) if newType ~= self._currentType then logError("springTypeMismatch", nil, newType, self._currentType) @@ -49,7 +52,10 @@ end If the type doesn't match the current type of the spring, an error will be thrown. ]] -function class:setVelocity(newValue: PubTypes.Animatable) +function class:setVelocity( + newValue: Types.Animatable +) + local self = self :: InternalTypes.Spring local newType = typeof(newValue) if newType ~= self._currentType then logError("springTypeMismatch", nil, newType, self._currentType) @@ -66,7 +72,10 @@ end If the type doesn't match the current type of the spring, an error will be thrown. ]] -function class:addVelocity(deltaValue: PubTypes.Animatable) +function class:addVelocity( + deltaValue: Types.Animatable +) + local self = self :: InternalTypes.Spring local deltaType = typeof(deltaValue) if deltaType ~= self._currentType then logError("springTypeMismatch", nil, deltaType, self._currentType) @@ -84,6 +93,7 @@ end changed. ]] function class:update(): boolean + local self = self :: InternalTypes.Spring local goalValue = peek(self._goalState) -- figure out if this was a goal change or a speed/damping change @@ -154,7 +164,8 @@ end --[[ Returns the interior value of this state object. ]] -function class:_peek(): any +function class:_peek(): unknown + local self = self :: InternalTypes.Spring return self._currentValue end @@ -163,9 +174,11 @@ function class:get() end function class:destroy() + local self = self :: InternalTypes.Spring if self.scope == nil then logError("destroyedTwice", nil, "Spring") end + SpringScheduler.remove(self) self.scope = nil for dependency in pairs(self.dependencySet) do dependency.dependentSet[self] = nil @@ -173,10 +186,10 @@ function class:destroy() end local function Spring( - scope: PubTypes.Scope, - goalState: PubTypes.StateObject, - speed: PubTypes.CanBeState?, - damping: PubTypes.CanBeState? + scope: Types.Scope, + goalState: Types.StateObject, + speed: Types.CanBeState?, + damping: Types.CanBeState? ): Types.Spring if isState(scope) then logError("scopeMissing", nil, "Springs", "myScope:Spring(goalState, speed, damping)") @@ -189,11 +202,13 @@ local function Spring( damping = 1 end - local dependencySet = {[goalState] = true} + local dependencySet: {[Types.Dependency]: unknown} = {[goalState] = true} if isState(speed) then + local speed = speed :: Types.StateObject dependencySet[speed] = true end if isState(damping) then + local damping = damping :: Types.StateObject dependencySet[damping] = true end @@ -202,9 +217,7 @@ local function Spring( kind = "Spring", scope = scope, dependencySet = dependencySet, - -- if we held strong references to the dependents, then they wouldn't be - -- able to get garbage collected when they fall out of scope - dependentSet = setmetatable({}, WEAK_KEYS_METATABLE), + dependentSet = {}, _speed = speed, _damping = damping, @@ -224,6 +237,8 @@ local function Spring( _startDisplacements = {}, _startVelocities = {} }, CLASS_METATABLE) + local self = (self :: any) :: InternalTypes.Spring + table.insert(scope, self) if goalState.scope == nil then logError("useAfterDestroy", nil, `The {goalState.kind} object`, `the Spring that is following it`) diff --git a/src/Animation/SpringScheduler.lua b/src/Animation/SpringScheduler.lua index 05a37ab54..165ec595d 100644 --- a/src/Animation/SpringScheduler.lua +++ b/src/Animation/SpringScheduler.lua @@ -1,26 +1,28 @@ --!strict +--!nolint LocalShadow --[[ Manages batch updating of spring objects. ]] local Package = script.Parent.Parent -local Types = require(Package.Types) +local InternalTypes = require(Package.InternalTypes) local External = require(Package.External) local packType = require(Package.Animation.packType) local springCoefficients = require(Package.Animation.springCoefficients) local updateAll = require(Package.State.updateAll) -type Set = {[T]: any} -type Spring = Types.Spring +type Set = {[T]: unknown} local SpringScheduler = {} local EPSILON = 0.0001 -local activeSprings: Set = {} +local activeSprings: Set> = {} local lastUpdateTime = External.lastUpdateStep() -function SpringScheduler.add(spring: Spring) +function SpringScheduler.add( + spring: InternalTypes.Spring +) -- we don't necessarily want to use the most accurate time - here we snap to -- the last update time so that springs started within the same frame have -- identical time steps @@ -35,14 +37,16 @@ function SpringScheduler.add(spring: Spring) activeSprings[spring] = true end -function SpringScheduler.remove(spring: Spring) +function SpringScheduler.remove( + spring: InternalTypes.Spring +) activeSprings[spring] = nil end local function updateAllSprings( now: number ) - local springsToSleep: Set = {} + local springsToSleep: Set> = {} lastUpdateTime = now for spring in pairs(activeSprings) do diff --git a/src/Animation/Tween.lua b/src/Animation/Tween.lua index e9738e1a1..574e81cec 100644 --- a/src/Animation/Tween.lua +++ b/src/Animation/Tween.lua @@ -1,4 +1,5 @@ ---!nonstrict +--!strict +--!nolint LocalShadow --[[ Constructs a new computed state object, which follows the value of another @@ -6,9 +7,9 @@ ]] local Package = script.Parent.Parent -local PubTypes = require(Package.PubTypes) -local External = require(Package.External) local Types = require(Package.Types) +local InternalTypes = require(Package.InternalTypes) +local External = require(Package.External) local TweenScheduler = require(Package.Animation.TweenScheduler) local logError = require(Package.Logging.logError) local logErrorNonFatal = require(Package.Logging.logErrorNonFatal) @@ -20,13 +21,13 @@ local logWarn = require(Package.Logging.logWarn) local class = {} local CLASS_METATABLE = {__index = class} -local WEAK_KEYS_METATABLE = {__mode = "k"} --[[ Called when the goal state changes value; this will initiate a new tween. Returns false as the current value doesn't change right away. ]] function class:update(): boolean + local self = self :: InternalTypes.Tween local goalValue = peek(self._goalState) -- if the goal hasn't changed, then this is a TweenInfo change. @@ -65,7 +66,8 @@ end --[[ Returns the interior value of this state object. ]] -function class:_peek(): any +function class:_peek(): unknown + local self = self :: InternalTypes.Tween return self._currentValue end @@ -74,9 +76,11 @@ function class:get() end function class:destroy() + local self = self :: InternalTypes.Tween if self.scope == nil then logError("destroyedTwice", nil, "Tween") end + TweenScheduler.remove(self) self.scope = nil for dependency in pairs(self.dependencySet) do dependency.dependentSet[self] = nil @@ -84,9 +88,9 @@ function class:destroy() end local function Tween( - scope: PubTypes.Scope, - goalState: PubTypes.StateObject, - tweenInfo: PubTypes.CanBeState? + scope: Types.Scope, + goalState: Types.StateObject, + tweenInfo: Types.CanBeState? ): Types.Tween if isState(scope) then logError("scopeMissing", nil, "Tweens", "myScope:Tween(goalState, tweenInfo)") @@ -98,9 +102,10 @@ local function Tween( tweenInfo = TweenInfo.new() end - local dependencySet = {[goalState] = true} + local dependencySet: {[Types.Dependency]: unknown} = {[goalState] = true} local tweenInfoIsState = isState(tweenInfo) if tweenInfoIsState then + local tweenInfo = tweenInfo :: Types.StateObject dependencySet[tweenInfo] = true end @@ -115,9 +120,7 @@ local function Tween( kind = "Tween", scope = scope, dependencySet = dependencySet, - -- if we held strong references to the dependents, then they wouldn't be - -- able to get garbage collected when they fall out of scope - dependentSet = setmetatable({}, WEAK_KEYS_METATABLE), + dependentSet = {}, _goalState = goalState, _tweenInfo = tweenInfo, _tweenInfoIsState = tweenInfoIsState, @@ -133,6 +136,7 @@ local function Tween( _currentTweenStartTime = 0, _currentlyAnimating = false }, CLASS_METATABLE) + local self = (self :: any) :: InternalTypes.Tween table.insert(scope, self) if goalState.scope == nil then diff --git a/src/Animation/TweenScheduler.lua b/src/Animation/TweenScheduler.lua index 548c900a9..5eebea3c3 100644 --- a/src/Animation/TweenScheduler.lua +++ b/src/Animation/TweenScheduler.lua @@ -1,11 +1,12 @@ --!strict +--!nolint LocalShadow --[[ Manages batch updating of tween objects. ]] local Package = script.Parent.Parent -local Types = require(Package.Types) +local InternalTypes = require(Package.InternalTypes) local External = require(Package.External) local lerpType = require(Package.Animation.lerpType) local getTweenRatio = require(Package.Animation.getTweenRatio) @@ -13,26 +14,26 @@ local updateAll = require(Package.State.updateAll) local TweenScheduler = {} -type Set = {[T]: any} -type Tween = Types.Tween - -local WEAK_KEYS_METATABLE = {__mode = "k"} +type Set = {[T]: unknown} -- all the tweens currently being updated -local allTweens: Set = {} -setmetatable(allTweens, WEAK_KEYS_METATABLE) +local allTweens: Set> = {} --[[ Adds a Tween to be updated every render step. ]] -function TweenScheduler.add(tween: Tween) +function TweenScheduler.add( + tween: InternalTypes.Tween +) allTweens[tween] = true end --[[ Removes a Tween from the scheduler. ]] -function TweenScheduler.remove(tween: Tween) +function TweenScheduler.remove( + tween: InternalTypes.Tween +) allTweens[tween] = nil end @@ -42,8 +43,7 @@ end local function updateAllTweens( now: number ) - -- FIXME: Typed Luau doesn't understand this loop yet - for tween: Tween in pairs(allTweens :: any) do + for tween in allTweens do local currentTime = now - tween._currentTweenStartTime if currentTime > tween._currentTweenDuration and tween._currentTweenInfo.RepeatCount > -1 then diff --git a/src/Animation/getTweenRatio.lua b/src/Animation/getTweenRatio.lua index 1803606d5..587701cc3 100644 --- a/src/Animation/getTweenRatio.lua +++ b/src/Animation/getTweenRatio.lua @@ -1,4 +1,5 @@ --!strict +--!nolint LocalShadow --[[ Given a `tweenInfo` and `currentTime`, returns a ratio which can be used to @@ -7,7 +8,10 @@ local TweenService = game:GetService("TweenService") -local function getTweenRatio(tweenInfo: TweenInfo, currentTime: number): number +local function getTweenRatio( + tweenInfo: TweenInfo, + currentTime: number +): number local delay = tweenInfo.DelayTime local duration = tweenInfo.Time local reverses = tweenInfo.Reverses diff --git a/src/Animation/lerpType.lua b/src/Animation/lerpType.lua index 2db0e1ddc..b9f2b3f3a 100644 --- a/src/Animation/lerpType.lua +++ b/src/Animation/lerpType.lua @@ -5,15 +5,16 @@ Linearly interpolates the given animatable types by a ratio. If the types are different or not animatable, then the first value will be returned for ratios below 0.5, and the second value for 0.5 and above. - - FIXME: This function uses a lot of redefinitions to suppress false positives - from the Luau typechecker - ideally these wouldn't be required ]] local Package = script.Parent.Parent local Oklab = require(Package.Colour.Oklab) -local function lerpType(from: any, to: any, ratio: number): any +local function lerpType( + from: unknown, + to: unknown, + ratio: number +): unknown local typeString = typeof(from) if typeof(to) == typeString then diff --git a/src/Animation/packType.lua b/src/Animation/packType.lua index 480024826..dcc5a6b6d 100644 --- a/src/Animation/packType.lua +++ b/src/Animation/packType.lua @@ -1,19 +1,19 @@ --!strict +--!nolint LocalShadow --[[ Packs an array of numbers into a given animatable data type. If the type is not animatable, nil will be returned. - - FUTURE: When Luau supports singleton types, those could be used in - conjunction with intersection types to make this function fully statically - type checkable. ]] local Package = script.Parent.Parent -local PubTypes = require(Package.PubTypes) +local Types = require(Package.Types) local Oklab = require(Package.Colour.Oklab) -local function packType(numbers: {number}, typeString: string): PubTypes.Animatable? +local function packType( + numbers: {number}, + typeString: string +): Types.Animatable? if typeString == "number" then return numbers[1] diff --git a/src/Animation/springCoefficients.lua b/src/Animation/springCoefficients.lua index 9f87a7823..940399746 100644 --- a/src/Animation/springCoefficients.lua +++ b/src/Animation/springCoefficients.lua @@ -1,4 +1,5 @@ --!strict +--!nolint LocalShadow --[[ Returns a 2x2 matrix of coefficients for a given time, damping and speed. @@ -11,7 +12,11 @@ Special thanks to AxisAngle for helping to improve numerical precision. ]] -local function springCoefficients(time: number, damping: number, speed: number): (number, number, number, number) +local function springCoefficients( + time: number, + damping: number, + speed: number +): (number, number, number, number) -- if time or speed is 0, then the spring won't move if time == 0 or speed == 0 then return 1, 0, 0, 1 diff --git a/src/Animation/unpackType.lua b/src/Animation/unpackType.lua index 1b8239006..877615353 100644 --- a/src/Animation/unpackType.lua +++ b/src/Animation/unpackType.lua @@ -4,56 +4,62 @@ --[[ Unpacks an animatable type into an array of numbers. If the type is not animatable, an empty array will be returned. - - FIXME: This function uses a lot of redefinitions to suppress false positives - from the Luau typechecker - ideally these wouldn't be required - - FUTURE: When Luau supports singleton types, those could be used in - conjunction with intersection types to make this function fully statically - type checkable. ]] local Package = script.Parent.Parent local Oklab = require(Package.Colour.Oklab) -local function unpackType(value: any, typeString: string): {number} +local function unpackType( + value: unknown, + typeString: string +): {number} if typeString == "number" then local value = value :: number return {value} elseif typeString == "CFrame" then + local value = value :: CFrame -- FUTURE: is there a better way of doing this? doing distance -- calculations on `angle` may be incorrect local axis, angle = value:ToAxisAngle() return {value.X, value.Y, value.Z, axis.X, axis.Y, axis.Z, angle} elseif typeString == "Color3" then + local value = value :: Color3 local lab = Oklab.to(value) return {lab.X, lab.Y, lab.Z} elseif typeString == "ColorSequenceKeypoint" then + local value = value :: ColorSequenceKeypoint local lab = Oklab.to(value.Value) return {lab.X, lab.Y, lab.Z, value.Time} elseif typeString == "DateTime" then + local value = value :: DateTime return {value.UnixTimestampMillis} elseif typeString == "NumberRange" then + local value = value :: NumberRange return {value.Min, value.Max} elseif typeString == "NumberSequenceKeypoint" then + local value = value :: NumberSequenceKeypoint return {value.Value, value.Time, value.Envelope} elseif typeString == "PhysicalProperties" then + local value = value :: PhysicalProperties return {value.Density, value.Friction, value.Elasticity, value.FrictionWeight, value.ElasticityWeight} elseif typeString == "Ray" then + local value = value :: Ray return {value.Origin.X, value.Origin.Y, value.Origin.Z, value.Direction.X, value.Direction.Y, value.Direction.Z} elseif typeString == "Rect" then + local value = value :: Rect return {value.Min.X, value.Min.Y, value.Max.X, value.Max.Y} elseif typeString == "Region3" then + local value = value :: Region3 -- FUTURE: support rotated Region3s if/when they become constructable return { value.CFrame.X, value.CFrame.Y, value.CFrame.Z, @@ -61,24 +67,31 @@ local function unpackType(value: any, typeString: string): {number} } elseif typeString == "Region3int16" then + local value = value :: Region3int16 return {value.Min.X, value.Min.Y, value.Min.Z, value.Max.X, value.Max.Y, value.Max.Z} elseif typeString == "UDim" then + local value = value :: UDim return {value.Scale, value.Offset} elseif typeString == "UDim2" then + local value = value :: UDim2 return {value.X.Scale, value.X.Offset, value.Y.Scale, value.Y.Offset} elseif typeString == "Vector2" then + local value = value :: Vector2 return {value.X, value.Y} elseif typeString == "Vector2int16" then + local value = value :: Vector2int16 return {value.X, value.Y} elseif typeString == "Vector3" then + local value = value :: Vector3 return {value.X, value.Y, value.Z} elseif typeString == "Vector3int16" then + local value = value :: Vector3int16 return {value.X, value.Y, value.Z} else return {} diff --git a/src/Colour/Oklab.lua b/src/Colour/Oklab.lua index ea090f91d..db3be8fc5 100644 --- a/src/Colour/Oklab.lua +++ b/src/Colour/Oklab.lua @@ -1,4 +1,5 @@ --!strict +--!nolint LocalShadow --[[ Provides functions for converting Color3s into Oklab space, for more diff --git a/src/External.lua b/src/External.lua index fd7c65590..62e1a1238 100644 --- a/src/External.lua +++ b/src/External.lua @@ -1,7 +1,9 @@ --!strict +--!nolint LocalShadow + --[[ - Abstraction layer between Fusion internals and external environments, - allowing for flexible integration with schedulers and test mocks. + Abstraction layer between Fusion internals and external environments, + allowing for flexible integration with schedulers and test mocks. ]] local Package = script.Parent @@ -14,14 +16,14 @@ local External = {} External.unitTestSilenceNonFatal = false export type Scheduler = { - doTaskImmediate: ( - resume: () -> () - ) -> (), - doTaskDeferred: ( - resume: () -> () - ) -> (), - startScheduler: () -> (), - stopScheduler: () -> () + doTaskImmediate: ( + resume: () -> () + ) -> (), + doTaskDeferred: ( + resume: () -> () + ) -> (), + startScheduler: () -> (), + stopScheduler: () -> () } local updateStepCallbacks = {} @@ -29,90 +31,90 @@ local currentScheduler: Scheduler? = nil local lastUpdateStep = 0 --[[ - Sets the external scheduler that Fusion will use for queuing async tasks. - Returns the previous scheduler so it can be reset later. + Sets the external scheduler that Fusion will use for queuing async tasks. + Returns the previous scheduler so it can be reset later. ]] function External.setExternalScheduler( - newScheduler: Scheduler? + newScheduler: Scheduler? ): Scheduler? - local oldScheduler = currentScheduler - if oldScheduler ~= nil then - oldScheduler.stopScheduler() - end - currentScheduler = newScheduler - if newScheduler ~= nil then - newScheduler.startScheduler() - end - return oldScheduler + local oldScheduler = currentScheduler + if oldScheduler ~= nil then + oldScheduler.stopScheduler() + end + currentScheduler = newScheduler + if newScheduler ~= nil then + newScheduler.startScheduler() + end + return oldScheduler end --[[ Sends an immediate task to the external scheduler. Throws if none is set. ]] function External.doTaskImmediate( - resume: () -> () + resume: () -> () ) - if currentScheduler == nil then - logError("noTaskScheduler") - else - currentScheduler.doTaskImmediate(resume) - end + if currentScheduler == nil then + logError("noTaskScheduler") + else + currentScheduler.doTaskImmediate(resume) + end end --[[ - Sends a deferred task to the external scheduler. Throws if none is set. + Sends a deferred task to the external scheduler. Throws if none is set. ]] function External.doTaskDeferred( - resume: () -> () + resume: () -> () ) - if currentScheduler == nil then - logError("noTaskScheduler") - else - currentScheduler.doTaskDeferred(resume) - end + if currentScheduler == nil then + logError("noTaskScheduler") + else + currentScheduler.doTaskDeferred(resume) + end end --[[ - Registers a callback to the update step of the external scheduler. - Returns a function that can be used to disconnect later. + Registers a callback to the update step of the external scheduler. + Returns a function that can be used to disconnect later. - Callbacks are given the current number of seconds since an arbitrary epoch. - - TODO: This epoch may change between schedulers. We could investigate ways - of allowing schedulers to co-operate to keep the epoch the same, so that - monotonicity can be better preserved. + Callbacks are given the current number of seconds since an arbitrary epoch. + + TODO: This epoch may change between schedulers. We could investigate ways + of allowing schedulers to co-operate to keep the epoch the same, so that + monotonicity can be better preserved. ]] function External.bindToUpdateStep( - callback: ( - now: number - ) -> () + callback: ( + now: number + ) -> () ): () -> () - local uniqueIdentifier = {} - updateStepCallbacks[uniqueIdentifier] = callback - return function() - updateStepCallbacks[uniqueIdentifier] = nil - end + local uniqueIdentifier = {} + updateStepCallbacks[uniqueIdentifier] = callback + return function() + updateStepCallbacks[uniqueIdentifier] = nil + end end --[[ - Steps time-dependent systems with the current number of seconds since an - arbitrary epoch. This should be called as early as possible in the external - scheduler's update cycle. + Steps time-dependent systems with the current number of seconds since an + arbitrary epoch. This should be called as early as possible in the external + scheduler's update cycle. ]] function External.performUpdateStep( - now: number + now: number ) - lastUpdateStep = now - for _, callback in updateStepCallbacks do - callback(now) - end + lastUpdateStep = now + for _, callback in updateStepCallbacks do + callback(now) + end end --[[ - Returns the timestamp of the last update step. + Returns the timestamp of the last update step. ]] function External.lastUpdateStep() - return lastUpdateStep + return lastUpdateStep end return External \ No newline at end of file diff --git a/src/Instances/Attribute.lua b/src/Instances/Attribute.lua index d66677aa7..d9065d8ff 100644 --- a/src/Instances/Attribute.lua +++ b/src/Instances/Attribute.lua @@ -1,4 +1,5 @@ --!strict +--!nolint LocalShadow --[[ A special key for property tables, which allows users to apply custom @@ -6,7 +7,7 @@ ]] local Package = script.Parent.Parent -local PubTypes = require(Package.PubTypes) +local Types = require(Package.Types) local External = require(Package.External) local logError = require(Package.Logging.logError) local logWarn = require(Package.Logging.logWarn) @@ -15,7 +16,9 @@ local Observer = require(Package.State.Observer) local peek = require(Package.State.peek) local whichLivesLonger = require(Package.Memory.whichLivesLonger) -local function Attribute(attributeName: string): PubTypes.SpecialKey +local function Attribute( + attributeName: string +): Types.SpecialKey if attributeName == nil then logError("attributeNameNil") end @@ -24,12 +27,13 @@ local function Attribute(attributeName: string): PubTypes.SpecialKey kind = "Attribute", stage = "self", apply = function( - self: PubTypes.SpecialKey, - scope: PubTypes.Scope, - value: any, + self: Types.SpecialKey, + scope: Types.Scope, + value: unknown, applyTo: Instance ) if isState(value) then + local value = value :: Types.StateObject if value.scope == nil then logError("useAfterDestroy", nil, `The {value.kind} object, bound to [Attribute "{attributeName}"],`, `the {applyTo.ClassName} instance`) elseif whichLivesLonger(scope, applyTo, value.scope, value) == "a" then diff --git a/src/Instances/AttributeChange.lua b/src/Instances/AttributeChange.lua index 5e9699246..07acb8f15 100644 --- a/src/Instances/AttributeChange.lua +++ b/src/Instances/AttributeChange.lua @@ -1,4 +1,5 @@ --!strict +--!nolint LocalShadow --[[ A special key for property tables, which allows users to connect to @@ -6,10 +7,12 @@ ]] local Package = script.Parent.Parent -local PubTypes = require(Package.PubTypes) +local Types = require(Package.Types) local logError = require(Package.Logging.logError) -local function AttributeChange(attributeName: string): PubTypes.SpecialKey +local function AttributeChange( + attributeName: string +): Types.SpecialKey if attributeName == nil then logError("attributeNameNil") end @@ -19,14 +22,15 @@ local function AttributeChange(attributeName: string): PubTypes.SpecialKey kind = "AttributeChange", stage = "observer", apply = function( - self: PubTypes.SpecialKey, - scope: PubTypes.Scope, - value: any, + self: Types.SpecialKey, + scope: Types.Scope, + value: unknown, applyTo: Instance ) if typeof(value) ~= "function" then logError("invalidAttributeChangeHandler", nil, attributeName) end + local value = value :: (...unknown) -> (...unknown) local ok, event = pcall(applyTo.GetAttributeChangedSignal, applyTo, attributeName) if not ok then logError("cannotConnectAttributeChange", nil, applyTo.ClassName, attributeName) diff --git a/src/Instances/AttributeOut.lua b/src/Instances/AttributeOut.lua index 33771e394..30f9ef54a 100644 --- a/src/Instances/AttributeOut.lua +++ b/src/Instances/AttributeOut.lua @@ -1,48 +1,58 @@ --!strict +--!nolint LocalShadow --[[ A special key for property tables, which allows users to save instance attributes - into state objects + into state objects ]] local Package = script.Parent.Parent -local PubTypes = require(Package.PubTypes) +local Types = require(Package.Types) local logError = require(Package.Logging.logError) local logWarn = require(Package.Logging.logWarn) -local xtypeof = require(Package.Utility.xtypeof) +local isState = require(Package.State.isState) local whichLivesLonger = require(Package.Memory.whichLivesLonger) -local function AttributeOut(attributeName: string): PubTypes.SpecialKey +local function AttributeOut( + attributeName: string +): Types.SpecialKey + if attributeName == nil then + logError("attributeNameNil") + end + return { type = "SpecialKey", kind = "AttributeOut", stage = "observer", apply = function( - self: PubTypes.SpecialKey, - scope: PubTypes.Scope, - stateObject: any, + self: Types.SpecialKey, + scope: Types.Scope, + value: unknown, applyTo: Instance ) - if xtypeof(stateObject) ~= "State" or stateObject.kind ~= "Value" then - logError("invalidAttributeOutType") - end - if attributeName == nil then - logError("attributeNameNil") - end local ok, event = pcall(applyTo.GetAttributeChangedSignal, applyTo, attributeName) if not ok then logError("invalidOutAttributeName", nil, applyTo.ClassName, attributeName) - else - if stateObject.scope == nil then - logError("useAfterDestroy", nil, `The Value object, which [AttributeOut "{attributeName}"] outputs to,`, `the {applyTo.ClassName} instance`) - elseif whichLivesLonger(scope, applyTo, stateObject.scope, stateObject) == "a" then - logWarn("possiblyOutlives", `The Value object, which [AttributeOut "{attributeName}"] outputs to,`, `the {applyTo.ClassName} instance`) - end - stateObject:set((applyTo :: any):GetAttribute(attributeName)) - table.insert(scope, event:Connect(function() - stateObject:set((applyTo :: any):GetAttribute(attributeName)) - end)) end + + if not isState(value) then + logError("invalidAttributeOutType") + end + local value = value :: Types.StateObject + if value.kind ~= "Value" then + logError("invalidAttributeOutType") + end + local value = value :: Types.Value + + if value.scope == nil then + logError("useAfterDestroy", nil, `The Value object, which [AttributeOut "{attributeName}"] outputs to,`, `the {applyTo.ClassName} instance`) + elseif whichLivesLonger(scope, applyTo, value.scope, value) == "a" then + logWarn("possiblyOutlives", `The Value object, which [AttributeOut "{attributeName}"] outputs to,`, `the {applyTo.ClassName} instance`) + end + value:set((applyTo :: any):GetAttribute(attributeName)) + table.insert(scope, event:Connect(function() + value:set((applyTo :: any):GetAttribute(attributeName)) + end)) end } end diff --git a/src/Instances/Children.lua b/src/Instances/Children.lua index 9f3f1f812..df3b46b04 100644 --- a/src/Instances/Children.lua +++ b/src/Instances/Children.lua @@ -1,4 +1,5 @@ --!strict +--!nolint LocalShadow --[[ A special key for property tables, which parents any given descendants into @@ -6,14 +7,14 @@ ]] local Package = script.Parent.Parent -local PubTypes = require(Package.PubTypes) +local Types = require(Package.Types) local External = require(Package.External) local logWarn = require(Package.Logging.logWarn) local Observer = require(Package.State.Observer) local peek = require(Package.State.peek) local isState = require(Package.State.isState) -type Set = {[T]: boolean} +type Set = {[T]: unknown} -- Experimental flag: name children based on the key used in the [Children] table local EXPERIMENTAL_AUTO_NAMING = false @@ -23,17 +24,17 @@ return { kind = "Children", stage = "descendants", apply = function( - self: PubTypes.SpecialKey, - scope: PubTypes.Scope, - propValue: any, + self: Types.SpecialKey, + scope: Types.Scope, + value: unknown, applyTo: Instance ) local newParented: Set = {} local oldParented: Set = {} -- save disconnection functions for state object observers - local newDisconnects: {[PubTypes.StateObject]: () -> ()} = {} - local oldDisconnects: {[PubTypes.StateObject]: () -> ()} = {} + local newDisconnects: {[Types.StateObject]: () -> ()} = {} + local oldDisconnects: {[Types.StateObject]: () -> ()} = {} local updateQueued = false local queueUpdate: () -> () @@ -52,11 +53,15 @@ return { table.clear(newParented) table.clear(newDisconnects) - local function processChild(child: any, autoName: string?) + local function processChild( + child: unknown, + autoName: string? + ) local childType = typeof(child) if childType == "Instance" then -- case 1; single instance + local child = child :: Instance newParented[child] = true if oldParented[child] == nil then @@ -76,6 +81,7 @@ return { elseif isState(child) then -- case 2; state object + local child = child :: Types.StateObject local value = peek(child) -- allow nil to represent the absence of a child @@ -97,14 +103,17 @@ return { elseif childType == "table" then -- case 3; table of objects + local child = child :: {[unknown]: unknown} for key, subChild in pairs(child) do local keyType = typeof(key) local subAutoName: string? = nil if keyType == "string" then + local key = key :: string subAutoName = key elseif keyType == "number" and autoName ~= nil then + local key = key :: number subAutoName = autoName .. "_" .. key end @@ -116,10 +125,10 @@ return { end end - if propValue ~= nil then + if value ~= nil then -- `propValue` is set to nil on cleanup, so we don't process children -- in that case - processChild(propValue) + processChild(value) end -- unparent any children that are no longer present @@ -141,7 +150,7 @@ return { end table.insert(scope, function() - propValue = nil + value = nil updateQueued = true updateChildren() end) @@ -150,4 +159,4 @@ return { updateQueued = true updateChildren() end -} :: PubTypes.SpecialKey \ No newline at end of file +} :: Types.SpecialKey \ No newline at end of file diff --git a/src/Instances/Hydrate.lua b/src/Instances/Hydrate.lua index 3f3976f5c..22d4b70e9 100644 --- a/src/Instances/Hydrate.lua +++ b/src/Instances/Hydrate.lua @@ -1,4 +1,5 @@ --!strict +--!nolint LocalShadow --[[ Processes and returns an existing instance, with options for setting @@ -6,19 +7,19 @@ ]] local Package = script.Parent.Parent -local PubTypes = require(Package.PubTypes) +local Types = require(Package.Types) local applyInstanceProps = require(Package.Instances.applyInstanceProps) local logError = require(Package.Logging.logError) local function Hydrate( - scope: PubTypes.Scope, + scope: Types.Scope, target: Instance ) if target :: any == nil then logError("scopeMissing", nil, "instances using Hydrate", "myScope:Hydrate (instance) { ... }") end return function( - props: PubTypes.PropertyTable + props: Types.PropertyTable ): Instance table.insert(scope, target) diff --git a/src/Instances/New.lua b/src/Instances/New.lua index a7b4276c5..c28203179 100644 --- a/src/Instances/New.lua +++ b/src/Instances/New.lua @@ -1,4 +1,5 @@ --!strict +--!nolint LocalShadow --[[ Constructs and returns a new instance, with options for setting properties, @@ -6,20 +7,21 @@ ]] local Package = script.Parent.Parent -local PubTypes = require(Package.PubTypes) +local Types = require(Package.Types) local defaultProps = require(Package.Instances.defaultProps) local applyInstanceProps = require(Package.Instances.applyInstanceProps) local logError= require(Package.Logging.logError) local function New( - scope: PubTypes.Scope, + scope: Types.Scope, className: string ) - if className == nil then + if (className :: any) == nil then + local scope = (scope :: any) :: string logError("scopeMissing", nil, "instances using New", "myScope:New \"" .. scope .. "\" { ... }") end return function( - props: PubTypes.PropertyTable + props: Types.PropertyTable ): Instance local ok, instance = pcall(Instance.new, className) if not ok then diff --git a/src/Instances/OnChange.lua b/src/Instances/OnChange.lua index c9f59be2c..15d7e5986 100644 --- a/src/Instances/OnChange.lua +++ b/src/Instances/OnChange.lua @@ -1,4 +1,5 @@ --!strict +--!nolint LocalShadow --[[ Constructs special keys for property tables which connect property change @@ -6,18 +7,20 @@ ]] local Package = script.Parent.Parent -local PubTypes = require(Package.PubTypes) +local Types = require(Package.Types) local logError = require(Package.Logging.logError) -local function OnChange(propertyName: string): PubTypes.SpecialKey +local function OnChange( + propertyName: string +): Types.SpecialKey return { type = "SpecialKey", kind = "OnChange", stage = "observer", apply = function( - self: PubTypes.SpecialKey, - scope: PubTypes.Scope, - callback: any, + self: Types.SpecialKey, + scope: Types.Scope, + callback: unknown, applyTo: Instance ) local ok, event = pcall(applyTo.GetPropertyChangedSignal, applyTo, propertyName) @@ -26,6 +29,7 @@ local function OnChange(propertyName: string): PubTypes.SpecialKey elseif typeof(callback) ~= "function" then logError("invalidChangeHandler", nil, propertyName) else + local callback = callback :: (...unknown) -> (...unknown) table.insert(scope, event:Connect(function() callback((applyTo :: any)[propertyName]) end)) diff --git a/src/Instances/OnEvent.lua b/src/Instances/OnEvent.lua index 812a86da7..418ef8356 100644 --- a/src/Instances/OnEvent.lua +++ b/src/Instances/OnEvent.lua @@ -1,4 +1,5 @@ --!strict +--!nolint LocalShadow --[[ Constructs special keys for property tables which connect event listeners to @@ -6,22 +7,27 @@ ]] local Package = script.Parent.Parent -local PubTypes = require(Package.PubTypes) +local Types = require(Package.Types) local logError = require(Package.Logging.logError) -local function getProperty_unsafe(instance: Instance, property: string) +local function getProperty_unsafe( + instance: Instance, + property: string +) return (instance :: any)[property] end -local function OnEvent(eventName: string): PubTypes.SpecialKey +local function OnEvent( + eventName: string +): Types.SpecialKey return { type = "SpecialKey", kind = "OnEvent", stage = "observer", apply = function( - self: PubTypes.SpecialKey, - scope: PubTypes.Scope, - callback: any, + self: Types.SpecialKey, + scope: Types.Scope, + callback: unknown, applyTo: Instance ) local ok, event = pcall(getProperty_unsafe, applyTo, eventName) diff --git a/src/Instances/Out.lua b/src/Instances/Out.lua index 72a7bde41..3c23af4f5 100644 --- a/src/Instances/Out.lua +++ b/src/Instances/Out.lua @@ -1,4 +1,5 @@ --!strict +--!nolint LocalShadow --[[ A special key for property tables, which allows users to extract values from @@ -6,42 +7,51 @@ ]] local Package = script.Parent.Parent -local PubTypes = require(Package.PubTypes) +local Types = require(Package.Types) local logError = require(Package.Logging.logError) local logWarn = require(Package.Logging.logWarn) -local xtypeof = require(Package.Utility.xtypeof) +local isState = require(Package.State.isState) local whichLivesLonger = require(Package.Memory.whichLivesLonger) -local function Out(propertyName: string): PubTypes.SpecialKey +local function Out( + propertyName: string +): Types.SpecialKey return { type = "SpecialKey", kind = "Out", stage = "observer", apply = function( - self: PubTypes.SpecialKey, - scope: PubTypes.Scope, - outState: any, + self: Types.SpecialKey, + scope: Types.Scope, + value: unknown, applyTo: Instance ) local ok, event = pcall(applyTo.GetPropertyChangedSignal, applyTo, propertyName) if not ok then logError("invalidOutProperty", nil, applyTo.ClassName, propertyName) - elseif xtypeof(outState) ~= "State" or outState.kind ~= "Value" then - logError("invalidOutType") - else - if outState.scope == nil then - logError("useAfterDestroy", nil, `The Value, which [Out "{propertyName}"] outputs to,`, `the {applyTo.ClassName} instance`) - elseif whichLivesLonger(scope, applyTo, outState.scope, outState) == "a" then - logWarn("possiblyOutlives", `The Value, which [Out "{propertyName}"] outputs to,`, `the {applyTo.ClassName} instance`) - end - outState:set((applyTo :: any)[propertyName]) - table.insert( - scope, - event:Connect(function() - outState:set((applyTo :: any)[propertyName]) - end) - ) end + + if not isState(value) then + logError("invalidAttributeOutType") + end + local value = value :: Types.StateObject + if value.kind ~= "Value" then + logError("invalidAttributeOutType") + end + local value = value :: Types.Value + + if value.scope == nil then + logError("useAfterDestroy", nil, `The Value, which [Out "{propertyName}"] outputs to,`, `the {applyTo.ClassName} instance`) + elseif whichLivesLonger(scope, applyTo, value.scope, value) == "a" then + logWarn("possiblyOutlives", `The Value, which [Out "{propertyName}"] outputs to,`, `the {applyTo.ClassName} instance`) + end + value:set((applyTo :: any)[propertyName]) + table.insert( + scope, + event:Connect(function() + value:set((applyTo :: any)[propertyName]) + end) + ) end } end diff --git a/src/Instances/Ref.lua b/src/Instances/Ref.lua index a67c1de30..cfb3b9ba9 100644 --- a/src/Instances/Ref.lua +++ b/src/Instances/Ref.lua @@ -1,4 +1,5 @@ --!strict +--!nolint LocalShadow --[[ A special key for property tables, which stores a reference to the instance @@ -6,7 +7,7 @@ ]] local Package = script.Parent.Parent -local PubTypes = require(Package.PubTypes) +local Types = require(Package.Types) local logWarn = require(Package.Logging.logWarn) local logError = require(Package.Logging.logError) local isState = require(Package.State.isState) @@ -17,20 +18,25 @@ return { kind = "Ref", stage = "observer", apply = function( - self: PubTypes.SpecialKey, - scope: PubTypes.Scope, - refState: any, + self: Types.SpecialKey, + scope: Types.Scope, + value: unknown, applyTo: Instance ) - if not isState(refState) or refState.kind ~= "Value" then + if not isState(value) then logError("invalidRefType") - else - if refState.scope == nil then - logError("useAfterDestroy", nil, "The Value object, which [Ref] outputs to,", `the {applyTo} instance`) - elseif whichLivesLonger(scope, applyTo, refState.scope, refState) == "a" then - logWarn("possiblyOutlives", "The Value object, which [Ref] outputs to,", `the {applyTo} instance`) - end - refState:set(applyTo) end + local value = value :: Types.StateObject + if value.kind ~= "Value" then + logError("invalidRefType") + end + local value = value :: Types.Value + + if value.scope == nil then + logError("useAfterDestroy", nil, "The Value object, which [Ref] outputs to,", `the {applyTo} instance`) + elseif whichLivesLonger(scope, applyTo, value.scope, value) == "a" then + logWarn("possiblyOutlives", "The Value object, which [Ref] outputs to,", `the {applyTo} instance`) + end + value:set(applyTo) end -} :: PubTypes.SpecialKey \ No newline at end of file +} :: Types.SpecialKey \ No newline at end of file diff --git a/src/Instances/applyInstanceProps.lua b/src/Instances/applyInstanceProps.lua index 58b61375d..2f25bf96d 100644 --- a/src/Instances/applyInstanceProps.lua +++ b/src/Instances/applyInstanceProps.lua @@ -1,4 +1,5 @@ --!strict +--!nolint LocalShadow --[[ Applies a table of properties to an instance, including binding to any @@ -14,7 +15,7 @@ ]] local Package = script.Parent.Parent -local PubTypes = require(Package.PubTypes) +local Types = require(Package.Types) local External = require(Package.External) local isState = require(Package.State.isState) local logError = require(Package.Logging.logError) @@ -27,7 +28,7 @@ local whichLivesLonger = require(Package.Memory.whichLivesLonger) local function setProperty_unsafe( instance: Instance, property: string, - value: any + value: unknown ) (instance :: any)[property] = value end @@ -42,7 +43,7 @@ end local function setProperty( instance: Instance, property: string, - value: any + value: unknown ) if not pcall(setProperty_unsafe, instance, property, value) then if not pcall(testPropertyAssignable, instance, property) then @@ -58,12 +59,13 @@ local function setProperty( end local function bindProperty( - scope: PubTypes.Scope, + scope: Types.Scope, instance: Instance, property: string, - value: PubTypes.CanBeState + value: Types.CanBeState ) if isState(value) then + local value = value :: Types.StateObject if value.scope == nil then logError("useAfterDestroy", nil, `The {value.kind} object, bound to {property},`, `the {instance.ClassName} instance`) elseif whichLivesLonger(scope, instance, value.scope, value) ~= "b" then @@ -91,15 +93,15 @@ local function bindProperty( end local function applyInstanceProps( - scope: PubTypes.Scope, - props: PubTypes.PropertyTable, + scope: Types.Scope, + props: Types.PropertyTable, applyTo: Instance ) local specialKeys = { - self = {} :: {[PubTypes.SpecialKey]: any}, - descendants = {} :: {[PubTypes.SpecialKey]: any}, - ancestor = {} :: {[PubTypes.SpecialKey]: any}, - observer = {} :: {[PubTypes.SpecialKey]: any} + self = {} :: {[Types.SpecialKey]: unknown}, + descendants = {} :: {[Types.SpecialKey]: unknown}, + ancestor = {} :: {[Types.SpecialKey]: unknown}, + observer = {} :: {[Types.SpecialKey]: unknown} } for key, value in pairs(props) do @@ -110,7 +112,7 @@ local function applyInstanceProps( bindProperty(scope, applyTo, key :: string, value) end elseif keyType == "SpecialKey" then - local stage = (key :: PubTypes.SpecialKey).stage + local stage = (key :: Types.SpecialKey).stage local keys = specialKeys[stage] if keys == nil then logError("unrecognisedPropertyStage", nil, stage) diff --git a/src/Instances/defaultProps.lua b/src/Instances/defaultProps.lua index afe0cf47a..792c7b841 100644 --- a/src/Instances/defaultProps.lua +++ b/src/Instances/defaultProps.lua @@ -1,4 +1,5 @@ --!strict +--!nolint LocalShadow --[[ Stores 'sensible default' properties to be applied to instances created by diff --git a/src/InternalTypes.lua b/src/InternalTypes.lua new file mode 100644 index 000000000..2bce47ab6 --- /dev/null +++ b/src/InternalTypes.lua @@ -0,0 +1,116 @@ +--!strict +--!nolint LocalShadow + +--[[ + Stores common type information used internally. + + These types may be used internally so Fusion code can type-check, but + should never be exposed to public users, as these definitions are fair game + for breaking changes. +]] + +local Package = script.Parent +local Types = require(Package.Types) + +type Set = {[T]: unknown} + +--[[ + General use types +]] + +-- Stores useful information about Luau errors. +export type Error = { + type: string, -- replace with "Error" when Luau supports singleton types + raw: string, + message: string, + trace: string +} + +--[[ + Generic reactive graph types +]] + +export type StateObject = Types.StateObject & { + _peek: (StateObject) -> T +} + +--[[ + Specific reactive graph types +]] + +-- A state object whose value can be set at any time by the user. +export type Value = Types.Value & { + _value: T +} + +-- A state object whose value is derived from other objects using a callback. +export type Computed = Types.Computed & { + scope: Types.Scope?, + _oldDependencySet: Set, + _processor: (Types.Use, Types.Scope) -> T, + _value: T, + _innerScope: Types.Scope? +} + +-- A state object which maps over keys and/or values in another table. +export type For = Types.For & { + scope: Types.Scope?, + _processor: ( + Types.Scope, + Types.StateObject<{key: KI, value: VI}> + ) -> (Types.StateObject<{key: KO?, value: VO?}>), + _inputTable: Types.CanBeState<{[KI]: VI}>, + _existingInputTable: {[KI]: VI}?, + _existingOutputTable: {[KO]: VO}, + _existingProcessors: {[ForProcessor]: true}, + _newOutputTable: {[KO]: VO}, + _newProcessors: {[ForProcessor]: true}, + _remainingPairs: {[KI]: {[VI]: true}} +} +type ForProcessor = { + inputPair: Types.Value<{key: unknown, value: unknown}>, + outputPair: Types.StateObject<{key: unknown, value: unknown}>, + cleanupTask: unknown +} + +-- A state object which follows another state object using tweens. +export type Tween = Types.Tween & { + _goalState: Value, + _tweenInfo: TweenInfo, + _prevValue: T, + _nextValue: T, + _currentValue: T, + _currentTweenInfo: TweenInfo, + _currentTweenDuration: number, + _currentTweenStartTime: number, + _currentlyAnimating: boolean +} + +-- A state object which follows another state object using spring simulation. +export type Spring = Types.Spring & { + _speed: Types.CanBeState, + _damping: Types.CanBeState, + _goalState: Value, + _goalValue: T, + + _currentType: string, + _currentValue: T, + _currentSpeed: number, + _currentDamping: number, + + _springPositions: {number}, + _springGoals: {number}, + _springVelocities: {number}, + + _lastSchedule: number, + _startDisplacements: {number}, + _startVelocities: {number} +} + +-- An object which can listen for updates on another state object. +export type Observer = Types.Observer & { + _changeListeners: {[{}]: () -> ()}, + _numChangeListeners: number +} + +return nil \ No newline at end of file diff --git a/src/Logging/logError.lua b/src/Logging/logError.lua index c680da2d6..589b3f479 100644 --- a/src/Logging/logError.lua +++ b/src/Logging/logError.lua @@ -1,14 +1,19 @@ --!strict +--!nolint LocalShadow --[[ Utility function to log a Fusion-specific error. ]] local Package = script.Parent.Parent -local Types = require(Package.Types) +local InternalTypes = require(Package.InternalTypes) local messages = require(Package.Logging.messages) -local function logError(messageID: string, errObj: Types.Error?, ...) +local function logError( + messageID: string, + errObj: InternalTypes.Error?, + ...: unknown +) local formatString: string if messages[messageID] ~= nil then diff --git a/src/Logging/logErrorNonFatal.lua b/src/Logging/logErrorNonFatal.lua index e4edc8f54..5f853dc51 100644 --- a/src/Logging/logErrorNonFatal.lua +++ b/src/Logging/logErrorNonFatal.lua @@ -1,15 +1,20 @@ --!strict +--!nolint LocalShadow --[[ Utility function to log a Fusion-specific error, without halting execution. ]] local Package = script.Parent.Parent -local Types = require(Package.Types) +local InternalTypes = require(Package.InternalTypes) local External = require(Package.External) local messages = require(Package.Logging.messages) -local function logErrorNonFatal(messageID: string, errObj: Types.Error?, ...) +local function logErrorNonFatal( + messageID: string, + errObj: InternalTypes.Error?, + ...: unknown +) if External.unitTestSilenceNonFatal then return end diff --git a/src/Logging/logWarn.lua b/src/Logging/logWarn.lua index 317e5241e..2216a0f30 100644 --- a/src/Logging/logWarn.lua +++ b/src/Logging/logWarn.lua @@ -1,4 +1,5 @@ --!strict +--!nolint LocalShadow --[[ Utility function to log a Fusion-specific warning. @@ -7,7 +8,10 @@ local Package = script.Parent.Parent local messages = require(Package.Logging.messages) -local function logWarn(messageID, ...) +local function logWarn( + messageID: string, + ...: unknown +) local formatString: string if messages[messageID] ~= nil then diff --git a/src/Logging/messages.lua b/src/Logging/messages.lua index 830c690d3..0dc9e8ce3 100644 --- a/src/Logging/messages.lua +++ b/src/Logging/messages.lua @@ -1,4 +1,5 @@ --!strict +--!nolint LocalShadow --[[ Stores templates for different kinds of logging messages. diff --git a/src/Logging/parseError.lua b/src/Logging/parseError.lua index 9703253b7..d5999ea4b 100644 --- a/src/Logging/parseError.lua +++ b/src/Logging/parseError.lua @@ -1,4 +1,5 @@ --!strict +--!nolint LocalShadow --[[ An xpcall() error handler to collect and parse useful information about @@ -6,9 +7,11 @@ ]] local Package = script.Parent.Parent -local Types = require(Package.Types) +local InternalTypes = require(Package.InternalTypes) -local function parseError(err: string): Types.Error +local function parseError( + err: string +): InternalTypes.Error return { type = "Error", raw = err, diff --git a/src/Memory/deriveScope.lua b/src/Memory/deriveScope.lua index 3f2c7992f..110e36947 100644 --- a/src/Memory/deriveScope.lua +++ b/src/Memory/deriveScope.lua @@ -1,15 +1,16 @@ --!strict +--!nolint LocalShadow --[[ Creates an empty scope with the same metatables as the original scope. Used for preserving access to constructors when creating inner scopes. ]] local Package = script.Parent.Parent -local PubTypes = require(Package.PubTypes) +local Types = require(Package.Types) -- This return type is technically a lie, but it's required for useful type -- checking behaviour. -local function deriveScope(scope: PubTypes.Scope): PubTypes.Scope +local function deriveScope(scope: Types.Scope): Types.Scope return setmetatable({}, getmetatable(scope)) :: any end diff --git a/src/Memory/doCleanup.lua b/src/Memory/doCleanup.lua index d1d57901c..cd1b78c16 100644 --- a/src/Memory/doCleanup.lua +++ b/src/Memory/doCleanup.lua @@ -1,4 +1,5 @@ --!strict +--!nolint LocalShadow --[[ Cleans up the tasks passed in as the arguments. @@ -11,32 +12,42 @@ - an array - `cleanup` will be called on each item ]] -local function doCleanupOne(task: any) +local function doCleanupOne( + task: unknown +) local taskType = typeof(task) -- case 1: Instance if taskType == "Instance" then + local task = task :: Instance task:Destroy() -- case 2: RBXScriptConnection elseif taskType == "RBXScriptConnection" then + local task = task :: RBXScriptConnection task:Disconnect() -- case 3: callback elseif taskType == "function" then + local task = task :: (...unknown) -> (...unknown) task() elseif taskType == "table" then + local task = task :: {destroy: unknown?, Destroy: unknown?} + -- case 4: destroy() function if typeof(task.destroy) == "function" then + local task = (task :: any) :: {destroy: (...unknown) -> (...unknown)} task:destroy() -- case 5: Destroy() function elseif typeof(task.Destroy) == "function" then + local task = (task :: any) :: {Destroy: (...unknown) -> (...unknown)} task:Destroy() -- case 6: array of tasks elseif task[1] ~= nil then + local task = task :: {unknown} -- It is important to iterate backwards through the table, since -- objects are added in order of construction. for index = #task, 1, -1 do @@ -47,7 +58,9 @@ local function doCleanupOne(task: any) end end -local function doCleanup(...: any) +local function doCleanup( + ...: unknown +) for index = 1, select("#", ...) do doCleanupOne(select(index, ...)) end diff --git a/src/Memory/doNothing.lua b/src/Memory/doNothing.lua deleted file mode 100644 index 7bdade2c4..000000000 --- a/src/Memory/doNothing.lua +++ /dev/null @@ -1,10 +0,0 @@ ---!strict - ---[[ - An empty function. Often used as a destructor to indicate no destruction. -]] - -local function doNothing(...: any) -end - -return doNothing \ No newline at end of file diff --git a/src/Memory/legacyCleanup.lua b/src/Memory/legacyCleanup.lua index 39a75a0b7..c84247bef 100644 --- a/src/Memory/legacyCleanup.lua +++ b/src/Memory/legacyCleanup.lua @@ -1,10 +1,13 @@ --!strict +--!nolint LocalShadow local Package = script.Parent.Parent local logWarn = require(Package.Logging.logWarn) local doCleanup = require(Package.Memory.doCleanup) -local function legacyCleanup(...: any) +local function legacyCleanup( + ...: unknown +) logWarn("cleanupWasRenamed") return doCleanup(...) end diff --git a/src/Memory/needsDestruction.lua b/src/Memory/needsDestruction.lua index 7b375e2c1..1d2d43be3 100644 --- a/src/Memory/needsDestruction.lua +++ b/src/Memory/needsDestruction.lua @@ -1,12 +1,15 @@ --!strict +--!nolint LocalShadow --[[ - Returns true if the given value is not automatically memory managed, and - requires manual cleanup. + Returns true if the given value is not automatically memory managed, and + requires manual cleanup. ]] -local function needsDestruction(x: any): boolean - return typeof(x) == "Instance" +local function needsDestruction( + x: unknown +): boolean + return typeof(x) == "Instance" end return needsDestruction \ No newline at end of file diff --git a/src/Memory/scoped.lua b/src/Memory/scoped.lua index 779096a6d..f661f376c 100644 --- a/src/Memory/scoped.lua +++ b/src/Memory/scoped.lua @@ -1,18 +1,19 @@ --!strict +--!nolint LocalShadow --[[ Creates cleanup tables with access to constructors as methods. ]] local Package = script.Parent.Parent -local PubTypes = require(Package.PubTypes) +local Types = require(Package.Types) local logError = require(Package.Logging.logError) local function merge( - into: any, - from: any?, - ...: any -): any + into: {[unknown]: unknown}, + from: {[unknown]: unknown}?, + ...: {[unknown]: unknown} +): {[unknown]: unknown} if from == nil then return into else @@ -28,25 +29,9 @@ local function merge( end local function scoped( - ...: any -): any + ...: {[unknown]: unknown} +): {[unknown]: unknown} return setmetatable({}, {__index = merge({}, ...)}) :: any end --- Is there a sane way to write out this type? --- ... I sure hope so. - -return (scoped :: any) :: - (() -> PubTypes.Scope<{}>) & - ((A & {}) -> PubTypes.Scope) & - ((A & {}, B & {}) -> PubTypes.Scope) & - ((A & {}, B & {}, C & {}) -> PubTypes.Scope) & - ((A & {}, B & {}, C & {}, D & {}) -> PubTypes.Scope) & - ((A & {}, B & {}, C & {}, D & {}, E & {}) -> PubTypes.Scope) & - ((A & {}, B & {}, C & {}, D & {}, E & {}, F & {}) -> PubTypes.Scope) & - ((A & {}, B & {}, C & {}, D & {}, E & {}, F & {}, G & {}) -> PubTypes.Scope) & - ((A & {}, B & {}, C & {}, D & {}, E & {}, F & {}, G & {}, H & {}) -> PubTypes.Scope) & - ((A & {}, B & {}, C & {}, D & {}, E & {}, F & {}, G & {}, H & {}, I & {}) -> PubTypes.Scope) & - ((A & {}, B & {}, C & {}, D & {}, E & {}, F & {}, G & {}, H & {}, I & {}, J & {}) -> PubTypes.Scope) & - ((A & {}, B & {}, C & {}, D & {}, E & {}, F & {}, G & {}, H & {}, I & {}, J & {}, K & {}) -> PubTypes.Scope) & - ((A & {}, B & {}, C & {}, D & {}, E & {}, F & {}, G & {}, H & {}, I & {}, J & {}, K & {}, L & {}) -> PubTypes.Scope) \ No newline at end of file +return (scoped :: any) :: Types.ScopedConstructor \ No newline at end of file diff --git a/src/Memory/whichLivesLonger.lua b/src/Memory/whichLivesLonger.lua index b7bf68ded..0a20f9336 100644 --- a/src/Memory/whichLivesLonger.lua +++ b/src/Memory/whichLivesLonger.lua @@ -7,11 +7,11 @@ to infer this from their scopes. ]] local Package = script.Parent.Parent -local PubTypes = require(Package.PubTypes) +local Types = require(Package.Types) local function whichScopeLivesLonger( - scopeA: PubTypes.Scope, - scopeB: PubTypes.Scope + scopeA: Types.Scope, + scopeB: Types.Scope ): "a" | "b" | "unknown" -- If we can prove one scope is inside of the other scope, then the outer -- scope must live longer than the inner scope (assuming idiomatic scopes). @@ -29,7 +29,7 @@ local function whichScopeLivesLonger( elseif inScope == scopeB then return "a" elseif typeof(inScope) == "table" then - local inScope: {any} = inScope + local inScope = inScope :: {unknown} if inScope[1] ~= nil and closedSet[scope] == nil then nextOpenSetSize += 1 nextOpenSet[nextOpenSetSize] = inScope @@ -45,13 +45,13 @@ local function whichScopeLivesLonger( end local function whichLivesLonger( - scopeA: PubTypes.Scope, - a: any, - scopeB: PubTypes.Scope, - b: any + scopeA: Types.Scope, + a: unknown, + scopeB: Types.Scope, + b: unknown ): "a" | "b" | "unknown" if scopeA == scopeB then - local scopeA: {any} = scopeA + local scopeA: {unknown} = scopeA for index = #scopeA, 1, -1 do local value = scopeA[index] if value == a then diff --git a/src/PubTypes.lua b/src/PubTypes.lua deleted file mode 100644 index a2845a96c..000000000 --- a/src/PubTypes.lua +++ /dev/null @@ -1,235 +0,0 @@ ---!strict - ---[[ - Stores common public-facing type information for Fusion APIs. -]] - -type Set = {[T]: any} - ---[[ - General use types -]] - --- Types that can be expressed as vectors of numbers, and so can be animated. -export type Animatable = - number | - CFrame | - Color3 | - ColorSequenceKeypoint | - DateTime | - NumberRange | - NumberSequenceKeypoint | - PhysicalProperties | - Ray | - Rect | - Region3 | - Region3int16 | - UDim | - UDim2 | - Vector2 | - Vector2int16 | - Vector3 | - Vector3int16 - --- A task which can be accepted for cleanup. -export type Task = - Instance | - RBXScriptConnection | - () -> () | - {destroy: (any) -> ()} | - {Destroy: (any) -> ()} | - {Task} - --- A scope of tasks to clean up. -export type Scope = {any} & Constructors - --- An object which uses a scope to dictate how long it lives. -export type ScopeLifetime = { - scope: Scope? -} - --- Script-readable version information. -export type Version = { - major: number, - minor: number, - isRelease: boolean -} ---[[ - Generic reactive graph types -]] - --- A graph object which can have dependents. -export type Dependency = ScopeLifetime & { - dependentSet: Set -} - --- A graph object which can have dependencies. -export type Dependent = ScopeLifetime & { - update: (Dependent) -> boolean, - dependencySet: Set -} - --- An object which stores a piece of reactive state. -export type StateObject = Dependency & { - type: "State", - kind: string, - _typeIdentifier: T -} - --- Either a constant value of type T, or a state object containing type T. -export type CanBeState = StateObject | T - --- Function signature for use callbacks. -export type Use = (target: CanBeState) -> T - ---[[ - Specific reactive graph types -]] - --- A state object whose value can be set at any time by the user. -export type Value = StateObject & { - kind: "State", - set: (Value, newValue: any, force: boolean?) -> (), - destroy: () -> () -} -type ValueConstructor = ( - scope: Scope, - initialValue: T -) -> Value - --- A state object whose value is derived from other objects using a callback. -export type Computed = StateObject & Dependent & { - kind: "Computed", - destroy: () -> () -} -type ComputedConstructor = ( - scope: Scope, - callback: (Use, Scope) -> T -) -> Computed - --- A state object which maps over keys and/or values in another table. -export type For = StateObject<{[KO]: VO}> & Dependent & { - kind: "For", - destroy: () -> () -} -export type ForPairsConstructor = ( - scope: Scope, - inputTable: CanBeState<{[KI]: VI}>, - processor: (Scope, Use, KI, VI) -> (KO, VO) -) -> For -export type ForKeysConstructor = ( - scope: Scope, - inputTable: CanBeState<{[KI]: V}>, - processor: (Scope, Use, KI) -> KO -) -> For -export type ForValuesConstructor = ( - scope: Scope, - inputTable: CanBeState<{[K]: VI}>, - processor: (Scope, Use, VI) -> VO -) -> For - --- An object which can listen for updates on another state object. -export type Observer = Dependent & { - kind: "Observer", - onChange: (Observer, callback: () -> ()) -> (() -> ()), - onBind: (Observer, callback: () -> ()) -> (() -> ()), - destroy: () -> () -} -type ObserverConstructor = ( - scope: Scope, - watchedState: StateObject -) -> Observer - --- A state object which follows another state object using tweens. -export type Tween = StateObject & Dependent & { - kind: "Tween", - destroy: () -> () -} -type TweenConstructor = ( - scope: Scope, - goalState: StateObject, - tweenInfo: TweenInfo? -) -> Tween - --- A state object which follows another state object using spring simulation. -export type Spring = StateObject & Dependent & { - kind: "Spring", - setPosition: (Spring, newPosition: Animatable) -> (), - setVelocity: (Spring, newVelocity: Animatable) -> (), - addVelocity: (Spring, deltaVelocity: Animatable) -> (), - destroy: () -> () -} -type SpringConstructor = ( - scope: Scope, - goalState: StateObject, - speed: CanBeState?, - damping: CanBeState? -) -> Spring - ---[[ - Instance related types -]] - --- Denotes children instances in an instance or component's property table. -export type SpecialKey = { - type: "SpecialKey", - kind: string, - stage: "self" | "descendants" | "ancestor" | "observer", - apply: ( - self: SpecialKey, - scope: Scope, - value: any, - applyTo: Instance - ) -> () -} - --- A collection of instances that may be parented to another instance. -export type Children = Instance | StateObject | {[any]: Children} - --- A table that defines an instance's properties, handlers and children. -export type PropertyTable = {[string | SpecialKey]: any} - -type NewConstructor = ( - scope: Scope, - className: string -) -> (propertyTable: PropertyTable) -> Instance - -type HydrateConstructor = ( - scope: Scope, - target: Instance -) -> (propertyTable: PropertyTable) -> Instance - -export type Fusion = { - version: Version, - - doCleanup: (...any) -> (), - doNothing: (...any) -> (), - scoped: (constructors: T) -> Scope, - deriveScope: (scope: Scope) -> Scope, - - peek: Use, - Value: ValueConstructor, - Computed: ComputedConstructor, - ForPairs: ForPairsConstructor, - ForKeys: ForKeysConstructor, - ForValues: ForValuesConstructor, - Observer: ObserverConstructor, - - Tween: TweenConstructor, - Spring: SpringConstructor, - - New: NewConstructor, - Hydrate: HydrateConstructor, - - Ref: SpecialKey, - Children: SpecialKey, - Out: (propertyName: string) -> SpecialKey, - OnEvent: (eventName: string) -> SpecialKey, - OnChange: (propertyName: string) -> SpecialKey, - Attribute: (attributeName: string) -> SpecialKey, - AttributeChange: (attributeName: string) -> SpecialKey, - AttributeOut: (attributeName: string) -> SpecialKey, - -} - -return nil diff --git a/src/RobloxExternal.lua b/src/RobloxExternal.lua index 779664365..f5e874aba 100644 --- a/src/RobloxExternal.lua +++ b/src/RobloxExternal.lua @@ -1,6 +1,8 @@ --!strict +--!nolint LocalShadow + --[[ - Roblox implementation for Fusion's abstract scheduler layer. + Roblox implementation for Fusion's abstract scheduler layer. ]] local RunService = game:GetService("RunService") @@ -15,63 +17,63 @@ local RobloxExternal = {} Sends an immediate task to the external scheduler. Throws if none is set. ]] function RobloxExternal.doTaskImmediate( - resume: () -> () + resume: () -> () ) task.spawn(resume) end --[[ - Sends a deferred task to the external scheduler. Throws if none is set. + Sends a deferred task to the external scheduler. Throws if none is set. ]] function RobloxExternal.doTaskDeferred( - resume: () -> () + resume: () -> () ) - task.defer(resume) + task.defer(resume) end --[[ - Sends an update step to Fusion using the Roblox clock time. + Sends an update step to Fusion using the Roblox clock time. ]] local function performUpdateStep() - External.performUpdateStep(os.clock()) + External.performUpdateStep(os.clock()) end --[[ - Binds Fusion's update step to RunService step events. + Binds Fusion's update step to RunService step events. ]] local stopSchedulerFunc = nil :: (() -> ())? function RobloxExternal.startScheduler() - if stopSchedulerFunc ~= nil then - return - end - if RunService:IsClient() then - -- In cases where multiple Fusion modules are running simultaneously, - -- this prevents collisions. - local id = "FusionUpdateStep_" .. HttpService:GenerateGUID() - RunService:BindToRenderStep( - id, - Enum.RenderPriority.First.Value, - performUpdateStep - ) - stopSchedulerFunc = function() - RunService:UnbindFromRenderStep(id) - end - else - local connection = RunService.Heartbeat:Connect(performUpdateStep) - stopSchedulerFunc = function() - connection:Disconnect() - end - end + if stopSchedulerFunc ~= nil then + return + end + if RunService:IsClient() then + -- In cases where multiple Fusion modules are running simultaneously, + -- this prevents collisions. + local id = "FusionUpdateStep_" .. HttpService:GenerateGUID() + RunService:BindToRenderStep( + id, + Enum.RenderPriority.First.Value, + performUpdateStep + ) + stopSchedulerFunc = function() + RunService:UnbindFromRenderStep(id) + end + else + local connection = RunService.Heartbeat:Connect(performUpdateStep) + stopSchedulerFunc = function() + connection:Disconnect() + end + end end --[[ - Unbinds Fusion's update step from RunService step events. + Unbinds Fusion's update step from RunService step events. ]] function RobloxExternal.stopScheduler() - if stopSchedulerFunc ~= nil then - stopSchedulerFunc() - stopSchedulerFunc = nil - end + if stopSchedulerFunc ~= nil then + stopSchedulerFunc() + stopSchedulerFunc = nil + end end return RobloxExternal \ No newline at end of file diff --git a/src/State/Computed.lua b/src/State/Computed.lua index d58f082eb..b76bdb6f4 100644 --- a/src/State/Computed.lua +++ b/src/State/Computed.lua @@ -1,13 +1,14 @@ ---!nonstrict +--!strict --!nolint LocalShadow + --[[ Constructs and returns objects which can be used to model derived reactive state. ]] local Package = script.Parent.Parent -local PubTypes = require(Package.PubTypes) local Types = require(Package.Types) +local InternalTypes = require(Package.InternalTypes) -- Logging local logError = require(Package.Logging.logError) local logErrorNonFatal = require(Package.Logging.logErrorNonFatal) @@ -32,6 +33,12 @@ local CLASS_METATABLE = {__index = class} Returns true if it changed, or false if it's identical. ]] function class:update(): boolean + local self = self :: InternalTypes.Computed + if self.scope == nil then + return false + end + local outerScope = self.scope :: Types.Scope + -- remove this object from its dependencies' dependent sets for dependency in pairs(self.dependencySet) do dependency.dependentSet[self] = nil @@ -44,17 +51,17 @@ function class:update(): boolean self._oldDependencySet, self.dependencySet = self.dependencySet, self._oldDependencySet table.clear(self.dependencySet) - local innerScope = deriveScope(self.scope) - local function use(target: PubTypes.CanBeState): T + local innerScope = deriveScope(outerScope) + local function use(target: Types.CanBeState): T if isState(target) then - local target = target :: PubTypes.StateObject + local target = target :: Types.StateObject if target.scope == nil then logError("useAfterDestroy", nil, `The {target.kind} object`, "the Computed that is use()-ing it") - elseif whichLivesLonger(self.scope, self, target.scope, target) == "a" then + elseif whichLivesLonger(outerScope, self, target.scope, target) == "a" then logWarn("possiblyOutlives", `The {target.kind} object`, "the Computed that is use()-ing it") end self.dependencySet[target] = true - return (target :: Types.StateObject):_peek() + return (target :: InternalTypes.StateObject):_peek() else return target :: T end @@ -77,9 +84,10 @@ function class:update(): boolean return not similar else + local errorObj = (newValue :: any) :: InternalTypes.Error -- this needs to be non-fatal, because otherwise it'd disrupt the -- update process - logErrorNonFatal("computedCallbackError", newValue) + logErrorNonFatal("computedCallbackError", errorObj) doCleanup(innerScope) @@ -98,7 +106,8 @@ end --[[ Returns the interior value of this state object. ]] -function class:_peek(): any +function class:_peek(): unknown + local self = self :: InternalTypes.Computed return self._value end @@ -107,6 +116,7 @@ function class:get() end function class:destroy() + local self = self :: InternalTypes.Computed if self.scope == nil then logError("destroyedTwice", nil, "Computed") end @@ -120,10 +130,10 @@ function class:destroy() end local function Computed( - scope: PubTypes.Scope, - processor: (PubTypes.Use, PubTypes.Scope) -> T, - destructor: any -): Types.Computed + scope: Types.Scope, + processor: (Types.Use, Types.Scope) -> T, + destructor: unknown? +): Types.Computed if typeof(scope) == "function" then logError("scopeMissing", nil, "Computeds", "myScope:Computed(function(use, scope) ... end)") elseif destructor ~= nil then @@ -140,6 +150,8 @@ local function Computed( _value = nil, _innerScope = nil }, CLASS_METATABLE) + local self = (self :: any) :: InternalTypes.Computed + table.insert(scope, self) self:update() diff --git a/src/State/For.lua b/src/State/For.lua index c210ee27c..0c83785b8 100644 --- a/src/State/For.lua +++ b/src/State/For.lua @@ -1,12 +1,13 @@ ---!nonstrict +--!strict +--!nolint LocalShadow --[[ The private generic implementation for all public `For` objects. ]] local Package = script.Parent.Parent -local PubTypes = require(Package.PubTypes) local Types = require(Package.Types) +local InternalTypes = require(Package.InternalTypes) -- Logging local logError = require(Package.Logging.logError) local logErrorNonFatal = require(Package.Logging.logErrorNonFatal) @@ -22,14 +23,17 @@ local deriveScope = require(Package.Memory.deriveScope) local class = {} local CLASS_METATABLE = { __index = class } -local WEAK_KEYS_METATABLE = { __mode = "k" } --[[ Called when the original table is changed. ]] function class:update(): boolean - + local self = self :: InternalTypes.For + if self.scope == nil then + return false + end + local outerScope = self.scope :: Types.Scope local existingInputTable = self._existingInputTable local existingOutputTable = self._existingOutputTable local existingProcessors = self._existingProcessors @@ -45,7 +49,8 @@ function class:update(): boolean table.clear(self.dependencySet) if isState(self._inputTable) then - self._inputTable.dependentSet[self], self.dependencySet[self._inputTable] = true, true + local inputTable = self._inputTable :: Types.StateObject<{[unknown]: unknown}> + inputTable.dependentSet[self], self.dependencySet[inputTable] = true, true end if newInputTable ~= existingInputTable then @@ -139,18 +144,19 @@ function class:update(): boolean for key, remainingValues in remainingPairs do for value in remainingValues do - local scope = deriveScope(self.scope) - local inputPair = Value(scope, {key = key, value = value}) - local processOK, outputPair = xpcall(self._processor, parseError, scope, inputPair) + local innerScope = deriveScope(outerScope) + local inputPair = Value(innerScope, {key = key, value = value}) + local processOK, outputPair = xpcall(self._processor, parseError, innerScope, inputPair) if processOK then local processor = { inputPair = inputPair, outputPair = outputPair, - cleanupTask = scope + cleanupTask = innerScope } newProcessors[processor] = true else - logErrorNonFatal("forProcessorError", outputPair) + local errorObj = (outputPair :: any) :: InternalTypes.Error + logErrorNonFatal("forProcessorError", errorObj) end end end @@ -169,7 +175,7 @@ function class:update(): boolean if newOutputTable[key] == nil then newOutputTable[key] = value else - logErrorNonFatal("forKeyCollision", key) + logErrorNonFatal("forKeyCollision", nil, key) end end @@ -187,7 +193,7 @@ end --[[ Returns the interior value of this state object. ]] -function class:_peek(): any +function class:_peek(): unknown return self._existingOutputTable end @@ -208,23 +214,21 @@ function class:destroy() end end -local function For( - scope: PubTypes.Scope, - inputTable: PubTypes.CanBeState<{ [KI]: VI }>, +local function For( + scope: Types.Scope, + inputTable: Types.CanBeState<{ [KI]: VI }>, processor: ( - PubTypes.Scope, - PubTypes.StateObject<{key: KI, value: VI}> - ) -> (PubTypes.StateObject<{key: KO?, value: VO?}>) -): Types.For + Types.Scope, + Types.StateObject<{key: KI, value: VI}> + ) -> (Types.StateObject<{key: KO?, value: VO?}>) +): Types.For local self = setmetatable({ type = "State", kind = "For", scope = scope, dependencySet = {}, - -- if we held strong references to the dependents, then they wouldn't be - -- able to get garbage collected when they fall out of scope - dependentSet = setmetatable({}, WEAK_KEYS_METATABLE), + dependentSet = {}, _processor = processor, _inputTable = inputTable, _existingInputTable = nil, @@ -234,10 +238,10 @@ local function For( _newProcessors = {}, _remainingPairs = {} }, CLASS_METATABLE) + local self = (self :: any) :: InternalTypes.For - self:update() - table.insert(scope, self) + self:update() return self end diff --git a/src/State/ForKeys.lua b/src/State/ForKeys.lua index d410894b3..3d5cfef4e 100644 --- a/src/State/ForKeys.lua +++ b/src/State/ForKeys.lua @@ -1,4 +1,5 @@ --!strict +--!nolint LocalShadow --[[ Constructs a new For object which maps keys of a table using a `processor` @@ -12,8 +13,8 @@ ]] local Package = script.Parent.Parent -local PubTypes = require(Package.PubTypes) local Types = require(Package.Types) +local InternalTypes = require(Package.InternalTypes) -- State local For = require(Package.State.For) local Computed = require(Package.State.Computed) @@ -26,11 +27,11 @@ local logWarn = require(Package.Logging.logWarn) local doCleanup = require(Package.Memory.doCleanup) local function ForKeys( - scope: PubTypes.Scope, - inputTable: PubTypes.CanBeState<{[KI]: V}>, - processor: (PubTypes.Use, PubTypes.Scope, KI) -> KO, - destructor: any? -): Types.For + scope: Types.Scope, + inputTable: Types.CanBeState<{[KI]: V}>, + processor: (Types.Use, Types.Scope, KI) -> KO, + destructor: unknown? +): Types.For if typeof(inputTable) == "function" then logError("scopeMissing", nil, "ForKeys", "myScope:ForKeys(inputTable, function(scope, use, key) ... end)") elseif destructor ~= nil then @@ -40,8 +41,8 @@ local function ForKeys( scope, inputTable, function( - scope: PubTypes.Scope, - inputPair: PubTypes.StateObject<{key: KI, value: V}> + scope: Types.Scope, + inputPair: Types.StateObject<{key: KI, value: V}> ) local inputKey = Computed(scope, function(use, scope): KI return use(inputPair).key @@ -51,7 +52,8 @@ local function ForKeys( if ok then return key else - logErrorNonFatal("forProcessorError", key :: any) + local errorObj = (key :: any) :: InternalTypes.Error + logErrorNonFatal("forProcessorError", errorObj) doCleanup(scope) table.clear(scope) return nil diff --git a/src/State/ForPairs.lua b/src/State/ForPairs.lua index 9ff9b7094..f802e91a8 100644 --- a/src/State/ForPairs.lua +++ b/src/State/ForPairs.lua @@ -1,4 +1,5 @@ --!strict +--!nolint LocalShadow --[[ Constructs a new For object which maps pairs of a table using a `processor` @@ -12,8 +13,8 @@ ]] local Package = script.Parent.Parent -local PubTypes = require(Package.PubTypes) local Types = require(Package.Types) +local InternalTypes = require(Package.InternalTypes) -- State local For = require(Package.State.For) local Computed = require(Package.State.Computed) @@ -26,11 +27,11 @@ local logWarn = require(Package.Logging.logWarn) local doCleanup = require(Package.Memory.doCleanup) local function ForPairs( - scope: PubTypes.Scope, - inputTable: PubTypes.CanBeState<{[KI]: VI}>, - processor: (PubTypes.Use, PubTypes.Scope, KI, VI) -> (KO, VO), - destructor: any? -): Types.For + scope: Types.Scope, + inputTable: Types.CanBeState<{[KI]: VI}>, + processor: (Types.Use, Types.Scope, KI, VI) -> (KO, VO), + destructor: unknown? +): Types.For if typeof(inputTable) == "function" then logError("scopeMissing", nil, "ForPairs", "myScope:ForPairs(inputTable, function(scope, use, key, value) ... end)") elseif destructor ~= nil then @@ -40,15 +41,16 @@ local function ForPairs( scope, inputTable, function( - scope: PubTypes.Scope, - inputPair: PubTypes.StateObject<{key: KI, value: VI}> + scope: Types.Scope, + inputPair: Types.StateObject<{key: KI, value: VI}> ) return Computed(scope, function(use, scope): {key: KO?, value: VO?} local ok, key, value = xpcall(processor, parseError, use, scope, use(inputPair).key, use(inputPair).value) if ok then return {key = key, value = value} else - logErrorNonFatal("forProcessorError", key :: any) + local errorObj = (key :: any) :: InternalTypes.Error + logErrorNonFatal("forProcessorError", errorObj) doCleanup(scope) table.clear(scope) return {key = nil, value = nil} diff --git a/src/State/ForValues.lua b/src/State/ForValues.lua index fd149803d..40098e63f 100644 --- a/src/State/ForValues.lua +++ b/src/State/ForValues.lua @@ -1,4 +1,5 @@ --!strict +--!nolint LocalShadow --[[ Constructs a new For object which maps values of a table using a `processor` @@ -12,8 +13,8 @@ ]] local Package = script.Parent.Parent -local PubTypes = require(Package.PubTypes) local Types = require(Package.Types) +local InternalTypes = require(Package.InternalTypes) -- State local For = require(Package.State.For) local Computed = require(Package.State.Computed) @@ -26,11 +27,11 @@ local logWarn = require(Package.Logging.logWarn) local doCleanup = require(Package.Memory.doCleanup) local function ForValues( - scope: PubTypes.Scope, - inputTable: PubTypes.CanBeState<{[K]: VI}>, - processor: (PubTypes.Use, PubTypes.Scope, VI) -> VO, - destructor: any? -): Types.For + scope: Types.Scope, + inputTable: Types.CanBeState<{[K]: VI}>, + processor: (Types.Use, Types.Scope, VI) -> VO, + destructor: unknown? +): Types.For if typeof(inputTable) == "function" then logError("scopeMissing", nil, "ForValues", "myScope:ForValues(inputTable, function(scope, use, value) ... end)") elseif destructor ~= nil then @@ -40,8 +41,8 @@ local function ForValues( scope, inputTable, function( - scope: PubTypes.Scope, - inputPair: PubTypes.StateObject<{key: K, value: VI}> + scope: Types.Scope, + inputPair: Types.StateObject<{key: K, value: VI}> ) local inputValue = Computed(scope, function(use, scope): VI return use(inputPair).value @@ -51,7 +52,8 @@ local function ForValues( if ok then return {key = nil, value = value} else - logErrorNonFatal("forProcessorError", value :: any) + local errorObj = (value :: any) :: InternalTypes.Error + logErrorNonFatal("forProcessorError", errorObj) doCleanup(scope) table.clear(scope) return {key = nil, value = nil} diff --git a/src/State/Observer.lua b/src/State/Observer.lua index ca1f81d86..21aad912c 100644 --- a/src/State/Observer.lua +++ b/src/State/Observer.lua @@ -1,22 +1,19 @@ ---!nonstrict +--!strict +--!nolint LocalShadow --[[ Constructs a new state object which can listen for updates on another state object. - - FIXME: enabling strict types here causes free types to leak ]] local Package = script.Parent.Parent -local PubTypes = require(Package.PubTypes) local Types = require(Package.Types) +local InternalTypes = require(Package.InternalTypes) local External = require(Package.External) local whichLivesLonger = require(Package.Memory.whichLivesLonger) local logWarn = require(Package.Logging.logWarn) local logError = require(Package.Logging.logError) -type Set = {[T]: any} - local class = {} local CLASS_METATABLE = {__index = class} @@ -24,6 +21,7 @@ local CLASS_METATABLE = {__index = class} Called when the watched state changes value. ]] function class:update(): boolean + local self = self :: InternalTypes.Observer for _, callback in pairs(self._changeListeners) do External.doTaskImmediate(callback) end @@ -38,7 +36,10 @@ end As long as there is at least one active change listener, this Observer will be held in memory, preventing GC, so disconnecting is important. ]] -function class:onChange(callback: () -> ()): () -> () +function class:onChange( + callback: () -> () +): () -> () + local self = self :: InternalTypes.Observer local uniqueIdentifier = {} self._changeListeners[uniqueIdentifier] = callback return function() @@ -50,12 +51,16 @@ end Similar to `class:onChange()`, however it runs the provided callback immediately. ]] -function class:onBind(callback: () -> ()): () -> () +function class:onBind( + callback: () -> () +): () -> () + local self = self :: InternalTypes.Observer External.doTaskImmediate(callback) return self:onChange(callback) end function class:destroy() + local self = self :: InternalTypes.Observer if self.scope == nil then logError("destroyedTwice", nil, "Observer") end @@ -66,8 +71,8 @@ function class:destroy() end local function Observer( - scope: PubTypes.Scope, - watchedState: PubTypes.StateObject + scope: Types.Scope, + watchedState: Types.StateObject ): Types.Observer if watchedState == nil then logError("scopeMissing", nil, "Observers", "myScope:Observer(watchedState)") @@ -81,6 +86,8 @@ local function Observer( dependentSet = {}, _changeListeners = {} }, CLASS_METATABLE) + local self = (self :: any) :: InternalTypes.Observer + table.insert(scope, self) if watchedState.scope == nil then diff --git a/src/State/Value.lua b/src/State/Value.lua index 765255132..bb9f95dd5 100644 --- a/src/State/Value.lua +++ b/src/State/Value.lua @@ -1,4 +1,5 @@ ---!nonstrict +--!strict +--!nolint LocalShadow --[[ Constructs and returns objects which can be used to model independent @@ -6,8 +7,8 @@ ]] local Package = script.Parent.Parent -local PubTypes = require(Package.PubTypes) local Types = require(Package.Types) +local InternalTypes = require(Package.InternalTypes) -- Logging local logError = require(Package.Logging.logError) -- State @@ -26,7 +27,11 @@ local CLASS_METATABLE = {__index = class} state object and any dependents - use this with care as this can lead to unnecessary updates. ]] -function class:set(newValue: any, force: boolean?) +function class:set( + newValue: unknown, + force: boolean? +) + local self = self :: InternalTypes.Value local oldValue = self._value if force or not isSimilar(oldValue, newValue) then self._value = newValue @@ -37,7 +42,8 @@ end --[[ Returns the interior value of this state object. ]] -function class:_peek(): any +function class:_peek(): unknown + local self = self :: InternalTypes.Value return self._value end @@ -46,6 +52,7 @@ function class:get() end function class:destroy() + local self = self :: InternalTypes.Value if self.scope == nil then logError("destroyedTwice", nil, "Value") end @@ -53,9 +60,9 @@ function class:destroy() end local function Value( - scope: PubTypes.Scope, + scope: Types.Scope, initialValue: T -): Types.State +): Types.Value if initialValue == nil and (typeof(scope) ~= "table" or (scope[1] == nil and next(scope) ~= nil)) then logError("scopeMissing", nil, "Value", "myScope:Value(initialValue)") end @@ -67,6 +74,7 @@ local function Value( dependentSet = {}, _value = initialValue }, CLASS_METATABLE) + local self = (self :: any) :: InternalTypes.Value table.insert(scope, self) diff --git a/src/State/isState.lua b/src/State/isState.lua index ac9a6e6bb..f8880687b 100644 --- a/src/State/isState.lua +++ b/src/State/isState.lua @@ -1,11 +1,18 @@ --!strict +--!nolint LocalShadow --[[ Returns true if the given value can be assumed to be a valid state object. ]] -local function isState(target: any): boolean - return typeof(target) == "table" and typeof(target._peek) == "function" +local function isState( + target: unknown +): boolean + if typeof(target) == "table" then + local target = target :: {_peek: unknown?} + return typeof(target._peek) == "function" + end + return false end return isState \ No newline at end of file diff --git a/src/State/peek.lua b/src/State/peek.lua index 123741caa..3bf0bb6d6 100644 --- a/src/State/peek.lua +++ b/src/State/peek.lua @@ -1,18 +1,21 @@ --!strict +--!nolint LocalShadow --[[ A common interface for accessing the values of state objects or constants. ]] local Package = script.Parent.Parent -local PubTypes = require(Package.PubTypes) local Types = require(Package.Types) +local InternalTypes = require(Package.InternalTypes) -- State local isState = require(Package.State.isState) -local function peek(target: PubTypes.CanBeState): T +local function peek( + target: Types.CanBeState +): T if isState(target) then - return (target :: Types.StateObject):_peek() + return (target :: InternalTypes.StateObject):_peek() else return target :: T end diff --git a/src/State/updateAll.lua b/src/State/updateAll.lua index adba1b84d..ab3e4e099 100644 --- a/src/State/updateAll.lua +++ b/src/State/updateAll.lua @@ -1,4 +1,5 @@ --!strict +--!nolint LocalShadow --[[ Given a reactive object, updates all dependent reactive objects. @@ -8,13 +9,14 @@ ]] local Package = script.Parent.Parent -local PubTypes = require(Package.PubTypes) +local Types = require(Package.Types) -type Set = {[T]: any} -type Descendant = (PubTypes.Dependent & PubTypes.Dependency) | PubTypes.Dependent +type Descendant = (Types.Dependent & Types.Dependency) | Types.Dependent -- Credit: https://blog.elttob.uk/2022/11/07/sets-efficient-topological-search.html -local function updateAll(root: PubTypes.Dependency) +local function updateAll( + root: Types.Dependency +) local counters: {[Descendant]: number} = {} local flags: {[Descendant]: boolean} = {} local queue: {Descendant} = {} @@ -33,7 +35,8 @@ local function updateAll(root: PubTypes.Dependency) local counter = counters[next] counters[next] = if counter == nil then 1 else counter + 1 if (next :: any).dependentSet ~= nil then - for object in (next :: any).dependentSet do + local next = next :: (Types.Dependent & Types.Dependency) + for object in next.dependentSet do queueSize += 1 queue[queueSize] = object end @@ -54,7 +57,8 @@ local function updateAll(root: PubTypes.Dependency) and next:update() and (next :: any).dependentSet ~= nil then - for object in (next :: any).dependentSet do + local next = next :: (Types.Dependent & Types.Dependency) + for object in next.dependentSet do flags[object] = true end end diff --git a/src/Types.lua b/src/Types.lua index 3eb124f25..bc053ff1f 100644 --- a/src/Types.lua +++ b/src/Types.lua @@ -1,114 +1,252 @@ --!strict +--!nolint LocalShadow --[[ - Stores common type information used internally. - - These types may be used internally so Fusion code can type-check, but - should never be exposed to public users, as these definitions are fair game - for breaking changes. + Stores common public-facing type information for Fusion APIs. ]] -local Package = script.Parent -local PubTypes = require(Package.PubTypes) - -type Set = {[T]: any} +type Set = {[T]: unknown} --[[ General use types ]] --- Stores useful information about Luau errors. -export type Error = { - type: string, -- replace with "Error" when Luau supports singleton types - raw: string, - message: string, - trace: string +-- Types that can be expressed as vectors of numbers, and so can be animated. +export type Animatable = + number | + CFrame | + Color3 | + ColorSequenceKeypoint | + DateTime | + NumberRange | + NumberSequenceKeypoint | + PhysicalProperties | + Ray | + Rect | + Region3 | + Region3int16 | + UDim | + UDim2 | + Vector2 | + Vector2int16 | + Vector3 | + Vector3int16 + +-- A task which can be accepted for cleanup. +export type Task = + Instance | + RBXScriptConnection | + () -> () | + {destroy: (unknown) -> ()} | + {Destroy: (unknown) -> ()} | + {Task} + +-- A scope of tasks to clean up. +export type Scope = {unknown} & Constructors + +-- An object which uses a scope to dictate how long it lives. +export type ScopeLifetime = { + scope: Scope? } +-- Script-readable version information. +export type Version = { + major: number, + minor: number, + isRelease: boolean +} --[[ Generic reactive graph types ]] -export type StateObject = PubTypes.StateObject & { - _peek: (StateObject) -> T +-- A graph object which can have dependents. +export type Dependency = ScopeLifetime & { + dependentSet: Set +} + +-- A graph object which can have dependencies. +export type Dependent = ScopeLifetime & { + update: (Dependent) -> boolean, + dependencySet: Set } +-- An object which stores a piece of reactive state. +export type StateObject = Dependency & { + type: "State", + kind: string, + [{}]: T -- unindexable phantom data so StateObject actually contains a T +} + +-- Either a constant value of type T, or a state object containing type T. +export type CanBeState = StateObject | T + +-- Function signature for use callbacks. +export type Use = (target: CanBeState) -> T + --[[ Specific reactive graph types ]] -- A state object whose value can be set at any time by the user. -export type State = PubTypes.Value & { - _value: T +export type Value = StateObject & { + kind: "State", + set: (Value, newValue: T, force: boolean?) -> (), + destroy: () -> () } +export type ValueConstructor = ( + scope: Scope, + initialValue: T +) -> Value -- A state object whose value is derived from other objects using a callback. -export type Computed = PubTypes.Computed & { - scope: PubTypes.Scope, - _oldDependencySet: Set, - _processor: (PubTypes.Use, PubTypes.Scope) -> T, - _value: T, - _innerScope: PubTypes.Scope? +export type Computed = StateObject & Dependent & { + kind: "Computed", + destroy: () -> () } +export type ComputedConstructor = ( + scope: Scope, + callback: (Use, Scope) -> T +) -> Computed -- A state object which maps over keys and/or values in another table. -export type For = PubTypes.For & { - _processor: ( - PubTypes.StateObject<{key: KI, value: VI}>, - PubTypes.Scope - ) -> (PubTypes.StateObject<{key: KO?, value: VO?}>), - _inputTable: PubTypes.CanBeState<{[KI]: VI}>, - _existingInputTable: {[KI]: VI}?, - _existingOutputTable: {[KO]: VO}, - _existingProcessors: {[ForProcessor]: true}, - _newOutputTable: {[KO]: VO}, - _newProcessors: {[ForProcessor]: true}, - _remainingPairs: {[KI]: {[VI]: true}} +export type For = StateObject<{[KO]: VO}> & Dependent & { + kind: "For", + destroy: () -> () } -type ForProcessor = { - inputPair: PubTypes.Value<{key: any, value: any}>, - outputPair: PubTypes.StateObject<{key: any, value: any}>, - cleanupTask: any +export type ForPairsConstructor = ( + scope: Scope, + inputTable: CanBeState<{[KI]: VI}>, + processor: (Scope, Use, KI, VI) -> (KO, VO) +) -> For +export type ForKeysConstructor = ( + scope: Scope, + inputTable: CanBeState<{[KI]: V}>, + processor: (Scope, Use, KI) -> KO +) -> For +export type ForValuesConstructor = ( + scope: Scope, + inputTable: CanBeState<{[K]: VI}>, + processor: (Scope, Use, VI) -> VO +) -> For + +-- An object which can listen for updates on another state object. +export type Observer = Dependent & { + kind: "Observer", + onChange: (Observer, callback: () -> ()) -> (() -> ()), + onBind: (Observer, callback: () -> ()) -> (() -> ()), + destroy: () -> () } +export type ObserverConstructor = ( + scope: Scope, + watchedState: StateObject +) -> Observer -- A state object which follows another state object using tweens. -export type Tween = PubTypes.Tween & { - _goalState: State, - _tweenInfo: TweenInfo, - _prevValue: T, - _nextValue: T, - _currentValue: T, - _currentTweenInfo: TweenInfo, - _currentTweenDuration: number, - _currentTweenStartTime: number, - _currentlyAnimating: boolean +export type Tween = StateObject & Dependent & { + kind: "Tween", + destroy: () -> () } +export type TweenConstructor = ( + scope: Scope, + goalState: StateObject, + tweenInfo: TweenInfo? +) -> Tween -- A state object which follows another state object using spring simulation. -export type Spring = PubTypes.Spring & { - _speed: PubTypes.CanBeState, - _damping: PubTypes.CanBeState, - _goalState: State, - _goalValue: T, - - _currentType: string, - _currentValue: T, - _currentSpeed: number, - _currentDamping: number, - - _springPositions: {number}, - _springGoals: {number}, - _springVelocities: {number}, - - _lastSchedule: number, - _startDisplacements: {number}, - _startVelocities: {number} +export type Spring = StateObject & Dependent & { + kind: "Spring", + setPosition: (Spring, newPosition: Animatable) -> (), + setVelocity: (Spring, newVelocity: Animatable) -> (), + addVelocity: (Spring, deltaVelocity: Animatable) -> (), + destroy: () -> () } +export type SpringConstructor = ( + scope: Scope, + goalState: StateObject, + speed: CanBeState?, + damping: CanBeState? +) -> Spring --- An object which can listen for updates on another state object. -export type Observer = PubTypes.Observer & { - _changeListeners: Set<() -> ()>, - _numChangeListeners: number +--[[ + Instance related types +]] + +-- Denotes children instances in an instance or component's property table. +export type SpecialKey = { + type: "SpecialKey", + kind: string, + stage: "self" | "descendants" | "ancestor" | "observer", + apply: ( + self: SpecialKey, + scope: Scope, + value: unknown, + applyTo: Instance + ) -> () +} + +-- A collection of instances that may be parented to another instance. +export type Children = Instance | StateObject | {[unknown]: Children} + +-- A table that defines an instance's properties, handlers and children. +export type PropertyTable = {[string | SpecialKey]: unknown} + +export type NewConstructor = ( + scope: Scope, + className: string +) -> (propertyTable: PropertyTable) -> Instance + +export type HydrateConstructor = ( + scope: Scope, + target: Instance +) -> (propertyTable: PropertyTable) -> Instance + +-- Is there a sane way to write out this type? +-- ... I sure hope so. + +export type ScopedConstructor = (() -> Scope<{}>) + & ((A & {}) -> Scope) + & ((A & {}, B & {}) -> Scope) + & ((A & {}, B & {}, C & {}) -> Scope) + & ((A & {}, B & {}, C & {}, D & {}) -> Scope) + & ((A & {}, B & {}, C & {}, D & {}, E & {}) -> Scope) + & ((A & {}, B & {}, C & {}, D & {}, E & {}, F & {}) -> Scope) + & ((A & {}, B & {}, C & {}, D & {}, E & {}, F & {}, G & {}) -> Scope) + & ((A & {}, B & {}, C & {}, D & {}, E & {}, F & {}, G & {}, H & {}) -> Scope) + & ((A & {}, B & {}, C & {}, D & {}, E & {}, F & {}, G & {}, H & {}, I & {}) -> Scope) + & ((A & {}, B & {}, C & {}, D & {}, E & {}, F & {}, G & {}, H & {}, I & {}, J & {}) -> Scope) + & ((A & {}, B & {}, C & {}, D & {}, E & {}, F & {}, G & {}, H & {}, I & {}, J & {}, K & {}) -> Scope) + & ((A & {}, B & {}, C & {}, D & {}, E & {}, F & {}, G & {}, H & {}, I & {}, J & {}, K & {}, L & {}) -> Scope) + +export type Fusion = { + version: Version, + + doCleanup: (...unknown) -> (), + scoped: ScopedConstructor, + deriveScope: (scope: Scope) -> Scope, + + peek: Use, + Value: ValueConstructor, + Computed: ComputedConstructor, + ForPairs: ForPairsConstructor, + ForKeys: ForKeysConstructor, + ForValues: ForValuesConstructor, + Observer: ObserverConstructor, + + Tween: TweenConstructor, + Spring: SpringConstructor, + + New: NewConstructor, + Hydrate: HydrateConstructor, + + Ref: SpecialKey, + Children: SpecialKey, + Out: (propertyName: string) -> SpecialKey, + OnEvent: (eventName: string) -> SpecialKey, + OnChange: (propertyName: string) -> SpecialKey, + Attribute: (attributeName: string) -> SpecialKey, + AttributeChange: (attributeName: string) -> SpecialKey, + AttributeOut: (attributeName: string) -> SpecialKey, + } -return nil \ No newline at end of file +return nil diff --git a/src/Utility/isSimilar.lua b/src/Utility/isSimilar.lua index 56d1f28a7..22927ed6c 100644 --- a/src/Utility/isSimilar.lua +++ b/src/Utility/isSimilar.lua @@ -1,18 +1,23 @@ --!strict +--!nolint LocalShadow + --[[ - Returns true if A and B are 'similar' - i.e. any user of A would not need - to recompute if it changed to B. + Returns true if A and B are 'similar' - i.e. any user of A would not need + to recompute if it changed to B. ]] -local function isSimilar(a: any, b: any): boolean - -- HACK: because tables are mutable data structures, don't make assumptions - -- about similarity from equality for now (see issue #44) - if typeof(a) == "table" then - return false - else - -- NaN does not equal itself but is the same - return a == b or a ~= a and b ~= b - end +local function isSimilar( + a: unknown, + b: unknown +): boolean + -- HACK: because tables are mutable data structures, don't make assumptions + -- about similarity from equality for now (see issue #44) + if typeof(a) == "table" then + return false + else + -- NaN does not equal itself but is the same + return a == b or a ~= a and b ~= b + end end return isSimilar diff --git a/src/Utility/xtypeof.lua b/src/Utility/xtypeof.lua index 4eea6e992..9157b56ab 100644 --- a/src/Utility/xtypeof.lua +++ b/src/Utility/xtypeof.lua @@ -1,4 +1,5 @@ --!strict +--!nolint LocalShadow --[[ Extended typeof, designed for identifying custom objects. @@ -6,14 +7,19 @@ Otherwise, returns `typeof()` the argument. ]] -local function xtypeof(x: any) +local function xtypeof( + x: unknown +): string local typeString = typeof(x) - if typeString == "table" and typeof(x.type) == "string" then - return x.type - else - return typeString + if typeString == "table" then + local x = x :: {type: unknown?} + if typeof(x.type) == "string" then + return x.type + end end + + return typeString end return xtypeof \ No newline at end of file diff --git a/src/init.lua b/src/init.lua index b16e39cd9..2e279a16b 100644 --- a/src/init.lua +++ b/src/init.lua @@ -1,30 +1,31 @@ --!strict +--!nolint LocalShadow --[[ The entry point for the Fusion library. ]] -local PubTypes = require(script.PubTypes) +local Types = require(script.Types) local External = require(script.External) -export type Animatable = PubTypes.Animatable -export type Task = PubTypes.Task -export type Scope = PubTypes.Scope -export type Version = PubTypes.Version -export type Dependency = PubTypes.Dependency -export type Dependent = PubTypes.Dependent -export type StateObject = PubTypes.StateObject -export type CanBeState = PubTypes.CanBeState -export type Use = PubTypes.Use -export type Value = PubTypes.Value -export type Computed = PubTypes.Computed -export type For = PubTypes.For -export type Observer = PubTypes.Observer -export type Tween = PubTypes.Tween -export type Spring = PubTypes.Spring -export type SpecialKey = PubTypes.SpecialKey -export type Children = PubTypes.Children -export type PropertyTable = PubTypes.PropertyTable +export type Animatable = Types.Animatable +export type Task = Types.Task +export type Scope = Types.Scope +export type Version = Types.Version +export type Dependency = Types.Dependency +export type Dependent = Types.Dependent +export type StateObject = Types.StateObject +export type CanBeState = Types.CanBeState +export type Use = Types.Use +export type Value = Types.Value +export type Computed = Types.Computed +export type For = Types.For +export type Observer = Types.Observer +export type Tween = Types.Tween +export type Spring = Types.Spring +export type SpecialKey = Types.SpecialKey +export type Children = Types.Children +export type PropertyTable = Types.PropertyTable -- Down the line, this will be conditional based on whether Fusion is being -- compiled for Roblox. @@ -33,21 +34,20 @@ do External.setExternalScheduler(RobloxExternal) end -local Fusion: PubTypes.Fusion = { +local Fusion: Types.Fusion = { version = {major = 0, minor = 3, isRelease = false}, cleanup = require(script.Memory.legacyCleanup), doCleanup = require(script.Memory.doCleanup), - doNothing = require(script.Memory.doNothing), scoped = require(script.Memory.scoped), deriveScope = require(script.Memory.deriveScope), peek = require(script.State.peek), Value = require(script.State.Value), Computed = require(script.State.Computed), - ForPairs = require(script.State.ForPairs), - ForKeys = require(script.State.ForKeys), - ForValues = require(script.State.ForValues), + ForPairs = require(script.State.ForPairs) :: Types.ForPairsConstructor, + ForKeys = require(script.State.ForKeys) :: Types.ForKeysConstructor, + ForValues = require(script.State.ForValues) :: Types.ForValuesConstructor, Observer = require(script.State.Observer), Tween = require(script.Animation.Tween), From b3ac50973e6fd1bdb252cc4e15c3bf319e84e846 Mon Sep 17 00:00:00 2001 From: Elttob Date: Fri, 29 Dec 2023 05:10:50 +0000 Subject: [PATCH 156/287] Reword 'When You'll Use This' objects section --- docs/tutorials/fundamentals/objects.md | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/docs/tutorials/fundamentals/objects.md b/docs/tutorials/fundamentals/objects.md index 41742c418..6886926d5 100644 --- a/docs/tutorials/fundamentals/objects.md +++ b/docs/tutorials/fundamentals/objects.md @@ -246,13 +246,11 @@ one place. ## When You'll Use This Scopes might sound like a lot of upfront work. However, you'll find in practice -that Fusion manages most of this for you. +that Fusion manages a lot of this for you. -You'll only ever have to manage scopes when you're creating and destroying them -directly. For example, you'll likely deal with them in your main code file, -where you need to create a scope directly in order to start using Fusion. +You'll need to create and destroy your own scopes manually sometimes. For +example, you'll need to create a scope in your main code file to start using +Fusion, and you might want to make a few more in other parts of your code. -However, Fusion manages most of your scopes for you. As you'll see, parts of -Fusion will often give you an automatically-created scope. In those cases, -Fusion takes responsibility for managing them, so you can use them without -thinking about how they work. \ No newline at end of file +However, Fusion manages most of your scopes for you, so for large parts of your +codebase, you won't have to consider scopes and destruction at all. \ No newline at end of file From 5ae3a19d26652dda16d82e0bbbb545f12338ed05 Mon Sep 17 00:00:00 2001 From: Elttob Date: Fri, 29 Dec 2023 06:20:12 +0000 Subject: [PATCH 157/287] Update Reusing UI tutorial with latest advice --- docs/tutorials/components/reusing-ui.md | 215 +++++++++++++----------- 1 file changed, 120 insertions(+), 95 deletions(-) diff --git a/docs/tutorials/components/reusing-ui.md b/docs/tutorials/components/reusing-ui.md index 154f4530a..f9217cc1a 100644 --- a/docs/tutorials/components/reusing-ui.md +++ b/docs/tutorials/components/reusing-ui.md @@ -21,10 +21,8 @@ For example, consider this function, which generates a button based on some `props` the user passes in: ```Lua -type Dependencies = typeof(Fusion) - local function Button( - scope: Fusion.Scope, + scope: Fusion.Scope, props: { Position: Fusion.CanBeState?, AnchorPoint: Fusion.CanBeState?, @@ -120,9 +118,7 @@ Here's an example of how you could split up some components into modules: local scoped, doCleanup = Fusion.scoped, Fusion.doCleanup local scope = scoped(Fusion, { - PopUp = require(script.Parent.PopUp), - Message = require(script.Parent.Message), - Button = require(script.Parent.Button) + PopUp = require(script.Parent.PopUp) }) local ui = scope:New "ScreenGui" { @@ -139,18 +135,19 @@ Here's an example of how you could split up some components into modules: ```Lua linenums="1" local Fusion = require(game:GetService("ReplicatedStorage").Fusion) - type Dependencies = typeof(Fusion) & { - Message: typeof(require(script.Parent.Message)), - Button: typeof(require(script.Parent.Button)), - } - local function PopUp( - scope: Fusion.Scope, + outerScope: Fusion.Scope<{}>, props: { Message: Fusion.CanBeState, DismissText: Fusion.CanBeState } ) + local scope = scoped(Fusion, { + Message = require(script.Parent.Message), + Button = require(script.Parent.Button) + }) + table.insert(outerScope, scope) + return scope:New "Frame" { -- ...some properties... @@ -175,10 +172,8 @@ Here's an example of how you could split up some components into modules: ```Lua linenums="1" local Fusion = require(game:GetService("ReplicatedStorage").Fusion) - type Dependencies = typeof(Fusion) - local function Message( - scope: Fusion.Scope, + scope: Fusion.Scope, props: { Text: Fusion.CanBeState } @@ -201,10 +196,8 @@ Here's an example of how you could split up some components into modules: ```Lua linenums="1" local Fusion = require(game:GetService("ReplicatedStorage").Fusion) - type Dependencies = typeof(Fusion) - local function Button( - scope: Fusion.Scope, + scope: Fusion.Scope, props: { Text: Fusion.CanBeState } @@ -222,108 +215,140 @@ Here's an example of how you could split up some components into modules: return Button ``` -??? tip "Type checking with components & scopes" - You might notice the large type definition at the top of `PopUp`. +!!! success "Provide a list of properties" + If you don't provide a list of properties for your component anywhere, it + might be hard to figure out how to use it. + + The best way to do this is using Luau types. You can specify your list of + properties inline with your function definition: ```Lua - type Dependencies = typeof(Fusion) & { - Message: typeof(require(script.Parent.Message)), - Button: typeof(require(script.Parent.Button)), - } - ``` + local function Cake( + -- ... some stuff here ... + props: { + Size: Vector3, + Colour: Color3, + IsTasty: boolean + } + ) + -- ... some other stuff here ... + end + ``` + + This isn't just good documentation - it also gives you useful autocomplete. + If you try to use properties incorrectly inside the function body, it will + raise a type checking error. Similarly, you'll get useful errors if you + accidentally leave out a property when you use the component later. - This type merges together the `Fusion` table with a second table containing - `Message` and `Button`. + Note that the above code only accepts constant values, not state objects. - It'd be a bit like writing this out: + If you want to accept *either* a constant or a state object, you can use the + `CanBeState` type. ```Lua - type Dependencies = { - Message: (scope, props) -> Instance, - Button: (scope, props) -> Instance, - Value: (scope, initialValue) -> Value, - Computed: (scope, processor) -> Computed, - -- etc... - } + local function Cake( + -- ... some stuff here ... + props: { + Size: Fusion.CanBeState, + Colour: Fusion.CanBeState, + IsTasty: Fusion.CanBeState + } + ) + -- ... some other stuff here ... + end ``` - Later on, this is passed to Fusion's `Scope` type. `T` is the table of - methods you want to access with `scoped()` syntax. + This is usually what you want, because it means the user can easily switch + a property to dynamically change over time, while still writing properties + normally when they don't change over time. You can mostly treat `CanBeState` + properties like they're state objects, because functions like `peek()` and + `use()` automatically choose the right behaviour for you. - ```Lua hl_lines="2" - local function PopUp( - scope: Fusion.Scope, + If something *absolutely must* be a state object, you can use the + `StateObject` type instead. You should only consider this when it doesn't + make sense for the property to stay the same forever. + + ```Lua + local function Cake( + -- ... some stuff here ... props: { - Message: Fusion.CanBeState, - DismissText: Fusion.CanBeState + Size: Fusion.StateObject, + Colour: Fusion.StateObject, + IsTasty: Fusion.StateObject } ) + -- ... some other stuff here ... + end ``` - Because the `Dependencies` type contains all of Fusion & the `Message` and - `Button` components, it tells Luau: + You can use the rest of Luau's type checking features to do more complex + things, like making certain properties optional, or restricting that values + are valid for a given property. Go wild! - - to reject scopes that don't have those methods - - to show you autocomplete information for those methods while working on - your code - - The scope defined in the main script contains all of those methods, so it - passes type checking: + Remember that, when working with `StateObject` and `CanBeState`, you should + be mindful of whether you're putting things inside the angled brackets, or + outside of them. Consider these two type definitions carefully: ```Lua - local scope = scoped(Fusion, { - PopUp = require(script.Parent.PopUp), - Message = require(script.Parent.Message), - Button = require(script.Parent.Button) - }) + -- always a state object, which stores either Vector3 or nil + Fusion.StateObject - -- this is ok - scope:PopUp { - Message = "Hello, world!", - DismissText = "Close" - } + -- either nil, or a state object which always stores Vector3 + Fusion.StateObject? ``` - However, removing one of the methods emits a type checking error, because - the scope can no longer support the `PopUp` component. +!!! tip "How to ask for a scope" + In addition to `props`, it's strongly recommended to provide a type for the + `scope` parameter. - ```Lua hl_lines="3" - local scope = scoped(Fusion, { - PopUp = require(script.Parent.PopUp), - Message = nil, - Button = require(script.Parent.Button) - }) + The type will look something like this: - -- the type checker will flag this up! - scope:PopUp { - Message = "Hello, world!", - DismissText = "Close" - } + ```Lua + scope: Fusion.Scope ``` - A nice benefit of this system is that components only specify the type of - the component, rather than actually loading the specific component they use. - This means you can substitute in other components if they provide the same - API. - - ```Lua hl_lines="3-4" - local scope = scoped(Fusion, { - PopUp = require(script.Parent.PopUp), - Message = require(script.Parent.Test.Message), - Button = require(script.Parent.Test.Button) - }) + This naturally leads to two strategies for dealing with scopes. + + The first strategy is to ask for certain methods. In `Button` and `Message`, + they ask for a scope containing Fusion methods, so they can easily access + Fusion with no extra effort. - -- works as long as `Test.Message` and `Test.Button` match the real counterparts - scope:PopUp { - Message = "Hello, world!", - DismissText = "Close" - } + ```Lua hl_lines="2" + local function Component( + scope: Fusion.Scope, + props: {} + ) + return scope:New "Thing" { + -- ... rest of code here ... + } + end ``` - This is particularly valuable for testing code in fictional environments or - for writing reusable code that can use custom implementations provided by - the developers using it. + The second strategy is not to ask for any methods. Instead, you create your + own scope with what you need, and add it to the scope the user gives you. + This is what `PopUp` does. -It might be scary at first to see a large list of modules, but because you can -browse visually by names and folders, it's almost always better than having one -long script. \ No newline at end of file + ```Lua hl_lines="2 5-9" + local function Component( + outerScope: Fusion.Scope<{}>, + props: {} + ) + local scope = scoped(Fusion, { + SpecialThing1 = require(script.SpecialThing1), + SpecialThing2 = require(script.SpecialThing2), + }) + table.insert(outerScope, scope) + + return scope:SpecialThing1 { + -- ... rest of code here ... + } + end + ``` + + The general advice is to only ask for methods if they're very common - for + example, if you only need access to the Fusion library, it can be convenient + to simply ask for it to be there. The user's scope probably has it already. + + If you need to access specialised things like other components, it's better + to create a new scope internally so users of your component don't have to + worry about those internal components. \ No newline at end of file From a05b5498a209ba8234725f55d48cb9a5c4c3872f Mon Sep 17 00:00:00 2001 From: Elttob Date: Fri, 29 Dec 2023 18:08:59 +0000 Subject: [PATCH 158/287] Fix background scrolling on home page --- docs/assets/theme/home.css | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/docs/assets/theme/home.css b/docs/assets/theme/home.css index 6eb6dc615..8b38869f6 100644 --- a/docs/assets/theme/home.css +++ b/docs/assets/theme/home.css @@ -41,6 +41,17 @@ background-repeat: no-repeat; background-size: 177vh 100vh; background-position: center right; + animation: home-fade-in-bg 0.5s ease 0.5s; + animation-fill-mode: forwards; +} + +@keyframes home-fade-in-bg { + 0% { + transform: translateY(-0.8rem); + } + 100% { + transform: translateY(0rem); + } } [data-md-color-scheme="fusiondoc-dark"] #fusiondoc-home-main::before { From 2b5b470f1a724e3c38801bdf1eca3f8192384b60 Mon Sep 17 00:00:00 2001 From: Elttob Date: Fri, 29 Dec 2023 20:38:02 +0000 Subject: [PATCH 159/287] Components tutorial refactoring --- docs/tutorials/components/children.md | 194 ---------- docs/tutorials/components/components.md | 341 +++++++++++++++++ .../tutorials/components/instance-handling.md | 183 +++++++++ .../Popup-Exploded-Dark.svg | 0 .../Popup-Exploded-Light.svg | 0 .../Popups-Dark.svg | 0 .../Popups-Light.svg | 0 docs/tutorials/components/reusing-ui.md | 354 ------------------ mkdocs.yml | 6 +- 9 files changed, 527 insertions(+), 551 deletions(-) delete mode 100644 docs/tutorials/components/children.md create mode 100644 docs/tutorials/components/components.md create mode 100644 docs/tutorials/components/instance-handling.md rename docs/tutorials/components/{children => instance-handling}/Popup-Exploded-Dark.svg (100%) rename docs/tutorials/components/{children => instance-handling}/Popup-Exploded-Light.svg (100%) rename docs/tutorials/components/{children => instance-handling}/Popups-Dark.svg (100%) rename docs/tutorials/components/{children => instance-handling}/Popups-Light.svg (100%) delete mode 100644 docs/tutorials/components/reusing-ui.md diff --git a/docs/tutorials/components/children.md b/docs/tutorials/components/children.md deleted file mode 100644 index 82d0501c3..000000000 --- a/docs/tutorials/components/children.md +++ /dev/null @@ -1,194 +0,0 @@ -Using components, you can assemble more complex instance hierarchies by -combining simpler, self-contained parts. To do that, you should pay attention to -how instances are passed between components. - ------ - -## Returning Children - -Components return a child when you call them. That means anything you return -from a component should be supported by `[Children]`. - -That means you can return *one* (and only one): - -- instance -- array of children -- or state object containing a child - -This should be familiar from parenting instances using `[Children]`. To recap: - -!!! success "Allowed" - You can return one value per component. - ```Lua - -- returns *one* instance - local function Component() - return New "Frame" {} - end - ``` - ```Lua - -- returns *one* array - local function Component() - return { - New "Frame" {}, - New "Frame" {}, - New "Frame" {} - } - end - ``` - ```Lua - -- returns *one* state object - local function Component() - return ForValues({1, 2, 3}, function(use, number) - return New "Frame" {} - end) - end - ``` -!!! success "Allowed" - Inside arrays or state objects, you can mix and match different children. - ```Lua - -- mix of arrays, instances and state objects - local function Component() - return { - New "Frame" {}, - { - New "Frame" {}, - ForValues( ... ) - } - ForValues( ... ) - } - end - ``` -!!! fail "Not allowed" - Don't return multiple values straight from your function. Prefer to use an - array instead. - ```Lua - -- returns *multiple* instances (not surrounded by curly braces!) - local function Component() - return - New "Frame" {}, - New "Frame" {}, - New "Frame" {} - end - ``` - Luau does not support multiple return values consistently. They can get lost - easily if you're not careful. - ------ - -## Parenting Components - -Components return the same values which `[Children]` uses. That means they're -directly compatible, and you can insert a component anywhere you'd normally -insert an instance. - -You can pass in one component on its own... - -```Lua -local ui = New "ScreenGui" { - [Children] = Button { - Text = "Hello, world!" - } -} -``` - -...you can include components as part of an array.. - -```Lua -local ui = New "ScreenGui" { - [Children] = { - New "UIListLayout" {}, - Button { - Text = "Hello, world!" - }, - Button { - Text = "Hello, again!" - } - } -} -``` - -...and you can return them from state objects, too. - -```Lua -local ui = New "ScreenGui" { - [Children] = { - New "UIListLayout" {}, - - ForValues({"Hello", "world", "from", "Fusion"}, function(use, text) - return Button { - Text = text - } - end) - } -} -``` - ------ - -## Accepting Children - -Some components, for example pop-ups, might contain lots of different content: - -![Examples of pop-ups with different content.](Popups-Dark.svg#only-dark) -![Examples of pop-ups with different content.](Popups-Light.svg#only-light) - -Ideally, you would be able to reuse the pop-up 'container', while placing your -own content inside. - -![Separating the pop-up container from the pop-up contents.](Popup-Exploded-Dark.svg#only-dark) -![Separating the pop-up container from the pop-up contents.](Popup-Exploded-Light.svg#only-light) - -The simplest way to do this is to pass children through to `[Children]`. For -example, if you accept a table of `props`, you can add a `[Children]` key: - -```Lua hl_lines="7" -local function PopUp(props) - return New "Frame" { - -- ... some other properties ... - - -- Since `props` is a table, and `[Children]` is a key, you can use it - -- yourself as a key in `props`: - [Children] = props[Children] - } -end -``` - -Later on, when a pop-up is created, children can now be parented into that -instance: - -```Lua -local popUp = PopUp { - [Children] = { - Label { - Text = "New item collected" - }, - ItemPreview { - Item = Items.BRICK - }, - Button { - Text = "Add to inventory" - } - } -} -``` - -You're not limited to passing it straight into `[Children]`. If you need to add -other children, you can still use arrays and state objects as normal: - -```Lua hl_lines="5-13" -local function PopUp(props) - return New "Frame" { - -- ... some other properties ... - - [Children] = { - -- the component provides some children here - New "UICorner" { - CornerRadius = UDim.new(0, 8) - }, - - -- include children from outside the component here - props[Children] - } - } -end -``` \ No newline at end of file diff --git a/docs/tutorials/components/components.md b/docs/tutorials/components/components.md new file mode 100644 index 000000000..3e16f28af --- /dev/null +++ b/docs/tutorials/components/components.md @@ -0,0 +1,341 @@ +You can use functions to create self-contained, reusable blocks of code. In the +world of UI, you may think of them as *components* - though they can be used for +much more than just UI. + +For example, consider this function, which generates a button based on some +`props` the user passes in: + +```Lua +local function Button( + scope: Fusion.Scope, + props: { + Position: Fusion.CanBeState?, + AnchorPoint: Fusion.CanBeState?, + Size: Fusion.CanBeState?, + LayoutOrder: Fusion.CanBeState?, + ButtonText: Fusion.CanBeState + } +) + return scope:New "TextButton" { + BackgroundColor3 = Color3.new(0, 0.25, 1), + Position = props.Position, + AnchorPoint = props.AnchorPoint, + Size = props.Size, + LayoutOrder = props.LayoutOrder, + + Text = props.ButtonText, + TextSize = 28, + TextColor3 = Color3.new(1, 1, 1), + + [Children] = UICorner { CornerRadius = UDim2.new(0, 8) } + } +end +``` + +You can call this function later to generate as many buttons as you need. + +```Lua +local helloBtn = Button(scope, { + ButtonText = "Hello", + Size = UDim2.fromOffset(200, 50) +}) + +helloBtn.Parent = Players.LocalPlayer.PlayerGui.ScreenGui +``` + +Since the `scope` is the first parameter, it can even be used with `scoped()` +syntax. + +```Lua +local scope = scoped(Fusion, { + Button = Button +}) + +local helloBtn = scope:Button { + ButtonText = "Hello", + Size = UDim2.fromOffset(200, 50) +} + +helloBtn.Parent = Players.LocalPlayer.PlayerGui.ScreenGui +``` + +This is the primary way of writing components in Fusion. You create functions +that accept `scope` and `props`, then return some content from them. + +----- + +## Properties + +If you don't say what `props` should contain, it might be hard to figure +out how to use it. + +You can specify your list of properties by adding a type to `props`, which gives +you useful autocomplete and type checking. + +```Lua +local function Cake( + -- ... some stuff here ... + props: { + Size: Vector3, + Colour: Color3, + IsTasty: boolean + } +) + -- ... some other stuff here ... +end +``` + +Note that the above code only accepts constant values, not state objects. If you +want to accept *either* a constant or a state object, you can use the +`CanBeState` type. + +```Lua +local function Cake( + -- ... some stuff here ... + props: { + Size: Fusion.CanBeState, + Colour: Fusion.CanBeState, + IsTasty: Fusion.CanBeState + } +) + -- ... some other stuff here ... +end +``` + +This is usually what you want, because it means the user can easily switch +a property to dynamically change over time, while still writing properties +normally when they don't change over time. You can mostly treat `CanBeState` +properties like they're state objects, because functions like `peek()` and +`use()` automatically choose the right behaviour for you. + +If something *absolutely must* be a state object, you can use the +`StateObject` type instead. You should only consider this when it doesn't +make sense for the property to stay the same forever. + +```Lua +local function Cake( + -- ... some stuff here ... + props: { + Size: Fusion.StateObject, + Colour: Fusion.StateObject, + IsTasty: Fusion.StateObject + } +) + -- ... some other stuff here ... +end +``` + +You can use the rest of Luau's type checking features to do more complex +things, like making certain properties optional, or restricting that values +are valid for a given property. Go wild! + +!!! warning "Be mindful of the angle brackets" + Remember that, when working with `StateObject` and `CanBeState`, you should + be mindful of whether you're putting things inside the angled brackets, or + outside of them. Putting some things inside of the angle brackets can change + their meaning, compared to putting them outside of the angle brackets. + + Consider these two type definitions carefully: + + ```Lua + -- either nil, or a state object which always stores Vector3 + Fusion.StateObject? + + -- always a state object, which stores either Vector3 or nil + Fusion.StateObject + ``` + + The first type is best for *optional properties*, where you provide a + default value if it isn't specified by the user. If the user *does* specify + it, they're forced to always give a valid value for it. + + The second type is best if the property understands `nil` as a valid value. + This means the user can set it to `nil` at any time. + +----- + +## Scopes + +In addition to `props`, you should also ask for a `scope`. The `scope` +parameter should come first, so that your users can use `scoped()` syntax to +create it. + +```Lua +-- barebones syntax +local thing = Component(scope, { + -- ... some properties here ... +}) + +-- scoped() syntax +local thing = scope:Component { + -- ... some properties here ... +} +``` + +It's a good idea to provide a type for `scope`. This lets you specify what +methods you need the scope to have. + +```Lua +scope: Fusion.Scope +``` + +If you don't know what methods to ask for, consider these two strategies. + +1. If you only use common methods (like Fusion's constructors) then it's a +safe assumption that the user will also have those methods. You can ask for +these directly. + + ```Lua hl_lines="2" + local function Component( + scope: Fusion.Scope, + props: {} + ) + return scope:New "Thing" { + -- ... rest of code here ... + } + end + ``` + +2. If you need more specific or niche things, that the user probably won't +have (for example, components you use internally), then you probably should +not ask for those. Instead, you can create a new scope and specify every +method you need on it, then add that new scope to the scope you were given. + + ```Lua hl_lines="2 5-9" + local function Component( + outerScope: Fusion.Scope<{}>, + props: {} + ) + local scope = scoped(Fusion, { + SpecialThing1 = require(script.SpecialThing1), + SpecialThing2 = require(script.SpecialThing2), + }) + table.insert(outerScope, scope) + + return scope:SpecialThing1 { + -- ... rest of code here ... + } + end + ``` + +If you're not sure which strategy to pick, the second is always safer, +because it assumes less about your users. + + +----- + +## Modules + +It's common to save different components inside of different files. +There's a number of advantages to this: + +- it's easier to find the source code for a specific component +- it keep each file shorter and simpler +- it makes sure components are properly independent, and can't interfere +- it encourages reusing components everywhere, not just in one file + +Here's an example of how you could split up some components into modules: + +=== "Main file" + + ```Lua linenums="1" + local Fusion = require(game:GetService("ReplicatedStorage").Fusion) + local scoped, doCleanup = Fusion.scoped, Fusion.doCleanup + + local scope = scoped(Fusion, { + PopUp = require(script.Parent.PopUp) + }) + + local ui = scope:New "ScreenGui" { + -- ...some properties... + + [Children] = scope:PopUp { + Message = "Hello, world!", + DismissText = "Close" + } + } + ``` +=== "PopUp" + + ```Lua linenums="1" + local Fusion = require(game:GetService("ReplicatedStorage").Fusion) + + local function PopUp( + outerScope: Fusion.Scope<{}>, + props: { + Message: Fusion.CanBeState, + DismissText: Fusion.CanBeState + } + ) + local scope = scoped(Fusion, { + Message = require(script.Parent.Message), + Button = require(script.Parent.Button) + }) + table.insert(outerScope, scope) + + return scope:New "Frame" { + -- ...some properties... + + [Children] = { + scope:Message { + Scope = scope, + Text = props.Message + } + scope:Button { + Scope = scope, + Text = props.DismissText + } + } + } + end + + return PopUp + ``` + +=== "Message" + + ```Lua linenums="1" + local Fusion = require(game:GetService("ReplicatedStorage").Fusion) + + local function Message( + scope: Fusion.Scope, + props: { + Text: Fusion.CanBeState + } + ) + return scope:New "TextLabel" { + AutomaticSize = "XY", + BackgroundTransparency = 1, + + -- ...some properties... + + Text = props.Text + } + end + + return Message + ``` + +=== "Button" + + ```Lua linenums="1" + local Fusion = require(game:GetService("ReplicatedStorage").Fusion) + + local function Button( + scope: Fusion.Scope, + props: { + Text: Fusion.CanBeState + } + ) + return scope:New "TextButton" { + BackgroundColor3 = Color3.new(0.25, 0.5, 1), + AutoButtonColor = true, + + -- ...some properties... + + Text = props.Text + } + end + + return Button + ``` \ No newline at end of file diff --git a/docs/tutorials/components/instance-handling.md b/docs/tutorials/components/instance-handling.md new file mode 100644 index 000000000..af25f8321 --- /dev/null +++ b/docs/tutorials/components/instance-handling.md @@ -0,0 +1,183 @@ +Components are a good fit for Roblox instances. You can assemble complex groups +of instances by combining simpler, self-contained parts. + +To ensure maximum compatibility, there are a few best practices to consider. + +----- + +## Returns + +Anything you return from a component should be supported by +[`[Children]`](../roblox/parenting.md). + +```Lua +-- returns an instance +return scope:New "Frame" {} + +-- returns an array of instances +return { + scope:New "Frame" {}, + scope:New "Frame" {}, + scope:New "Frame" {} +} + +-- returns a state object containing instances +return scope:ForValues({1, 2, 3}, function(use, scope, number) + return scope:New "Frame" {} +end) + +-- mix of arrays, instances and state objects +return { + scope:New "Frame" {}, + { + scope:New "Frame" {}, + scope:ForValues( ... ) + } + scope:ForValues( ... ) +} +``` + +!!! fail "Multiple returns are fragile" + Don't return multiple values directly from your function. When a function + returns multiple values directly, the extra returned values can easily get + lost. + + ```Lua + local function BadThing(scope, props) + -- returns *multiple* instances (not surrounded by curly braces!) + return + scope:New "Frame" {}, + scope:New "Frame" {}, + scope:New "Frame" {} + end + + local things = { + -- Luau doesn't let you add multiple returns to a list like this. + -- Only the first Frame will be added. + scope:BadThing {}, + scope:New "TextButton" {} + } + print(things) --> { Frame, TextButton } + ``` + + Instead, you should return them inside of an array. Because the array is a + single return value, it won't get lost. + +If your returns are compatible with `[Children]` like above, you can insert a +component anywhere you'd normally insert an instance. + +You can pass in one component on its own... + +```Lua hl_lines="2-4" +local ui = scope:New "ScreenGui" { + [Children] = scope:Button { + Text = "Hello, world!" + } +} +``` + +...you can include components as part of an array.. + +```Lua hl_lines="5-7 9-11" +local ui = scope:New "ScreenGui" { + [Children] = { + scope:New "UIListLayout" {}, + + scope:Button { + Text = "Hello, world!" + }, + + scope:Button { + Text = "Hello, again!" + } + } +} +``` + +...and you can return them from state objects, too. + +```Lua hl_lines="8-10" +local stuff = {"Hello", "world", "from", "Fusion"} + +local ui = scope:New "ScreenGui" { + [Children] = { + scope:New "UIListLayout" {}, + + scope:ForValues(stuff, function(use, scope, text) + return scope:Button { + Text = text + } + end) + } +} +``` + +----- + +## Containers + +Some components, for example pop-ups, might contain lots of different content: + +![Examples of pop-ups with different content.](Popups-Dark.svg#only-dark) +![Examples of pop-ups with different content.](Popups-Light.svg#only-light) + +Ideally, you would be able to reuse the pop-up 'container', while placing your +own content inside. + +![Separating the pop-up container from the pop-up contents.](Popup-Exploded-Dark.svg#only-dark) +![Separating the pop-up container from the pop-up contents.](Popup-Exploded-Light.svg#only-light) + +The simplest way to do this is to pass instances through to `[Children]`. For +example, if you accept a table of `props`, you can add a `[Children]` key: + +```Lua hl_lines="4 8" +local function PopUp( + scope: Fusion.Scope, + props: { + [typeof(Children)]: Fusion.Children + } +) + return scope:New "Frame" { + [Children] = props[Children] + } +end +``` + +Later on, when a pop-up is created, instances can now be parented into that +pop-up: + +```Lua +scope:PopUp { + [Children] = { + scope:Label { + Text = "New item collected" + }, + scope:ItemPreview { + Item = Items.BRICK + }, + scope:Button { + Text = "Add to inventory" + } + } +} +``` + +If you need to add other instances, you can still use arrays and state objects +as normal. You can include instances you're given, in exactly the same way you +would include any other instances. + +```Lua +scope:New "Frame" { + -- ... some other properties ... + + [Children] = { + -- the component provides some children here + scope:New "UICorner" { + CornerRadius = UDim.new(0, 8) + }, + + -- include children from outside the component here + props[Children] + } +} +``` \ No newline at end of file diff --git a/docs/tutorials/components/children/Popup-Exploded-Dark.svg b/docs/tutorials/components/instance-handling/Popup-Exploded-Dark.svg similarity index 100% rename from docs/tutorials/components/children/Popup-Exploded-Dark.svg rename to docs/tutorials/components/instance-handling/Popup-Exploded-Dark.svg diff --git a/docs/tutorials/components/children/Popup-Exploded-Light.svg b/docs/tutorials/components/instance-handling/Popup-Exploded-Light.svg similarity index 100% rename from docs/tutorials/components/children/Popup-Exploded-Light.svg rename to docs/tutorials/components/instance-handling/Popup-Exploded-Light.svg diff --git a/docs/tutorials/components/children/Popups-Dark.svg b/docs/tutorials/components/instance-handling/Popups-Dark.svg similarity index 100% rename from docs/tutorials/components/children/Popups-Dark.svg rename to docs/tutorials/components/instance-handling/Popups-Dark.svg diff --git a/docs/tutorials/components/children/Popups-Light.svg b/docs/tutorials/components/instance-handling/Popups-Light.svg similarity index 100% rename from docs/tutorials/components/children/Popups-Light.svg rename to docs/tutorials/components/instance-handling/Popups-Light.svg diff --git a/docs/tutorials/components/reusing-ui.md b/docs/tutorials/components/reusing-ui.md deleted file mode 100644 index f9217cc1a..000000000 --- a/docs/tutorials/components/reusing-ui.md +++ /dev/null @@ -1,354 +0,0 @@ -Up until this point, you have been creating and parenting instances directly -without much organisation or code reuse. However, those two factors will become -increasingly important as you start building more game-ready UIs. - -These next few pages won't introduce new features of Fusion, but instead will -focus on techniques for making your UI more modular, portable and easy to -maintain. - ------ - -## Components - -One of the greatest advantages of libraries like Fusion is that UI and code are -the same thing. Any tool you can use on one, you can use on the other. - -To reduce repetition in your codebases, you'll often use functions to run small -reusable blocks of code, sometimes with parameters you can change. You can use -functions to organise your UI code, too. - -For example, consider this function, which generates a button based on some -`props` the user passes in: - -```Lua -local function Button( - scope: Fusion.Scope, - props: { - Position: Fusion.CanBeState?, - AnchorPoint: Fusion.CanBeState?, - Size: Fusion.CanBeState?, - LayoutOrder: Fusion.CanBeState?, - ButtonText: Fusion.CanBeState - } -) - return scope:New "TextButton" { - BackgroundColor3 = Color3.new(0, 0.25, 1), - Position = props.Position, - AnchorPoint = props.AnchorPoint, - Size = props.Size, - LayoutOrder = props.LayoutOrder, - - Text = props.ButtonText, - TextSize = 28, - TextColor3 = Color3.new(1, 1, 1), - - [Children] = UICorner { CornerRadius = UDim2.new(0, 8) } - } -end -``` - -You can call this function later to generate as many buttons as you need: - -```Lua -local helloBtn = Button(scope, { - ButtonText = "Hello", - Size = UDim2.fromOffset(200, 50) -}) - -helloBtn.Parent = Players.LocalPlayer.PlayerGui.ScreenGui -``` - -This is the primary way UI is reused in Fusion. These kinds of functions are -common enough that they have a special name: **components**. Specifically, -components are functions which return a child. - -In the above example, `Button` is a component, because it's a function that -returns a TextButton. - -!!! tip "Components can be scoped, too" - You may remember that `scoped()` lets you add functions as methods, so long - as the function takes a scope as its first parameter. - - If you define your components with `(scope, props)` as its arguments - like - above - then you can add it to `scoped()` too. - - ```Lua - local scope = scoped(Fusion, { - Button = Button - }) - ``` - - This gives you the same clean syntax as all other objects in Fusion. - - ```Lua - local helloBtn = scope:Button({ - ButtonText = "Hello", - Size = UDim2.fromOffset(200, 50) - }) - ``` - - In addition, much like `New`, you can remove the parentheses for clean and - visually consistent code. - - ```Lua - local helloBtn = scope:Button { - ButtonText = "Hello", - Size = UDim2.fromOffset(200, 50) - } - ``` - ------ - -## Modules - -It's common to save different components inside of different ModuleScripts. -There's a number of advantages to this: - -- it's easier to find the source code for a specific component -- it keep each script shorter and simpler -- it makes sure components are properly independent, and can't interfere -- it encourages reusing components everywhere, not just in one script - -Here's an example of how you could split up some components into modules: - -=== "Main script" - - ```Lua linenums="1" - local Fusion = require(game:GetService("ReplicatedStorage").Fusion) - local scoped, doCleanup = Fusion.scoped, Fusion.doCleanup - - local scope = scoped(Fusion, { - PopUp = require(script.Parent.PopUp) - }) - - local ui = scope:New "ScreenGui" { - -- ...some properties... - - [Children] = scope:PopUp { - Message = "Hello, world!", - DismissText = "Close" - } - } - ``` -=== "PopUp" - - ```Lua linenums="1" - local Fusion = require(game:GetService("ReplicatedStorage").Fusion) - - local function PopUp( - outerScope: Fusion.Scope<{}>, - props: { - Message: Fusion.CanBeState, - DismissText: Fusion.CanBeState - } - ) - local scope = scoped(Fusion, { - Message = require(script.Parent.Message), - Button = require(script.Parent.Button) - }) - table.insert(outerScope, scope) - - return scope:New "Frame" { - -- ...some properties... - - [Children] = { - scope:Message { - Scope = scope, - Text = props.Message - } - scope:Button { - Scope = scope, - Text = props.DismissText - } - } - } - end - - return PopUp - ``` - -=== "Message" - - ```Lua linenums="1" - local Fusion = require(game:GetService("ReplicatedStorage").Fusion) - - local function Message( - scope: Fusion.Scope, - props: { - Text: Fusion.CanBeState - } - ) - return scope:New "TextLabel" { - AutomaticSize = "XY", - BackgroundTransparency = 1, - - -- ...some properties... - - Text = props.Text - } - end - - return Message - ``` - -=== "Button" - - ```Lua linenums="1" - local Fusion = require(game:GetService("ReplicatedStorage").Fusion) - - local function Button( - scope: Fusion.Scope, - props: { - Text: Fusion.CanBeState - } - ) - return scope:New "TextButton" { - BackgroundColor3 = Color3.new(0.25, 0.5, 1), - AutoButtonColor = true, - - -- ...some properties... - - Text = props.Text - } - end - - return Button - ``` - -!!! success "Provide a list of properties" - If you don't provide a list of properties for your component anywhere, it - might be hard to figure out how to use it. - - The best way to do this is using Luau types. You can specify your list of - properties inline with your function definition: - - ```Lua - local function Cake( - -- ... some stuff here ... - props: { - Size: Vector3, - Colour: Color3, - IsTasty: boolean - } - ) - -- ... some other stuff here ... - end - ``` - - This isn't just good documentation - it also gives you useful autocomplete. - If you try to use properties incorrectly inside the function body, it will - raise a type checking error. Similarly, you'll get useful errors if you - accidentally leave out a property when you use the component later. - - Note that the above code only accepts constant values, not state objects. - - If you want to accept *either* a constant or a state object, you can use the - `CanBeState` type. - - ```Lua - local function Cake( - -- ... some stuff here ... - props: { - Size: Fusion.CanBeState, - Colour: Fusion.CanBeState, - IsTasty: Fusion.CanBeState - } - ) - -- ... some other stuff here ... - end - ``` - - This is usually what you want, because it means the user can easily switch - a property to dynamically change over time, while still writing properties - normally when they don't change over time. You can mostly treat `CanBeState` - properties like they're state objects, because functions like `peek()` and - `use()` automatically choose the right behaviour for you. - - If something *absolutely must* be a state object, you can use the - `StateObject` type instead. You should only consider this when it doesn't - make sense for the property to stay the same forever. - - ```Lua - local function Cake( - -- ... some stuff here ... - props: { - Size: Fusion.StateObject, - Colour: Fusion.StateObject, - IsTasty: Fusion.StateObject - } - ) - -- ... some other stuff here ... - end - ``` - - You can use the rest of Luau's type checking features to do more complex - things, like making certain properties optional, or restricting that values - are valid for a given property. Go wild! - - Remember that, when working with `StateObject` and `CanBeState`, you should - be mindful of whether you're putting things inside the angled brackets, or - outside of them. Consider these two type definitions carefully: - - ```Lua - -- always a state object, which stores either Vector3 or nil - Fusion.StateObject - - -- either nil, or a state object which always stores Vector3 - Fusion.StateObject? - ``` - -!!! tip "How to ask for a scope" - In addition to `props`, it's strongly recommended to provide a type for the - `scope` parameter. - - The type will look something like this: - - ```Lua - scope: Fusion.Scope - ``` - - This naturally leads to two strategies for dealing with scopes. - - The first strategy is to ask for certain methods. In `Button` and `Message`, - they ask for a scope containing Fusion methods, so they can easily access - Fusion with no extra effort. - - ```Lua hl_lines="2" - local function Component( - scope: Fusion.Scope, - props: {} - ) - return scope:New "Thing" { - -- ... rest of code here ... - } - end - ``` - - The second strategy is not to ask for any methods. Instead, you create your - own scope with what you need, and add it to the scope the user gives you. - This is what `PopUp` does. - - ```Lua hl_lines="2 5-9" - local function Component( - outerScope: Fusion.Scope<{}>, - props: {} - ) - local scope = scoped(Fusion, { - SpecialThing1 = require(script.SpecialThing1), - SpecialThing2 = require(script.SpecialThing2), - }) - table.insert(outerScope, scope) - - return scope:SpecialThing1 { - -- ... rest of code here ... - } - end - ``` - - The general advice is to only ask for methods if they're very common - for - example, if you only need access to the Fusion library, it can be convenient - to simply ask for it to be there. The user's scope probably has it already. - - If you need to access specialised things like other components, it's better - to create a new scope internally so users of your component don't have to - worry about those internal components. \ No newline at end of file diff --git a/mkdocs.yml b/mkdocs.yml index 47de82a2d..97f699167 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -74,9 +74,9 @@ nav: - Change Events: tutorials/roblox/change-events.md - Outputs: tutorials/roblox/outputs.md - References: tutorials/roblox/references.md - - Components: - - Reusing UI: tutorials/components/reusing-ui.md - - Children: tutorials/components/children.md + - Best Practices: + - Components: tutorials/components/components.md + - Instance Handling: tutorials/components/instance-handling.md - Callbacks: tutorials/components/callbacks.md - State: tutorials/components/state.md From 1332267a694c6e253c44a5ba36f6dde5c4867d46 Mon Sep 17 00:00:00 2001 From: Elttob Date: Sat, 30 Dec 2023 01:21:40 +0000 Subject: [PATCH 160/287] Less awkward wording in instance handling tutorial --- docs/tutorials/components/instance-handling.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/tutorials/components/instance-handling.md b/docs/tutorials/components/instance-handling.md index af25f8321..ffded14af 100644 --- a/docs/tutorials/components/instance-handling.md +++ b/docs/tutorials/components/instance-handling.md @@ -37,7 +37,7 @@ return { } ``` -!!! fail "Multiple returns are fragile" +!!! fail "Returning multiple values is fragile" Don't return multiple values directly from your function. When a function returns multiple values directly, the extra returned values can easily get lost. From 50e79e4edf8d0e8cd496d84795b7b496b3a132c5 Mon Sep 17 00:00:00 2001 From: Elttob Date: Tue, 16 Jan 2024 15:39:22 +0000 Subject: [PATCH 161/287] Tutorials update --- docs/assets/404-dark.svg | 16 +++++ docs/assets/404-light.svg | 16 +++++ docs/assets/404.svg | 3 - docs/assets/overrides/404.html | 65 +++++++++++++------ docs/assets/theme/404.css | 43 +++++++++--- docs/tutorials/components/components.md | 15 ++--- .../tutorials/components/instance-handling.md | 4 ++ .../fundamentals/{objects.md => scopes.md} | 0 mkdocs.yml | 2 +- 9 files changed, 123 insertions(+), 41 deletions(-) create mode 100644 docs/assets/404-dark.svg create mode 100644 docs/assets/404-light.svg delete mode 100644 docs/assets/404.svg rename docs/tutorials/fundamentals/{objects.md => scopes.md} (100%) diff --git a/docs/assets/404-dark.svg b/docs/assets/404-dark.svg new file mode 100644 index 000000000..4236207d4 --- /dev/null +++ b/docs/assets/404-dark.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/docs/assets/404-light.svg b/docs/assets/404-light.svg new file mode 100644 index 000000000..0a69ff624 --- /dev/null +++ b/docs/assets/404-light.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/docs/assets/404.svg b/docs/assets/404.svg deleted file mode 100644 index 16d14df04..000000000 --- a/docs/assets/404.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/docs/assets/overrides/404.html b/docs/assets/overrides/404.html index 59ac717f1..390cf5bbf 100644 --- a/docs/assets/overrides/404.html +++ b/docs/assets/overrides/404.html @@ -2,31 +2,54 @@ {% block content %} - ----- @@ -571,6 +572,7 @@ local config = New "Configuration" { [AttributeChange "Ammo"] = "guns" } ``` +
+ {% endblock %} -{% block disqus %}{% endblock %} \ No newline at end of file +{% block disqus %}{% endblock %} + +{% block site_nav %} + + {% if nav %} + {% if page.meta and page.meta.hide %} + {% set hidden = "hidden" if "navigation" in page.meta.hide %} + {% endif %} + + {% endif %} +{% endblock %} \ No newline at end of file diff --git a/docs/assets/theme/404.css b/docs/assets/theme/404.css index 1e6a0d140..06b0931a4 100644 --- a/docs/assets/theme/404.css +++ b/docs/assets/theme/404.css @@ -1,13 +1,40 @@ -.fusion-404 h1 { - display: flex; - align-items: center; - justify-content: left; +#fusion-404 .graphic { + display: block; + width: 16rem; + max-width: 80vmin; + aspect-ratio: 1; + margin: 0 auto; + margin-bottom: 2rem; + background-size: contain; + background-position: center; + background-repeat: no-repeat; +} + +[data-md-color-scheme="fusiondoc-light"] #fusion-404 .graphic { + background-image: url("../404-light.svg"); +} + +[data-md-color-scheme="fusiondoc-dark"] #fusion-404 .graphic { + background-image: url("../404-dark.svg"); +} + +#fusion-404 h1 { + text-align: center; font-size: 2rem; + margin-top: 0; margin-bottom: 1rem; } -.fusion-404 h1 img { - width: 6rem; - height: 6rem; - margin-right: 2rem; +#fusion-404 h2 { + text-align: center; + font-size: 1rem; + font-weight: 400; + + margin-top: 0; + margin-bottom: 2rem; } + +#fusion-404 .advice { + max-width: max-content; + margin: 0 auto; +} \ No newline at end of file diff --git a/docs/tutorials/components/components.md b/docs/tutorials/components/components.md index 3e16f28af..f219c47c3 100644 --- a/docs/tutorials/components/components.md +++ b/docs/tutorials/components/components.md @@ -182,8 +182,8 @@ scope: Fusion.Scope If you don't know what methods to ask for, consider these two strategies. 1. If you only use common methods (like Fusion's constructors) then it's a -safe assumption that the user will also have those methods. You can ask for -these directly. +safe assumption that the user will also have those methods. You can ask for a +scope with those methods pre-defined. ```Lua hl_lines="2" local function Component( @@ -196,10 +196,9 @@ these directly. end ``` -2. If you need more specific or niche things, that the user probably won't -have (for example, components you use internally), then you probably should -not ask for those. Instead, you can create a new scope and specify every -method you need on it, then add that new scope to the scope you were given. +2. If you need more specific or niche things that the user likely won't have +(for example, components you use internally), then you should not ask for those. +Instead, create a new inner scope with the methods you need. ```Lua hl_lines="2 5-9" local function Component( @@ -218,8 +217,8 @@ method you need on it, then add that new scope to the scope you were given. end ``` -If you're not sure which strategy to pick, the second is always safer, -because it assumes less about your users. +If you're not sure which strategy to pick, the second is always a safe fallback, +because it assumes less about your users and helps hide implementation details. ----- diff --git a/docs/tutorials/components/instance-handling.md b/docs/tutorials/components/instance-handling.md index ffded14af..b43722802 100644 --- a/docs/tutorials/components/instance-handling.md +++ b/docs/tutorials/components/instance-handling.md @@ -143,6 +143,10 @@ local function PopUp( end ``` +!!! tip "Accepting multiple instances" + If you have multiple 'slots' where you want to pass through instances, you + can make other properties and give them the `Fusion.Children` type. + Later on, when a pop-up is created, instances can now be parented into that pop-up: diff --git a/docs/tutorials/fundamentals/objects.md b/docs/tutorials/fundamentals/scopes.md similarity index 100% rename from docs/tutorials/fundamentals/objects.md rename to docs/tutorials/fundamentals/scopes.md diff --git a/mkdocs.yml b/mkdocs.yml index 97f699167..b5eae57dd 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -55,7 +55,7 @@ nav: - Tutorials: - Get Started: tutorials/index.md - Fundamentals: - - Objects: tutorials/fundamentals/objects.md + - Scopes: tutorials/fundamentals/scopes.md - Values: tutorials/fundamentals/values.md - Observers: tutorials/fundamentals/observers.md - Computeds: tutorials/fundamentals/computeds.md From 8f579ba1825dea37a90db65c4a51738a09f89dd8 Mon Sep 17 00:00:00 2001 From: Elttob Date: Tue, 16 Jan 2024 16:33:31 +0000 Subject: [PATCH 162/287] Update callbacks tutorial to use scopes --- docs/tutorials/components/callbacks.md | 35 +++++++++----------------- 1 file changed, 12 insertions(+), 23 deletions(-) diff --git a/docs/tutorials/components/callbacks.md b/docs/tutorials/components/callbacks.md index c5ce8379f..7a792836e 100644 --- a/docs/tutorials/components/callbacks.md +++ b/docs/tutorials/components/callbacks.md @@ -11,17 +11,9 @@ example to report when button clicks occur. ## In Luau -Callbacks are functions which you pass into other functions. They're part of -normal Luau code. - -```Lua -workspace.ChildAdded:Connect(function() - -- this function is a callback! -end) -``` - -They're useful because they allow the function to 'call back' into your code, -so your code can do something in response: +Callbacks are functions which you pass into other functions. They're useful +because they allow the function to 'call back' into your code, so your code can +do something in response: ```Lua local function printMessage() @@ -71,16 +63,15 @@ In this example, the `fiveTimes` function calls a callback five times: Components can use callbacks the same way. Consider this button component; when the button is clicked, the button needs to run some external code: -```Lua hl_lines="18" +```Lua hl_lines="17" local function Button( + scope: Fusion.Scope props: { - Scope: Fusion.Scope, Position: Fusion.CanBeState?, Size: Fusion.CanBeState?, Text: Fusion.CanBeState? } ) - local scope = props.Scope return scope:New "TextButton" { BackgroundColor3 = Color3.new(0.25, 0.5, 1), Position = props.Position, @@ -94,10 +85,10 @@ local function Button( end ``` -It can ask the controlling code to provide a callback in `props`, called OnClick: +It can ask the controlling code to provide an `OnClick` callback in `props`. -```Lua hl_lines="3-5" -local button = Button { +```Lua +local button = scope:Button { Text = "Hello, world!", OnClick = function() print("The button was clicked") @@ -109,17 +100,16 @@ Assuming that callback is passed in, the callback can be passed directly into `[OnEvent]`, because `[OnEvent]` accepts functions. It can even be optional - Luau won't add the key to the table if the value is `nil`. -```Lua hl_lines="7 19" +```Lua hl_lines="7 18" local function Button( + scope: Fusion.Scope, props: { - Scope: Fusion.Scope, Position: Fusion.CanBeState?, Size: Fusion.CanBeState?, Text: Fusion.CanBeState?, OnClick: (() -> ())? } ) - local scope = props.Scope return scope:New "TextButton" { BackgroundColor3 = Color3.new(0.25, 0.5, 1), Position = props.Position, @@ -136,10 +126,10 @@ end Alternatively, we can call `props.OnClick` manually, which is useful if you want to do your own processing first: -```Lua hl_lines="7 20-24" +```Lua hl_lines="19-23" local function Button( + scope: Fusion.Scope, props: { - Scope: Fusion.Scope, Position: Fusion.CanBeState?, Size: Fusion.CanBeState?, Text: Fusion.CanBeState?, @@ -147,7 +137,6 @@ local function Button( OnClick: (() -> ())? } ) - local scope = props.Scope return scope:New "TextButton" { BackgroundColor3 = Color3.new(0.25, 0.5, 1), Position = props.Position, From 879495b4579862a6eb247c38dd0b90b47c67282e Mon Sep 17 00:00:00 2001 From: Elttob Date: Tue, 16 Jan 2024 17:32:27 +0000 Subject: [PATCH 163/287] Update State best practices to use scopes --- docs/tutorials/components/state.md | 187 ++++++++++++++++++----------- 1 file changed, 115 insertions(+), 72 deletions(-) diff --git a/docs/tutorials/components/state.md b/docs/tutorials/components/state.md index 83518d451..694dee4d3 100644 --- a/docs/tutorials/components/state.md +++ b/docs/tutorials/components/state.md @@ -3,38 +3,34 @@ useful, but you should be careful when adding state. ----- -## Creating State Objects +## Creation -Inside a component, state objects can be created and used the same way as usual: +You can create state objects inside components as you would anywhere else. -```Lua hl_lines="5 8-10 13 17" +```Lua hl_lines="10 13-15" local HOVER_COLOUR = Color3.new(0.5, 0.75, 1) local REST_COLOUR = Color3.new(0.25, 0.5, 1) -local function Button(props) - local isHovering = Value(false) +local function Button( + scope: Fusion.Scope, + props: { + -- ... some properties ... + } +) + local isHovering = scope:Value(false) - return New "TextButton" { - BackgroundColor3 = Computed(function(use) + return scope:New "TextButton" { + BackgroundColor3 = scope:Computed(function(use) return if use(isHovering) then HOVER_COLOUR else REST_COLOUR end), - [OnEvent "MouseEnter"] = function() - isHovering:set(true) - end, - - [OnEvent "MouseLeave"] = function() - isHovering:set(false) - end, - - -- ... some properties ... + -- ... ... some more code ... } end ``` -Like regular Luau, state objects stay around as long as they're being used. Once -your component is destroyed and your code no longer uses the objects, they'll be -cleaned up. +Because these state objects are made with the same `scope` as the rest of the +component, they're destroyed alongside the rest of the component. ----- @@ -53,70 +49,111 @@ object under the hood: ![Showing check boxes connected to Value objects.](Check-Boxes-Dark.svg#only-dark) ![Showing check boxes connected to Value objects.](Check-Boxes-Light.svg#only-light) -It might *seem* logical to store the state object inside the check box: +It might *seem* logical to store the state object inside the check box, but +this causes a few problems: + +- because the state is hidden, it's awkward to read and write from outside +- often, the user already has a state object representing the same setting, so +now there's two state objects where one would have sufficed -```Lua hl_lines="2" -local function CheckBox(props) - local isChecked = Value(false) +```Lua hl_lines="7" +local function CheckBox( + scope: Fusion.Scope, + props: { + -- ... some properties ... + } +) + local isChecked = scope:Value(false) -- problematic - return New "ImageButton" { - -- ... some properties ... + return scope:New "ImageButton" { + [OnEvent "Activated"] = function() + isChecked:set(not peek(isChecked)) + end, + + -- ... some more code ... } end ``` -However, hiding away important state in components causes a few problems: - -- to control the appearance of the check box, you're forced to change the -internal state -- clicking the check box has hard-coded behaviour, which is bad if you need to -intercept the click (e.g. to show a confirmation dialogue) -- if you already had a state object for that setting, now the check box has a -duplicate state object representing the same setting - -Therefore, it's better for the controlling code to hold the state object, and -use callbacks to switch the value when the check box is clicked: - -```Lua -local playMusic = Value(true) +A *slightly better* solution is to pass the state object in. This ensures the +controlling code has easy access to the state if it needs it. However, this is +not a complete solution: + +- the user is forced to store the state in a `Value` object, but they might be +computing the value dynamically with other state objects instead +- the behaviour of clicking the check box is hardcoded; the user cannot +intercept the click or toggle a different state + +```Lua hl_lines="4" +local function CheckBox( + scope: Fusion.Scope, + props: { + IsChecked: Fusion.Value -- slightly better + } +) + return scope:New "ImageButton" { + [OnEvent "Activated"] = function() + props.IsChecked:set(not peek(props.IsChecked)) + end, + + -- ... some more code ... + } +end +``` -local checkBox = CheckBox { - Text = "Play music", - IsChecked = playMusic, - OnClick = function() - playMusic:set(not peek(playMusic)) - end -} +That's why the *best* solution is to use `CanBeState` to create read-only +properties, and add callbacks for signalling actions and events. + +- because `CanBeState` is read-only, it lets the user plug in any data +source, including dynamic computations +- because the callback is provided by the user, the behaviour of clicking the +check box is completely customisable + +```Lua hl_lines="4-5 10" +local function CheckBox( + scope: Fusion.Scope, + props: { + IsChecked: Fusion.CanBeState, -- best + OnClick: () -> () + } +) + return scope:New "ImageButton" { + [OnEvent "Activated"] = function() + props.OnClick() + end, + + -- ... some more code ... + } +end ``` The control is always top-down here; the check box's appearance is fully controlled by the creator. The creator of the check box *decides* to switch the setting when the check box is clicked. -The check box itself is an inert, visual element; it just shows a graphic and -reports clicks. +### In Practice ------ +Setting up your components in this way makes extending their behaviour +incredibly straightforward. -Setting up the check box this way also allows for more complex behaviour later -on. Suppose we wanted to group together multiple options under a 'main' check -box, so you can turn them all on/off at once. +Consider a scenario where you wish to group multiple options under a 'main' +check box, so you can turn them all on/off at once. ![Showing check boxes connected to Value objects.](Master-Check-Box-Dark.svg#only-dark) ![Showing check boxes connected to Value objects.](Master-Check-Box-Light.svg#only-light) The appearance of that check box would not be controlled by a single state, but -instead reflects the combination of multiple states. We can use a `Computed` -for that: +instead reflects the combination of multiple states. Because the code uses +`CanBeState`, you can represent this with a `Computed` object. ```Lua hl_lines="7-18" -local playMusic = Value(true) -local playSFX = Value(false) -local playNarration = Value(true) +local playMusic = scope:Value(true) +local playSFX = scope:Value(false) +local playNarration = scope:Value(true) -local checkBox = CheckBox { +local checkBox = scope:CheckBox { Text = "Play sounds", - Appearance = Computed(function(use) + IsChecked = scope:Computed(function(use) local anyChecked = use(playMusic) or use(playSFX) or use(playNarration) local allChecked = use(playMusic) and use(playSFX) and use(playNarration) @@ -131,14 +168,14 @@ local checkBox = CheckBox { } ``` -We can then implement the 'check all'/'uncheck all' behaviour inside `OnClick`: +You can then implement the 'check all'/'uncheck all' behaviour inside `OnClick`: ```Lua hl_lines="7-13" -local playMusic = Value(true) -local playSFX = Value(false) -local playNarration = Value(true) +local playMusic = scope:Value(true) +local playSFX = scope:Value(false) +local playNarration = scope:Value(true) -local checkBox = CheckBox { +local checkBox = scope:CheckBox { -- ... same properties as before ... OnClick = function() local allChecked = peek(playMusic) and peek(playSFX) and peek(playNarration) @@ -150,22 +187,28 @@ local checkBox = CheckBox { } ``` -By keeping the check box 'stateless', we can make it behave much more flexibly. +Because the check box was written to be flexible, it can handle complex usage +easily. ----- ## Best Practices -Those examples lead us into the golden rule when adding state to components. +Those examples lead us to the golden rule of reusable components: !!! tip "Golden Rule" - It's better for reusable components to *reflect* program state. They should - not usually *contain* program state. + Reusable components should *reflect* program state. They should + not *control* program state. + +At the bottom of the chain of control, components shouldn't be massively +responsible. At these levels, reflective components are easier to work with. + +As you go up the chain of control, components get broader in scope and less +reusable; those places are often suitable for controlling components. -State objects are best suited to self-contained use cases, such as implementing -hover effects, animations or responsive design. As such, you should think about -whether you really need to add state to components, or whether it's better to -add it higher up. +A well-balanced codebase places controlling components at key, strategic +locations. They allow higher-up components to operate without special knowledge +about what goes on below. At first, this might be difficult to do well, but with experience you'll have a better intuition for it. Remember that you can always rewrite your code if it From 8c2be5065f493468f3d4c555460ef28425825ecc Mon Sep 17 00:00:00 2001 From: Elttob Date: Wed, 17 Jan 2024 02:03:39 +0000 Subject: [PATCH 164/287] Rename Children type to Child for clarity --- src/Types.lua | 2 +- src/init.lua | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Types.lua b/src/Types.lua index bc053ff1f..3bff35d1a 100644 --- a/src/Types.lua +++ b/src/Types.lua @@ -185,7 +185,7 @@ export type SpecialKey = { } -- A collection of instances that may be parented to another instance. -export type Children = Instance | StateObject | {[unknown]: Children} +export type Child = Instance | StateObject | {[unknown]: Child} -- A table that defines an instance's properties, handlers and children. export type PropertyTable = {[string | SpecialKey]: unknown} diff --git a/src/init.lua b/src/init.lua index 2e279a16b..b31ccdf1c 100644 --- a/src/init.lua +++ b/src/init.lua @@ -24,7 +24,7 @@ export type Observer = Types.Observer export type Tween = Types.Tween export type Spring = Types.Spring export type SpecialKey = Types.SpecialKey -export type Children = Types.Children +export type Child = Types.Child export type PropertyTable = Types.PropertyTable -- Down the line, this will be conditional based on whether Fusion is being From f16abb5bfcfb8faf652d0019f186dfd567f15e75 Mon Sep 17 00:00:00 2001 From: Elttob Date: Wed, 17 Jan 2024 04:08:09 +0000 Subject: [PATCH 165/287] Updated Animated Computed & Button Component --- docs/examples/cookbook/animated-computed.md | 133 +++++++++--- docs/examples/cookbook/button-component.md | 214 +++++++++++++------- docs/examples/index.md | 4 +- 3 files changed, 243 insertions(+), 108 deletions(-) diff --git a/docs/examples/cookbook/animated-computed.md b/docs/examples/cookbook/animated-computed.md index a460dadf6..414a5403e 100644 --- a/docs/examples/cookbook/animated-computed.md +++ b/docs/examples/cookbook/animated-computed.md @@ -1,43 +1,114 @@ +This example shows you how to animate a single value with an animation curve of +your preference. + +For demonstration, the example uses Roblox API members. + +----- + +## Overview + ```Lua linenums="1" --- [Fusion imports omitted for clarity] +local Players = game:GetService("Players") --- Oftentimes we calculate values for a single purpose, such as the position of --- a single UI element. These values are often calculated inline, like this: +local Fusion = -- initialise Fusion here however you please! +local scoped = Fusion.scoped +local Children = Fusion.Children -local menuBar = New "Frame" { - AnchorPoint = Computed(function(use) - return if use(menuIsOpen) then Vector2.new(0.5, 0) else Vector2.new(0.5, -1) - end) -} +local TWEEN_INFO = TweenInfo.new( + 0.5, + Enum.EasingStyle.Sine, + Enum.EasingDirection.InOut +) --- If you want to animate these inline values, you can pass them through an --- object such as Spring and Tween- you don't have to do it separately. +-- Don't forget to pass this to `doCleanup` if you disable the script. +local scope = scoped(Fusion) -local menuBar = New "Frame" { - -- Use tweens for highly controllable animations: - AnchorPoint = Tween(Computed(function(use) - return if use(menuIsOpen) then Vector2.new(0.5, 0) else Vector2.new(0.5, -1) - end), TweenInfo.new(0.2, Enum.EasingStyle.Sine, Enum.EasingDirection.InOut)), +-- You can set this at any time to indicate where The Thing should be. +local showTheThing = scope:Value(false) - -- Or use springs for more natural and responsive movement: - AnchorPoint = Spring(Computed(function(use) - return if use(menuIsOpen) then Vector2.new(0.5, 0) else Vector2.new(0.5, -1) - end), 20, 0.5) -} +local exampleUI = scope:New "ScreenGui" { + Parent = Players.LocalPlayer:FindFirstChildOfClass("PlayerGui"), + Name = "Example UI", --- The equivalent 'expanded' code looks like this: + [Children] = scope:New "Frame" { + Name = "The Thing", + Position = scope:Tween( + scope:Computed(function(use) + local CENTRE = UDim2.fromScale(0.5, 0.5) + local OFFSCREEN = UDim2.fromScale(-0.5, 0.5) + return if use(showTheThing) then CENTRE else OFFSCREEN + end), + TWEEN_INFO + ), + Size = UDim2.fromOffset(200, 200) + } +} -local anchorPoint = Computed(function(use) - return if use(menuIsOpen) then Vector2.new(0.5, 0) else Vector2.new(0.5, -1) +-- Without toggling the value, you won't see it animate. +task.defer(function() + while true do + task.wait(1) + showTheThing:set(not peek(showTheThing)) + end end) +``` -local smoothAnchorPoint = Spring(anchorPoint, 20, 0.5) -- or equivalent Tween +----- -local menuBar = New "Frame" { - AnchorPoint = smoothAnchorPoint -} +## Explanation + +There's three key components to the above code snippet. + +Firstly, there's `showTheThing`. When this is `true`, The Thing should be in +the centre of the screen. Otherwise, The Thing should be off-screen. + +```Lua linenums="13" +-- You can set this at any time to indicate where The Thing should be. +local showTheThing = scope:Value(false) +``` + +Next, there's the computed object on line 26. This takes that boolean value, and +turns it into a UDim2 position for The Thing to use. You can imagine this as the +'non-animated' version of what you want The Thing to do, if it were to instantly +teleport around. + +```Lua linenums="26" + scope:Computed(function(use) + local CENTRE = UDim2.fromScale(0.5, 0.5) + local OFFSCREEN = UDim2.fromScale(-0.5, 0.5) + return if use(showTheThing) then CENTRE else OFFSCREEN + end), +``` + +Finally, there's the tween object that the computed is being passed into. The +tween object will smoothly move towards the computed over time. If needed, you +could separate the computed into a dedicated variable to access it +independently. + +```Lua linenums="25" + Position = scope:Tween( + scope:Computed(function(use) + local CENTRE = UDim2.fromScale(0.5, 0.5) + local OFFSCREEN = UDim2.fromScale(-0.5, 0.5) + return if use(showTheThing) then CENTRE else OFFSCREEN + end), + TWEEN_INFO + ), +``` + +The 'shape' of the animation is saved in a `TWEEN_INFO` constant defined earlier +in the code. [The Tween tutorial](../../../tutorials/animation/tweens) explains +how each parameter shapes the motion. + +```Lua linenums="7" +local TWEEN_INFO = TweenInfo.new( + 0.5, + Enum.EasingStyle.Sine, + Enum.EasingDirection.InOut +) +``` --- Keep in mind that you probably shouldn't use inline animation for everything. --- Sometimes you need to use the expanded form, or the expanded form would be --- more efficient, and that's okay - choose what works best for your code :) -``` \ No newline at end of file +!!! tip "Fluid animations with springs" + For extra smooth animation shapes that preserve velocity, consider trying + [spring objects](../../../tutorials/animation/springs). They're very similar + in usage and can help improve the responsiveness of the motion. \ No newline at end of file diff --git a/docs/examples/cookbook/button-component.md b/docs/examples/cookbook/button-component.md index 848222743..98d6c3e7b 100644 --- a/docs/examples/cookbook/button-component.md +++ b/docs/examples/cookbook/button-component.md @@ -1,116 +1,180 @@ +This example is a relatively complete button component implemented using +Fusion's Roblox API. It handles many common interactions such as hovering and +clicking. + +This should be a generally useful template for assembling components of your +own. Unless you're prototyping, it's probably wise to stick to some good +guidelines. The [Tutorials](../../../tutorials) have some tips if you don't have +any existing guidelines of your own. + +----- + +## Overview + ```Lua linenums="1" --- [Fusion imports omitted for clarity] - --- This is a relatively complete example of a button component. --- It handles many common interactions such as hovering and clicking. - --- This should be a generally useful template for assembling components of your --- own. Unless you're prototyping, it's probably wise to stick to some good --- guidelines; the Tutorials have some tips if you don't have any existing --- guidelines of your own. - --- Defining the names of properties the button accepts, and their types. This is --- useful for autocomplete and helps catch some typos, but is optional. -export type Props = { - -- some generic properties we'll allow other code to control directly - Name: CanBeState?, - LayoutOrder: CanBeState?, - Position: CanBeState?, - AnchorPoint: CanBeState?, - Size: CanBeState?, - AutomaticSize: CanBeState?, - ZIndex: CanBeState?, - - -- button-specific properties - Text: CanBeState?, - OnClick: (() -> ())?, - Disabled: CanBeState? -} - --- Returns `Child` to match Fusion's `Component` type. This should work for most --- use cases, and offers the greatest encapsulation as you're able to swap out --- your return type for an array or state object if you want to. -local function Button(props: Props): Child - -- We should generally be careful about storing state in widely reused - -- components, as the Tutorials explain, but for contained use cases such as - -- hover states, it should be perfectly fine. - local isHovering = Value(false) - local isHeldDown = Value(false) - - return New "TextButton" { +local Fusion = -- initialise Fusion here however you please! +local scoped = Fusion.scoped +local Children, OnEvent = Fusion.Children, Fusion.OnEvent + +local COLOUR_BLACK = Color3.new(0, 0, 0) +local COLOUR_WHITE = Color3.new(1, 1, 1) + +local COLOUR_TEXT = COLOUR_WHITE +local COLOUR_BG_REST = Color3.fromHex("0085FF") +local COLOUR_BG_HOVER = COLOUR_BG_REST:Lerp(COLOUR_WHITE, 0.25) +local COLOUR_BG_HELD = COLOUR_BG_REST:Lerp(COLOUR_BLACK, 0.25) +local COLOUR_BG_DISABLED = Color3.fromHex("CCCCCC") + +local BG_FADE_SPEED = 20 -- spring speed units + +local ROUNDED_CORNERS = UDim.new(0, 4) +local PADDING = UDim2.fromOffset(6, 4) + +local function Button( + scope: Fusion.Scope, + props: { + Name: Fusion.CanBeState?, + Layout: { + LayoutOrder: Fusion.CanBeState?, + Position: Fusion.CanBeState?, + AnchorPoint: Fusion.CanBeState?, + ZIndex: Fusion.CanBeState?, + Size: Fusion.CanBeState?, + AutomaticSize: Fusion.CanBeState? + }, + Text: Fusion.CanBeState?, + Disabled: Fusion.CanBeState?, + OnClick: (() -> ())? + } +): Fusion.Child + local isHovering = scope:Value(false) + local isHeldDown = scope:Value(false) + + return scope:New "TextButton" { Name = props.Name, - LayoutOrder = props.LayoutOrder, - Position = props.Position, - AnchorPoint = props.AnchorPoint, - Size = props.Size, - AutomaticSize = props.AutomaticSize, - ZIndex = props.ZIndex, + + LayoutOrder = props.Layout.LayoutOrder, + Position = props.Layout.Position, + AnchorPoint = props.Layout.AnchorPoint, + ZIndex = props.Layout.ZIndex, + Size = props.Layout.Size, + AutomaticSize = props.Layout.AutomaticSize, Text = props.Text, - TextColor3 = Color3.fromHex("FFFFFF"), - - BackgroundColor3 = Spring(Computed(function(use) - if use(props.Disabled) then - return Color3.fromHex("CCCCCC") - else - local baseColour = Color3.fromHex("0085FF") - -- darken/lighten when hovered or held down - if use(isHeldDown) then - baseColour = baseColour:Lerp(Color3.new(0, 0, 0), 0.25) - elseif use(isHovering) then - baseColour = baseColour:Lerp(Color3.new(1, 1, 1), 0.25) + TextColor3 = COLOUR_TEXT, + + BackgroundColor3 = scope:Spring( + scope:Computed(function(use) + -- The order of conditions matter here; it defines which states + -- visually override other states, with earlier states being + -- more important. + return + if use(props.Disabled) then COLOUR_BG_DISABLED + elseif use(isHeldDown) then COLOUR_BG_HELD + elseif use(isHovering) then COLOUR_BG_HOVER + else return COLOUR_BG_REST end - return baseColour - end - end), 20), + end), + BG_FADE_SPEED + ), [OnEvent "Activated"] = function() if props.OnClick ~= nil and not peek(props.Disabled) then - -- We're explicitly calling this function with no arguments to - -- match the types we specified above. If we just passed it - -- straight into the event, the function would receive arguments - -- from the Activated event, which might not be desirable. + -- Explicitly called with no arguments to match the typedef. + -- If passed straight to `OnEvent`, the function might receive + -- arguments from the event. If the function secretly *does* + -- take arguments (despite the type) this would cause problems. props.OnClick() end end, [OnEvent "MouseButton1Down"] = function() - isHeldDown:set(true) -- it's good UX to give immediate feedback + isHeldDown:set(true) end, - [OnEvent "MouseButton1Up"] = function() isHeldDown:set(false) end, [OnEvent "MouseEnter"] = function() -- Roblox calls this event even if the button is being covered by - -- other UI. For simplicity, we won't worry about that. + -- other UI. For simplicity, this does not account for that. isHovering:set(true) end, - [OnEvent "MouseLeave"] = function() - isHovering:set(false) -- If the button is being held down, but the cursor moves off the -- button, then we won't receive the mouse up event. To make sure -- the button doesn't get stuck held down, we'll release it if the -- cursor leaves the button. isHeldDown:set(false) + isHovering:set(false) end, [Children] = { New "UICorner" { - CornerRadius = UDim.new(0, 4) + CornerRadius = ROUNDED_CORNERS }, New "UIPadding" { - PaddingTop = UDim.new(0, 6), - PaddingBottom = UDim.new(0, 6), - PaddingLeft = UDim.new(0, 6), - PaddingRight = UDim.new(0, 6) + PaddingTop = PADDING.Y, + PaddingBottom = PADDING.Y, + PaddingLeft = PADDING.X, + PaddingRight = PADDING.X } } } end return Button -``` \ No newline at end of file +``` + +----- + +## Explanation + +The main part of note is the function signature. It's highly recommended that +you statically type the function signature for components, because it not only +improves autocomplete and error checking, but also acts as up-to-date, machine +readable documentation. + +```Lua +local function Button( + scope: Fusion.Scope, + props: { + Name: Fusion.CanBeState?, + Layout: { + LayoutOrder: Fusion.CanBeState?, + Position: Fusion.CanBeState?, + AnchorPoint: Fusion.CanBeState?, + ZIndex: Fusion.CanBeState?, + Size: Fusion.CanBeState?, + AutomaticSize: Fusion.CanBeState? + }, + Text: Fusion.CanBeState?, + Disabled: Fusion.CanBeState?, + OnClick: (() -> ())? + } +): Fusion.Child +``` + +The `scope` parameter specifies that the component depends on Fusion's methods. +If you're not sure how to write type definitions for scopes, +[the 'Scopes' section of the Components tutorial](../../../tutorials/components/components/#scopes) +goes into further detail. + +The property table is laid out with each property on a new line, so it's easy to +scan the list and see what properties are available. Most are `CanBeState`, +which allows the user to use state objects if they desire. They're also `?` +(optional), which can reduce boilerplate when using the component. Not all +properties have to be that way, but usually it's better to have the flexibility. + +!!! tip "Property grouping" + You can group properties together in nested tables, like the `Layout` table + above, to avoid long mixed lists of properties. In addition to being more + readable, this can sometimes help with passing around lots of properties at + once, because you can pass the whole nested table as one value if you'd like + to. + +The return type of the function is `Fusion.Child`, which tells the user that the +component is compatible with Fusion's `[Children]` API, without exposing what +children it's returning specifically. This helps ensure the user doesn't +accidentally depend on the internal structure of the component. \ No newline at end of file diff --git a/docs/examples/index.md b/docs/examples/index.md index 5eaa91e4a..be7c9b88a 100644 --- a/docs/examples/index.md +++ b/docs/examples/index.md @@ -24,7 +24,7 @@ animations and responding to different events. ## Open-Source Projects -### Fusion Wordle +### Fusion Wordle (for Fusion 0.2) ![A photo taken in Fusion Wordle, showing a completed game board](place-thumbnails/Fusion-Wordle.jpg) @@ -33,7 +33,7 @@ validation, spring animations and sounds. [Play and edit the game on Roblox.](https://www.roblox.com/games/12178127791/) -### Fusion Obby +### Fusion Obby (for Fusion 0.1) ![A photo taken in Fusion Obby, showing the counter and confetti](place-thumbnails/Fusion-Obby.jpg) From 97bc5b44e62680b4e64ddc3c05b627ef7bb072d9 Mon Sep 17 00:00:00 2001 From: Elttob Date: Thu, 18 Jan 2024 09:21:04 +0000 Subject: [PATCH 166/287] Initial work on Drag and Drop tutorial --- docs/examples/cookbook/button-component.md | 6 +- docs/examples/cookbook/drag-and-drop.md | 590 +++++++++------------ 2 files changed, 263 insertions(+), 333 deletions(-) diff --git a/docs/examples/cookbook/button-component.md b/docs/examples/cookbook/button-component.md index 98d6c3e7b..942315266 100644 --- a/docs/examples/cookbook/button-component.md +++ b/docs/examples/cookbook/button-component.md @@ -3,9 +3,9 @@ Fusion's Roblox API. It handles many common interactions such as hovering and clicking. This should be a generally useful template for assembling components of your -own. Unless you're prototyping, it's probably wise to stick to some good -guidelines. The [Tutorials](../../../tutorials) have some tips if you don't have -any existing guidelines of your own. +own. For further ideas and best practices for building components, see +[the Components tutorial](../../../tutorials/components/components). + ----- diff --git a/docs/examples/cookbook/drag-and-drop.md b/docs/examples/cookbook/drag-and-drop.md index 2f4ede297..3af04629f 100644 --- a/docs/examples/cookbook/drag-and-drop.md +++ b/docs/examples/cookbook/drag-and-drop.md @@ -1,223 +1,150 @@ -```Lua linenums="1" -local GuiService = game:GetService("GuiService") -local HttpService = game:GetService("HttpService") -local UserInputService = game:GetService("UserInputService") --- [Fusion imports omitted for clarity] - --- This example shows a full drag-and-drop implementation for mouse input only. --- Extending this system to generically work with other input types, such as --- touch gestures or gamepads, is left as an exercise to the reader. However, it --- should robustly support dragging many types of UI around flexibly. +This example shows a full drag-and-drop implementation for mouse input only, +using Fusion's Roblox API. --- To ensure best accessibility, any interactions you implement shouldn't force --- the player to hold the mouse button down. Either allow drag-and-drop using --- single inputs, or provide a non-dragging alternative; this will ensure that --- players with reduced motor ability aren't locked out of UI functions. +To ensure best accessibility, any interactions you implement shouldn't force you +to hold the mouse button down. Either allow drag-and-drop with single clicks, or +provide a non-dragging alternative. This ensures people with reduced motor +ability aren't locked out of UI functions. --- We're going to need to account for the UI inset sometimes. We cache it here. -local TOP_LEFT_INSET = GuiService:GetGuiInset() +```Lua linenums="1" +local Players = game:GetService("Players") +local UserInputService = game:GetService("UserInputService") --- To reflect the current position of the cursor on-screen, we'll use a state --- object that's updated using UserInputService. -local mousePos = Value(UserInputService:GetMouseLocation() - TOP_LEFT_INSET) -local mousePosConn = UserInputService.InputChanged:Connect(function(inputObject) - if inputObject.UserInputType == Enum.UserInputType.MouseMovement then - mousePos:set(Vector2.new(inputObject.Position.X, inputObject.Position.Y)) - end -end) +local Fusion = -- initialise Fusion here however you please! +local scoped = Fusion.scoped +local Children, OnEvent = Fusion.Children, Fusion.OnEvent --- We need to keep drag of which item is currently being dragged. Only one item --- can be dragged at a time. This type stores all the information needed: -export type CurrentlyDragging = { - -- Each draggable item will have a unique ID; the ID stored here represents - -- which item is being dragged right now. We'll use strings for this, but - -- you could use numbers if that's more convenient for you. +type DragInfo = { id: string, - -- When a drag is started, we store the mouse's offset relative to the item - -- being dragged. When the mouse moves, we can then apply the same offset to - -- make it look like the item is 'pinned' to the cursor. - offset: Vector2 + mouseOffset: Vector2 -- relative to the dragged item } --- This state object stores the above during a drag, or `nil` when not dragging. -local currentlyDragging: Value = Value(nil) --- Now we need a component to encapsulate all of our dragging behaviour, such --- as moving our UI between different parents, placing it at the mouse cursor, --- managing sizing, and so on. - -export type DraggableProps = { - -- This should uniquely identify the draggable item apart from all other - -- draggable items. This is constant and so shouldn't be a state object. - ID: string, - -- It doesn't make sense for a draggable item to have a constant parent. You - -- wouldn't be able to drop it anywhere else, so we enforce that Parent is a - -- state object for our own convenience. - Parent: StateObject, - -- When an item is being dragged, it needs to appear above all other UI. We - -- will create an overlay frame that fills the screen to achieve this. - OverlayFrame: Instance, - -- To start a drag, we'll need to know where the top-left corner of the item - -- is, so we can calculate `currentlyDragging.offset`. We'll allow the - -- calling code to pass through a Value object to [Out "AbsolutePosition"]. - OutAbsolutePosition: Value?, - - Name: CanBeState?, - LayoutOrder: CanBeState?, - Position: CanBeState?, - AnchorPoint: CanBeState?, - Size: CanBeState?, - AutomaticSize: CanBeState?, - ZIndex: CanBeState?, - [Children]: Child -} - -local function Draggable(props: DraggableProps): Child - -- If we need something to be cleaned up when our item is destroyed, we can - -- add it to this array. It'll be passed to `[Cleanup]` later. - local cleanupTasks = {} - - -- This acts like `currentlyDragging`, but filters out anything without a - -- matching ID, so it'll only exist when this specific item is dragged. - local thisDragging = Computed(function(use) - local dragInfo = use(currentlyDragging) - return if dragInfo ~= nil and dragInfo.id == props.ID then dragInfo else nil - end) - - -- Our item controls its own parent - one of the few times you'll see this - -- done in Fusion. This means we don't have to destroy and re-build the item - -- when it moves to a new location. - local itemParent = Computed(function(use) - return if use(thisDragging) ~= nil then props.OverlayFrame else use(props.Parent) - end, Fusion.doNothing) - - -- If we move a scaled UI into the `overlayBox`, by default it will stretch - -- to the screen size. Ideally we want it to preserve its current size while - -- it's being dragged, so we need to track the parent's size and calculate - -- the item size ourselves. - - -- To start with, we'll store the parent's absolute size. This takes a bit - -- of legwork to get right, and we need to remember the UI might not have a - -- parent which we can measure the size of - we'll represent that as `nil`. - -- Feel free to extract this into a separate function if you want to. - local parentSize = Value(nil) +local function Draggable( + scope: Fusion.Scope, + props: { + ID: string, + Name: Fusion.CanBeState?, + Parent: Fusion.StateObject, -- StateObject so it's observable + Layout: { + LayoutOrder: Fusion.CanBeState?, + Position: Fusion.CanBeState?, + AnchorPoint: Fusion.CanBeState?, + ZIndex: Fusion.CanBeState?, + Size: Fusion.CanBeState?, + OutAbsolutePosition: Fusion.Value?, + }, + Dragging: { + SelfDragInfo: Fusion.CanBeState, + OverlayFrame: Fusion.CanBeState + } + [typeof(Children)]: Fusion.Child + } +): Fusion.Child + -- When `nil`, the parent can't be measured for some reason. + local parentSize = scope:Value(nil) do - -- We'll call this whenever the parent's AbsoluteSize changes, or when - -- the parent changes (because different parents might have different - -- absolute sizes, if any) - local function recalculateParentSize() + local function measureParentNow() local parent = peek(props.Parent) - local parentHasSize = parent ~= nil and parent:IsA("GuiObject") - parentSize:set(if parentHasSize then parent.AbsoluteSize else nil) + parentSize:set( + if parent ~= nil and parent:IsA("GuiObject") + then parent.AbsoluteSize + else nil + ) end - - -- We don't just need to connect to the AbsoluteSize changed event of - -- the parent we have *right now*! If the parent changes, we need to - -- disconnect the old event and re-connect on the new parent, which we - -- do here. - local parentSizeConn = nil - local function rebindToParentSize() - if parentSizeConn ~= nil then - parentSizeConn:Disconnect() - parentSizeConn = nil + local resizeConn = nil + local function stopMeasuring() + if resizeConn ~= nil then + resizeConn:Disconnect() + resizeConn = nil end - local parent = peek(props.Parent) - local parentHasSize = parent ~= nil and parent:IsA("GuiObject") - if parentHasSize then - parentSizeConn = parent:GetPropertyChangedSignal("AbsoluteSize"):Connect(recalculateParentSize) - end - recalculateParentSize() end - local disconnect = Observer(props.Parent):onBind(rebindToParentSize) - - -- When the item gets destroyed, we need to disconnect that observer and - -- our AbsoluteSize change event (if any is active right now) - table.insert(cleanupTasks, function() - disconnect() - if parentSizeConn ~= nil then - parentSizeConn:Disconnect() - parentSizeConn = nil + scope:Observer(props.Parent):onBind(function() + stopMeasuring() + measureParentNow() + if peek(parentSize) ~= nil then + resizeConn = parent:GetPropertyChangedSignal("AbsoluteSize") + :Connect(measureParentNow) end end) + table.insert(scope, stopMeasuring) end - -- Now that we have a reliable parent size, we can calculate the item's size - -- without worrying about all of those event connections. - local itemSize = Computed(function(use) - local udim2 = use(props.Size) or UDim2.fromOffset(0, 0) - local scaleSize = use(parentSize) or Vector2.zero -- might be nil! - return UDim2.fromOffset( - udim2.X.Scale * scaleSize.X + udim2.X.Offset, - udim2.Y.Scale * scaleSize.Y + udim2.Y.Offset - ) - end) - - -- Similarly, we'll need to override the item's position while it's being - -- dragged. Happily, this is simpler to do :) - local itemPosition = Computed(function(use) - local dragInfo = use(thisDragging) - if dragInfo == nil then - return use(props.Position) or UDim2.fromOffset(0, 0) - else - -- `dragInfo.offset` stores the distance from the top-left corner - -- of the item to the mouse position. Subtracting the offset from - -- the mouse position therefore gives us the item's position. - local position = use(mousePos) - dragInfo.offset - return UDim2.fromOffset(position.X, position.Y) - end - end) - return New "Frame" { Name = props.Name or "Draggable", - LayoutOrder = props.LayoutOrder, - AnchorPoint = props.AnchorPoint, - AutomaticSize = props.AutomaticSize, - ZIndex = props.ZIndex, - - Parent = itemParent, - Position = itemPosition, - Size = itemSize, + Parent = scope:Computed(function(use) + return + if use(props.Dragging.SelfDragInfo) ~= nil + then use(props.Dragging.OverlayFrame) + else use(props.Parent) + end), + + LayoutOrder = props.Layout.LayoutOrder, + AnchorPoint = props.Layout.AnchorPoint, + ZIndex = props.Layout.ZIndex, + AutomaticSize = props.Layout.AutomaticSize, BackgroundTransparency = 1, - [Out "AbsolutePosition"] = props.OutAbsolutePosition, + Position = Computed(function(use) + local dragInfo = use(props.Dragging.SelfDragInfo) + if dragInfo == nil then + return use(props.Layout.Position) or UDim2.fromOffset(0, 0) + else + local topLeftCorner = use(mousePos) - dragInfo.mouseOffset + return UDim2.fromOffset(topLeftCorner.X, topLeftCorner.Y) + end + end), + -- Calculated manually so the Scale can be set relative to + -- `props.Parent` at all times, rather than the `Parent` of this Frame. + Size = scope:Computed(function(use) + local udim2 = use(props.Layout.Size) or UDim2.fromOffset(0, 0) + local parentSize = use(parentSize) or Vector2.zero + return UDim2.fromOffset( + udim2.X.Scale * parentSize.X + udim2.X.Offset, + udim2.Y.Scale * parentSize.Y + udim2.Y.Offset + ) + end), + [Out "AbsolutePosition"] = props.OutAbsolutePosition, [Children] = props[Children] } end --- The hard part is over! Now we just need to create some draggable items and --- start/stop drags in response to mouse events. We'll use a very basic example. +local COLOUR_COMPLETED = Color3.new(0, 1, 0) +local COLOUR_NOT_COMPLETED = Color3.new(1, 1, 1) + +local TODO_ITEM_SIZE = UDim2.new(1, 0, 0, 50) --- Let's make some to-do items. They'll show up in two lists - one for --- incomplete tasks, and another for complete tasks. You'll be able to drag --- items between the lists to mark them as complete. The lists will be sorted --- alphabetically so we don't have to deal with calculating where the items --- should be placed when they're dropped. +local function newUniqueID() + -- You can replace this with a better method for generating unique IDs. + return game:GetService("HttpService"):GenerateGUID() +end -export type TodoItem = { +type TodoItem = { id: string, text: string, - completed: Value + completed: Fusion.Value } -local todoItems: Value = { +local todoItems: Fusion.Value = { { - -- You can use HttpService to easily generate unique IDs statelessly. - id = HttpService:GenerateGUID(), + id = newUniqueID(), text = "Wake up today", completed = Value(true) }, { - id = HttpService:GenerateGUID(), + id = newUniqueID(), text = "Read the Fusion docs", completed = Value(true) }, { - id = HttpService:GenerateGUID(), + id = newUniqueID(), text = "Take over the universe", completed = Value(false) } } -local function getTodoItemForID(id: string): TodoItem? +local function getTodoItemForID( + id: string +): TodoItem? for _, item in todoItems do if item.id == id then return item @@ -226,192 +153,195 @@ local function getTodoItemForID(id: string): TodoItem? return nil end --- These represent the individual draggable to-do item entries in the lists. --- This is where we'll use our `Draggable` component! -export type TodoEntryProps = { - Item: TodoItem, - Parent: StateObject, - OverlayFrame: Instance, -} -local function TodoEntry(props: TodoEntryProps): Child - local absolutePosition = Value(nil) - - -- Using our item's ID, we can figure out if we're being dragged to apply - -- some styling for dragged items only! - local isDragging = Computed(function(use) - local dragInfo = use(currentlyDragging) +local function TodoEntry( + outerScope: Fusion.Scope<{}>, + props: { + Item: TodoItem, + Parent: Fusion.StateObject, + Layout: { + LayoutOrder: Fusion.CanBeState?, + Position: Fusion.CanBeState?, + AnchorPoint: Fusion.CanBeState?, + ZIndex: Fusion.CanBeState?, + Size: Fusion.CanBeState?, + OutAbsolutePosition: Fusion.Value?, + }, + Dragging: { + SelfDragInfo: Fusion.CanBeState, + OverlayFrame: Fusion.CanBeState? + }, + OnMouseDown: () -> ()? + } +): Child + local scope = scoped(Fusion, { + Draggable = Draggable + }) + table.insert(outerScope, scope) + + local itemPosition = scope:Value(nil) + local itemIsDragging = scope:Computed(function(use) + local dragInfo = use(props.CurrentlyDragging) return dragInfo ~= nil and dragInfo.id == props.Item.id end) - return Draggable { + return scope:Draggable { ID = props.Item.id, - Parent = props.Parent, - OverlayFrame = props.OverlayFrame, - OutAbsolutePosition = absolutePosition, - Name = props.Item.text, - Size = UDim2.new(1, 0, 0, 50), + Parent = props.Parent, + Layout = props.Layout, + Dragging = props.Dragging, - [Children] = New "TextButton" { + [Children] = scope:New "TextButton" { Name = "TodoEntry", Size = UDim2.fromScale(1, 1), - BackgroundColor3 = Computed(function(use) - if use(isDragging) then - return Color3.new(1, 1, 1) - elseif use(props.Item.completed) then - return Color3.new(0, 1, 0) - else - return Color3.new(1, 0, 0) + BackgroundColor3 = scope:Computed(function(use) + return + if use(props.Item.completed) + then COLOUR_COMPLETED + else COLOUR_NOT_COMPLETED end end), Text = props.Item.text, TextSize = 28, + + [OnEvent "MouseButton1Down"] = props.OnMouseDown - -- This is where we'll detect mouse down. When the mouse is pressed - -- over this item, we should pick it up. - [OnEvent "MouseButton1Down"] = function() - -- only start a drag if we're not already dragging - if peek(currentlyDragging) == nil then - local itemPos = peek(absolutePosition) or Vector2.zero - local offset = peek(mousePos) - itemPos - currentlyDragging:set({ - id = props.Item.id, - offset = offset - }) - end - end - - -- We're not going to detect mouse up here, because in some rare - -- cases the event could be dropped due to lag between the item's - -- position and the cursor position. We'll deal with this at a - -- global level instead. + -- Don't detect mouse up here, because in some rare cases, the event + -- could be missed due to lag between the item's position and the + -- cursor position. } } end --- Now we should construct our two task lists for housing our to-do entries. --- Notice that they don't manage the entries themselves! The entries don't --- belong to these lists after all, so that'd be nonsense :) - --- When we release our mouse, we need to know where to drop any dragged item we --- have. This will tell us if we're hovering over either list. -local dropAction = Value(nil) - -local incompleteList = New "ScrollingFrame" { - Name = "IncompleteTasks", - Position = UDim2.fromScale(0.1, 0.1), - Size = UDim2.fromScale(0.35, 0.9), - - BackgroundTransparency = 0.75, - BackgroundColor3 = Color3.new(1, 0, 0), - - [OnEvent "MouseEnter"] = function() - dropAction:set("incomplete") - end, - - [OnEvent "MouseLeave"] = function() - if peek(dropAction) == "incomplete" then - dropAction:set(nil) -- only clear this if it's not overwritten yet +-- Don't forget to pass this to `doCleanup` if you disable the script. +local scope = scoped(Fusion) + +local currentlyDragging: Fusion.Value = scope:Value(nil) +local mousePos = scope:Value(UserInputService:GetMouseLocation()) +table.insert(scope, + UserInputService.InputChanged:Connect(function(inputObject) + if inputObject.UserInputType == Enum.UserInputType.MouseMovement then + -- If this code did not read coordinates from the same method, it + -- might inconsistently handle UI insets. So, keep it simple! + mousePos:set(UserInputService:GetMouseLocation()) end - end, - - [Children] = { - New "UIListLayout" { - SortOrder = "Name", - Padding = UDim.new(0, 5) - } - } -} - -local completedList = New "ScrollingFrame" { - Name = "CompletedTasks", - Position = UDim2.fromScale(0.55, 0.1), - Size = UDim2.fromScale(0.35, 0.9), - - BackgroundTransparency = 0.75, - BackgroundColor3 = Color3.new(0, 1, 0), - - [OnEvent "MouseEnter"] = function() - dropAction:set("completed") - end, - - [OnEvent "MouseLeave"] = function() - if peek(dropAction) == "completed" then - dropAction:set(nil) -- only clear this if it's not overwritten yet - end - end, - - [Children] = { - New "UIListLayout" { - SortOrder = "Name", - Padding = UDim.new(0, 5) - } - } -} - --- Now we can write a mouse up handler to drop our items. + end) +) +local dropAction = scope:Value(nil) -local mouseUpConn = UserInputService.InputEnded:Connect(function(inputObject) - if inputObject.UserInputType ~= Enum.UserInputType.MouseButton1 then - return - end - local dragInfo = peek(currentlyDragging) - if dragInfo == nil then - return - end - local item = getTodoItemForID(dragInfo.id) - local action = peek(dropAction) - if item ~= nil then - if action == "incomplete" then - item.completed:set(false) - elseif action == "completed" then - item.completed:set(true) - end +local taskLists = scope:ForPairs( + { + incomplete = "mark-as-incomplete", + completed = "mark-as-completed" + }, + function(use, scope, listName, listDropAction) + return + listName, + scope:New "ScrollingFrame" { + Name = `TaskList ({listName})`, + Position = UDim2.fromScale(0.1, 0.1), + Size = UDim2.fromScale(0.35, 0.9), + + BackgroundTransparency = 0.75, + BackgroundColor3 = Color3.new(1, 0, 0), + + [OnEvent "MouseEnter"] = function() + dropAction:set(listDropAction) + end, + + [OnEvent "MouseLeave"] = function() + -- A different item might have overwritten this already. + if peek(dropAction) == listDropAction then + dropAction:set(nil) + end + end, + + [Children] = { + New "UIListLayout" { + SortOrder = "Name", + Padding = UDim.new(0, 5) + } + } + } end - currentlyDragging:set(nil) -end) +) --- We'll need to construct an overlay frame for our items to live in while they --- get dragged around. - -local overlayFrame = New "Frame" { +local overlayFrame = scope:New "Frame" { Size = UDim2.fromScale(1, 1), ZIndex = 10, BackgroundTransparency = 1 } --- Let's construct the items themselves! Because we're constructing them at the --- global level like this, they're only created and destroyed when they're added --- and removed from the list. - -local allEntries = ForValues(todoItems, function(use, item) - return TodoEntry { - Item = item, - Parent = Computed(function() - return if use(item.completed) then completedList else incompleteList - end, Fusion.doNothing), - OverlayFrame = overlayFrame - } -end, Fusion.cleanup) - --- Finally, construct the whole UI :) +local allEntries = scope:ForValues( + todoItems, + function(use, scope, item) + local itemPosition = scope:Value(nil) + return scope:TodoEntry { + Item = item, + Parent = Computed(function(use) + return + if use(item.completed) + then use(taskLists).completed + else use(taskLists).incomplete + end), + Layout = { + Size = TODO_ITEM_SIZE, + OutAbsolutePosition = itemPosition + }, + Dragging = { + SelfDragInfo = scope:Computed(function(use) + local dragInfo = use(currentlyDragging) + return + if dragInfo == nil or dragInfo.id ~= item.id + then nil + else dragInfo + end) + OverlayFrame = overlayFrame + }, + OnMouseDown = function() + if peek(currentlyDragging) == nil then + local itemPos = peek(itemPosition) or Vector2.zero + local mouseOffset = peek(mousePos) - itemPos + currentlyDragging:set({ + id = item.id, + mouseOffset = mouseOffset + }) + end + end + } + end +) -local ui = New "ScreenGui" { - Parent = game:GetService("Players").LocalPlayer.PlayerGui, +table.insert(scope, + UserInputService.InputEnded:Connect(function(inputObject) + if inputObject.UserInputType ~= Enum.UserInputType.MouseButton1 then + return + end + local dragInfo = peek(currentlyDragging) + if dragInfo == nil then + return + end + local item = getTodoItemForID(dragInfo.id) + local action = peek(dropAction) + if item ~= nil then + if action == "mark-as-incomplete" then + item.completed:set(false) + elseif action == "mark-as-completed" then + item.completed:set(true) + end + end + currentlyDragging:set(nil) + end) +) - [Cleanup] = { - mousePosConn, - mouseUpConn - }, +local ui = scope:New "ScreenGui" { + Parent = Players.LocalPlayer:FindFirstChildOfClass("PlayerGui") [Children] = { overlayFrame, - incompleteList, - completedList - - -- We don't have to pass `allEntries` in here - they manage their own - -- parenting thanks to `Draggable` :) + taskLists, + -- Don't pass `allEntries` in here - they manage their own parent! } } ``` \ No newline at end of file From 93b7c6a5c9b1e8cad13fe30c4601ee448b832c18 Mon Sep 17 00:00:00 2001 From: Elttob Date: Sun, 21 Jan 2024 20:51:28 +0000 Subject: [PATCH 167/287] Begin epxlanations --- docs/examples/cookbook/drag-and-drop.md | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/docs/examples/cookbook/drag-and-drop.md b/docs/examples/cookbook/drag-and-drop.md index 3af04629f..3c884fefe 100644 --- a/docs/examples/cookbook/drag-and-drop.md +++ b/docs/examples/cookbook/drag-and-drop.md @@ -34,6 +34,7 @@ local function Draggable( OutAbsolutePosition: Fusion.Value?, }, Dragging: { + MousePosition: Fusion.CanBeState, SelfDragInfo: Fusion.CanBeState, OverlayFrame: Fusion.CanBeState } @@ -90,7 +91,8 @@ local function Draggable( if dragInfo == nil then return use(props.Layout.Position) or UDim2.fromOffset(0, 0) else - local topLeftCorner = use(mousePos) - dragInfo.mouseOffset + local mousePos = use(props.Dragging.MousePosition) + local topLeftCorner = mousePos - dragInfo.mouseOffset return UDim2.fromOffset(topLeftCorner.X, topLeftCorner.Y) end end), @@ -167,6 +169,7 @@ local function TodoEntry( OutAbsolutePosition: Fusion.Value?, }, Dragging: { + MousePosition: Fusion.CanBeState, SelfDragInfo: Fusion.CanBeState, OverlayFrame: Fusion.CanBeState? }, @@ -290,6 +293,7 @@ local allEntries = scope:ForValues( OutAbsolutePosition = itemPosition }, Dragging = { + MousePosition = mousePos, SelfDragInfo = scope:Computed(function(use) local dragInfo = use(currentlyDragging) return @@ -344,4 +348,13 @@ local ui = scope:New "ScreenGui" { -- Don't pass `allEntries` in here - they manage their own parent! } } -``` \ No newline at end of file +``` + +------ + +## Explanation + +The basic idea is to create a container which stores the UI you want to drag. +This container then reparents itself as it gets dragged around between +different containers + From 946bf15125af6f160bb74b46767faf48f94066c9 Mon Sep 17 00:00:00 2001 From: Elttob Date: Sun, 21 Jan 2024 22:24:47 +0000 Subject: [PATCH 168/287] Drag and drop explanation --- docs/examples/cookbook/drag-and-drop.md | 354 +++++++++++++++++++++++- 1 file changed, 349 insertions(+), 5 deletions(-) diff --git a/docs/examples/cookbook/drag-and-drop.md b/docs/examples/cookbook/drag-and-drop.md index 3c884fefe..e274566c9 100644 --- a/docs/examples/cookbook/drag-and-drop.md +++ b/docs/examples/cookbook/drag-and-drop.md @@ -86,7 +86,7 @@ local function Draggable( BackgroundTransparency = 1, - Position = Computed(function(use) + Position = scope:Computed(function(use) local dragInfo = use(props.Dragging.SelfDragInfo) if dragInfo == nil then return use(props.Layout.Position) or UDim2.fromOffset(0, 0) @@ -175,7 +175,7 @@ local function TodoEntry( }, OnMouseDown: () -> ()? } -): Child +): Fusion.Child local scope = scoped(Fusion, { Draggable = Draggable }) @@ -220,7 +220,6 @@ end -- Don't forget to pass this to `doCleanup` if you disable the script. local scope = scoped(Fusion) -local currentlyDragging: Fusion.Value = scope:Value(nil) local mousePos = scope:Value(UserInputService:GetMouseLocation()) table.insert(scope, UserInputService.InputChanged:Connect(function(inputObject) @@ -231,6 +230,7 @@ table.insert(scope, end end) ) + local dropAction = scope:Value(nil) local taskLists = scope:ForPairs( @@ -276,13 +276,15 @@ local overlayFrame = scope:New "Frame" { BackgroundTransparency = 1 } +local currentlyDragging: Fusion.Value = scope:Value(nil) + local allEntries = scope:ForValues( todoItems, function(use, scope, item) local itemPosition = scope:Value(nil) return scope:TodoEntry { Item = item, - Parent = Computed(function(use) + Parent = scope:Computed(function(use) return if use(item.completed) then use(taskLists).completed @@ -356,5 +358,347 @@ local ui = scope:New "ScreenGui" { The basic idea is to create a container which stores the UI you want to drag. This container then reparents itself as it gets dragged around between -different containers +different containers. + +The `Draggable` component implements everything necessary to make a seamlessly +re-parentable container. + +```Lua linenums="13" +type DragInfo = { + id: string, + mouseOffset: Vector2 -- relative to the dragged item +} + +local function Draggable( + scope: Fusion.Scope, + props: { + ID: string, + Name: Fusion.CanBeState?, + Parent: Fusion.StateObject, -- StateObject so it's observable + Layout: { + LayoutOrder: Fusion.CanBeState?, + Position: Fusion.CanBeState?, + AnchorPoint: Fusion.CanBeState?, + ZIndex: Fusion.CanBeState?, + Size: Fusion.CanBeState?, + OutAbsolutePosition: Fusion.Value?, + }, + Dragging: { + MousePosition: Fusion.CanBeState, + SelfDragInfo: Fusion.CanBeState, + OverlayFrame: Fusion.CanBeState + } + [typeof(Children)]: Fusion.Child + } +): Fusion.Child +``` + +By default, `Draggable` behaves like a regular Frame, parenting itself to the +`Parent` property and applying its `Layout` properties. + +It only behaves specially when `Dragging.SelfDragInfo` is provided. Firstly, +it reparents itself to `Dragging.OverlayFrame`, so it can be seen in front of +other UI. + +```Lua linenums="66" + Parent = scope:Computed(function(use) + return + if use(props.Dragging.SelfDragInfo) ~= nil + then use(props.Dragging.OverlayFrame) + else use(props.Parent) + end), +``` + +Because of this reparenting, `Draggable` has to do some extra work to keep the +size consistent; it manually calculates the size based on the size of `Parent`, +so it doesn't change size when moved to `Dragging.OverlayFrame`. + +```Lua linenums="90" + -- Calculated manually so the Scale can be set relative to + -- `props.Parent` at all times, rather than the `Parent` of this Frame. + Size = scope:Computed(function(use) + local udim2 = use(props.Layout.Size) or UDim2.fromOffset(0, 0) + local parentSize = use(parentSize) or Vector2.zero + return UDim2.fromOffset( + udim2.X.Scale * parentSize.X + udim2.X.Offset, + udim2.Y.Scale * parentSize.Y + udim2.Y.Offset + ) + end), +``` + +The `Draggable` also needs to snap to the mouse cursor, so it can be moved by +the user. Ideally, the mouse would stay fixed in position relative to the +`Draggable`, so there are no abrupt changes in the position of any elements. + +As part of `Dragging.SelfDragInfo`, a `mouseOffset` is provided, which describes +how far the mouse should stay from the top-left corner. So, when setting the +position of the `Draggable`, that offset can be applied to keep the UI fixed +in position relative to the mouse. + +```Lua linenums="80" + Position = scope:Computed(function(use) + local dragInfo = use(props.Dragging.SelfDragInfo) + if dragInfo == nil then + return use(props.Layout.Position) or UDim2.fromOffset(0, 0) + else + local mousePos = use(props.Dragging.MousePosition) + local topLeftCorner = mousePos - dragInfo.mouseOffset + return UDim2.fromOffset(topLeftCorner.X, topLeftCorner.Y) + end + end), +``` + +This is all that's needed to make a generic container that can seamlessly move +between distinct parts of the UI. The rest of the example demonstrates how this +can be integrated into real world UI. + +The example creates a list of `TodoItem` objects, each with a unique ID, text +message, and completion status. Because we don't expect the ID or text to +change, they're just constant values. However, the completion status *is* +expected to change, so that's specified to be a `Value` object. + +```Lua linenums="116" +type TodoItem = { + id: string, + text: string, + completed: Fusion.Value +} +local todoItems: Fusion.Value = { + { + id = newUniqueID(), + text = "Wake up today", + completed = Value(true) + }, + { + id = newUniqueID(), + text = "Read the Fusion docs", + completed = Value(true) + }, + { + id = newUniqueID(), + text = "Take over the universe", + completed = Value(false) + } +} +``` +The `TodoEntry` component is meant to represent one individual `TodoItem`. + +```Lua linenums="149" +local function TodoEntry( + outerScope: Fusion.Scope<{}>, + props: { + Item: TodoItem, + Parent: Fusion.StateObject, + Layout: { + LayoutOrder: Fusion.CanBeState?, + Position: Fusion.CanBeState?, + AnchorPoint: Fusion.CanBeState?, + ZIndex: Fusion.CanBeState?, + Size: Fusion.CanBeState?, + OutAbsolutePosition: Fusion.Value?, + }, + Dragging: { + MousePosition: Fusion.CanBeState, + SelfDragInfo: Fusion.CanBeState, + OverlayFrame: Fusion.CanBeState? + }, + OnMouseDown: () -> ()? + } +): Fusion.Child +``` + +Notice that it shares many of the same property groups as `Draggable` - these +can be passed directly through. + +```Lua linenums="181" + return scope:Draggable { + ID = props.Item.id, + Name = props.Item.text, + Parent = props.Parent, + Layout = props.Layout, + Dragging = props.Dragging, +``` + +It also provides an `OnMouseDown` callback, which can be used to pick up the +entry if the mouse is pressed down above the entry. Note the comment about why +it is *not* desirable to detect mouse-up here; the UI should unconditionally +respond to mouse-up, even if the mouse happens to briefly leave this element. + +```Lua linenums="202" + [OnEvent "MouseButton1Down"] = props.OnMouseDown + + -- Don't detect mouse up here, because in some rare cases, the event + -- could be missed due to lag between the item's position and the + -- cursor position. +``` + +Now, the destinations for these entries can be created. To help decide where to +drop items later, the `dropAction` tracks which destination the mouse is hovered +over. + +```Lua linenums="225" +local dropAction = scope:Value(nil) + +local taskLists = scope:ForPairs( + { + incomplete = "mark-as-incomplete", + completed = "mark-as-completed" + }, + function(use, scope, listName, listDropAction) + return + listName, + scope:New "ScrollingFrame" { + Name = `TaskList ({listName})`, + Position = UDim2.fromScale(0.1, 0.1), + Size = UDim2.fromScale(0.35, 0.9), + + BackgroundTransparency = 0.75, + BackgroundColor3 = Color3.new(1, 0, 0), + + [OnEvent "MouseEnter"] = function() + dropAction:set(listDropAction) + end, + + [OnEvent "MouseLeave"] = function() + -- A different item might have overwritten this already. + if peek(dropAction) == listDropAction then + dropAction:set(nil) + end + end, + + [Children] = { + New "UIListLayout" { + SortOrder = "Name", + Padding = UDim.new(0, 5) + } + } + } + end +) +``` + +This is also where the 'overlay frame' is created, which gives currently-dragged +UI a dedicated layer above all other UI to freely move around. + +```Lua linenums="264" +local overlayFrame = scope:New "Frame" { + Size = UDim2.fromScale(1, 1), + ZIndex = 10, + BackgroundTransparency = 1 +} +``` + +Finally, each `TodoItem` is created as a `TodoEntry`. Some state is also created +to track which entry is being dragged at the moment. + +```Lua linenums="270" +local currentlyDragging: Fusion.Value = scope:Value(nil) + +local allEntries = scope:ForValues( + todoItems, + function(use, scope, item) + local itemPosition = scope:Value(nil) + return scope:TodoEntry { + Item = item, +``` + +Each entry dynamically picks one of the two destinations based on its +completion status. + +```Lua linenums="278" + Parent = scope:Computed(function(use) + return + if use(item.completed) + then use(taskLists).completed + else use(taskLists).incomplete + end), +``` + +It also provides the information needed by the `Draggable`. + +Note that the current drag information is filtered from the `currentlyDragging` +state so the `Draggable` won't see information about other entries being +dragged. + +```Lua linenums="288" + Dragging = { + MousePosition = mousePos, + SelfDragInfo = scope:Computed(function(use) + local dragInfo = use(currentlyDragging) + return + if dragInfo == nil or dragInfo.id ~= item.id + then nil + else dragInfo + end) + OverlayFrame = overlayFrame + }, +``` + +Now it's time to handle starting and stopping the drag. + +To begin the drag, this code makes use of the `OnMouseDown` callback. If nothing +else is being dragged right now, the position of the mouse relative to the item +is captured. Then, that `mouseOffset` and the `id` of the item are passed into +the `currentlyDragging` state to indicate this entry is being dragged. + +```Lua linenums="299" + OnMouseDown = function() + if peek(currentlyDragging) == nil then + local itemPos = peek(itemPosition) or Vector2.zero + local mouseOffset = peek(mousePos) - itemPos + currentlyDragging:set({ + id = item.id, + mouseOffset = mouseOffset + }) + end + end +``` + +To end the drag, a global `InputEnded` listener is created, which should +reliably fire no matter where or when the event occurs. + +If there's a `dropAction` to take, for example `mark-as-completed`, then that +action is executed here. + +In all cases, `currentlyDragging` is cleared, so the entry is no longer dragged. + +```Lua linenums="313" +table.insert(scope, + UserInputService.InputEnded:Connect(function(inputObject) + if inputObject.UserInputType ~= Enum.UserInputType.MouseButton1 then + return + end + local dragInfo = peek(currentlyDragging) + if dragInfo == nil then + return + end + local item = getTodoItemForID(dragInfo.id) + local action = peek(dropAction) + if item ~= nil then + if action == "mark-as-incomplete" then + item.completed:set(false) + elseif action == "mark-as-completed" then + item.completed:set(true) + end + end + currentlyDragging:set(nil) + end) +) +``` + +All that remains is to parent the task lists and overlay frames to a UI, so they +can be seen. Because the `TodoEntry` component manages their own parent, this +code shouldn't pass in `allEntries` as a child here. + +```Lua linenums="335" +local ui = scope:New "ScreenGui" { + Parent = Players.LocalPlayer:FindFirstChildOfClass("PlayerGui") + + [Children] = { + overlayFrame, + taskLists, + -- Don't pass `allEntries` in here - they manage their own parent! + } +} +``` \ No newline at end of file From 6fec8180661e4c6e21e92b43a665e9ad1688f5df Mon Sep 17 00:00:00 2001 From: Elttob Date: Mon, 22 Jan 2024 00:23:45 +0000 Subject: [PATCH 169/287] Fetch Data from Server updated example --- .../cookbook/fetch-data-from-server.md | 179 +++++++++++++----- 1 file changed, 136 insertions(+), 43 deletions(-) diff --git a/docs/examples/cookbook/fetch-data-from-server.md b/docs/examples/cookbook/fetch-data-from-server.md index 86d74e483..9506482a5 100644 --- a/docs/examples/cookbook/fetch-data-from-server.md +++ b/docs/examples/cookbook/fetch-data-from-server.md @@ -1,56 +1,149 @@ +This code shows how to deal with yielding/blocking code, such as fetching data +from a server. + +Because these tasks don't complete immediately, they can't be directly run +inside of a `Computed`, so this example provides a robust framework for handling +this in a way that doesn't corrupt your code. + +This example assumes the presence of a Roblox-like `task` scheduler. + +----- + +## Overview + ```Lua linenums="1" --- [Fusion imports omitted for clarity] +local Fusion = -- initialise Fusion here however you please! +local scoped = Fusion.scoped + +local function fetchUserBio( + userID: number +): string + -- pretend this calls out to a server somewhere, causing this code to yield + task.wait(1) + return "This is the bio for user " .. userID .. "!" +end --- This code assumes that there is a RemoteFunction at this location, which --- accepts a user ID and will return a string with that user's bio text. --- The server implementation is not shown here. -local FetchUserBio = game:GetService("ReplicatedStorage").FetchUserBio +-- Don't forget to pass this to `doCleanup` if you disable the script. +local scope = scoped(Fusion) --- Creating a Value object to store the user ID we're currently looking at -local currentUserID = Value(1670764) +-- This doesn't have to be a `Value` - any kind of state object works too. +local currentUserID = scope:Value(1670764) --- If we could instantly calculate the user's bio text, we could use a Computed --- here. However, fetching data from the server takes time, which means we can't --- use Computed without introducing serious consistency errors into our program. +-- While the bio is loading, this is `nil` instead of a string. +local currentUserBio: Fusion.Value = scope:Value(nil) --- Instead, we fall back to using an observer to manually manage our own value: -local currentUserBio = Value(nil) --- Using a scope to hide our management code from the rest of the script: do - local lastFetchTime = nil - local function fetch() - local fetchTime = os.clock() - lastFetchTime = fetchTime - currentUserBio:set(nil) -- set to a default value to indicate loading - task.spawn(function() - local bio = FetchUserBio:InvokeServer(peek(currentUserID)) - -- If these two are not equal, then that means another fetch was - -- started while we were waiting for the server to return a value. - -- In that case, the more recent call will be more up-to-date, so we - -- shouldn't overwrite it. This adds a nice layer of reassurance, - -- but if your value doesn't change often, this might be optional. - if lastFetchTime == fetchTime then - currentUserBio:set(bio) - end + local fetchInProgress = nil + local function performFetch() + local userID = peek(currentUserID) + currentUserBio:set(nil) + if fetchInProgress ~= nil then + task.cancel(fetchInProgress) + end + fetchInProgress = task.spawn(function() + currentUserBio:set(fetchUserBio()) + fetchInProgress = nil end) end + scope:Observer(currentUserID):onBind(performFetch) +end + +scope:Observer(currentUserBio):onBind(function() + local bio = peek(currentUserBio) + if bio == nil then + print("User bio is loading...") + else + print("Loaded user bio:", bio) + end +end) +``` + +----- - fetch() -- get the bio for the initial user ID - -- when the user ID changes, reload the bio - local disconnect = Observer(currentUserID):onChange(fetch) +## Explanation - -- Don't forget to call `disconnect` when you're done with `currentUserBio`. - -- That's not included in this code snippet, but it's important if you want - -- to avoid leaking memory. +If you yield or wait inside of a `Computed`, you can easily corrupt your entire +program. + +However, this example has a function, `fetchUserBio`, that yields. + +```Lua linenums="5" +local function fetchUserBio( + userID: number +): string + -- pretend this calls out to a server somewhere, causing this code to yield + task.wait(1) + return "This is the bio for user " .. userID .. "!" end +``` + +It also has some arbitrary state object, `currentUserID`, that it needs to +convert into a bio somehow. --- Now, you can use `currentUserBio` just like any other state object! Note that --- `nil` is used to represent a bio that hasn't loaded yet, so you'll want to --- handle that case before passing it into any code that expects a solid value. +```Lua linenums="15" +-- This doesn't have to be a `Value` - any kind of state object works too. +local currentUserID = scope:Value(1670764) +``` + +Because `Computed` can't yield, this code has to manually manage a +`currentUserBio` object, which will store the output of the code in a way that +can be used by other Fusion objects later. + +Notice that the 'loading' state is explicitly documented. It's a good idea to +be clear and honest when you have no data to show, because it allows other code +to respond to that case flexibly. + +```Lua linenums="18" +-- While the bio is loading, this is `nil` instead of a string. +local currentUserBio: Fusion.Value = scope:Value(nil) +``` + +To perform the actual fetch, a simple function can be written which calls +`fetchUserBio` in a separate task. Once it returns a bio, the `currentUserBio` +can be updated. + +To avoid two fetches overwriting each other, any existing fetch task is canceled +before the new task is created. + +```Lua linenums="22" + local fetchInProgress = nil + local function performFetch() + local userID = peek(currentUserID) + currentUserBio:set(nil) + if fetchInProgress ~= nil then + task.cancel(fetchInProgress) + end + fetchInProgress = task.spawn(function() + currentUserBio:set(fetchUserBio()) + fetchInProgress = nil + end) + end +``` + +Finally, to run this function when the `currentUserID` changes, `performFetch` +can be added to an `Observer`. + +The `onBind` method also runs `performFetch` once at the start of the program, +so the request is sent out automatically. + +```Lua linenums="34" +scope:Observer(currentUserID):onBind(performFetch) +``` + +That's all you need - now, any other Fusion code can read and depend upon +`currentUserBio` as if it were any other kind of state object. Just remember to +handle the 'loading' state as well as the successful state. + +```Lua linenums="37" +scope:Observer(currentUserBio):onBind(function() + local bio = peek(currentUserBio) + if bio == nil then + print("User bio is loading...") + else + print("Loaded user bio:", bio) + end +end) +``` -local bioLabel = New "TextLabel" { - Text = Computed(function(use) - return use(currentUserBio) or "Loading user bio..." - end) -} -``` \ No newline at end of file +You may wish to expand this code with error handling if `fetchUserBio()` can +throw errors. \ No newline at end of file From f594a6da309547d9f3effb2b06597b8e855aebf7 Mon Sep 17 00:00:00 2001 From: Elttob Date: Mon, 22 Jan 2024 01:31:26 +0000 Subject: [PATCH 170/287] Update Loading Spinner example --- .../examples/cookbook/light-and-dark-theme.md | 96 ++++++++--- docs/examples/cookbook/loading-spinner.md | 159 ++++++++++++++---- 2 files changed, 195 insertions(+), 60 deletions(-) diff --git a/docs/examples/cookbook/light-and-dark-theme.md b/docs/examples/cookbook/light-and-dark-theme.md index a9ffb1b27..3aec057e7 100644 --- a/docs/examples/cookbook/light-and-dark-theme.md +++ b/docs/examples/cookbook/light-and-dark-theme.md @@ -1,42 +1,88 @@ +This example demonstrates how to create dynamic theme colours using Fusion's +state objects. + +----- + +## Overview + ```Lua linenums="1" --- [Fusion imports omitted for clarity] +local Fusion = --initialise Fusion here however you please! +local scoped = Fusion.scoped --- Defining some theme colours. Something to note; I'm intentionally putting the --- actual colour names as the topmost keys here, and putting `light` and `dark` --- keys inside the colours. If you did it the other way around, then there's no --- single source of truth for what colour names are available, and it's hard to --- keep in sync. If a theme doesn't have a colour, it's better to explicitly not --- specify it under the colour name. +local Theme = {} -local THEME_COLOURS = { +Theme.colours = { background = { light = Color3.fromHex("FFFFFF"), - dark = Color3.fromHex("2D2D2D") + dark = Color3.fromHex("222222") }, text = { - light = Color3.fromHex("2D2D2D"), + light = Color3.fromHex("222222"), dark = Color3.fromHex("FFFFFF") - }, - -- [etc, for all the colours you'd want] + } } --- This will control which colours we're using at the moment. You could expose --- this to the rest of your code directly, or calculate it using a Computed. -local currentTheme = Value("light") +-- Don't forget to pass this to `doCleanup` if you disable the script. +local scope = scoped(Fusion) --- Now we'll create a Computed for every theme colour, which will pick a colour --- from `THEME_COLS` based on our `currentTheme`. -local currentColours = {} -for colourName, colourOptions in THEME_COLOURS do - currentColours[colourName] = Computed(function(use) - return colourOptions[use(currentTheme)] +Theme.current = scope:Value("light") +Theme.dynamic = {} +for colour, variants in Theme.colours do + Theme.dynamic[colour] = scope:Computed(function(use) + return variants[use(Theme.current)] end) end --- Now you can expose `colourOptions` to the rest of your code, preferably under --- a convenient name :) +Theme.current:set("light") +print(peek(Theme.dynamic.background)) --> 255, 255, 255 + +Theme.current:set("dark") +print(peek(Theme.dynamic.background)) --> 34, 34, 34 +``` + +----- -local text = New "TextLabel" { - TextColor3 = currentColours.text +## Explanation + +To begin, this example defines a set of colours with light and dark variants. + +```Lua linenums="6" +Theme.colours = { + background = { + light = Color3.fromHex("FFFFFF"), + dark = Color3.fromHex("222222") + }, + text = { + light = Color3.fromHex("222222"), + dark = Color3.fromHex("FFFFFF") + } } +``` + +A `Value` object stores which variant is in use right now. + +```Lua linenums="20" +Theme.current = scope:Value("light") +``` + +Finally, each colour is turned into a `Computed`, which dynamically pulls the +desired variant from the list. + +```Lua linenums="21" +Theme.dynamic = {} +for colour, variants in Theme.colours do + Theme.dynamic[colour] = scope:Computed(function(use) + return variants[use(Theme.current)] + end) +end +``` + +This allows other code to easily access theme colours from `Theme.dynamic`. + +```Lua linenums="28" +Theme.current:set("light") +print(peek(Theme.dynamic.background)) --> 255, 255, 255 + +Theme.current:set("dark") +print(peek(Theme.dynamic.background)) --> 34, 34, 34 ``` \ No newline at end of file diff --git a/docs/examples/cookbook/loading-spinner.md b/docs/examples/cookbook/loading-spinner.md index 70e71017b..8a212366c 100644 --- a/docs/examples/cookbook/loading-spinner.md +++ b/docs/examples/cookbook/loading-spinner.md @@ -1,39 +1,128 @@ +This example implements a procedural spinning animation using Fusion's Roblox +APIs. + +----- + +## Overview + ```Lua linenums="1" local RunService = game:GetService("RunService") --- [Fusion imports omitted for clarity] - --- Loading spinners generally don't use transition-based animations like tweens. --- Instead, they animate continuously and independently, so we'll need to set up --- our own animation clock that will drive the animation. --- We can set up one clock and use it everywhere. -local timer = Value(os.clock()) -local timerConn = RunService.RenderStepped:Connect(function() - -- Remember to disconnect this event when you're done using it! - timer:set(os.clock()) -end) - --- Our loading spinner will consist of an image which rotates around. You could --- do something more complex or intricate for spice, but in the interest of --- providing a simple starting point, let's keep it simple. -local spinner = New "ImageLabel" { - Position = UDim2.fromScale(0.5, 0.5), - AnchorPoint = Vector2.new(0.5, 0.5), - Size = UDim2.fromOffset(50, 50), - - BackgroundTransparency = 1, - Image = "rbxassetid://your-loading-spinner-image", -- replace this! - - -- As the timer runs, this will automatically update and rotate our image. - Rotation = Computed(function(use) - local time = use(timer) - local angle = time * 180 -- Spin at a rate of 180 degrees per second - angle %= 360 -- Don't need to go beyond 360 degrees; wrap instead - return angle - end), - - -- If your `timer` is only used by this one loading spinner, you can clean - -- up the `timerConn` here. If you're re-using one timer for all of your - -- spinners, you don't need to do this here. - [Cleanup] = timerConn + +local Fusion = -- initialise Fusion here however you please! +local scoped = Fusion.scoped +local Children = Fusion.Children + +local SPIN_DEGREES_PER_SECOND = 180 +local SPIN_SIZE = 50 + +local function Spinner( + scope: Fusion.Scope, + props: { + Layout: { + LayoutOrder: Fusion.CanBeState?, + Position: Fusion.CanBeState?, + AnchorPoint: Fusion.CanBeState?, + ZIndex: Fusion.CanBeState? + }, + CurrentTime: Fusion.CanBeState, + } +): Fusion.Child + return New "ImageLabel" { + Name = "Spinner", + + LayoutOrder = props.Layout.LayoutOrder, + Position = props.Layout.Position, + AnchorPoint = props.Layout.AnchorPoint, + ZIndex = props.Layout.ZIndex, + + Size = UDim2.fromOffset(SPIN_SIZE, SPIN_SIZE), + + BackgroundTransparency = 1, + Image = "rbxassetid://your-loading-spinner-image", -- replace this! + + Rotation = Computed(function(use) + return (use(props.CurrentTime) * SPIN_DEGREES_PER_SECOND) % 360 + end) + } +end + +-- Don't forget to pass this to `doCleanup` if you disable the script. +local scope = scoped(Fusion, { + Spinner = Spinner +}) + +local currentTime = Value(os.clock()) +table.insert(scope, + RunService.RenderStepped:Connect(function() + currentTime:set(os.clock()) + end) +) + +local spinner = scope:Spinner { + Layout = { + Position = UDim2.fromScale(0.5, 0.5), + AnchorPoint = Vector2.new(0.5, 0.5), + Size = UDim2.fromOffset(50, 50) + }, + CurrentTime = currentTime +} +``` + +----- + +## Explanation + +The `Spinner` components implements the animation for the loading spinner. It's +largely a standard Fusion component definition. + +The main thing to note is that it asks for a `CurrentTime` property. + +```Lua linenums="10" hl_lines="10" +local function Spinner( + scope: Fusion.Scope, + props: { + Layout: { + LayoutOrder: Fusion.CanBeState?, + Position: Fusion.CanBeState?, + AnchorPoint: Fusion.CanBeState?, + ZIndex: Fusion.CanBeState? + }, + CurrentTime: Fusion.CanBeState, + } +): Fusion.Child +``` + +The `CurrentTime` is used to drive the rotation of the loading spinner. + +```Lua linenums="35" + Rotation = Computed(function(use) + return (use(props.CurrentTime) * SPIN_DEGREES_PER_SECOND) % 360 + end) +``` + +That's all that's required for the `Spinner` component. + +Later on, the example creates a `Value` object that will store the current time, +and starts a process to keep it up to date. + +```Lua linenums="46" +local currentTime = Value(os.clock()) +table.insert(scope, + RunService.RenderStepped:Connect(function() + currentTime:set(os.clock()) + end) +) +``` + +This can then be passed in as `CurrentTime` when the `Spinner` is created. + +```Lua linenums="53" hl_lines="7" +local spinner = scope:Spinner { + Layout = { + Position = UDim2.fromScale(0.5, 0.5), + AnchorPoint = Vector2.new(0.5, 0.5), + Size = UDim2.fromOffset(50, 50) + }, + CurrentTime = currentTime } ``` \ No newline at end of file From 31fadc9ff6b4b5e40003ac044b92a2341eaa3fd1 Mon Sep 17 00:00:00 2001 From: Elttob Date: Mon, 22 Jan 2024 01:32:04 +0000 Subject: [PATCH 171/287] Fix scope: prefix in Loading Spinner --- docs/examples/cookbook/loading-spinner.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/docs/examples/cookbook/loading-spinner.md b/docs/examples/cookbook/loading-spinner.md index 8a212366c..c944684f2 100644 --- a/docs/examples/cookbook/loading-spinner.md +++ b/docs/examples/cookbook/loading-spinner.md @@ -27,7 +27,7 @@ local function Spinner( CurrentTime: Fusion.CanBeState, } ): Fusion.Child - return New "ImageLabel" { + return scope:New "ImageLabel" { Name = "Spinner", LayoutOrder = props.Layout.LayoutOrder, @@ -40,7 +40,7 @@ local function Spinner( BackgroundTransparency = 1, Image = "rbxassetid://your-loading-spinner-image", -- replace this! - Rotation = Computed(function(use) + Rotation = scope:Computed(function(use) return (use(props.CurrentTime) * SPIN_DEGREES_PER_SECOND) % 360 end) } @@ -51,7 +51,7 @@ local scope = scoped(Fusion, { Spinner = Spinner }) -local currentTime = Value(os.clock()) +local currentTime = scope:Value(os.clock()) table.insert(scope, RunService.RenderStepped:Connect(function() currentTime:set(os.clock()) @@ -95,7 +95,7 @@ local function Spinner( The `CurrentTime` is used to drive the rotation of the loading spinner. ```Lua linenums="35" - Rotation = Computed(function(use) + Rotation = scope:Computed(function(use) return (use(props.CurrentTime) * SPIN_DEGREES_PER_SECOND) % 360 end) ``` @@ -106,7 +106,7 @@ Later on, the example creates a `Value` object that will store the current time, and starts a process to keep it up to date. ```Lua linenums="46" -local currentTime = Value(os.clock()) +local currentTime = scope:Value(os.clock()) table.insert(scope, RunService.RenderStepped:Connect(function() currentTime:set(os.clock()) From 683988132098369562648db8e1bd5c59f4f44184 Mon Sep 17 00:00:00 2001 From: Elttob Date: Mon, 22 Jan 2024 02:09:20 +0000 Subject: [PATCH 172/287] Update PlayerList example to use scopes --- docs/examples/cookbook/player-list.md | 191 +++++++++++++++++--------- 1 file changed, 123 insertions(+), 68 deletions(-) diff --git a/docs/examples/cookbook/player-list.md b/docs/examples/cookbook/player-list.md index dd169286e..3ca94f20c 100644 --- a/docs/examples/cookbook/player-list.md +++ b/docs/examples/cookbook/player-list.md @@ -1,47 +1,24 @@ -```Lua linenums="1" --- [Fusion imports omitted for clarity] - -type Set = {[T]: true} +This shows how to use Fusion's Roblox API to create a simple, dynamically +updating player list. --- Defining a component for each row of the player list. --- Each row represents a player currently logged into the server. --- We set the `Name` to the player's name so the rows can be sorted by name. +----- -type PlayerListRowProps = { - Player: Player -} +## Overview -local function PlayerListRow(props: PlayerListRowProps) - return New "TextLabel" { - Name = props.Player.DisplayName, - - Size = UDim2.new(1, 0, 0, 25), - BackgroundTransparency = 1, +```Lua linenums="1" +local Players = game:GetService("Players") - Text = props.Player.DisplayName, - TextColor3 = Color3.new(1, 1, 1), - Font = Enum.Font.GothamMedium, - FontSize = 16, - TextXAlignment = "Right", - TextTruncate = "AtEnd", +local Fusion = -- initialise Fusion here however you please! +local scoped = Fusion.scoped +local Children = Fusion.Children - [Children] = New "UIPadding" { - PaddingLeft = UDim.new(0, 10), - PaddingRight = UDim.new(0, 10) - } +local function PlayerList( + scope: Fusion.Scope, + props: { + Players: Fusion.CanBeState<{Player}> } -end - --- Defining a component for the entire player list. --- It should take in a set of all logged-in players, and it should be a state --- object so the set of players can change as players join and leave. - -type PlayerListProps = { - PlayerSet: Fusion.StateObject> -} - -local function PlayerList(props: PlayerListProps) - return New "Frame" { +): Fusion.Child + return scope:New "Frame" { Name = "PlayerList", Position = UDim2.fromScale(1, 0), @@ -53,53 +30,131 @@ local function PlayerList(props: PlayerListProps) BackgroundColor3 = Color3.new(0, 0, 0), [Children] = { - New "UICorner" {}, - New "UIListLayout" { + scope:New "UICorner" { + CornerRadius = UDim.new(0, 8) + }, + scope:New "UIListLayout" { SortOrder = "Name", FillDirection = "Vertical" }, - ForPairs(props.PlayerSet, function(use, player, _) - return player, PlayerListRow { - Player = player + scope:ForValues(props.Players, function(use, scope, player) + return scope:New "TextLabel" { + Name = "PlayerListRow: " .. player.DisplayName, + + Size = UDim2.new(1, 0, 0, 25), + BackgroundTransparency = 1, + + Text = player.DisplayName, + TextColor3 = Color3.new(1, 1, 1), + Font = Enum.Font.GothamMedium, + FontSize = 16, + TextXAlignment = "Right", + TextTruncate = "AtEnd", + + [Children] = New "UIPadding" { + PaddingLeft = UDim.new(0, 10), + PaddingRight = UDim.new(0, 10) + } } - end, Fusion.cleanup) + end) } } end --- To create the PlayerList component, first we need a state object that stores --- the set of logged-in players, and updates as players join and leave. - -local Players = game:GetService("Players") +-- Don't forget to pass this to `doCleanup` if you disable the script. +local scope = scoped(Fusion, { + PlayerList = PlayerList +}) -local playerSet = Value() -local function updatePlayerSet() - local newPlayerSet = {} - for _, player in Players:GetPlayers() do - newPlayerSet[player] = true - end - playerSet:set(newPlayerSet) +local players = scope:Value(Players:GetPlayers()) +local function updatePlayers() + players:set(Players:GetPlayers()) end -local playerConnections = { - Players.PlayerAdded:Connect(updatePlayerSet), - Players.PlayerRemoving:Connect(updatePlayerSet) +table.insert(scope, { + Players.PlayerAdded:Connect(updatePlayers), + Players.PlayerRemoving:Connect(updatePlayers) +}) + +local gui = scope:New "ScreenGui" { + Name = "PlayerListGui", + Parent = Players.LocalPlayer:FindFirstChildOfClass("PlayerGui"), + + [Children] = scope:PlayerList { + Players = players + } } -updatePlayerSet() +``` --- Now, we can create the component and pass in `playerSet`. --- Don't forget to clean up your connections when your UI is destroyed; to do --- that, we're using the `[Cleanup]` key to clean up `playerConnections` later. +----- -local gui = New "ScreenGui" { +## Explanation + +The `PlayerList` component is designed to be simple and self-contained. The only +thing it needs is a `Players` list - it handles everything else, including its +position, size, appearance and behaviour. + +```Lua linenums="7" +local function PlayerList( + scope: Fusion.Scope, + props: { + Players: Fusion.CanBeState<{Player}> + } +): Fusion.Child +``` + +After creating a vertically expanding Frame with some style and layout added, +it turns the `Players` into a series of text labels using `ForValues`, which +will automatically create and remove them as the `Players` list changes. + +```Lua linenums="33" + scope:ForValues(props.Players, function(use, scope, player) + return scope:New "TextLabel" { + Name = "PlayerListRow: " .. player.DisplayName, + + Size = UDim2.new(1, 0, 0, 25), + BackgroundTransparency = 1, + + Text = player.DisplayName, + TextColor3 = Color3.new(1, 1, 1), + Font = Enum.Font.GothamMedium, + FontSize = 16, + TextXAlignment = "Right", + TextTruncate = "AtEnd", + + [Children] = New "UIPadding" { + PaddingLeft = UDim.new(0, 10), + PaddingRight = UDim.new(0, 10) + } + } + end) +``` + +That's all that the `PlayerList` component has to do. + +Later on, the code creates a `Value` object to store a list of players, and +update it every time a player joins or leaves the game. + +```Lua linenums="62" +local players = scope:Value(Players:GetPlayers()) +local function updatePlayers() + players:set(Players:GetPlayers()) +end +table.insert(scope, { + Players.PlayerAdded:Connect(updatePlayers), + Players.PlayerRemoving:Connect(updatePlayers) +}) +``` + +That object can then be passed in as `Players` when creating the `PlayerList`. + +```Lua linenums="71" hl_lines="6" +local gui = scope:New "ScreenGui" { Name = "PlayerListGui", Parent = Players.LocalPlayer:FindFirstChildOfClass("PlayerGui"), - [Cleanup] = playerConnections, - - [Children] = PlayerList { - PlayerSet = playerSet + [Children] = scope:PlayerList { + Players = players } } - ``` \ No newline at end of file From 0b228da0bdbb928d4d80fb3fd9f82fab6e7f73a8 Mon Sep 17 00:00:00 2001 From: Elttob Date: Mon, 22 Jan 2024 02:09:50 +0000 Subject: [PATCH 173/287] Fix missing scope: in Player List --- docs/examples/cookbook/player-list.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/examples/cookbook/player-list.md b/docs/examples/cookbook/player-list.md index 3ca94f20c..4d4a34102 100644 --- a/docs/examples/cookbook/player-list.md +++ b/docs/examples/cookbook/player-list.md @@ -52,7 +52,7 @@ local function PlayerList( TextXAlignment = "Right", TextTruncate = "AtEnd", - [Children] = New "UIPadding" { + [Children] = scope:New "UIPadding" { PaddingLeft = UDim.new(0, 10), PaddingRight = UDim.new(0, 10) } @@ -122,7 +122,7 @@ will automatically create and remove them as the `Players` list changes. TextXAlignment = "Right", TextTruncate = "AtEnd", - [Children] = New "UIPadding" { + [Children] = scope:New "UIPadding" { PaddingLeft = UDim.new(0, 10), PaddingRight = UDim.new(0, 10) } From 71b4736ea140b5c0355a24bc29f2b18e31d3331e Mon Sep 17 00:00:00 2001 From: Elttob Date: Mon, 22 Jan 2024 02:30:21 +0000 Subject: [PATCH 174/287] Fix malformed errors page --- docs/api-reference/errors/index.md | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/docs/api-reference/errors/index.md b/docs/api-reference/errors/index.md index 89c95fad1..cc1e09874 100644 --- a/docs/api-reference/errors/index.md +++ b/docs/api-reference/errors/index.md @@ -16,13 +16,12 @@ the details for you. - -----
@@ -44,6 +43,7 @@ local folder = New "Configuration" { [Attribute(nil)] = "Foo" } ``` +
----- @@ -127,6 +127,7 @@ the connection failed to register. -- An attribute change shouldn't fail, as GetAttributeChangedSignal -- doesn't error if the attribute doesn't exist. ``` +
----- @@ -618,7 +620,7 @@ This usually occurs with the [New](../instances/new) or ```Lua local ui = New "Frame" { Size = Computed(function() - return Color3.new(1, 0, 0) + return Color3.new(1, 0, 0) end) } ``` @@ -693,6 +695,7 @@ local config = New "Configuration" { [AttributeChange "Ammo"] = "guns" } ``` +
----- @@ -740,6 +743,7 @@ shouldn't occur, however it is here for a failsafe. ```Lua -- Once again, how does an example for this work? ``` + ----- @@ -909,6 +913,7 @@ For users of Fusion on Roblox, this generally shouldn't occur as Fusion should automatically be bound to Roblox's task scheduler. However, when using Fusion in other environments, the fix is to provide Fusion with all the task scheduler callbacks necessary to schedule tasks for execution in the future. + ----- From 5caf16e9854838d3c04c5b0cf525363817f21d73 Mon Sep 17 00:00:00 2001 From: Elttob Date: Mon, 22 Jan 2024 03:01:59 +0000 Subject: [PATCH 175/287] Enrol in error message weight loss class --- docs/api-reference/errors/error-paste-box.js | 4 +- docs/api-reference/errors/index.md | 405 +------------------ src/Instances/Attribute.lua | 3 - src/Instances/AttributeChange.lua | 16 +- src/Instances/AttributeOut.lua | 9 +- src/Instances/Out.lua | 4 +- src/Logging/messages.lua | 9 +- src/State/Computed.lua | 2 +- src/State/For.lua | 2 +- src/State/ForKeys.lua | 2 +- src/State/ForPairs.lua | 2 +- src/State/ForValues.lua | 2 +- test/Instances/Attribute.spec.lua | 21 - 13 files changed, 24 insertions(+), 457 deletions(-) diff --git a/docs/api-reference/errors/error-paste-box.js b/docs/api-reference/errors/error-paste-box.js index 76487a5fd..d060d48b2 100644 --- a/docs/api-reference/errors/error-paste-box.js +++ b/docs/api-reference/errors/error-paste-box.js @@ -48,6 +48,4 @@ try { } catch(e) { alert("Couldn't instantiate the error paste box - " + e); -} - -const input = "[Fusion] Computed callback error: attempt to index nil with 'get' (ID: computedCallbackError) ---- Stack trace ---- Replica"; \ No newline at end of file +} \ No newline at end of file diff --git a/docs/api-reference/errors/index.md b/docs/api-reference/errors/index.md index cc1e09874..a920b1738 100644 --- a/docs/api-reference/errors/index.md +++ b/docs/api-reference/errors/index.md @@ -24,29 +24,6 @@ the details for you. ----- -
-

- since v0.3 -

- -## attributeNameNil - -``` -Attribute name cannot be nil. -``` - -This message occurs when you try to set an attribute with a 'nil' name. -This error shouldn't occur, however it is a failsafe incase something goes wrong. - -```Lua -local folder = New "Configuration" { - [Attribute(nil)] = "Foo" -} -``` -
- ------ -

since v0.1 @@ -108,29 +85,6 @@ local textBox = New "TextBox" { ----- -

-

- since v0.3 -

- -## cannotConnectAttributeChange - -``` -The Configuration class doesn't have an attribute called 'Foo'. -``` - -This message means you tried to connect to an attribute change event, however -the connection failed to register. - -```Lua --- How does an example for this work? --- An attribute change shouldn't fail, as GetAttributeChangedSignal --- doesn't error if the attribute doesn't exist. -``` -
- ------ -

since v0.1 @@ -189,14 +143,18 @@ local instance = New "ThisClassTypeIsInvalid" { since v0.1

-## computedCallbackError +## callbackError ``` -Computed callback error: attempt to index a nil value +Callback error: attempt to index a nil value ``` -This message means the callback of a [computed object](../state/computed) -encountered an error. +This message means you provided a callback to Fusion, but it ran into an error. +For example, a [computed object](../state/computed) might have failed to compute +a value. + +Review the stack trace that came with the error to see what part of the code +may have caused the error. ```Lua local example = Computed(function() @@ -212,151 +170,7 @@ end) since v0.2

-## destructorNeededComputed - -``` -To return instances from Computeds, provide a destructor function. This will be an error soon - see discussion #183 on GitHub. -``` - -This message shows if you return destructible values from a -[computed object](../state/computed), without also specifying how to destroy -those values using a destructor. - -[Learn more by visiting this discussion on GitHub.](https://github.com/Elttob/Fusion/discussions/183) - -```Lua -local badComputed = Computed(function() - return New "Folder" { ... } -end, nil) -``` -
- ------ - -
-

- since v0.2 -

- -## destructorNeededForKeys - -``` -To return instances from ForKeys, provide a destructor function. This will be an error soon - see discussion #183 on GitHub. -``` - -This message shows if you return destructible values from a -[ForKeys object](../state/forkeys), without also specifying how to destroy -those values using a destructor. - -[Learn more by visiting this discussion on GitHub.](https://github.com/Elttob/Fusion/discussions/183) - -```Lua -local badForKeys = ForKeys(array, function(key) - return New "Folder" { ... } -end, nil) -``` - -!!! note - For some time during the development of v0.2, `ForKeys` would implicitly - insert a destructor for you. This behaviour still works, but it's going to - be removed in an upcoming version. -
- ------ - -
-

- since v0.2 -

- -## destructorNeededForPairs - -``` -To return instances from ForPairs, provide a destructor function. This will be an error soon - see discussion #183 on GitHub. -``` - -This message shows if you return destructible values from a -[ForPairs object](../state/forpairs), without also specifying how to destroy -those values using a destructor. - -[Learn more by visiting this discussion on GitHub.](https://github.com/Elttob/Fusion/discussions/183) - -```Lua -local badForPairs = ForPairs(array, function(key, value) - return key, New "Folder" { ... } -end, nil) -``` - -!!! note - For some time during the development of v0.2, `ForPairs` would implicitly - insert a destructor for you. This behaviour still works, but it's going to - be removed in an upcoming version. -
- ------ - -
-

- since v0.2 -

- -## destructorNeededForValues - -``` -To return instances from ForValues, provide a destructor function. This will be an error soon - see discussion #183 on GitHub. -``` - -This message shows if you return destructible values from a -[ForValues object](../state/forvalues), without also specifying how to destroy -those values using a destructor. - -[Learn more by visiting this discussion on GitHub.](https://github.com/Elttob/Fusion/discussions/183) - -```Lua -local badForValues = ForValues(array, function(value) - return New "Folder" { ... } -end, nil) -``` - -!!! note - For some time during the development of v0.2, `ForValues` would implicitly - insert a destructor for you. This behaviour still works, but it's going to - be removed in an upcoming version. -
- ------ - -
-

- since v0.2 -

- -## forKeysDestructorError - -``` -ForKeys destructor error: attempt to index a nil value -``` - -This message means the destructor passed to a [ForKeys object](../state/forkeys) -encountered an error. - -```Lua -local function destructor(x) - local badMath = 2 + "fish" -end - -local example = ForKeys(array, doSomething, destructor) -``` -
- ------ - -
-

- since v0.2 -

- -## forKeysKeyCollision +## forKeyCollision ``` ForKeys should only write to output key 'Charlie' once when processing key changes, but it wrote to it twice. Previously input key: 'Alice'; New input key: 'Bob' @@ -385,7 +199,7 @@ end) since v0.2

-## forKeysProcessorError +## forProcessorError ``` ForKeys callback error: attempt to index a nil value @@ -408,131 +222,6 @@ end) since v0.2

-## forPairsDestructorError - -``` -ForPairs destructor error: attempt to index a nil value -``` - -This message means the destructor passed to a [ForPairs object](../state/forpairs) -encountered an error. - -```Lua -local function destructor(x, y) - local badMath = 2 + "fish" -end - -local example = ForPairs(array, doSomething, destructor) -``` -
- ------ - -
-

- since v0.2 -

- -## forPairsKeyCollision - -``` -ForPairs should only write to output key 'Charlie' once when processing key changes, but it wrote to it twice. Previously input key: 'Alice'; New input key: 'Bob' -``` - -This message means you returned the same value twice for two different keys in -a [ForPairs object](../state/forpairs). - -```Lua -local data = { - Alice = true, - Bob = true -} -local example = ForPairs(data, function(key, value) - if key == "Alice" or key == "Bob" then - return "Charlie", value - end -end) -``` -
- ------ - -
-

- since v0.2 -

- -## forPairsProcessorError - -``` -ForPairs callback error: attempt to index a nil value -``` - -This message means the callback of a [ForPairs object](../state/forpairs) -encountered an error. - -```Lua -local example = ForPairs(array, function(key, value) - local badMath = 2 + "fish" -end) -``` -
- ------ - -
-

- since v0.2 -

- -## forValuesDestructorError - -``` -ForValues destructor error: attempt to index a nil value -``` - -This message means the destructor passed to a [ForValues object](../state/forvalues) -encountered an error. - -```Lua -local function destructor(x) - local badMath = 2 + "fish" -end - -local example = ForValues(array, doSomething, destructor) -``` -
- ------ - -
-

- since v0.2 -

- -## forValuesProcessorError - -``` -ForValues callback error: attempt to index a nil value -``` - -This message means the callback of a [ForValues object](../state/forvalues) -encountered an error. - -```Lua -local example = ForValues(array, function(value) - local badMath = 2 + "fish" -end) -``` -
- ------ - -
-

- since v0.2 -

- ## invalidChangeHandler ``` @@ -725,28 +414,6 @@ local thing = New "Part" { ----- -
-

- since v0.3 -

- -## invalidAttributeOutName - -``` -The Configuration class doesn't have an attribute called 'Ammo' -``` - -This message means you tried to read an attribute of an instance using -[AttributeOut](../instances/attributeout), but the attribute can't be read. This error -shouldn't occur, however it is here for a failsafe. - -```Lua --- Once again, how does an example for this work? -``` -
- ------ -

since v0.1 @@ -865,36 +532,6 @@ local tween = Tween(state, tweenInfo) ----- -

-

- since v0.2 -

- -## multiReturnComputed - -``` -Returning multiple values from Computeds is discouraged, as behaviour will change soon - see discussion #189 on GitHub. -``` - -This message means you returned more than one value from a [computed object](../state/computed). -There are two ways this could occur; either you're explicitly returning two -values (e.g. `return 1, 2`) or you're calling a function which returns two -values (e.g. `string.find`). - -A simple fix is to surround your return expression with parentheses `()`, or to -save it into a variable before returning it. - -[Learn more by visiting this discussion on GitHub.](https://github.com/Elttob/Fusion/discussions/189) - -```Lua -local badComputed = Computed(function() - return 1, 2, "foo", true -end, nil) -``` -
- ------ -

since v0.3 @@ -975,28 +612,6 @@ print(value:get()) -- should be print(peek(value)) since v0.1

-## strictReadError - -``` -'thisDoesNotExist' is not a valid member of 'Fusion'. -``` - -This message means you tried to access something that doesn't exist. This -specifically occurs with a few 'locked' tables in Fusion, such as the table -returned by the module directly. - -```Lua -local Foo = Fusion.thisDoesNotExist -``` -
- ------ - -
-

- since v0.1 -

- ## unknownMessage ``` diff --git a/src/Instances/Attribute.lua b/src/Instances/Attribute.lua index d9065d8ff..a34304e7d 100644 --- a/src/Instances/Attribute.lua +++ b/src/Instances/Attribute.lua @@ -19,9 +19,6 @@ local whichLivesLonger = require(Package.Memory.whichLivesLonger) local function Attribute( attributeName: string ): Types.SpecialKey - if attributeName == nil then - logError("attributeNameNil") - end return { type = "SpecialKey", kind = "Attribute", diff --git a/src/Instances/AttributeChange.lua b/src/Instances/AttributeChange.lua index 07acb8f15..556448b82 100644 --- a/src/Instances/AttributeChange.lua +++ b/src/Instances/AttributeChange.lua @@ -13,10 +13,6 @@ local logError = require(Package.Logging.logError) local function AttributeChange( attributeName: string ): Types.SpecialKey - if attributeName == nil then - logError("attributeNameNil") - end - return { type = "SpecialKey", kind = "AttributeChange", @@ -31,15 +27,11 @@ local function AttributeChange( logError("invalidAttributeChangeHandler", nil, attributeName) end local value = value :: (...unknown) -> (...unknown) - local ok, event = pcall(applyTo.GetAttributeChangedSignal, applyTo, attributeName) - if not ok then - logError("cannotConnectAttributeChange", nil, applyTo.ClassName, attributeName) - else + local event = applyTo:GetAttributeChangedSignal(attributeName) + value((applyTo :: any):GetAttribute(attributeName)) + table.insert(scope, event:Connect(function() value((applyTo :: any):GetAttribute(attributeName)) - table.insert(scope, event:Connect(function() - value((applyTo :: any):GetAttribute(attributeName)) - end)) - end + end)) end } end diff --git a/src/Instances/AttributeOut.lua b/src/Instances/AttributeOut.lua index 30f9ef54a..871fea02d 100644 --- a/src/Instances/AttributeOut.lua +++ b/src/Instances/AttributeOut.lua @@ -16,10 +16,6 @@ local whichLivesLonger = require(Package.Memory.whichLivesLonger) local function AttributeOut( attributeName: string ): Types.SpecialKey - if attributeName == nil then - logError("attributeNameNil") - end - return { type = "SpecialKey", kind = "AttributeOut", @@ -30,10 +26,7 @@ local function AttributeOut( value: unknown, applyTo: Instance ) - local ok, event = pcall(applyTo.GetAttributeChangedSignal, applyTo, attributeName) - if not ok then - logError("invalidOutAttributeName", nil, applyTo.ClassName, attributeName) - end + local event = applyTo:GetAttributeChangedSignal(attributeName) if not isState(value) then logError("invalidAttributeOutType") diff --git a/src/Instances/Out.lua b/src/Instances/Out.lua index 3c23af4f5..3fce39f4d 100644 --- a/src/Instances/Out.lua +++ b/src/Instances/Out.lua @@ -32,11 +32,11 @@ local function Out( end if not isState(value) then - logError("invalidAttributeOutType") + logError("invalidOutType") end local value = value :: Types.StateObject if value.kind ~= "Value" then - logError("invalidAttributeOutType") + logError("invalidOutType") end local value = value :: Types.Value diff --git a/src/Logging/messages.lua b/src/Logging/messages.lua index 0dc9e8ce3..6a12c3f55 100644 --- a/src/Logging/messages.lua +++ b/src/Logging/messages.lua @@ -6,20 +6,15 @@ ]] return { - attributeNameNil = "Attribute name cannot be nil", cannotAssignProperty = "The class type '%s' has no assignable property '%s'.", cannotConnectChange = "The %s class doesn't have a property called '%s'.", - cannotConnectAttributeChange = "The %s class doesn't have an attribute called '%s'.", cannotConnectEvent = "The %s class doesn't have an event called '%s'.", cannotCreateClass = "Can't create a new instance of class '%s'.", + callbackError = "Error in callback: ERROR_MESSAGE", cleanupWasRenamed = "`Fusion.cleanup` was renamed to `Fusion.doCleanup`. This will be an error in future versions of Fusion.", - computedCallbackError = "Computed callback error: ERROR_MESSAGE", destroyedTwice = "Attempted to destroy %s twice; ensure you're not manually calling `:destroy()` while using scopes. See discussion #292 on GitHub for advice.", - destructorNeededComputed = "To return instances from Computeds, provide a destructor function. This will be an error soon - see discussion #183 on GitHub.", destructorRedundant = "%s destructors no longer do anything. If you wish to run code on destroy, `table.insert` a function into the `scope` argument. See discussion #292 on GitHub for advice.", - multiReturnComputed = "Returning multiple values from Computeds is discouraged, as behaviour will change soon - see discussion #189 on GitHub.", forKeyCollision = "The key '%s' was returned multiple times simultaneously, which is not allowed in `For` objects.", - forProcessorError = "Error while processing `For` object: ERROR_MESSAGE", invalidChangeHandler = "The change handler for the '%s' property must be a function.", invalidAttributeChangeHandler = "The change handler for the '%s' attribute must be a function.", invalidEventHandler = "The handler for the '%s' event must be a function.", @@ -28,7 +23,6 @@ return { invalidOutType = "[Out] properties must be given Value objects.", invalidAttributeOutType = "[AttributeOut] properties must be given Value objects.", invalidOutProperty = "The %s class doesn't have a property called '%s'.", - invalidOutAttributeName = "The %s class doesn't have an attribute called '%s'.", invalidSpringDamping = "The damping ratio for a spring must be >= 0. (damping was %.2f)", invalidSpringSpeed = "The speed of a spring must be >= 0. (speed was %.2f)", mistypedSpringDamping = "The damping ratio for a spring must be a number. (got a %s)", @@ -40,7 +34,6 @@ return { scopeMissing = "To create %s, provide a scope. (e.g. `%s`). See discussion #292 on GitHub for advice.", springTypeMismatch = "The type '%s' doesn't match the spring's type '%s'.", stateGetWasRemoved = "`StateObject:get()` has been replaced by `use()` and `peek()` - see discussion #217 on GitHub.", - strictReadError = "'%s' is not a valid member of '%s'.", unknownMessage = "Unknown error: ERROR_MESSAGE", unrecognisedChildType = "'%s' type children aren't accepted by `[Children]`.", unrecognisedPropertyKey = "'%s' keys aren't accepted in property tables.", diff --git a/src/State/Computed.lua b/src/State/Computed.lua index b76bdb6f4..b2de2a890 100644 --- a/src/State/Computed.lua +++ b/src/State/Computed.lua @@ -87,7 +87,7 @@ function class:update(): boolean local errorObj = (newValue :: any) :: InternalTypes.Error -- this needs to be non-fatal, because otherwise it'd disrupt the -- update process - logErrorNonFatal("computedCallbackError", errorObj) + logErrorNonFatal("callbackError", errorObj) doCleanup(innerScope) diff --git a/src/State/For.lua b/src/State/For.lua index 0c83785b8..7d315c5fb 100644 --- a/src/State/For.lua +++ b/src/State/For.lua @@ -156,7 +156,7 @@ function class:update(): boolean newProcessors[processor] = true else local errorObj = (outputPair :: any) :: InternalTypes.Error - logErrorNonFatal("forProcessorError", errorObj) + logErrorNonFatal("callbackError", errorObj) end end end diff --git a/src/State/ForKeys.lua b/src/State/ForKeys.lua index 3d5cfef4e..0c79bc24b 100644 --- a/src/State/ForKeys.lua +++ b/src/State/ForKeys.lua @@ -53,7 +53,7 @@ local function ForKeys( return key else local errorObj = (key :: any) :: InternalTypes.Error - logErrorNonFatal("forProcessorError", errorObj) + logErrorNonFatal("callbackError", errorObj) doCleanup(scope) table.clear(scope) return nil diff --git a/src/State/ForPairs.lua b/src/State/ForPairs.lua index f802e91a8..c1c0949de 100644 --- a/src/State/ForPairs.lua +++ b/src/State/ForPairs.lua @@ -50,7 +50,7 @@ local function ForPairs( return {key = key, value = value} else local errorObj = (key :: any) :: InternalTypes.Error - logErrorNonFatal("forProcessorError", errorObj) + logErrorNonFatal("callbackError", errorObj) doCleanup(scope) table.clear(scope) return {key = nil, value = nil} diff --git a/src/State/ForValues.lua b/src/State/ForValues.lua index 40098e63f..bb8548e66 100644 --- a/src/State/ForValues.lua +++ b/src/State/ForValues.lua @@ -53,7 +53,7 @@ local function ForValues( return {key = nil, value = value} else local errorObj = (value :: any) :: InternalTypes.Error - logErrorNonFatal("forProcessorError", errorObj) + logErrorNonFatal("callbackError", errorObj) doCleanup(scope) table.clear(scope) return {key = nil, value = nil} diff --git a/test/Instances/Attribute.spec.lua b/test/Instances/Attribute.spec.lua index 51fe0486c..c9f3c4bfe 100644 --- a/test/Instances/Attribute.spec.lua +++ b/test/Instances/Attribute.spec.lua @@ -37,27 +37,6 @@ return function() doCleanup(scope) end) - it("errors when given nil names (constant)", function() - expect(function() - local scope = {} - local child = New(scope, "Folder") { - [Attribute(nil :: any)] = "foo" - } - doCleanup(scope) - end).to.throw("attributeNameNil") - end) - - it("errors when given nil names (state)", function() - expect(function() - local scope = {} - local attributeValue = Value(scope, "foo") - local child = New(scope, "Folder") { - [Attribute(nil :: any)] = attributeValue - } - doCleanup(scope) - end).to.throw("attributeNameNil") - end) - it("defers attribute changes", function() local scope = {} local value = Value(scope, "Bar") From 08ac9ee5bdbfe8dd0887583b9e20d06be7a6036e Mon Sep 17 00:00:00 2001 From: Elttob Date: Mon, 22 Jan 2024 03:59:45 +0000 Subject: [PATCH 176/287] Remove 'since' pills from API reference --- docs/api-reference/animation/animatable.md | 1 - docs/api-reference/animation/spring.md | 13 --- docs/api-reference/animation/tween.md | 1 - docs/api-reference/errors/index.md | 81 ------------------- docs/api-reference/instances/attribute.md | 1 - .../instances/attributechange.md | 1 - docs/api-reference/instances/attributeout.md | 1 - docs/api-reference/instances/child.md | 1 - docs/api-reference/instances/children.md | 1 - docs/api-reference/instances/cleanup.md | 1 - docs/api-reference/instances/component.md | 1 - docs/api-reference/instances/hydrate.md | 1 - docs/api-reference/instances/new.md | 1 - docs/api-reference/instances/onchange.md | 1 - docs/api-reference/instances/onevent.md | 1 - docs/api-reference/instances/out.md | 1 - docs/api-reference/instances/ref.md | 1 - docs/api-reference/instances/specialkey.md | 1 - docs/api-reference/memory/docleanup.md | 1 - docs/api-reference/memory/donothing.md | 1 - docs/api-reference/memory/scoped.md | 1 - docs/api-reference/memory/task.md | 1 - docs/api-reference/state/canbestate.md | 1 - docs/api-reference/state/computed.md | 1 - docs/api-reference/state/dependency.md | 1 - docs/api-reference/state/dependent.md | 5 -- docs/api-reference/state/forkeys.md | 1 - docs/api-reference/state/forpairs.md | 1 - docs/api-reference/state/forvalues.md | 1 - docs/api-reference/state/observer.md | 9 --- docs/api-reference/state/peek.md | 1 - docs/api-reference/state/stateobject.md | 1 - docs/api-reference/state/use.md | 1 - docs/api-reference/state/value.md | 5 -- docs/assets/theme/api-reference.css | 5 -- 35 files changed, 147 deletions(-) diff --git a/docs/api-reference/animation/animatable.md b/docs/api-reference/animation/animatable.md index 7dcb1b680..8c517a207 100644 --- a/docs/api-reference/animation/animatable.md +++ b/docs/api-reference/animation/animatable.md @@ -8,7 +8,6 @@ Animatable type - since v0.1 diff --git a/docs/api-reference/animation/spring.md b/docs/api-reference/animation/spring.md index ec4bca534..d0e67b4a1 100644 --- a/docs/api-reference/animation/spring.md +++ b/docs/api-reference/animation/spring.md @@ -8,7 +8,6 @@ Spring state object - since v0.1 @@ -40,10 +39,6 @@ without overshooting or oscillating. Defaults to `1`. ## Methods -

- since v0.2 -

- ### :octicons-code-24: Spring:setPosition() Instantaneously moves the spring to a new position. This does not affect the @@ -62,10 +57,6 @@ the position will snap instantly to the new value. ----- -

- since v0.2 -

- ### :octicons-code-24: Spring:setVelocity() Overwrites the velocity of this spring. This does not have an immediate effect @@ -84,10 +75,6 @@ the velocity will snap instantly to the new value. ----- -

- since v0.2 -

- ### :octicons-code-24: Spring:addVelocity() Adds to the velocity of this spring. This does not have an immediate effect diff --git a/docs/api-reference/animation/tween.md b/docs/api-reference/animation/tween.md index 15a361e33..869977480 100644 --- a/docs/api-reference/animation/tween.md +++ b/docs/api-reference/animation/tween.md @@ -8,7 +8,6 @@ Tween state object - since v0.1 diff --git a/docs/api-reference/errors/index.md b/docs/api-reference/errors/index.md index a920b1738..754e0d9e7 100644 --- a/docs/api-reference/errors/index.md +++ b/docs/api-reference/errors/index.md @@ -25,9 +25,6 @@ the details for you. -----
-

- since v0.1 -

## cannotAssignProperty @@ -58,9 +55,6 @@ local folder = New "Folder" { -----
-

- since v0.1 -

## cannotConnectChange @@ -86,9 +80,6 @@ local textBox = New "TextBox" { -----
-

- since v0.1 -

## cannotConnectEvent @@ -114,9 +105,6 @@ local button = New "TextButton" { -----
-

- since v0.1 -

## cannotCreateClass @@ -139,9 +127,6 @@ local instance = New "ThisClassTypeIsInvalid" { -----
-

- since v0.1 -

## callbackError @@ -166,9 +151,6 @@ end) -----
-

- since v0.2 -

## forKeyCollision @@ -195,9 +177,6 @@ end) -----
-

- since v0.2 -

## forProcessorError @@ -218,9 +197,6 @@ end) -----
-

- since v0.2 -

## invalidChangeHandler @@ -242,9 +218,6 @@ local input = New "TextBox" { -----
-

- since v0.3 -

## invalidAttributeChangeHandler @@ -266,9 +239,6 @@ local config = New "Configuration" { -----
-

- since v0.2 -

## invalidEventHandler @@ -290,9 +260,6 @@ local button = New "TextButton" { -----
-

- since v0.2 -

## invalidPropertyType @@ -318,9 +285,6 @@ local ui = New "Frame" { -----
-

- since v0.2 -

## invalidRefType @@ -341,9 +305,6 @@ local thing = New "Part" { -----
-

- since v0.2 -

## invalidOutType @@ -365,9 +326,6 @@ local thing = New "Part" { -----
-

- since v0.3 -

## invalidAttributeOutType @@ -389,9 +347,6 @@ local config = New "Configuration" { -----
-

- since v0.2 -

## invalidOutProperty @@ -415,9 +370,6 @@ local thing = New "Part" { -----
-

- since v0.1 -

## invalidSpringDamping @@ -441,9 +393,6 @@ physically simulatable. -----
-

- since v0.1 -

## invalidSpringSpeed @@ -466,9 +415,6 @@ is not simulatable or physically sensible. -----
-

- since v0.1 -

## mistypedSpringDamping @@ -489,9 +435,6 @@ local spring = Spring(state, speed, damping) -----
-

- since v0.1 -

## mistypedSpringSpeed @@ -511,9 +454,6 @@ local spring = Spring(state, speed) -----
-

- since v0.1 -

## mistypedTweenInfo @@ -533,9 +473,6 @@ local tween = Tween(state, tweenInfo) -----
-

- since v0.3 -

## noTaskScheduler @@ -555,9 +492,6 @@ callbacks necessary to schedule tasks for execution in the future. -----
-

- since v0.2 -

## springTypeMismatch @@ -582,9 +516,6 @@ colourSpring:addVelocity(Vector2.new(2, 3)) -----
-

- since v0.3 -

## stateGetWasRemoved @@ -608,9 +539,6 @@ print(value:get()) -- should be print(peek(value)) -----
-

- since v0.1 -

## unknownMessage @@ -631,9 +559,6 @@ with this one. -----
-

- since v0.1 -

## unrecognisedChildType @@ -665,9 +590,6 @@ local instance = New "Folder" { -----
-

- since v0.1 -

## unrecognisedPropertyKey @@ -695,9 +617,6 @@ local folder = New "Folder" { -----
-

- since v0.2 -

## unrecognisedPropertyStage diff --git a/docs/api-reference/instances/attribute.md b/docs/api-reference/instances/attribute.md index cc0dea384..ee6f82a63 100644 --- a/docs/api-reference/instances/attribute.md +++ b/docs/api-reference/instances/attribute.md @@ -8,7 +8,6 @@ Attribute special key - since v0.3 diff --git a/docs/api-reference/instances/attributechange.md b/docs/api-reference/instances/attributechange.md index 7d4ba3e3c..6f715ddec 100644 --- a/docs/api-reference/instances/attributechange.md +++ b/docs/api-reference/instances/attributechange.md @@ -8,7 +8,6 @@ AttributeChange function - since v0.3 diff --git a/docs/api-reference/instances/attributeout.md b/docs/api-reference/instances/attributeout.md index 5532c3012..b4a2a9967 100644 --- a/docs/api-reference/instances/attributeout.md +++ b/docs/api-reference/instances/attributeout.md @@ -8,7 +8,6 @@ AttributeOut function - since v0.3 diff --git a/docs/api-reference/instances/child.md b/docs/api-reference/instances/child.md index db1a358f7..392ac1c5b 100644 --- a/docs/api-reference/instances/child.md +++ b/docs/api-reference/instances/child.md @@ -8,7 +8,6 @@ Child type - since v0.2 diff --git a/docs/api-reference/instances/children.md b/docs/api-reference/instances/children.md index 6c64fe7d8..5b9061350 100644 --- a/docs/api-reference/instances/children.md +++ b/docs/api-reference/instances/children.md @@ -8,7 +8,6 @@ Children special key - since v0.1 diff --git a/docs/api-reference/instances/cleanup.md b/docs/api-reference/instances/cleanup.md index bb9182d96..63becf874 100644 --- a/docs/api-reference/instances/cleanup.md +++ b/docs/api-reference/instances/cleanup.md @@ -8,7 +8,6 @@ Cleanup special key - since v0.2 diff --git a/docs/api-reference/instances/component.md b/docs/api-reference/instances/component.md index d657c640d..60aaea991 100644 --- a/docs/api-reference/instances/component.md +++ b/docs/api-reference/instances/component.md @@ -8,7 +8,6 @@ Component type - since v0.2 diff --git a/docs/api-reference/instances/hydrate.md b/docs/api-reference/instances/hydrate.md index 21847db32..50d3445f3 100644 --- a/docs/api-reference/instances/hydrate.md +++ b/docs/api-reference/instances/hydrate.md @@ -8,7 +8,6 @@ Hydrate function - since v0.2 diff --git a/docs/api-reference/instances/new.md b/docs/api-reference/instances/new.md index d4ae58aed..a1b40fa3e 100644 --- a/docs/api-reference/instances/new.md +++ b/docs/api-reference/instances/new.md @@ -8,7 +8,6 @@ New function - since v0.1 diff --git a/docs/api-reference/instances/onchange.md b/docs/api-reference/instances/onchange.md index 6abc86a92..67577656d 100644 --- a/docs/api-reference/instances/onchange.md +++ b/docs/api-reference/instances/onchange.md @@ -8,7 +8,6 @@ OnChange function - since v0.1 diff --git a/docs/api-reference/instances/onevent.md b/docs/api-reference/instances/onevent.md index d7fb98d7c..9a8cff2aa 100644 --- a/docs/api-reference/instances/onevent.md +++ b/docs/api-reference/instances/onevent.md @@ -8,7 +8,6 @@ OnEvent function - since v0.1 diff --git a/docs/api-reference/instances/out.md b/docs/api-reference/instances/out.md index ae96c955c..68eb65c08 100644 --- a/docs/api-reference/instances/out.md +++ b/docs/api-reference/instances/out.md @@ -8,7 +8,6 @@ Out function - since v0.2 diff --git a/docs/api-reference/instances/ref.md b/docs/api-reference/instances/ref.md index c5d951a79..1e06550e6 100644 --- a/docs/api-reference/instances/ref.md +++ b/docs/api-reference/instances/ref.md @@ -8,7 +8,6 @@ Ref special key - since v0.2 diff --git a/docs/api-reference/instances/specialkey.md b/docs/api-reference/instances/specialkey.md index 452d58238..7d3cf528e 100644 --- a/docs/api-reference/instances/specialkey.md +++ b/docs/api-reference/instances/specialkey.md @@ -8,7 +8,6 @@ SpecialKey type - since v0.2 diff --git a/docs/api-reference/memory/docleanup.md b/docs/api-reference/memory/docleanup.md index cdb1b3be4..94370f27e 100644 --- a/docs/api-reference/memory/docleanup.md +++ b/docs/api-reference/memory/docleanup.md @@ -8,7 +8,6 @@ doCleanup function - since v0.3 diff --git a/docs/api-reference/memory/donothing.md b/docs/api-reference/memory/donothing.md index 41df1a10f..8bb8ea4df 100644 --- a/docs/api-reference/memory/donothing.md +++ b/docs/api-reference/memory/donothing.md @@ -8,7 +8,6 @@ doNothing function - since v0.3 diff --git a/docs/api-reference/memory/scoped.md b/docs/api-reference/memory/scoped.md index f02ddb3fc..396b600ba 100644 --- a/docs/api-reference/memory/scoped.md +++ b/docs/api-reference/memory/scoped.md @@ -8,7 +8,6 @@ scoped function - since v0.3 diff --git a/docs/api-reference/memory/task.md b/docs/api-reference/memory/task.md index 2d0662908..32fab663d 100644 --- a/docs/api-reference/memory/task.md +++ b/docs/api-reference/memory/task.md @@ -8,7 +8,6 @@ Task type - since v0.3 diff --git a/docs/api-reference/state/canbestate.md b/docs/api-reference/state/canbestate.md index 9b2b36372..5db49e7da 100644 --- a/docs/api-reference/state/canbestate.md +++ b/docs/api-reference/state/canbestate.md @@ -8,7 +8,6 @@ CanBeState type - since v0.2 diff --git a/docs/api-reference/state/computed.md b/docs/api-reference/state/computed.md index 57bda66dc..39706dda1 100644 --- a/docs/api-reference/state/computed.md +++ b/docs/api-reference/state/computed.md @@ -8,7 +8,6 @@ Computed state object - since v0.1 diff --git a/docs/api-reference/state/dependency.md b/docs/api-reference/state/dependency.md index f500c05b2..3e2f5a849 100644 --- a/docs/api-reference/state/dependency.md +++ b/docs/api-reference/state/dependency.md @@ -8,7 +8,6 @@ Dependency type - since v0.2 diff --git a/docs/api-reference/state/dependent.md b/docs/api-reference/state/dependent.md index 507c8ecf3..5b27bb9cd 100644 --- a/docs/api-reference/state/dependent.md +++ b/docs/api-reference/state/dependent.md @@ -8,7 +8,6 @@ Dependent type - since v0.2 @@ -36,10 +35,6 @@ updates from ## Methods -

- since v0.2 -

- ### :octicons-code-24: Dependent:update() Called when this object receives an update from one or more dependencies. diff --git a/docs/api-reference/state/forkeys.md b/docs/api-reference/state/forkeys.md index 69992c7ed..1a2c3bd68 100644 --- a/docs/api-reference/state/forkeys.md +++ b/docs/api-reference/state/forkeys.md @@ -8,7 +8,6 @@ ForKeys state object - since v0.2 diff --git a/docs/api-reference/state/forpairs.md b/docs/api-reference/state/forpairs.md index 704ba3a9d..a617b4fb1 100644 --- a/docs/api-reference/state/forpairs.md +++ b/docs/api-reference/state/forpairs.md @@ -8,7 +8,6 @@ ForPairs state object - since v0.2 diff --git a/docs/api-reference/state/forvalues.md b/docs/api-reference/state/forvalues.md index b815a1bec..732b99071 100644 --- a/docs/api-reference/state/forvalues.md +++ b/docs/api-reference/state/forvalues.md @@ -8,7 +8,6 @@ ForValues state object - since v0.2 diff --git a/docs/api-reference/state/observer.md b/docs/api-reference/state/observer.md index ab53d4cfc..a435f2fa6 100644 --- a/docs/api-reference/state/observer.md +++ b/docs/api-reference/state/observer.md @@ -8,7 +8,6 @@ Observer graph object - since v0.2 @@ -30,10 +29,6 @@ Observes various updates and events on a given dependency. ## Object Methods -

- since v0.2 -

- ### :octicons-code-24: Observer:onChange() Connects the given callback as a change handler, and returns a function which @@ -58,10 +53,6 @@ dependency is updated. ----- -

- since v0.3 -

- ### :octicons-code-24: Observer:onBind() Connects the given callback as a change handler, and returns a function which will disconnect the callback. The callback is run immediately, and re-run whenever the observed dependency is updated. diff --git a/docs/api-reference/state/peek.md b/docs/api-reference/state/peek.md index cfd14a47a..a52c3a86e 100644 --- a/docs/api-reference/state/peek.md +++ b/docs/api-reference/state/peek.md @@ -8,7 +8,6 @@ peek function - since v0.3 diff --git a/docs/api-reference/state/stateobject.md b/docs/api-reference/state/stateobject.md index daa3fc1c7..f0957df49 100644 --- a/docs/api-reference/state/stateobject.md +++ b/docs/api-reference/state/stateobject.md @@ -8,7 +8,6 @@ StateObject type - since v0.2 diff --git a/docs/api-reference/state/use.md b/docs/api-reference/state/use.md index baf155729..bcb0a55b7 100644 --- a/docs/api-reference/state/use.md +++ b/docs/api-reference/state/use.md @@ -8,7 +8,6 @@ Use type - since v0.3 diff --git a/docs/api-reference/state/value.md b/docs/api-reference/state/value.md index 1db40e04c..0d9fc6cef 100644 --- a/docs/api-reference/state/value.md +++ b/docs/api-reference/state/value.md @@ -8,7 +8,6 @@ Value state object - since v0.2 @@ -30,10 +29,6 @@ Stores a single value which can be updated at any time. ## Methods -

- since v0.2 -

- ### :octicons-code-24: Value:set() Replaces the currently stored value, updating any other state objects that diff --git a/docs/assets/theme/api-reference.css b/docs/assets/theme/api-reference.css index 3485f6a58..3b375fa75 100644 --- a/docs/assets/theme/api-reference.css +++ b/docs/assets/theme/api-reference.css @@ -59,11 +59,6 @@ color: var(--fusiondoc-accent-text); } -.fusiondoc-api-pills .fusiondoc-api-pill-since { - background-color: var(--fusiondoc-grey-a20); - color: var(--fusiondoc-fg-1); -} - .fusiondoc-api-breadcrumbs { display: flex; align-items: center; From 9ec60c85066b24430fefafee167037b81c9fb8d4 Mon Sep 17 00:00:00 2001 From: Elttob Date: Mon, 22 Jan 2024 05:46:14 +0000 Subject: [PATCH 177/287] Initial work on API reference --- .../{errors/index.md => general/errors.md} | 3 +- docs/api-reference/general/members/version.md | 26 +++++ docs/api-reference/general/types/version.md | 20 ++++ docs/api-reference/memory/docleanup.md | 39 -------- docs/api-reference/memory/donothing.md | 35 ------- .../memory/members/derivescope.md | 52 ++++++++++ .../api-reference/memory/members/docleanup.md | 53 ++++++++++ docs/api-reference/memory/members/scoped.md | 53 ++++++++++ docs/api-reference/memory/scoped.md | 46 --------- docs/api-reference/memory/task.md | 48 ---------- docs/api-reference/memory/types/scope.md | 21 ++++ docs/api-reference/memory/types/task.md | 27 ++++++ .../scripts}/error-paste-box.js | 0 docs/assets/theme/api-reference.css | 65 +++---------- mkdocs.yml | 96 +++++++++++-------- src/Memory/deriveScope.lua | 6 +- src/Types.lua | 2 +- src/init.lua | 60 ++++++------ 18 files changed, 360 insertions(+), 292 deletions(-) rename docs/api-reference/{errors/index.md => general/errors.md} (99%) create mode 100644 docs/api-reference/general/members/version.md create mode 100644 docs/api-reference/general/types/version.md delete mode 100644 docs/api-reference/memory/docleanup.md delete mode 100644 docs/api-reference/memory/donothing.md create mode 100644 docs/api-reference/memory/members/derivescope.md create mode 100644 docs/api-reference/memory/members/docleanup.md create mode 100644 docs/api-reference/memory/members/scoped.md delete mode 100644 docs/api-reference/memory/scoped.md delete mode 100644 docs/api-reference/memory/task.md create mode 100644 docs/api-reference/memory/types/scope.md create mode 100644 docs/api-reference/memory/types/task.md rename docs/{api-reference/errors => assets/scripts}/error-paste-box.js (100%) diff --git a/docs/api-reference/errors/index.md b/docs/api-reference/general/errors.md similarity index 99% rename from docs/api-reference/errors/index.md rename to docs/api-reference/general/errors.md index 754e0d9e7..2da745658 100644 --- a/docs/api-reference/errors/index.md +++ b/docs/api-reference/general/errors.md @@ -1,5 +1,6 @@

diff --git a/docs/api-reference/general/members/version.md b/docs/api-reference/general/members/version.md new file mode 100644 index 000000000..569d06230 --- /dev/null +++ b/docs/api-reference/general/members/version.md @@ -0,0 +1,26 @@ + + +

+ :octicons-workflow-24: + version + + : Version + +

+ +```Lua +Fusion.version = { + major = 0, + minor = 3, + isRelease = false +} +``` + +The version of the Fusion source code. + +`isRelease` is only `true` when using a version of Fusion downloaded from +[the Releases page](https://github.com/dphfox/Fusion/releases). \ No newline at end of file diff --git a/docs/api-reference/general/types/version.md b/docs/api-reference/general/types/version.md new file mode 100644 index 000000000..f6f301824 --- /dev/null +++ b/docs/api-reference/general/types/version.md @@ -0,0 +1,20 @@ + + +

+ :octicons-note-24: + Version +

+ +```Lua +export type Version = { + major: number, + minor: number, + isRelease: boolean +} +``` + +Describes a version of Fusion. \ No newline at end of file diff --git a/docs/api-reference/memory/docleanup.md b/docs/api-reference/memory/docleanup.md deleted file mode 100644 index 94370f27e..000000000 --- a/docs/api-reference/memory/docleanup.md +++ /dev/null @@ -1,39 +0,0 @@ - - -

- :octicons-code-24: - doCleanup - - function - -

- -Attempts to clean up all [tasks](../task) passed to it. Values which are not tasks -are ignored. - -```Lua -(...any) -> () -``` - ------ - -## Parameters - -- `...` - Any objects that need to be destroyed. - ------ - -## Example Usage - -```Lua -doCleanup( - workspace.Part1, - RunService.RenderStepped:Connect(print), - function() - print("I will be run!") - end -) -``` \ No newline at end of file diff --git a/docs/api-reference/memory/donothing.md b/docs/api-reference/memory/donothing.md deleted file mode 100644 index 8bb8ea4df..000000000 --- a/docs/api-reference/memory/donothing.md +++ /dev/null @@ -1,35 +0,0 @@ - - -

- :octicons-code-24: - doNothing - - function - -

- -No-op function - does nothing at all, and returns nothing at all. Intended for -use as a destructor when no destruction is needed. - -```Lua -(...any) -> () -``` - ------ - -## Parameters - -- `...` - Any objects. - ------ - -## Example Usage - -```Lua -local foo = Computed(function(use) - return workspace.Part -end, Fusion.doNothing) -``` \ No newline at end of file diff --git a/docs/api-reference/memory/members/derivescope.md b/docs/api-reference/memory/members/derivescope.md new file mode 100644 index 000000000..299b49be9 --- /dev/null +++ b/docs/api-reference/memory/members/derivescope.md @@ -0,0 +1,52 @@ + + +

+ :octicons-workflow-24: + deriveScope + + -> Scope<T> + +

+ +```Lua +function Fusion.deriveScope( + existing: Scope +): Scope +``` + +Creates a new [scope](../../types/scope) with the same methods as an existing +scope. + +----- + +## Parameters + +

+ existing + + : Scope<T> + +

+ +An existing scope, whose methods should be re-used for the new scope. + +----- + +

+ Returns + + -> Scope<T> + +

+ +A freshly-made, blank scope with the same methods. + +----- + +## Learn More + +- [Scopes tutorial](../../../../tutorials/fundamentals/scopes) \ No newline at end of file diff --git a/docs/api-reference/memory/members/docleanup.md b/docs/api-reference/memory/members/docleanup.md new file mode 100644 index 000000000..0e0d3f436 --- /dev/null +++ b/docs/api-reference/memory/members/docleanup.md @@ -0,0 +1,53 @@ + + +

+ :octicons-workflow-24: + doCleanup + + -> () + +

+ +```Lua +function Fusion.doCleanup( + ...: unknown +): () +``` + +Attempts to destroy all arguments based on their runtime type. + +----- + +## Parameters + +

+ ... + + : unknown + +

+ +A value which should be disposed of; the value's runtime type will be inspected +to determine what should happen. + +- if `function`, it is called +- ...else if `{destroy: (self) -> ()}`, `:destroy()` is called +- ...else if `{Destroy: (self) -> ()}`, `:Destroy()` is called +- ...else if `{any}`, `doCleanup` is called on all members + +When Fusion is running inside of Roblox: + +- if `Instance`, `:Destroy()` is called +- ...else if `RBXScriptConnection`, `:Disconnect()` is called + +If none of these conditions match, the value is ignored. + +----- + +## Learn More + +- [Scopes tutorial](../../../../tutorials/fundamentals/scopes) \ No newline at end of file diff --git a/docs/api-reference/memory/members/scoped.md b/docs/api-reference/memory/members/scoped.md new file mode 100644 index 000000000..645bbe3a7 --- /dev/null +++ b/docs/api-reference/memory/members/scoped.md @@ -0,0 +1,53 @@ + + +

+ :octicons-workflow-24: + scoped + + -> Scope<T> + +

+ +```Lua +function Fusion.scoped( + constructors: T +): Scope +``` + +Creates and returns a blank [scope](../../types/scope), with the `__index` +metatable pointing at the given list of constructors for syntax convenience. + +----- + +## Parameters + +

+ constructors + + : T + +

+ +A table, ideally including functions which take a scope as their first +parameter. + +----- + +

+ Returns + + -> Scope<T> + +

+ +A freshly created, blank scope. + +----- + +## Learn More + +- [Scopes tutorial](../../../../tutorials/fundamentals/scopes) \ No newline at end of file diff --git a/docs/api-reference/memory/scoped.md b/docs/api-reference/memory/scoped.md deleted file mode 100644 index 396b600ba..000000000 --- a/docs/api-reference/memory/scoped.md +++ /dev/null @@ -1,46 +0,0 @@ - - -

- :octicons-code-24: - scoped - - function - -

- -Creates and returns a blank cleanup table, with the `__index` metatable pointing -at the given list of constructors for syntax convenience. - -```Lua -(constructors: T) -> {Task} & T -``` - -!!! info "Approximated type" - The return type of this function is approximate. Luau does not offer a way - of annotating metatable types as of v0.3, so the type signature is - intentionally incorrect to try and usefully annotate for common usage. - ------ - -## Parameters - -- `constructors: T` - A table including constructors with cleanup tables as the -first parameter. - ------ - -## Example Usage - -```Lua -local scope = scoped(Fusion) -local value = scope:Value(5) -doCleanup(scope) - --- equivalent to -local scope = {} -local value = Value(scope, 5) -doCleanup(scope) -``` \ No newline at end of file diff --git a/docs/api-reference/memory/task.md b/docs/api-reference/memory/task.md deleted file mode 100644 index 32fab663d..000000000 --- a/docs/api-reference/memory/task.md +++ /dev/null @@ -1,48 +0,0 @@ - - -

- :octicons-checklist-24: - Task - - type - -

- -Represents types which have default cleanup behaviour defined by Fusion. - -```Lua -Instance | RBXScriptConnection | () -> () | {destroy: (self) -> ()} | {Destroy: (self) -> ()} | {Task} -``` - ------ - -## Example Usage - -```Lua -local stuff: {Task} = { - workspace.Part1, - RunService.RenderStepped:Connect(print), - function() - print("I will be run!") - end -} - -doCleanup(stuff) -``` - ------ - -## Destruction Behaviour - -Destruction behaviour varies by type: - -- if `Instance`, `:Destroy()` is called -- ...else if `RBXScriptConnection`, `:Disconnect()` is called -- ...else if `function`, it is called -- ...else if `{destroy: (self) -> ()}`, `:destroy()` is called -- ...else if `{Destroy: (self) -> ()}`, `:Destroy()` is called -- ...else if `{any}`, `Fusion.cleanup` is called on all members -- ...else nothing occurs. \ No newline at end of file diff --git a/docs/api-reference/memory/types/scope.md b/docs/api-reference/memory/types/scope.md new file mode 100644 index 000000000..80d7d9259 --- /dev/null +++ b/docs/api-reference/memory/types/scope.md @@ -0,0 +1,21 @@ + + +

+ :octicons-note-24: + Scope +

+ +```Lua +export type Scope = {unknown} & Constructors +``` + +A table collecting all objects created as part of an independent unit of code, +with optional `Constructors` as methods which can be called. + +!!! note "Approximated type" + Luau does not yet have syntax for annotating metatables, so scopes created + with constructor methods cannot be represented in text. \ No newline at end of file diff --git a/docs/api-reference/memory/types/task.md b/docs/api-reference/memory/types/task.md new file mode 100644 index 000000000..61cae4b2a --- /dev/null +++ b/docs/api-reference/memory/types/task.md @@ -0,0 +1,27 @@ + + +

+ :octicons-note-24: + Task +

+ +```Lua +export type Task = + Instance + | RBXScriptConnection + | () -> () + | {destroy: (self) -> ()} + | {Destroy: (self) -> ()} + | {Task} +``` +Types which [`doCleanup`](../../members/docleanup) has defined behaviour for. + +!!! warning "Not enforced" + Fusion does not use static types to enforce that `doCleanup` is given a type + which it can process. + + This type is only exposed for your own use. \ No newline at end of file diff --git a/docs/api-reference/errors/error-paste-box.js b/docs/assets/scripts/error-paste-box.js similarity index 100% rename from docs/api-reference/errors/error-paste-box.js rename to docs/assets/scripts/error-paste-box.js diff --git a/docs/assets/theme/api-reference.css b/docs/assets/theme/api-reference.css index 3b375fa75..ab8a5cc30 100644 --- a/docs/assets/theme/api-reference.css +++ b/docs/assets/theme/api-reference.css @@ -1,62 +1,31 @@ .fusiondoc-api-header { display: flex; - align-items: center; - gap: 0.75rem; + align-items: baseline; margin: 0 !important; flex-wrap: wrap; } -.fusiondoc-api-header .headerlink { - display: none; +.fusiondoc-api-header .fusiondoc-api-icon { + margin-right: 0.5em; } .fusiondoc-api-header > * { flex-shrink: 0; } -.fusiondoc-api-header .fusiondoc-api-name { - margin-right: auto; -} - -.fusiondoc-api-pills { - display: flex; - align-items: center; - justify-content: flex-end; - gap: 0.5em; - flex-wrap: wrap; - line-height: 1.5em; +.fusiondoc-api-type { + vertical-align: middle; + font-size: calc((0.5em + 1rem) / 2); + flex-shrink: 0; - font-size: 0.75rem; margin-left: 0.5em; - margin-bottom: -2em; - letter-spacing: -0.02em; + font-family: var(--md-code-font-family); font-weight: 400; - vertical-align: middle; } -.fusiondoc-api-pills + h1, -.fusiondoc-api-pills + h2, -.fusiondoc-api-pills + h3, -.fusiondoc-api-pills + h4 { - margin-top: 0; -} - -.fusiondoc-api-header .fusiondoc-api-pills { - font-size: 0.5em; - margin-bottom: 0em; -} - -.fusiondoc-api-pills > * { - padding: 0.25em 0.75em; - border-radius: 0.25rem; - height: 2em; - flex-shrink: 0; -} - -.fusiondoc-api-pills .fusiondoc-api-pill-type { - background-color: var(--fusiondoc-orange-a20); - color: var(--fusiondoc-accent-text); +.fusiondoc-api-type:not(a){ + opacity: 0.8; } .fusiondoc-api-breadcrumbs { @@ -65,19 +34,13 @@ height: 2rem; } -.fusiondoc-api-breadcrumbs > a { - color: var(--fusiondoc-fg-3); -} - -.fusiondoc-api-breadcrumbs > a:hover { - color: var(--fusiondoc-accent-hover); +.fusiondoc-api-breadcrumbs { + opacity: 0.8; } -.fusiondoc-api-breadcrumbs > a::after { - content: "/"; +.fusiondoc-api-breadcrumbs > *:not(:last-child)::after { + content: "›"; margin: 0 0.25rem; - opacity: 0.5; - color: var(--fusiondoc-fg-3); } .fusiondoc-api-index-header { diff --git a/mkdocs.yml b/mkdocs.yml index b5eae57dd..17dbd7654 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -92,50 +92,64 @@ nav: - Loading Spinner: examples/cookbook/loading-spinner.md - Drag & Drop: examples/cookbook/drag-and-drop.md - API Reference: - - Home: api-reference/index.md - - Errors: - - api-reference/errors/index.md + - api-reference/index.md + - General: + - Errors: api-reference/general/errors.md + - Types: + - Version: api-reference/general/types/version.md + - Members: + - version: api-reference/general/members/version.md - Memory: - - api-reference/memory/index.md - - doCleanup: api-reference/memory/docleanup.md - - doNothing: api-reference/memory/donothing.md - - Task: api-reference/memory/task.md - - scoped: api-reference/memory/scoped.md + - Types: + - Scope: api-reference/memory/types/scope.md + - Task: api-reference/memory/types/task.md + - Members: + - deriveScope: api-reference/memory/members/derivescope.md + - doCleanup: api-reference/memory/members/docleanup.md + - scoped: api-reference/memory/members/scoped.md - State: - - api-reference/state/index.md - - CanBeState: api-reference/state/canbestate.md - - Computed: api-reference/state/computed.md - - Dependency: api-reference/state/dependency.md - - Dependent: api-reference/state/dependent.md - - ForKeys: api-reference/state/forkeys.md - - ForPairs: api-reference/state/forpairs.md - - ForValues: api-reference/state/forvalues.md - - Observer: api-reference/state/observer.md - - peek: api-reference/state/peek.md - - StateObject: api-reference/state/stateobject.md - - Use: api-reference/state/use.md - - Value: api-reference/state/value.md - - Instances: - - api-reference/instances/index.md - - Attribute: api-reference/instances/attribute.md - - AttributeChange: api-reference/instances/attributechange.md - - AttributeOut: api-reference/instances/attributeout.md - - Child: api-reference/instances/child.md - - Children: api-reference/instances/children.md - - Cleanup: api-reference/instances/cleanup.md - - Component: api-reference/instances/component.md - - Hydrate: api-reference/instances/hydrate.md - - New: api-reference/instances/new.md - - OnChange: api-reference/instances/onchange.md - - OnEvent: api-reference/instances/onevent.md - - Out: api-reference/instances/out.md - - Ref: api-reference/instances/ref.md - - SpecialKey: api-reference/instances/specialkey.md + - Types: + - CanBeState: api-reference/state/types/canbestate.md + - Computed: api-reference/state/types/computed.md + - Dependency: api-reference/state/types/dependency.md + - Dependent: api-reference/state/types/dependent.md + - For: api-reference/state/types/for.md + - Observer: api-reference/state/types/observer.md + - StateObject: api-reference/state/types/stateobject.md + - Use: api-reference/state/types/use.md + - Value: api-reference/state/types/value.md + - Members: + - Computed: api-reference/state/members/computed.md + - ForKeys: api-reference/state/members/forkeys.md + - ForPairs: api-reference/state/members/forpairs.md + - ForValues: api-reference/state/members/forvalues.md + - Observer: api-reference/state/members/observer.md + - peek: api-reference/state/members/peek.md + - Value: api-reference/state/members/value.md + - Roblox: + - Types: + - Child: api-reference/roblox/types/child.md + - PropertyTable: api-reference/roblox/types/propertytable.md + - SpecialKey: api-reference/roblox/types/specialkey.md + - Members: + - Attribute: api-reference/roblox/members/attribute.md + - AttributeChange: api-reference/roblox/members/attributechange.md + - AttributeOut: api-reference/roblox/members/attributeout.md + - Children: api-reference/roblox/members/children.md + - Hydrate: api-reference/roblox/members/hydrate.md + - New: api-reference/roblox/members/new.md + - OnChange: api-reference/roblox/members/onchange.md + - OnEvent: api-reference/roblox/members/onevent.md + - Out: api-reference/roblox/members/out.md + - Ref: api-reference/roblox/members/ref.md - Animation: - - api-reference/animation/index.md - - Animatable: api-reference/animation/animatable.md - - Tween: api-reference/animation/tween.md - - Spring: api-reference/animation/spring.md + - Types: + - Animatable: api-reference/animation/types/animatable.md + - Spring: api-reference/animation/types/spring.md + - Tween: api-reference/animation/types/tween.md + - Members: + - Tween: api-reference/animation/members/tween.md + - Spring: api-reference/animation/members/spring.md - Extras: - Home: extras/index.md - Backgrounds: extras/backgrounds.md diff --git a/src/Memory/deriveScope.lua b/src/Memory/deriveScope.lua index 110e36947..dce483dff 100644 --- a/src/Memory/deriveScope.lua +++ b/src/Memory/deriveScope.lua @@ -10,8 +10,10 @@ local Types = require(Package.Types) -- This return type is technically a lie, but it's required for useful type -- checking behaviour. -local function deriveScope(scope: Types.Scope): Types.Scope - return setmetatable({}, getmetatable(scope)) :: any +local function deriveScope( + existing: Types.Scope +): Types.Scope + return setmetatable({}, getmetatable(existing)) :: any end return deriveScope \ No newline at end of file diff --git a/src/Types.lua b/src/Types.lua index 3bff35d1a..b88ade816 100644 --- a/src/Types.lua +++ b/src/Types.lua @@ -222,7 +222,7 @@ export type Fusion = { doCleanup: (...unknown) -> (), scoped: ScopedConstructor, - deriveScope: (scope: Scope) -> Scope, + deriveScope: (existing: Scope) -> Scope, peek: Use, Value: ValueConstructor, diff --git a/src/init.lua b/src/init.lua index b31ccdf1c..967b2fc5d 100644 --- a/src/init.lua +++ b/src/init.lua @@ -9,23 +9,23 @@ local Types = require(script.Types) local External = require(script.External) export type Animatable = Types.Animatable -export type Task = Types.Task -export type Scope = Types.Scope -export type Version = Types.Version -export type Dependency = Types.Dependency -export type Dependent = Types.Dependent -export type StateObject = Types.StateObject export type CanBeState = Types.CanBeState -export type Use = Types.Use -export type Value = Types.Value +export type Child = Types.Child export type Computed = Types.Computed +export type Dependency = Types.Dependency +export type Dependent = Types.Dependent export type For = Types.For export type Observer = Types.Observer -export type Tween = Types.Tween -export type Spring = Types.Spring -export type SpecialKey = Types.SpecialKey -export type Child = Types.Child export type PropertyTable = Types.PropertyTable +export type Scope = Types.Scope +export type SpecialKey = Types.SpecialKey +export type Spring = Types.Spring +export type StateObject = Types.StateObject +export type Task = Types.Task +export type Tween = Types.Tween +export type Use = Types.Use +export type Value = Types.Value +export type Version = Types.Version -- Down the line, this will be conditional based on whether Fusion is being -- compiled for Roblox. @@ -35,35 +35,39 @@ do end local Fusion: Types.Fusion = { + -- General version = {major = 0, minor = 3, isRelease = false}, + -- Memory cleanup = require(script.Memory.legacyCleanup), + deriveScope = require(script.Memory.deriveScope), doCleanup = require(script.Memory.doCleanup), scoped = require(script.Memory.scoped), - deriveScope = require(script.Memory.deriveScope), - peek = require(script.State.peek), - Value = require(script.State.Value), + -- State Computed = require(script.State.Computed), - ForPairs = require(script.State.ForPairs) :: Types.ForPairsConstructor, ForKeys = require(script.State.ForKeys) :: Types.ForKeysConstructor, + ForPairs = require(script.State.ForPairs) :: Types.ForPairsConstructor, ForValues = require(script.State.ForValues) :: Types.ForValuesConstructor, Observer = require(script.State.Observer), + peek = require(script.State.peek), + Value = require(script.State.Value), - Tween = require(script.Animation.Tween), - Spring = require(script.Animation.Spring), - - New = require(script.Instances.New), - Hydrate = require(script.Instances.Hydrate), - - Ref = require(script.Instances.Ref), - Out = require(script.Instances.Out), - Children = require(script.Instances.Children), - OnEvent = require(script.Instances.OnEvent), - OnChange = require(script.Instances.OnChange), + -- Roblox API Attribute = require(script.Instances.Attribute), AttributeChange = require(script.Instances.AttributeChange), - AttributeOut = require(script.Instances.AttributeOut) + AttributeOut = require(script.Instances.AttributeOut), + Children = require(script.Instances.Children), + Hydrate = require(script.Instances.Hydrate), + New = require(script.Instances.New), + OnChange = require(script.Instances.OnChange), + OnEvent = require(script.Instances.OnEvent), + Out = require(script.Instances.Out), + Ref = require(script.Instances.Ref), + + -- Animation + Tween = require(script.Animation.Tween), + Spring = require(script.Animation.Spring), } return Fusion From fdd1a77b1d151c042074ba75dbbb2c5244ab89a6 Mon Sep 17 00:00:00 2001 From: Elttob Date: Mon, 22 Jan 2024 05:58:35 +0000 Subject: [PATCH 178/287] Remove API index pages --- docs/api-reference/animation/index.md | 42 ---------- docs/api-reference/instances/index.md | 110 -------------------------- docs/api-reference/memory/index.md | 48 ----------- docs/api-reference/state/index.md | 100 ----------------------- 4 files changed, 300 deletions(-) delete mode 100644 docs/api-reference/animation/index.md delete mode 100644 docs/api-reference/instances/index.md delete mode 100644 docs/api-reference/memory/index.md delete mode 100644 docs/api-reference/state/index.md diff --git a/docs/api-reference/animation/index.md b/docs/api-reference/animation/index.md deleted file mode 100644 index e93666a41..000000000 --- a/docs/api-reference/animation/index.md +++ /dev/null @@ -1,42 +0,0 @@ - - -

- :octicons-list-unordered-24: - Animation -

- -Utilities for adding transitions and animations to state objects. - ------ - - \ No newline at end of file diff --git a/docs/api-reference/instances/index.md b/docs/api-reference/instances/index.md deleted file mode 100644 index 1546e67eb..000000000 --- a/docs/api-reference/instances/index.md +++ /dev/null @@ -1,110 +0,0 @@ - - -

- :octicons-list-unordered-24: - Instances -

- -Utilities for connecting state objects to instances via code. - ------ - - \ No newline at end of file diff --git a/docs/api-reference/memory/index.md b/docs/api-reference/memory/index.md deleted file mode 100644 index c304c84ce..000000000 --- a/docs/api-reference/memory/index.md +++ /dev/null @@ -1,48 +0,0 @@ - - -

- :octicons-list-unordered-24: - Memory -

- -Functions and utilities for managing memory and object destruction. - ------ - - \ No newline at end of file diff --git a/docs/api-reference/state/index.md b/docs/api-reference/state/index.md deleted file mode 100644 index d83aba1df..000000000 --- a/docs/api-reference/state/index.md +++ /dev/null @@ -1,100 +0,0 @@ - - -

- :octicons-list-unordered-24: - State -

- -Fundamental state objects and utilities for working with reactive graphs. - ------ - - \ No newline at end of file From b97d9698b41c68c6ada96b3e2bb719aefc52b113 Mon Sep 17 00:00:00 2001 From: Elttob Date: Mon, 22 Jan 2024 07:42:47 +0000 Subject: [PATCH 179/287] Expose and document ScopeLifetime --- .../memory/types/scopelifetime.md | 47 +++++++++++++++++++ mkdocs.yml | 1 + src/init.lua | 1 + 3 files changed, 49 insertions(+) create mode 100644 docs/api-reference/memory/types/scopelifetime.md diff --git a/docs/api-reference/memory/types/scopelifetime.md b/docs/api-reference/memory/types/scopelifetime.md new file mode 100644 index 000000000..7570839de --- /dev/null +++ b/docs/api-reference/memory/types/scopelifetime.md @@ -0,0 +1,47 @@ + + +

+ :octicons-note-24: + ScopeLifetime +

+ +```Lua +export type ScopeLifetime = { + scope: Scope? +} +``` + +An object which uses a [scope](../../types/scope) to dictate how long it lives. + +----- + +## Members + +

+ scope + + : Scope<unknown>? + +

+ +The scope which this object was constructed within, or `nil` if the object has +been destroyed. + +!!! note "Unchanged until destruction" + The `scope` is expected to be set once upon construction. It should not be + assigned to again, except when the scope is destroyed - at which point it + should be set to `nil` to indicate that it no longer exists inside of a + scope. This is typically done inside the `:destroy()` method, if it exists. + +!!! tip "Double-destruction prevention" + Fusion's objects throw + [`destroyedTwice`](../../../general/errors/#destroyedtwice) if they detect + a `nil` scope during`:destroy()`. + + It's strongly recommended that you emulate this behaviour if you're + implementing your own objects, as this protects against double-destruction + and exposes potential scoping issues further ahead of time. \ No newline at end of file diff --git a/mkdocs.yml b/mkdocs.yml index 17dbd7654..7e158e2c2 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -102,6 +102,7 @@ nav: - Memory: - Types: - Scope: api-reference/memory/types/scope.md + - ScopeLifetime: api-reference/memory/types/scopelifetime.md - Task: api-reference/memory/types/task.md - Members: - deriveScope: api-reference/memory/members/derivescope.md diff --git a/src/init.lua b/src/init.lua index 967b2fc5d..d37d415f0 100644 --- a/src/init.lua +++ b/src/init.lua @@ -18,6 +18,7 @@ export type For = Types.For export type Observer = Types.Observer export type PropertyTable = Types.PropertyTable export type Scope = Types.Scope +export type ScopeLifetime = Types.ScopeLifetime export type SpecialKey = Types.SpecialKey export type Spring = Types.Spring export type StateObject = Types.StateObject From 942632692dbf7e053637e512b1ca7f7ec92ba46e Mon Sep 17 00:00:00 2001 From: Elttob Date: Mon, 22 Jan 2024 08:08:58 +0000 Subject: [PATCH 180/287] Add member descriptions for Version --- docs/api-reference/general/types/version.md | 37 ++++++++++++++++++- .../memory/types/scopelifetime.md | 5 ++- 2 files changed, 40 insertions(+), 2 deletions(-) diff --git a/docs/api-reference/general/types/version.md b/docs/api-reference/general/types/version.md index f6f301824..f80ae760c 100644 --- a/docs/api-reference/general/types/version.md +++ b/docs/api-reference/general/types/version.md @@ -17,4 +17,39 @@ export type Version = { } ``` -Describes a version of Fusion. \ No newline at end of file +Describes a version of Fusion's source code. + +----- + +## Members + +

+ major + + : number + +

+ +The major version number. If this is greater than `0`, then two versions sharing +the same major version number are not expected to be incompatible or have +breaking changes. + +

+ minor + + : number + +

+ +The minor version number. Describes version updates that are not enumerated by +the major version number, such as versions prior to 1.0, or versions which +are non-breaking. + +

+ isRelease + + : boolean + +

+ +Describes whether the version was sourced from an official release package. \ No newline at end of file diff --git a/docs/api-reference/memory/types/scopelifetime.md b/docs/api-reference/memory/types/scopelifetime.md index 7570839de..6ab9bb3c8 100644 --- a/docs/api-reference/memory/types/scopelifetime.md +++ b/docs/api-reference/memory/types/scopelifetime.md @@ -16,6 +16,9 @@ export type ScopeLifetime = { ``` An object which uses a [scope](../../types/scope) to dictate how long it lives. +Objects satisfying this interface can be probed for information about their +lifetime and how long they live relative to other objects satisfying this +interface. ----- @@ -25,7 +28,7 @@ An object which uses a [scope](../../types/scope) to dictate how long it lives. scope
: Scope<unknown>? - + The scope which this object was constructed within, or `nil` if the object has From cc21b8c56e86db4fb7a0b5a90a4e3816b098e92d Mon Sep 17 00:00:00 2001 From: Elttob Date: Mon, 22 Jan 2024 08:11:08 +0000 Subject: [PATCH 181/287] Use 16x icons on API home --- docs/api-reference/index.md | 56 ++++++++++++++++++------------------- 1 file changed, 28 insertions(+), 28 deletions(-) diff --git a/docs/api-reference/index.md b/docs/api-reference/index.md index fd05c58bc..ef6d39cec 100644 --- a/docs/api-reference/index.md +++ b/docs/api-reference/index.md @@ -26,99 +26,99 @@ page. From 0664d7e62909d0d8aa98b9d3e7f68c60a3c7188b Mon Sep 17 00:00:00 2001 From: Elttob Date: Thu, 25 Jan 2024 05:59:13 +0000 Subject: [PATCH 182/287] Use/CanBeState API reference --- docs/api-reference/memory/members/scoped.md | 2 +- docs/api-reference/state/types/canbestate.md | 35 +++++++++++ docs/api-reference/state/types/use.md | 58 +++++++++++++++++++ .../callbacks.md | 0 .../callbacks/Top-Down-Control-Dark.svg | 0 .../callbacks/Top-Down-Control-Light.svg | 0 .../components.md | 0 .../instance-handling.md | 0 .../instance-handling/Popup-Exploded-Dark.svg | 0 .../Popup-Exploded-Light.svg | 0 .../instance-handling/Popups-Dark.svg | 0 .../instance-handling/Popups-Light.svg | 0 .../{components => best-practices}/state.md | 0 .../state/Check-Boxes-Dark.svg | 0 .../state/Check-Boxes-Light.svg | 0 .../state/Master-Check-Box-Dark.svg | 0 .../state/Master-Check-Box-Light.svg | 0 mkdocs.yml | 8 +-- 18 files changed, 98 insertions(+), 5 deletions(-) create mode 100644 docs/api-reference/state/types/canbestate.md create mode 100644 docs/api-reference/state/types/use.md rename docs/tutorials/{components => best-practices}/callbacks.md (100%) rename docs/tutorials/{components => best-practices}/callbacks/Top-Down-Control-Dark.svg (100%) rename docs/tutorials/{components => best-practices}/callbacks/Top-Down-Control-Light.svg (100%) rename docs/tutorials/{components => best-practices}/components.md (100%) rename docs/tutorials/{components => best-practices}/instance-handling.md (100%) rename docs/tutorials/{components => best-practices}/instance-handling/Popup-Exploded-Dark.svg (100%) rename docs/tutorials/{components => best-practices}/instance-handling/Popup-Exploded-Light.svg (100%) rename docs/tutorials/{components => best-practices}/instance-handling/Popups-Dark.svg (100%) rename docs/tutorials/{components => best-practices}/instance-handling/Popups-Light.svg (100%) rename docs/tutorials/{components => best-practices}/state.md (100%) rename docs/tutorials/{components => best-practices}/state/Check-Boxes-Dark.svg (100%) rename docs/tutorials/{components => best-practices}/state/Check-Boxes-Light.svg (100%) rename docs/tutorials/{components => best-practices}/state/Master-Check-Box-Dark.svg (100%) rename docs/tutorials/{components => best-practices}/state/Master-Check-Box-Light.svg (100%) diff --git a/docs/api-reference/memory/members/scoped.md b/docs/api-reference/memory/members/scoped.md index 645bbe3a7..f881a4b20 100644 --- a/docs/api-reference/memory/members/scoped.md +++ b/docs/api-reference/memory/members/scoped.md @@ -41,7 +41,7 @@ parameter. Returns -> Scope<T> - + A freshly created, blank scope. diff --git a/docs/api-reference/state/types/canbestate.md b/docs/api-reference/state/types/canbestate.md new file mode 100644 index 000000000..826c74278 --- /dev/null +++ b/docs/api-reference/state/types/canbestate.md @@ -0,0 +1,35 @@ + + +

+ :octicons-note-24: + CanBeState +

+ +```Lua +export type CanBeState = T | StateObject +``` + +Something which describes a value of type `T`. When it is [used](../use) in a +calculation, it becomes that value. + +!!! success "Recommended" + Instead of using one of the more specific variants, your code should aim to + use this type as often as possible. It allows your logic to deal with many + representations of values at once, + +----- + +## Variants + +- `T` - represents unchanging constant values +- [`StateObject`](../stateobject) - represents dynamically updating values + +----- + +## Learn More + +- [Components tutorial](../../../../tutorials/best-practices/components/) \ No newline at end of file diff --git a/docs/api-reference/state/types/use.md b/docs/api-reference/state/types/use.md new file mode 100644 index 000000000..c106eb853 --- /dev/null +++ b/docs/api-reference/state/types/use.md @@ -0,0 +1,58 @@ + + +

+ :octicons-note-24: + Use +

+ +```Lua +export type Use = (target: CanBeState) -> T +``` + +A function which extracts a value of `T` from abstract representations of `T` +([`CanBeState`](../canbestate)). + +The most generic implementation of this is +[the `peek()` function](../../members/peek), which performs this extraction with +no additional steps. + +However, certain APIs may provide their own implementation, +so they can perform additional processing for certain representations. Most +notably, [computeds](../../members/computed) provide their own `use()` function +which adds inputs to a watchlist, which allows them to re-calculate as inputs +change. + +----- + +## Parameters + +

+ target + + : CanBeState<T> + +

+ +The abstract representation of `T` to extract a value from. + +----- + +

+ Returns + + -> T + +

+ +The current value of `T`, derived from `target`. + +----- + +## Learn More + +- [Values tutorial](../../../../tutorials/fundamentals/values/) +- [Computeds tutorial](../../../../tutorials/fundamentals/computeds/) \ No newline at end of file diff --git a/docs/tutorials/components/callbacks.md b/docs/tutorials/best-practices/callbacks.md similarity index 100% rename from docs/tutorials/components/callbacks.md rename to docs/tutorials/best-practices/callbacks.md diff --git a/docs/tutorials/components/callbacks/Top-Down-Control-Dark.svg b/docs/tutorials/best-practices/callbacks/Top-Down-Control-Dark.svg similarity index 100% rename from docs/tutorials/components/callbacks/Top-Down-Control-Dark.svg rename to docs/tutorials/best-practices/callbacks/Top-Down-Control-Dark.svg diff --git a/docs/tutorials/components/callbacks/Top-Down-Control-Light.svg b/docs/tutorials/best-practices/callbacks/Top-Down-Control-Light.svg similarity index 100% rename from docs/tutorials/components/callbacks/Top-Down-Control-Light.svg rename to docs/tutorials/best-practices/callbacks/Top-Down-Control-Light.svg diff --git a/docs/tutorials/components/components.md b/docs/tutorials/best-practices/components.md similarity index 100% rename from docs/tutorials/components/components.md rename to docs/tutorials/best-practices/components.md diff --git a/docs/tutorials/components/instance-handling.md b/docs/tutorials/best-practices/instance-handling.md similarity index 100% rename from docs/tutorials/components/instance-handling.md rename to docs/tutorials/best-practices/instance-handling.md diff --git a/docs/tutorials/components/instance-handling/Popup-Exploded-Dark.svg b/docs/tutorials/best-practices/instance-handling/Popup-Exploded-Dark.svg similarity index 100% rename from docs/tutorials/components/instance-handling/Popup-Exploded-Dark.svg rename to docs/tutorials/best-practices/instance-handling/Popup-Exploded-Dark.svg diff --git a/docs/tutorials/components/instance-handling/Popup-Exploded-Light.svg b/docs/tutorials/best-practices/instance-handling/Popup-Exploded-Light.svg similarity index 100% rename from docs/tutorials/components/instance-handling/Popup-Exploded-Light.svg rename to docs/tutorials/best-practices/instance-handling/Popup-Exploded-Light.svg diff --git a/docs/tutorials/components/instance-handling/Popups-Dark.svg b/docs/tutorials/best-practices/instance-handling/Popups-Dark.svg similarity index 100% rename from docs/tutorials/components/instance-handling/Popups-Dark.svg rename to docs/tutorials/best-practices/instance-handling/Popups-Dark.svg diff --git a/docs/tutorials/components/instance-handling/Popups-Light.svg b/docs/tutorials/best-practices/instance-handling/Popups-Light.svg similarity index 100% rename from docs/tutorials/components/instance-handling/Popups-Light.svg rename to docs/tutorials/best-practices/instance-handling/Popups-Light.svg diff --git a/docs/tutorials/components/state.md b/docs/tutorials/best-practices/state.md similarity index 100% rename from docs/tutorials/components/state.md rename to docs/tutorials/best-practices/state.md diff --git a/docs/tutorials/components/state/Check-Boxes-Dark.svg b/docs/tutorials/best-practices/state/Check-Boxes-Dark.svg similarity index 100% rename from docs/tutorials/components/state/Check-Boxes-Dark.svg rename to docs/tutorials/best-practices/state/Check-Boxes-Dark.svg diff --git a/docs/tutorials/components/state/Check-Boxes-Light.svg b/docs/tutorials/best-practices/state/Check-Boxes-Light.svg similarity index 100% rename from docs/tutorials/components/state/Check-Boxes-Light.svg rename to docs/tutorials/best-practices/state/Check-Boxes-Light.svg diff --git a/docs/tutorials/components/state/Master-Check-Box-Dark.svg b/docs/tutorials/best-practices/state/Master-Check-Box-Dark.svg similarity index 100% rename from docs/tutorials/components/state/Master-Check-Box-Dark.svg rename to docs/tutorials/best-practices/state/Master-Check-Box-Dark.svg diff --git a/docs/tutorials/components/state/Master-Check-Box-Light.svg b/docs/tutorials/best-practices/state/Master-Check-Box-Light.svg similarity index 100% rename from docs/tutorials/components/state/Master-Check-Box-Light.svg rename to docs/tutorials/best-practices/state/Master-Check-Box-Light.svg diff --git a/mkdocs.yml b/mkdocs.yml index 7e158e2c2..ec5ed7d82 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -75,10 +75,10 @@ nav: - Outputs: tutorials/roblox/outputs.md - References: tutorials/roblox/references.md - Best Practices: - - Components: tutorials/components/components.md - - Instance Handling: tutorials/components/instance-handling.md - - Callbacks: tutorials/components/callbacks.md - - State: tutorials/components/state.md + - Components: tutorials/best-practices/components.md + - Instance Handling: tutorials/best-practices/instance-handling.md + - Callbacks: tutorials/best-practices/callbacks.md + - State: tutorials/best-practices/state.md - Examples: - Home: examples/index.md From 34ff6a35343db6a86f12989b46ca2e29a0e0283b Mon Sep 17 00:00:00 2001 From: Elttob Date: Thu, 25 Jan 2024 06:24:09 +0000 Subject: [PATCH 183/287] peek() API reference --- docs/api-reference/state/canbestate.md | 43 --------------- docs/api-reference/state/members/peek.md | 69 ++++++++++++++++++++++++ docs/api-reference/state/peek.md | 42 --------------- docs/api-reference/state/use.md | 34 ------------ 4 files changed, 69 insertions(+), 119 deletions(-) delete mode 100644 docs/api-reference/state/canbestate.md create mode 100644 docs/api-reference/state/members/peek.md delete mode 100644 docs/api-reference/state/peek.md delete mode 100644 docs/api-reference/state/use.md diff --git a/docs/api-reference/state/canbestate.md b/docs/api-reference/state/canbestate.md deleted file mode 100644 index 5db49e7da..000000000 --- a/docs/api-reference/state/canbestate.md +++ /dev/null @@ -1,43 +0,0 @@ - - -

- :octicons-checklist-24: - CanBeState - - type - -

- -A value which may either be a [state object](../stateobject) or a constant. - -Provided as a convenient shorthand for indicating that constant-ness is not -important. - -```Lua -StateObject | T -``` - ------ - -## Example Usage - -```Lua -local function printItem(item: CanBeState) - if typeof(item) == "string" then - -- constant - print("Got constant: ", item) - else - -- state object - print("Got state object: ", peek(item)) - end -end - -local constant = "Hello" -local state = Value("World") - -printItem(constant) --> Got constant: Hello -printItem(state) --> Got state object: World -``` \ No newline at end of file diff --git a/docs/api-reference/state/members/peek.md b/docs/api-reference/state/members/peek.md new file mode 100644 index 000000000..ed17bc1b5 --- /dev/null +++ b/docs/api-reference/state/members/peek.md @@ -0,0 +1,69 @@ + + +

+ :octicons-workflow-24: + peek + + : Use + +

+ +```Lua +function Fusion.peek( + target: CanBeState +): T +``` + +Extract a value of type `T` from its input. + +This is a general-purpose implementation of [`Use`](../../types/use). It does +not do any extra processing or book-keeping beyond what is required to determine +the returned value. + +!!! warning "Specific implementations" + If you're given a specific implementation of `Use` by an API, it's highly + likely that you are expected to use that implementation instead of `peek()`. + + This applies to reusable code too. It's often best to ask for a `Use` + callback if your code needs to extract values, so an appropriate + implementation can be passed in. + + Alternatively for reusable code, you can avoid extracting values entirely, + and expect the user to do it prior to calling your code. This can work well + if you unconditionally use all inputs, but beware that you may end up + extracting more values than you need - this can have performance + implications. + +----- + +## Parameters + +

+ target + + : CanBeState<T> + +

+ +The abstract representation of `T` to extract a value from. + +----- + +

+ Returns + + -> T + +

+ +The current value of `T`, derived from `target`. + +----- + +## Learn More + +- [Values tutorial](../../../../tutorials/fundamentals/values/) \ No newline at end of file diff --git a/docs/api-reference/state/peek.md b/docs/api-reference/state/peek.md deleted file mode 100644 index a52c3a86e..000000000 --- a/docs/api-reference/state/peek.md +++ /dev/null @@ -1,42 +0,0 @@ - - -

- :octicons-code-24: - peek - - function - -

- -The most basic [use callback](./use.md), which returns the interior value of -state objects without adding any dependencies. - -```Lua -(target: CanBeState) -> T -``` - ------ - -## Parameters - -- `target: CanBeState` - The argument to attempt to unwrap. - ------ - -## Returns - -If the argument is a state object, returns the interior value of the state -object. Otherwise, returns the argument itself. - ------ - -## Example Usage - -```Lua -local thing = Value(5) - -print(peek(thing)) --> 5 -``` \ No newline at end of file diff --git a/docs/api-reference/state/use.md b/docs/api-reference/state/use.md deleted file mode 100644 index bcb0a55b7..000000000 --- a/docs/api-reference/state/use.md +++ /dev/null @@ -1,34 +0,0 @@ - - -

- :octicons-checklist-24: - Use - - type - -

- -The general function signature for unwrapping [state objects](./stateobject.md) -while transparently passing through other (constant) values. - -Functions of this shape are often referred to as 'use callbacks', and are often -provided by dependency capturers such as [computeds](./computed.md) for the -purposes of tracking used state objects in a processor function. - -```Lua -(target: CanBeState) -> T -``` - ------ - -## Example Usage - -```Lua -local foo: Value = Value(2) -local doubleFoo = Computed(function(use: Fusion.Use) - return use(foo) * 2 -end) -``` \ No newline at end of file From a745e8c76b23b72c086bf0984fff672f4b680e0f Mon Sep 17 00:00:00 2001 From: Elttob Date: Sat, 27 Jan 2024 10:20:37 +0000 Subject: [PATCH 184/287] Fix For constructor types --- src/Types.lua | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Types.lua b/src/Types.lua index b88ade816..2ae205fb2 100644 --- a/src/Types.lua +++ b/src/Types.lua @@ -116,17 +116,17 @@ export type For = StateObject<{[KO]: VO}> & Dependent & { export type ForPairsConstructor = ( scope: Scope, inputTable: CanBeState<{[KI]: VI}>, - processor: (Scope, Use, KI, VI) -> (KO, VO) + processor: (Use, Scope, KI, VI) -> (KO, VO) ) -> For export type ForKeysConstructor = ( scope: Scope, inputTable: CanBeState<{[KI]: V}>, - processor: (Scope, Use, KI) -> KO + processor: (Use, Scope, KI) -> KO ) -> For export type ForValuesConstructor = ( scope: Scope, inputTable: CanBeState<{[K]: VI}>, - processor: (Scope, Use, VI) -> VO + processor: (Use, Scope, VI) -> VO ) -> For -- An object which can listen for updates on another state object. From 3d340696de8f8252287666e5a85b19faca922a59 Mon Sep 17 00:00:00 2001 From: Elttob Date: Sun, 28 Jan 2024 06:43:09 +0000 Subject: [PATCH 185/287] Fix whichLivesLonger inference --- src/Animation/Spring.lua | 2 +- src/Animation/Tween.lua | 2 +- src/Instances/Attribute.lua | 2 +- src/Instances/AttributeOut.lua | 2 +- src/Instances/Out.lua | 2 +- src/Instances/Ref.lua | 2 +- src/Instances/applyInstanceProps.lua | 2 +- src/Memory/whichLivesLonger.lua | 16 ++++++++-------- src/State/Computed.lua | 2 +- src/State/Observer.lua | 2 +- 10 files changed, 17 insertions(+), 17 deletions(-) diff --git a/src/Animation/Spring.lua b/src/Animation/Spring.lua index c4a81c833..2c6676ed8 100644 --- a/src/Animation/Spring.lua +++ b/src/Animation/Spring.lua @@ -242,7 +242,7 @@ local function Spring( table.insert(scope, self) if goalState.scope == nil then logError("useAfterDestroy", nil, `The {goalState.kind} object`, `the Spring that is following it`) - elseif whichLivesLonger(scope, self, goalState.scope, goalState) == "a" then + elseif whichLivesLonger(scope, self, goalState.scope, goalState) == "definitely-a" then logWarn("possiblyOutlives", `The {goalState.kind} object`, `the Spring that is following it`) end diff --git a/src/Animation/Tween.lua b/src/Animation/Tween.lua index 574e81cec..7bfd4e529 100644 --- a/src/Animation/Tween.lua +++ b/src/Animation/Tween.lua @@ -141,7 +141,7 @@ local function Tween( table.insert(scope, self) if goalState.scope == nil then logError("useAfterDestroy", nil, `The {goalState.kind} object`, `the Tween that is following it`) - elseif whichLivesLonger(scope, self, goalState.scope, goalState) == "a" then + elseif whichLivesLonger(scope, self, goalState.scope, goalState) == "definitely-a" then logWarn("possiblyOutlives", `The {goalState.kind} object`, `the Tween that is following it`) end diff --git a/src/Instances/Attribute.lua b/src/Instances/Attribute.lua index a34304e7d..059ec4d6a 100644 --- a/src/Instances/Attribute.lua +++ b/src/Instances/Attribute.lua @@ -33,7 +33,7 @@ local function Attribute( local value = value :: Types.StateObject if value.scope == nil then logError("useAfterDestroy", nil, `The {value.kind} object, bound to [Attribute "{attributeName}"],`, `the {applyTo.ClassName} instance`) - elseif whichLivesLonger(scope, applyTo, value.scope, value) == "a" then + elseif whichLivesLonger(scope, applyTo, value.scope, value) == "definitely-a" then logWarn("possiblyOutlives", `The {value.kind} object, bound to [Attribute "{attributeName}"],`, `the {applyTo.ClassName} instance`) end local didDefer = false diff --git a/src/Instances/AttributeOut.lua b/src/Instances/AttributeOut.lua index 871fea02d..39c43ad4c 100644 --- a/src/Instances/AttributeOut.lua +++ b/src/Instances/AttributeOut.lua @@ -39,7 +39,7 @@ local function AttributeOut( if value.scope == nil then logError("useAfterDestroy", nil, `The Value object, which [AttributeOut "{attributeName}"] outputs to,`, `the {applyTo.ClassName} instance`) - elseif whichLivesLonger(scope, applyTo, value.scope, value) == "a" then + elseif whichLivesLonger(scope, applyTo, value.scope, value) == "definitely-a" then logWarn("possiblyOutlives", `The Value object, which [AttributeOut "{attributeName}"] outputs to,`, `the {applyTo.ClassName} instance`) end value:set((applyTo :: any):GetAttribute(attributeName)) diff --git a/src/Instances/Out.lua b/src/Instances/Out.lua index 3fce39f4d..47d4c9ab0 100644 --- a/src/Instances/Out.lua +++ b/src/Instances/Out.lua @@ -42,7 +42,7 @@ local function Out( if value.scope == nil then logError("useAfterDestroy", nil, `The Value, which [Out "{propertyName}"] outputs to,`, `the {applyTo.ClassName} instance`) - elseif whichLivesLonger(scope, applyTo, value.scope, value) == "a" then + elseif whichLivesLonger(scope, applyTo, value.scope, value) == "definitely-a" then logWarn("possiblyOutlives", `The Value, which [Out "{propertyName}"] outputs to,`, `the {applyTo.ClassName} instance`) end value:set((applyTo :: any)[propertyName]) diff --git a/src/Instances/Ref.lua b/src/Instances/Ref.lua index cfb3b9ba9..031eb61c2 100644 --- a/src/Instances/Ref.lua +++ b/src/Instances/Ref.lua @@ -34,7 +34,7 @@ return { if value.scope == nil then logError("useAfterDestroy", nil, "The Value object, which [Ref] outputs to,", `the {applyTo} instance`) - elseif whichLivesLonger(scope, applyTo, value.scope, value) == "a" then + elseif whichLivesLonger(scope, applyTo, value.scope, value) == "definitely-a" then logWarn("possiblyOutlives", "The Value object, which [Ref] outputs to,", `the {applyTo} instance`) end value:set(applyTo) diff --git a/src/Instances/applyInstanceProps.lua b/src/Instances/applyInstanceProps.lua index 2f25bf96d..99402966c 100644 --- a/src/Instances/applyInstanceProps.lua +++ b/src/Instances/applyInstanceProps.lua @@ -68,7 +68,7 @@ local function bindProperty( local value = value :: Types.StateObject if value.scope == nil then logError("useAfterDestroy", nil, `The {value.kind} object, bound to {property},`, `the {instance.ClassName} instance`) - elseif whichLivesLonger(scope, instance, value.scope, value) ~= "b" then + elseif whichLivesLonger(scope, instance, value.scope, value) == "definitely-a" then logWarn("possiblyOutlives", `The {value.kind} object, bound to {property},`, `the {instance.ClassName} instance`) end diff --git a/src/Memory/whichLivesLonger.lua b/src/Memory/whichLivesLonger.lua index 0a20f9336..90296770c 100644 --- a/src/Memory/whichLivesLonger.lua +++ b/src/Memory/whichLivesLonger.lua @@ -12,7 +12,7 @@ local Types = require(Package.Types) local function whichScopeLivesLonger( scopeA: Types.Scope, scopeB: Types.Scope -): "a" | "b" | "unknown" +): "definitely-a" | "definitely-b" | "unsure" -- If we can prove one scope is inside of the other scope, then the outer -- scope must live longer than the inner scope (assuming idiomatic scopes). -- So, we will search the scopes recursively until we find one of them, at @@ -25,9 +25,9 @@ local function whichScopeLivesLonger( closedSet[scope] = true for _, inScope in ipairs(scope) do if inScope == scopeA then - return "b" + return "definitely-b" elseif inScope == scopeB then - return "a" + return "definitely-a" elseif typeof(inScope) == "table" then local inScope = inScope :: {unknown} if inScope[1] ~= nil and closedSet[scope] == nil then @@ -41,7 +41,7 @@ local function whichScopeLivesLonger( openSet, nextOpenSet = nextOpenSet, openSet openSetSize, nextOpenSetSize = nextOpenSetSize, 0 end - return "unknown" + return "unsure" end local function whichLivesLonger( @@ -49,18 +49,18 @@ local function whichLivesLonger( a: unknown, scopeB: Types.Scope, b: unknown -): "a" | "b" | "unknown" +): "definitely-a" | "definitely-b" | "unsure" if scopeA == scopeB then local scopeA: {unknown} = scopeA for index = #scopeA, 1, -1 do local value = scopeA[index] if value == a then - return "b" + return "definitely-b" elseif value == b then - return "a" + return "definitely-a" end end - return "unknown" + return "unsure" else return whichScopeLivesLonger(scopeA, scopeB) end diff --git a/src/State/Computed.lua b/src/State/Computed.lua index b2de2a890..e3e616cd3 100644 --- a/src/State/Computed.lua +++ b/src/State/Computed.lua @@ -57,7 +57,7 @@ function class:update(): boolean local target = target :: Types.StateObject if target.scope == nil then logError("useAfterDestroy", nil, `The {target.kind} object`, "the Computed that is use()-ing it") - elseif whichLivesLonger(outerScope, self, target.scope, target) == "a" then + elseif whichLivesLonger(outerScope, self, target.scope, target) == "definitely-a" then logWarn("possiblyOutlives", `The {target.kind} object`, "the Computed that is use()-ing it") end self.dependencySet[target] = true diff --git a/src/State/Observer.lua b/src/State/Observer.lua index 21aad912c..5e0a1c5d6 100644 --- a/src/State/Observer.lua +++ b/src/State/Observer.lua @@ -92,7 +92,7 @@ local function Observer( if watchedState.scope == nil then logError("useAfterDestroy", nil, `The {watchedState.kind} object`, `the Observer that is watching it`) - elseif whichLivesLonger(scope, self, watchedState.scope, watchedState) == "a" then + elseif whichLivesLonger(scope, self, watchedState.scope, watchedState) == "definitely-a" then logWarn("possiblyOutlives", `The {watchedState.kind} object`, `the Observer that is watching it`) end From 3e23bb4649a2a472f9fa6ae47a3b0e99a01091f8 Mon Sep 17 00:00:00 2001 From: Elttob Date: Sun, 28 Jan 2024 06:49:46 +0000 Subject: [PATCH 186/287] Fix [Children] mis-scoped Observer objects --- src/Instances/Children.lua | 26 ++++++++++++++------------ 1 file changed, 14 insertions(+), 12 deletions(-) diff --git a/src/Instances/Children.lua b/src/Instances/Children.lua index df3b46b04..c88c83c79 100644 --- a/src/Instances/Children.lua +++ b/src/Instances/Children.lua @@ -13,6 +13,7 @@ local logWarn = require(Package.Logging.logWarn) local Observer = require(Package.State.Observer) local peek = require(Package.State.peek) local isState = require(Package.State.isState) +local doCleanup = require(Package.Memory.doCleanup) type Set = {[T]: unknown} @@ -32,9 +33,9 @@ return { local newParented: Set = {} local oldParented: Set = {} - -- save disconnection functions for state object observers - local newDisconnects: {[Types.StateObject]: () -> ()} = {} - local oldDisconnects: {[Types.StateObject]: () -> ()} = {} + -- save scopes for state object observers + local newScopes: {[Types.StateObject]: Types.Scope} = {} + local oldScopes: {[Types.StateObject]: Types.Scope} = {} local updateQueued = false local queueUpdate: () -> () @@ -49,9 +50,9 @@ return { updateQueued = false oldParented, newParented = newParented, oldParented - oldDisconnects, newDisconnects = newDisconnects, oldDisconnects + oldScopes, newScopes = newScopes, oldScopes table.clear(newParented) - table.clear(newDisconnects) + table.clear(newScopes) local function processChild( child: unknown, @@ -89,17 +90,18 @@ return { processChild(value, autoName) end - local disconnect = oldDisconnects[child] - if disconnect == nil then + local childScope = oldScopes[child] + if childScope == nil then -- wasn't previously present - disconnect = Observer(scope, child):onChange(queueUpdate) + childScope = {} + Observer(childScope, child):onChange(queueUpdate) else -- previously here; we want to reuse, so remove from old -- set so we don't encounter it during unparenting - oldDisconnects[child] = nil + oldScopes[child] = nil end - newDisconnects[child] = disconnect + newScopes[child] = childScope elseif childType == "table" then -- case 3; table of objects @@ -137,8 +139,8 @@ return { end -- disconnect observers which weren't reused - for oldState, disconnect in pairs(oldDisconnects) do - disconnect() + for oldState, childScope in pairs(oldScopes) do + doCleanup(childScope) end end From baf92bb6551b32e9b93561a3c002755ea3c249e7 Mon Sep 17 00:00:00 2001 From: Elttob Date: Mon, 29 Jan 2024 10:10:33 +0000 Subject: [PATCH 187/287] Contextuals tutorial --- .../best-practices/sharing-values.md | 293 ++++++++++++++++++ mkdocs.yml | 3 + 2 files changed, 296 insertions(+) create mode 100644 docs/tutorials/best-practices/sharing-values.md diff --git a/docs/tutorials/best-practices/sharing-values.md b/docs/tutorials/best-practices/sharing-values.md new file mode 100644 index 000000000..f77ec6143 --- /dev/null +++ b/docs/tutorials/best-practices/sharing-values.md @@ -0,0 +1,293 @@ +Sometimes values are used in far-away parts of the codebase. For example, many +UI elements might share theme colours for light and dark theme. + +----- + +## Globals + +Typically, values are shared by placing them in modules. These modules can be +required from anywhere in the codebase, and their values can be imported into +any code. + +Values shared in this way are known as *globals*. + +=== "Theme.luau" + + ```Lua linenums="1" + local Theme = {} + + Theme.colours = { + background = Color3.fromHex("FFFFFF"), + text = Color3.fromHex("222222"), + -- etc. + } + + return Theme + ``` + +=== "Somewhere else" + + ```Lua linenums="1" + local Theme = require("path/to/Theme.luau") + + local textColour = Theme.colours.text + print(textColour) --> 34, 34, 34 + ``` + +In particular, you can share state objects this way, and every part of the +codebase will be able to see and interact with those state objects. + +=== "Theme.luau" + + ```Lua linenums="1" + local Fusion = require("path/to/Fusion.luau") + + local Theme = {} + + Theme.colours = { + background = { + light = Color3.fromHex("FFFFFF"), + dark = Color3.fromHex("222222") + }, + text = { + light = Color3.fromHex("FFFFFF"), + dark = Color3.fromHex("222222") + }, + -- etc. + } + + function Theme.init( + scope: Fusion.Scope + ) + Theme.currentTheme = scope:Value("light") + end + + return Theme + ``` + +=== "Somewhere else" + + ```Lua linenums="1" + local Fusion = require("path/to/Fusion.luau") + local scoped, peek = Fusion.scoped, Fusion.peek + + local Theme = require("path/to/Theme.luau") + + local function printTheme() + local theme = Theme.currentTheme + print( + peek(theme), + if typeof(theme) == "string" then "constant" else "state object" + ) + end + + local scope = scoped(Fusion) + Theme.init(scope) + printTheme() --> light state object + + Theme.currentTheme:set("dark") + printTheme() --> dark state object + ``` + +Globals are very straightforward to implement and can be useful, but they can +quickly cause problems if not used carefully. + +### Hidden dependencies + +When you use a global inside a block of reusable code such as a component, you +are making your code dependent on another code file without declaring it to the +outside world. + +To some extent, this is entirely why using globals is desirable. While it's more +'correct' to accept the `Theme` via the parameters of your function, it often +means the `Theme` has to be passed down through multiple layers of functions. +This is known as [prop drilling](https://kentcdodds.com/blog/prop-drilling) and +is widely considered bad practice, because it clutters up unrelated code with +parameters that are only passed through functions. + +To avoid prop drilling, globals are often used, which 'hides' the dependency on +that external code file. You no longer have to pass it down through parameters. +However, relying too heavily on these hidden dependencies can cause your code to +behave in surprising, unintuitive ways, or it can obscure what functionality is +available to developers using your code. + +### Hard-to-locate writes + +If you write into globals from deep inside your code base, it becomes very hard +to figure out where the global is being changed from, which significantly hurts +debugging. + +Generally, it's best to treat globals as *read-only*. If you're writing to a +global, it should be coming from a single well-signposted, easy-to-find place. + +You should also keep the principles of [top-down control](../callbacks) in mind; +think of globals as 'flowing down' from the root of the program. Globals are +best managed from high up in the program, because they have widespread effects, +so consider using callbacks to pass control up the chain, rather than managing +globals directly from every part of the code base. + +### Memory management + +In addition, globals can complicate memory management. Because every part of +your code base can access them, you can't destroy globals until the very end of +your program. + +In the above example, this is solved with an `init()` method which passes the +main scope to `Theme`. Because `init()` is called before anything else that uses +`Theme`, the objects that `Theme` creates will be added to the scope first. + +When the main scope is cleaned up, `doCleanup()` will destroy things in reverse +order. This means the `Theme` objects will be cleaned up last, after everything +else in the program has been cleaned up. + +This only works if you know that the main script is the only entry point in your +program. If you have two scripts running concurrently which try to `init()` the +`Theme` module, they will overwrite each other. + +### Non-replaceable for testing + +When your code uses a global, you're hard-coding a connection between your code +and that specific global. + +This is problematic for testing; unless you're using an advanced testing +framework with code injection, it's pretty much impossible to separate your code +from that global code, which makes it impossible to replace global values for +testing purposes. + +For example, if you wanted to write automated tests that verify light theme and +dark theme are correctly applied throughout your UI, you can't replace any +values stored in `Theme`. + +You might be able to write to the `Theme` by going through the normal process, +but this fundamentally limits how you can test. For example, you couldn't run a +test for light theme and dark theme at the same time. + +----- + +## Contextuals + +The main drawback of globals is that they hold one value for all code. To solve +this, Fusion introduces *contextual values*, which can be temporarily changed +for the duration of a code block. + +To create a contextual, call the `Contextual` function from Fusion. It asks for +a default value. + +```Lua +local myContextual = Contextual("foo") +``` + +At any time, you can query its current value using the `:now()` method. + +```Lua +local myContextual = Contextual("foo") +print(myContextual:now()) --> foo +``` + +You can override the value for a limited span of time using `:is():during()`. +Pass the temporary value to `:is()`, and pass a callback to `:during()`. + +While the callback is running, the contextual will adopt the temporary value. + +```Lua +local myContextual = Contextual("foo") +print(myContextual:now()) --> foo + +myContextual:is("bar"):during(function() + print(myContextual:now()) --> bar +end) + +print(myContextual:now()) --> foo +``` + +By storing widely-used values inside contextuals, you can isolate different +code paths from each other, while retaining the easy, hidden referencing that +globals offer. This makes testing and memory management significantly easier, +and helps you locate which code is modifying any shared values. + +To demonstrate, the `Theme` example can be rewritten to use contextuals. + +=== "Theme.luau" + + ```Lua linenums="1" + local Fusion = require("path/to/Fusion.luau") + local Contextual = Fusion.Contextual + + local Theme = {} + + Theme.colours = { + background = { + light = Color3.fromHex("FFFFFF"), + dark = Color3.fromHex("222222") + }, + text = { + light = Color3.fromHex("FFFFFF"), + dark = Color3.fromHex("222222") + }, + -- etc. + } + + Theme.currentTheme = Contextual("light") + + return Theme + ``` + +=== "Somewhere else" + + ```Lua linenums="1" + local Fusion = require("path/to/Fusion.luau") + local scoped, peek = Fusion.scoped, Fusion.peek + + local Theme = require("path/to/Theme.luau") + + local function printTheme() + local theme = Theme.currentTheme:now() + print( + peek(theme), + if typeof(theme) == "string" then "constant" else "state object" + ) + end + + printTheme() --> light constant + + local scope = scoped(Fusion) + local override = scope:Value("light") + Theme.currentTheme:is(override):during(function() + printTheme() --> light state object + override:set("dark") + printTheme() --> dark state object + end) + + printTheme() --> light constant + ``` + +In this rewritten example, `Theme` no longer requires an `init()` function, +because - instead of defining a state object globally - `Theme` only defines +`"light"` as the default value. + +You're expected to replace the default value with a state object when you want +to make the theme dynamic. This has a number of benefits: + +- Because the override is time-limited to one span of your code, you can have +multiple scripts running at the same time with completely different overrides. + +- It also explicitly places your code in charge of memory management, because +you're creating the object yourself. + +- It's easy to locate where changes are coming from, because you can look for +the nearest `:is():during()` call. Optionally, you could share a limited, +read-only version of the value, while retaining private access to write to the +value wherever you're overriding the contextual from. + +- Testing becomes much simpler; you can override the contextual for different +parts of your testing, without ever having to inject code, and without altering +how you read and override the contextual in your production code. + +It's still possible to run into issues with contextuals, though. + +- You're still hiding a dependency of your code, which can still lead to +confusion and obscuring available features, just the same as globals. +- Unlike globals, contextuals are time-limited. If you connect to an event or +start a delayed task, you won't be able to access the value anymore. Instead, +capture the value at the start of the code block, so you can use it in delayed +tasks. \ No newline at end of file diff --git a/mkdocs.yml b/mkdocs.yml index ec5ed7d82..d108bdfb7 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -79,6 +79,7 @@ nav: - Instance Handling: tutorials/best-practices/instance-handling.md - Callbacks: tutorials/best-practices/callbacks.md - State: tutorials/best-practices/state.md + - Sharing Values: tutorials/best-practices/sharing-values.md - Examples: - Home: examples/index.md @@ -96,8 +97,10 @@ nav: - General: - Errors: api-reference/general/errors.md - Types: + - Contextual: api-reference/general/types/contextual.md - Version: api-reference/general/types/version.md - Members: + - Contextual: api-reference/general/members/contextual.md - version: api-reference/general/members/version.md - Memory: - Types: From 50c88f63dae9a1fe7037a8d568ff871a41a93d2d Mon Sep 17 00:00:00 2001 From: Elttob Date: Mon, 29 Jan 2024 10:10:50 +0000 Subject: [PATCH 188/287] Contextual type API ref --- .../api-reference/general/types/contextual.md | 87 +++++++++++++++++++ src/Types.lua | 2 +- 2 files changed, 88 insertions(+), 1 deletion(-) create mode 100644 docs/api-reference/general/types/contextual.md diff --git a/docs/api-reference/general/types/contextual.md b/docs/api-reference/general/types/contextual.md new file mode 100644 index 000000000..4580746d1 --- /dev/null +++ b/docs/api-reference/general/types/contextual.md @@ -0,0 +1,87 @@ + + +

+ :octicons-note-24: + Contextual +

+ +```Lua +export type Contextual = { + type: "Contextual", + now: (self) -> T, + is: (self, newValue: T) -> { + during: (self, callback: (A...) -> R, A...) -> R + } +} +``` + +An object representing a widely-accessible value, which can take on different +values at different times in different coroutines. + +!!! note "Non-standard type syntax" + The above type definition uses `self` to denote methods. At time of writing, + Luau does not interpret `self` specially. + +----- + +## Fields + +

+ type + + : "Contextual" + +

+ +A type string which can be used for runtime type checking. + +----- + +## Methods + +

+ now + + -> T + +

+ +```Lua +function Contextual:now(): T +``` + +Returns the current value of this contextual. This varies based on when the +function is called, and in what coroutine it was called. + +

+ is/during + + -> R + +

+ +```Lua +function Contextual:is( + newValue: T +): { + during: ( + self, + callback: (A...) -> R, + A... + ) -> R +} +``` + +Runs the `callback` with the arguments `A...` and returns the value the callback +returns (`R`). The `Contextual` will appear to be `newValue` in the callback, +unless it's overridden by another `:is():during()` call. + +----- + +## Learn More + +- [Sharing Values tutorial](../../../../tutorials/best-practices/sharing-values) \ No newline at end of file diff --git a/src/Types.lua b/src/Types.lua index 3ca57f5ed..c4d57a8ae 100644 --- a/src/Types.lua +++ b/src/Types.lua @@ -64,7 +64,7 @@ export type Contextual = { } type ContextualIsMethods = { - during: (ContextualIsMethods, (A...) -> T, A...) -> T + during: (ContextualIsMethods, (A...) -> R, A...) -> R } --[[ From 69c3684418dccc691a12efb3c60ac0dd52f8d25c Mon Sep 17 00:00:00 2001 From: Elttob Date: Mon, 29 Jan 2024 12:01:58 +0000 Subject: [PATCH 189/287] Contextual member API ref --- .../general/members/contextual.md | 52 +++++++++++++++++++ 1 file changed, 52 insertions(+) create mode 100644 docs/api-reference/general/members/contextual.md diff --git a/docs/api-reference/general/members/contextual.md b/docs/api-reference/general/members/contextual.md new file mode 100644 index 000000000..72e57e706 --- /dev/null +++ b/docs/api-reference/general/members/contextual.md @@ -0,0 +1,52 @@ + + +

+ :octicons-workflow-24: + Contextual + + -> Contextual<T> + +

+ +```Lua +function Fusion.Contextual( + defaultValue: T +): Contextual +``` + +Constructs and returns a new [contextual](../../types/contextual). + +----- + +## Parameters + +

+ defaultValue + + : T + +

+ +The value which `Contextual:now()` should return if no value has been specified +by `Contextual:is():during()`. + +----- + +

+ Returns + + -> Contextual<T> + +

+ +A freshly constructed contextual. + +----- + +## Learn More + +- [Sharing Values tutorial](../../../../tutorials/best-practices/sharing-values) \ No newline at end of file From 323961da2b0febc39a45ecee5f9d536f75c9d1da Mon Sep 17 00:00:00 2001 From: Elttob Date: Mon, 29 Jan 2024 12:02:21 +0000 Subject: [PATCH 190/287] Fix category of Contextual type API ref --- docs/api-reference/general/types/contextual.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/api-reference/general/types/contextual.md b/docs/api-reference/general/types/contextual.md index 4580746d1..0053f9a50 100644 --- a/docs/api-reference/general/types/contextual.md +++ b/docs/api-reference/general/types/contextual.md @@ -1,5 +1,5 @@ From ce18e50477a6f27344ebc0084895585e28607a48 Mon Sep 17 00:00:00 2001 From: Elttob Date: Mon, 29 Jan 2024 12:09:55 +0000 Subject: [PATCH 191/287] Value member API ref --- docs/api-reference/state/members/value.md | 51 +++++++++++++++++++++++ 1 file changed, 51 insertions(+) create mode 100644 docs/api-reference/state/members/value.md diff --git a/docs/api-reference/state/members/value.md b/docs/api-reference/state/members/value.md new file mode 100644 index 000000000..988073f10 --- /dev/null +++ b/docs/api-reference/state/members/value.md @@ -0,0 +1,51 @@ + + +

+ :octicons-workflow-24: + Value + + -> Value<T> + +

+ +```Lua +function Fusion.Value( + initialValue: T +) -> Value +``` + +Constructs and returns a new [value state object](../../types/value). + +----- + +## Parameters + +

+ initialValue + + : T + +

+ +The initial value that will be stored until the next value is `:set()`. + +----- + +

+ Returns + + -> Value<T> + +

+ +A freshly constructed value state object. + +----- + +## Learn More + +- [Values tutorial](../../../../tutorials/fundamentals/values) \ No newline at end of file From 283d023e6a51c0c4916079710aa2e20fc6c2cdce Mon Sep 17 00:00:00 2001 From: Elttob Date: Mon, 29 Jan 2024 17:44:59 +0000 Subject: [PATCH 192/287] Value member API ref has scopes --- docs/api-reference/state/members/value.md | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/docs/api-reference/state/members/value.md b/docs/api-reference/state/members/value.md index 988073f10..086111521 100644 --- a/docs/api-reference/state/members/value.md +++ b/docs/api-reference/state/members/value.md @@ -14,6 +14,7 @@ ```Lua function Fusion.Value( + scope: Scope, initialValue: T ) -> Value ``` @@ -24,6 +25,16 @@ Constructs and returns a new [value state object](../../types/value). ## Parameters +

+ scope + + : Scope<S> + +

+ +The [scope](../../../memory/types/scope) which should be used to store +destruction tasks for this object. +

initialValue From ed690c6f5716cb9420fa80b2575033b3750b16fc Mon Sep 17 00:00:00 2001 From: Elttob Date: Mon, 29 Jan 2024 17:52:46 +0000 Subject: [PATCH 193/287] Computed member API ref --- docs/api-reference/state/members/computed.md | 67 ++++++++++++++++++++ 1 file changed, 67 insertions(+) create mode 100644 docs/api-reference/state/members/computed.md diff --git a/docs/api-reference/state/members/computed.md b/docs/api-reference/state/members/computed.md new file mode 100644 index 000000000..889af3788 --- /dev/null +++ b/docs/api-reference/state/members/computed.md @@ -0,0 +1,67 @@ + + +

+ :octicons-workflow-24: + Computed + + -> Computed<T> + +

+ +```Lua +function Fusion.Computed( + scope: Scope, + processor: (Use, Scope) -> T +) -> Computed +``` + +Constructs and returns a new [computed state object](../../types/computed). + +----- + +## Parameters + +

+ scope + + : Scope<S> + +

+ +The [scope](../../../memory/types/scope) which should be used to store +destruction tasks for this object. + +

+ processor + + : (Use, + Scope<S>) -> T + +

+ +Computes the value that will be used by the computed. The processor is given a +[use function](../../../memory/types/use) for including other objects in the +computation, and a [scope](../../../memory/types/scope) for queueing destruction +tasks to run on re-computation. The given scope has the same methods as the +scope used to create the computed. + +----- + +

+ Returns + + -> Computed<T> + +

+ +A freshly constructed computed state object. + +----- + +## Learn More + +- [Computeds tutorial](../../../../tutorials/fundamentals/computeds) \ No newline at end of file From 12b2b9edfe56989af9bc824152f33730f59ad5e1 Mon Sep 17 00:00:00 2001 From: Elttob Date: Mon, 29 Jan 2024 17:55:24 +0000 Subject: [PATCH 194/287] Add notes on scoped syntax --- docs/api-reference/state/members/computed.md | 6 ++++++ docs/api-reference/state/members/value.md | 6 ++++++ 2 files changed, 12 insertions(+) diff --git a/docs/api-reference/state/members/computed.md b/docs/api-reference/state/members/computed.md index 889af3788..07e385150 100644 --- a/docs/api-reference/state/members/computed.md +++ b/docs/api-reference/state/members/computed.md @@ -21,6 +21,12 @@ function Fusion.Computed( Constructs and returns a new [computed state object](../../types/computed). +!!! success "Use scoped() method syntax" + This function is intended to be accessed as a method on a scope: + ```Lua + local computed = scope:Computed(processor) + ``` + ----- ## Parameters diff --git a/docs/api-reference/state/members/value.md b/docs/api-reference/state/members/value.md index 086111521..208f829cd 100644 --- a/docs/api-reference/state/members/value.md +++ b/docs/api-reference/state/members/value.md @@ -21,6 +21,12 @@ function Fusion.Value( Constructs and returns a new [value state object](../../types/value). +!!! success "Use scoped() method syntax" + This function is intended to be accessed as a method on a scope: + ```Lua + local computed = scope:Computed(processor) + ``` + ----- ## Parameters From ae429c430dffc59aa9750a2ed163a0edba396891 Mon Sep 17 00:00:00 2001 From: Elttob Date: Mon, 29 Jan 2024 17:56:39 +0000 Subject: [PATCH 195/287] Add contextual constructor type --- src/Types.lua | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/Types.lua b/src/Types.lua index c4d57a8ae..52de8429f 100644 --- a/src/Types.lua +++ b/src/Types.lua @@ -229,8 +229,11 @@ export type ScopedConstructor = (() -> Scope<{}>) & ((A & {}, B & {}, C & {}, D & {}, E & {}, F & {}, G & {}, H & {}, I & {}, J & {}, K & {}) -> Scope) & ((A & {}, B & {}, C & {}, D & {}, E & {}, F & {}, G & {}, H & {}, I & {}, J & {}, K & {}, L & {}) -> Scope) +export type ContextualConstructor = (defaultValue: T) -> Contextual + export type Fusion = { version: Version, + Contextual: ContextualConstructor, doCleanup: (...unknown) -> (), scoped: ScopedConstructor, From e9e4f2b502d49fea2a93e66fc0207b81af9e25b1 Mon Sep 17 00:00:00 2001 From: Elttob Date: Mon, 29 Jan 2024 18:00:58 +0000 Subject: [PATCH 196/287] Share type strings in state object metatables --- src/Animation/Spring.lua | 4 ++-- src/Animation/Tween.lua | 4 ++-- src/State/Computed.lua | 4 ++-- src/State/For.lua | 4 ++-- src/State/Observer.lua | 5 +++-- src/State/Value.lua | 4 ++-- src/Utility/Contextual.lua | 2 +- 7 files changed, 14 insertions(+), 13 deletions(-) diff --git a/src/Animation/Spring.lua b/src/Animation/Spring.lua index 2c6676ed8..40de55bb2 100644 --- a/src/Animation/Spring.lua +++ b/src/Animation/Spring.lua @@ -20,6 +20,8 @@ local whichLivesLonger = require(Package.Memory.whichLivesLonger) local logWarn = require(Package.Logging.logWarn) local class = {} +class.type = "State" +class.kind = "Spring" local CLASS_METATABLE = {__index = class} @@ -213,8 +215,6 @@ local function Spring( end local self = setmetatable({ - type = "State", - kind = "Spring", scope = scope, dependencySet = dependencySet, dependentSet = {}, diff --git a/src/Animation/Tween.lua b/src/Animation/Tween.lua index 7bfd4e529..6e301f371 100644 --- a/src/Animation/Tween.lua +++ b/src/Animation/Tween.lua @@ -19,6 +19,8 @@ local whichLivesLonger = require(Package.Memory.whichLivesLonger) local logWarn = require(Package.Logging.logWarn) local class = {} +class.type = "State" +class.kind = "Tween" local CLASS_METATABLE = {__index = class} @@ -116,8 +118,6 @@ local function Tween( end local self = setmetatable({ - type = "State", - kind = "Tween", scope = scope, dependencySet = dependencySet, dependentSet = {}, diff --git a/src/State/Computed.lua b/src/State/Computed.lua index e3e616cd3..87950e683 100644 --- a/src/State/Computed.lua +++ b/src/State/Computed.lua @@ -24,6 +24,8 @@ local deriveScope = require(Package.Memory.deriveScope) local whichLivesLonger = require(Package.Memory.whichLivesLonger) local class = {} +class.type = "State" +class.kind = "Computed" local CLASS_METATABLE = {__index = class} @@ -140,8 +142,6 @@ local function Computed( logWarn("destructorRedundant", "Computed") end local self = setmetatable({ - type = "State", - kind = "Computed", scope = scope, dependencySet = {}, dependentSet = {}, diff --git a/src/State/For.lua b/src/State/For.lua index 7d315c5fb..4bafc01e6 100644 --- a/src/State/For.lua +++ b/src/State/For.lua @@ -21,6 +21,8 @@ local doCleanup = require(Package.Memory.doCleanup) local deriveScope = require(Package.Memory.deriveScope) local class = {} +class.type = "State" +class.kind = "For" local CLASS_METATABLE = { __index = class } @@ -224,8 +226,6 @@ local function For( ): Types.For local self = setmetatable({ - type = "State", - kind = "For", scope = scope, dependencySet = {}, dependentSet = {}, diff --git a/src/State/Observer.lua b/src/State/Observer.lua index 5e0a1c5d6..f63faf133 100644 --- a/src/State/Observer.lua +++ b/src/State/Observer.lua @@ -15,6 +15,9 @@ local logWarn = require(Package.Logging.logWarn) local logError = require(Package.Logging.logError) local class = {} +class.type = "State" +class.kind = "Observer" + local CLASS_METATABLE = {__index = class} --[[ @@ -79,8 +82,6 @@ local function Observer( end local self = setmetatable({ - type = "State", - kind = "Observer", scope = scope, dependencySet = {[watchedState] = true}, dependentSet = {}, diff --git a/src/State/Value.lua b/src/State/Value.lua index bb9f95dd5..ac68d81bc 100644 --- a/src/State/Value.lua +++ b/src/State/Value.lua @@ -17,6 +17,8 @@ local updateAll = require(Package.State.updateAll) local isSimilar = require(Package.Utility.isSimilar) local class = {} +class.type = "State" +class.kind = "Value" local CLASS_METATABLE = {__index = class} @@ -68,8 +70,6 @@ local function Value( end local self = setmetatable({ - type = "State", - kind = "Value", scope = scope, dependentSet = {}, _value = initialValue diff --git a/src/Utility/Contextual.lua b/src/Utility/Contextual.lua index 9ba418813..18b9d9393 100644 --- a/src/Utility/Contextual.lua +++ b/src/Utility/Contextual.lua @@ -14,6 +14,7 @@ local logError = require(Package.Logging.logError) local parseError = require(Package.Logging.parseError) local class = {} +class.type = "Contextual" local CLASS_METATABLE = {__index = class} local WEAK_KEYS_METATABLE = {__mode = "k"} @@ -68,7 +69,6 @@ local function Contextual( defaultValue: T ): Types.Contextual local self = setmetatable({ - type = "Contextual", -- if we held strong references to threads here, then if a thread was -- killed before this contextual had a chance to finish executing its -- callback, it would be held strongly in this table forever From 83ba8dfc8905d32bb04c8527ea72f035d57e0873 Mon Sep 17 00:00:00 2001 From: Elttob Date: Sat, 3 Feb 2024 11:22:16 +0000 Subject: [PATCH 197/287] ForValues API reference --- docs/api-reference/state/members/forvalues.md | 89 +++++++++++++++++++ 1 file changed, 89 insertions(+) create mode 100644 docs/api-reference/state/members/forvalues.md diff --git a/docs/api-reference/state/members/forvalues.md b/docs/api-reference/state/members/forvalues.md new file mode 100644 index 000000000..746b405da --- /dev/null +++ b/docs/api-reference/state/members/forvalues.md @@ -0,0 +1,89 @@ + + +

+ :octicons-workflow-24: + ForValues + + -> For<K, VO> + +

+ +```Lua +function Fusion.ForValues( + scope: Scope, + inputTable: CanBeState<{[K]: VI}>, + processor: (Use, Scope, value: VI) -> VO +) -> For +``` + +Constructs and returns a new [For state object](../../types/for) which processes +values and preserves keys. + +!!! success "Use scoped() method syntax" + This function is intended to be accessed as a method on a scope: + ```Lua + local forObj = scope:ForValues(inputTable, processor) + ``` + +----- + +## Parameters + +

+ scope + + : Scope<S> + +

+ +The [scope](../../../memory/types/scope) which should be used to store +destruction tasks for this object. + +

+ inputTable + + : CanBeState<{[K]: VI}> + +

+ +The table which will provide the input keys and input values for this object. + +If it is a state object, this object will respond to changes in that state. + +

+ processor + + : (Use, + Scope<S>, + value: VI) -> VO + +

+ +Accepts a `VI` value from the input table, and returns the `VO` value that +should appear in the output table. + +The processor is given a [use function](../../../memory/types/use) for including +other objects in the computation, and a [scope](../../../memory/types/scope) for +queueing destruction tasks to run on re-computation. The given scope has the +same methods as the scope used to create the whole object. + +----- + +

+ Returns + + -> For<K, VO> + +

+ +A freshly constructed For state object. + +----- + +## Learn More + +- [ForValues tutorial](../../../../tutorials/tables/forvalues) \ No newline at end of file From ecfdd7865dad75c80c2d3f338b5e27817984bddd Mon Sep 17 00:00:00 2001 From: Elttob Date: Sat, 3 Feb 2024 11:40:13 +0000 Subject: [PATCH 198/287] Unify type links --- .../general/members/contextual.md | 12 +++++------ docs/api-reference/general/members/version.md | 6 +++--- .../memory/members/derivescope.md | 18 ++++++++--------- docs/api-reference/memory/members/scoped.md | 12 +++++------ .../memory/types/scopelifetime.md | 6 +++--- docs/api-reference/state/members/computed.md | 20 +++++++++---------- docs/api-reference/state/members/peek.md | 12 +++++------ docs/api-reference/state/members/value.md | 18 ++++++++--------- docs/api-reference/state/types/use.md | 6 +++--- 9 files changed, 55 insertions(+), 55 deletions(-) diff --git a/docs/api-reference/general/members/contextual.md b/docs/api-reference/general/members/contextual.md index 72e57e706..089f04819 100644 --- a/docs/api-reference/general/members/contextual.md +++ b/docs/api-reference/general/members/contextual.md @@ -7,9 +7,9 @@

:octicons-workflow-24: Contextual - - -> Contextual<T> - + + -> Contextual<T> +

```Lua @@ -38,9 +38,9 @@ by `Contextual:is():during()`.

Returns - - -> Contextual<T> - + + -> Contextual<T> +

A freshly constructed contextual. diff --git a/docs/api-reference/general/members/version.md b/docs/api-reference/general/members/version.md index 569d06230..0dd5f55b1 100644 --- a/docs/api-reference/general/members/version.md +++ b/docs/api-reference/general/members/version.md @@ -7,9 +7,9 @@

:octicons-workflow-24: version - - : Version - + + : Version +

```Lua diff --git a/docs/api-reference/memory/members/derivescope.md b/docs/api-reference/memory/members/derivescope.md index 299b49be9..12d471f07 100644 --- a/docs/api-reference/memory/members/derivescope.md +++ b/docs/api-reference/memory/members/derivescope.md @@ -7,9 +7,9 @@

:octicons-workflow-24: deriveScope - - -> Scope<T> - + + -> Scope<T> +

```Lua @@ -27,9 +27,9 @@ scope.

existing - - : Scope<T> - + + : Scope<T> +

An existing scope, whose methods should be re-used for the new scope. @@ -38,9 +38,9 @@ An existing scope, whose methods should be re-used for the new scope.

Returns - - -> Scope<T> - + + -> Scope<T> +

A freshly-made, blank scope with the same methods. diff --git a/docs/api-reference/memory/members/scoped.md b/docs/api-reference/memory/members/scoped.md index f881a4b20..294223ccd 100644 --- a/docs/api-reference/memory/members/scoped.md +++ b/docs/api-reference/memory/members/scoped.md @@ -7,9 +7,9 @@

:octicons-workflow-24: scoped - - -> Scope<T> - + + -> Scope<T> +

```Lua @@ -39,9 +39,9 @@ parameter.

Returns - - -> Scope<T> - + + -> Scope<T> +

A freshly created, blank scope. diff --git a/docs/api-reference/memory/types/scopelifetime.md b/docs/api-reference/memory/types/scopelifetime.md index 6ab9bb3c8..51a7fdf77 100644 --- a/docs/api-reference/memory/types/scopelifetime.md +++ b/docs/api-reference/memory/types/scopelifetime.md @@ -26,9 +26,9 @@ interface.

scope - - : Scope<unknown>? - + + : Scope<unknown>? +

The scope which this object was constructed within, or `nil` if the object has diff --git a/docs/api-reference/state/members/computed.md b/docs/api-reference/state/members/computed.md index 07e385150..5b4846dd3 100644 --- a/docs/api-reference/state/members/computed.md +++ b/docs/api-reference/state/members/computed.md @@ -7,9 +7,9 @@

:octicons-workflow-24: Computed - - -> Computed<T> - + + -> Computed<T> +

```Lua @@ -33,9 +33,9 @@ Constructs and returns a new [computed state object](../../types/computed).

scope - - : Scope<S> - + + : Scope<S> +

The [scope](../../../memory/types/scope) which should be used to store @@ -45,7 +45,7 @@ destruction tasks for this object. processor : (Use, - Scope<S>) -> T + Scope<S>) -> T

@@ -59,9 +59,9 @@ scope used to create the computed.

Returns - - -> Computed<T> - + + -> Computed<T> +

A freshly constructed computed state object. diff --git a/docs/api-reference/state/members/peek.md b/docs/api-reference/state/members/peek.md index ed17bc1b5..84a793632 100644 --- a/docs/api-reference/state/members/peek.md +++ b/docs/api-reference/state/members/peek.md @@ -7,9 +7,9 @@

:octicons-workflow-24: peek - - : Use - + + : Use +

```Lua @@ -44,9 +44,9 @@ the returned value.

target - - : CanBeState<T> - + + : CanBeState<T> +

The abstract representation of `T` to extract a value from. diff --git a/docs/api-reference/state/members/value.md b/docs/api-reference/state/members/value.md index 208f829cd..369b42cee 100644 --- a/docs/api-reference/state/members/value.md +++ b/docs/api-reference/state/members/value.md @@ -7,9 +7,9 @@

:octicons-workflow-24: Value - - -> Value<T> - + + -> Value<T> +

```Lua @@ -33,9 +33,9 @@ Constructs and returns a new [value state object](../../types/value).

scope - - : Scope<S> - + + : Scope<S> +

The [scope](../../../memory/types/scope) which should be used to store @@ -54,9 +54,9 @@ The initial value that will be stored until the next value is `:set()`.

Returns - - -> Value<T> - + + -> Value<T> +

A freshly constructed value state object. diff --git a/docs/api-reference/state/types/use.md b/docs/api-reference/state/types/use.md index c106eb853..13885608d 100644 --- a/docs/api-reference/state/types/use.md +++ b/docs/api-reference/state/types/use.md @@ -32,9 +32,9 @@ change.

target - - : CanBeState<T> - + + : CanBeState<T> +

The abstract representation of `T` to extract a value from. From 81f7a37ae21b76b6229387fe2a409f17940903db Mon Sep 17 00:00:00 2001 From: Elttob Date: Sat, 3 Feb 2024 12:00:25 +0000 Subject: [PATCH 199/287] Add parameter names to For constructors --- src/Types.lua | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Types.lua b/src/Types.lua index 52de8429f..e6f56d2d9 100644 --- a/src/Types.lua +++ b/src/Types.lua @@ -128,17 +128,17 @@ export type For = StateObject<{[KO]: VO}> & Dependent & { export type ForPairsConstructor = ( scope: Scope, inputTable: CanBeState<{[KI]: VI}>, - processor: (Use, Scope, KI, VI) -> (KO, VO) + processor: (Use, Scope, key: KI, value: VI) -> (KO, VO) ) -> For export type ForKeysConstructor = ( scope: Scope, inputTable: CanBeState<{[KI]: V}>, - processor: (Use, Scope, KI) -> KO + processor: (Use, Scope, key: KI) -> KO ) -> For export type ForValuesConstructor = ( scope: Scope, inputTable: CanBeState<{[K]: VI}>, - processor: (Use, Scope, VI) -> VO + processor: (Use, Scope, value: VI) -> VO ) -> For -- An object which can listen for updates on another state object. From 3dc25963e0b7a00d3728f742d7e2f21ec689976c Mon Sep 17 00:00:00 2001 From: Elttob Date: Sat, 3 Feb 2024 12:10:06 +0000 Subject: [PATCH 200/287] ForKeys API reference --- docs/api-reference/state/members/forkeys.md | 89 +++++++++++++++++++++ 1 file changed, 89 insertions(+) create mode 100644 docs/api-reference/state/members/forkeys.md diff --git a/docs/api-reference/state/members/forkeys.md b/docs/api-reference/state/members/forkeys.md new file mode 100644 index 000000000..f67ff5231 --- /dev/null +++ b/docs/api-reference/state/members/forkeys.md @@ -0,0 +1,89 @@ + + +

+ :octicons-workflow-24: + ForKeys + + -> For<KO, V> + +

+ +```Lua +function Fusion.ForKeys( + scope: Scope, + inputTable: CanBeState<{[KI]: V}>, + processor: (Use, Scope, key: KI) -> KO +) -> For +``` + +Constructs and returns a new [For state object](../../types/for) which processes +keys and preserves values. + +!!! success "Use scoped() method syntax" + This function is intended to be accessed as a method on a scope: + ```Lua + local forObj = scope:ForKeys(inputTable, processor) + ``` + +----- + +## Parameters + +

+ scope + + : Scope<S> + +

+ +The [scope](../../../memory/types/scope) which should be used to store +destruction tasks for this object. + +

+ inputTable + + : CanBeState<{[KI]: V}> + +

+ +The table which will provide the input keys and input values for this object. + +If it is a state object, this object will respond to changes in that state. + +

+ processor + + : (Use, + Scope<S>, + key: KI) -> KO + +

+ +Accepts a `KI` key from the input table, and returns the `KO` key that should +appear in the output table. + +The processor is given a [use function](../../../memory/types/use) for including +other objects in the computation, and a [scope](../../../memory/types/scope) for +queueing destruction tasks to run on re-computation. The given scope has the +same methods as the scope used to create the whole object. + +----- + +

+ Returns + + -> For<KO, V> + +

+ +A freshly constructed For state object. + +----- + +## Learn More + +- [ForKeys tutorial](../../../../tutorials/tables/forkeys) \ No newline at end of file From 3a0e4c121a5890b0dd4b7ea3e23f6c1fce75117d Mon Sep 17 00:00:00 2001 From: Elttob Date: Sat, 3 Feb 2024 12:13:14 +0000 Subject: [PATCH 201/287] ForPairs API ref --- docs/api-reference/state/members/forpairs.md | 89 ++++++++++++++++++++ 1 file changed, 89 insertions(+) create mode 100644 docs/api-reference/state/members/forpairs.md diff --git a/docs/api-reference/state/members/forpairs.md b/docs/api-reference/state/members/forpairs.md new file mode 100644 index 000000000..4ea1f7034 --- /dev/null +++ b/docs/api-reference/state/members/forpairs.md @@ -0,0 +1,89 @@ + + +

+ :octicons-workflow-24: + ForPairs + + -> For<KO, VO> + +

+ +```Lua +function Fusion.ForPairs( + scope: Scope, + inputTable: CanBeState<{[KI]: VI}>, + processor: (Use, Scope, key: KI, value: VI) -> (KO, VO) +) -> For +``` + +Constructs and returns a new [For state object](../../types/for) which processes +keys and values in pairs. + +!!! success "Use scoped() method syntax" + This function is intended to be accessed as a method on a scope: + ```Lua + local forObj = scope:ForPairs(inputTable, processor) + ``` + +----- + +## Parameters + +

+ scope + + : Scope<S> + +

+ +The [scope](../../../memory/types/scope) which should be used to store +destruction tasks for this object. + +

+ inputTable + + : CanBeState<{[KI]: VI}> + +

+ +The table which will provide the input keys and input values for this object. + +If it is a state object, this object will respond to changes in that state. + +

+ processor + + : (Use, + Scope<S>, + key: KI, value: VI) -> (KO, VO) + +

+ +Accepts a `KI` key and `VI` value pair from the input table, and returns the +`KO` key and `VO` value pair that should appear in the output table. + +The processor is given a [use function](../../../memory/types/use) for including +other objects in the computation, and a [scope](../../../memory/types/scope) for +queueing destruction tasks to run on re-computation. The given scope has the +same methods as the scope used to create the whole object. + +----- + +

+ Returns + + -> For<KO, VO> + +

+ +A freshly constructed For state object. + +----- + +## Learn More + +- [ForPairs tutorial](../../../../tutorials/tables/forpairs) \ No newline at end of file From df5710531ced5da3683b745364f7603cb7a34104 Mon Sep 17 00:00:00 2001 From: Elttob Date: Sat, 3 Feb 2024 12:28:05 +0000 Subject: [PATCH 202/287] Observer API ref --- docs/api-reference/state/members/observer.md | 69 ++++++++++++++++++++ 1 file changed, 69 insertions(+) create mode 100644 docs/api-reference/state/members/observer.md diff --git a/docs/api-reference/state/members/observer.md b/docs/api-reference/state/members/observer.md new file mode 100644 index 000000000..fed2d1e36 --- /dev/null +++ b/docs/api-reference/state/members/observer.md @@ -0,0 +1,69 @@ + + +

+ :octicons-workflow-24: + Observer + + -> Observer + +

+ +```Lua +function Fusion.Observer( + scope: Scope, + watchedState: StateObject +) -> Observer +``` + +Constructs and returns a new [observer](../../types/observer). + +!!! success "Use scoped() method syntax" + This function is intended to be accessed as a method on a scope: + ```Lua + local observer = scope:Observer(watchedState) + ``` + +----- + +## Parameters + +

+ scope + + : Scope<unknown> + +

+ +The [scope](../../../memory/types/scope) which should be used to store +destruction tasks for this object. + +

+ watchedState + + : StateObject<unknown> + +

+ +The state object which this +object should respond to. + +----- + +

+ Returns + + -> Observer + +

+ +A freshly constructed observer. + +----- + +## Learn More + +- [Observers tutorial](../../../../tutorials/fundamentals/observers) \ No newline at end of file From bd8e3c0904f8ff0e7278f467180a7799f821145e Mon Sep 17 00:00:00 2001 From: Elttob Date: Sat, 3 Feb 2024 12:32:27 +0000 Subject: [PATCH 203/287] Clean up old constructor API refs --- docs/api-reference/state/forkeys.md | 92 ------------------------- docs/api-reference/state/forpairs.md | 97 --------------------------- docs/api-reference/state/forvalues.md | 92 ------------------------- 3 files changed, 281 deletions(-) delete mode 100644 docs/api-reference/state/forkeys.md delete mode 100644 docs/api-reference/state/forpairs.md delete mode 100644 docs/api-reference/state/forvalues.md diff --git a/docs/api-reference/state/forkeys.md b/docs/api-reference/state/forkeys.md deleted file mode 100644 index 1a2c3bd68..000000000 --- a/docs/api-reference/state/forkeys.md +++ /dev/null @@ -1,92 +0,0 @@ - - -

- :octicons-package-24: - ForKeys - - state object - -

- -Processes a table from another state object by transforming its keys only. - -```Lua -( - input: CanBeState<{[KI]: V}>, - keyProcessor: (Use, KI) -> (KO, M), - keyDestructor: ((KO, M) -> ())? -) -> ForKeys -``` - ------ - -## Parameters - -- `input: CanBeState<{[KI]: V}>` - the table to be processed, either as a state -object or a constant value -- `keyProcessor: (Use, KI) -> (KO, M)` - transforms input keys into new -keys, optionally providing metadata for the destructor alone -- `keyDestructor: ((KO, M) -> ())?` - disposes of values generated by -`keyProcessor` when they are no longer in use - ------ - -## Example Usage - -```Lua -local data = Value({ - one = 1, - two = 2, - three = 3, - four = 4 -}) - -local transformed = ForKeys(data, function(use, key) - local newKey = string.upper(key) - return newKey -end) - -print(peek(transformed)) --> {ONE = 1, TWO = 2 ... } -``` - ------ - -## Dependency Management - -By default, ForKeys runs the processor function once per key in the input, then -caches the result indefinitely. To specify the calculation should re-run for the -key when a state object changes value, the objects can be passed to the -[use callback](./use.md) passed to the processor function for that key. The use -callback will unwrap the value as normal, but any state objects will become -dependencies of that key. - ------ - -## Destructors - -The `keyDestructor` callback, if provided, is called when this object swaps out -an old key for a newly-generated one. It is called with the old key as the first -parameter, and - if provided - an extra value returned from `keyProcessor` as a -customisable second parameter. - -Destructors are required when working with data types that require destruction, -such as instances. Otherwise, they are optional, so not all calculations have to -specify destruction behaviour. - -Fusion guarantees that values passed to destructors by default will never be -used again by the library, so it is safe to finalise them. This does not apply -to the customisable second parameter, which the user is responsible for handling -properly. - ------ - -## Optimisations - -ForKeys does not allow access to the values of the input table. This guarantees -that all generated keys are completely independent of any values. This means -keys only need to be calculated when they're added to the input table - all -other changes are simply forwarded to the output table. Since keys are also -unique, all calculations are unique, so caching and reuse are not required. \ No newline at end of file diff --git a/docs/api-reference/state/forpairs.md b/docs/api-reference/state/forpairs.md deleted file mode 100644 index a617b4fb1..000000000 --- a/docs/api-reference/state/forpairs.md +++ /dev/null @@ -1,97 +0,0 @@ - - -

- :octicons-package-24: - ForPairs - - state object - -

- -Processes a table from another state object by transforming its keys and values. - -```Lua -( - input: CanBeState<{[KI]: VI}>, - pairProcessor: (Use, KI, VI) -> (KO, VO, M), - pairDestructor: ((KO, VO, M) -> ())? -) -> ForPairs -``` - ------ - -## Parameters - -- `input: CanBeState<{[KI]: VI}>` - the table to be processed, either as a state -object or a constant value -- `pairProcessor: (Use, KI, VI) -> (KO, VO, M)` - transforms input key-value -pairs into new key-value pairs, optionally providing metadata for the destructor -alone -- `pairDestructor: ((KO, VO, M) -> ())?` - disposes of values generated by -`pairProcessor` when they are no longer in use - ------ - -## Example Usage - -```Lua -local data = Value({ - one = 1, - two = 2, - three = 3, - four = 4 -}) - -local transformed = ForPairs(data, function(use, key, value) - local newKey = value - local newValue = string.upper(key) - return newKey, newValue -end) - -print(peek(transformed)) --> {[1] = "ONE", [2] = "TWO" ... } -``` - ------ - -## Dependency Management - -By default, ForPairs runs the processor function once per key/value pair in the -input, then caches the result indefinitely. To specify the calculation should -re-run for the key/value pair when a state object changes value, the objects can -be passed to the [use callback](./use.md) passed to the processor function for -that key/value pair. The use callback will unwrap the value as normal, but any -state objects will become dependencies of that key/value pair. - ------ - -## Destructors - -The `pairDestructor` callback, if provided, is called when this object swaps -out an old key-value pair for a newly-generated one. It is called with the old -pair as the first and second parameters, and - if provided - an extra value -returned from `pairProcessor` as a customisable third parameter. - -Destructors are required when working with data types that require destruction, -such as instances. Otherwise, they are optional, so not all calculations have to -specify destruction behaviour. - -Fusion guarantees that values passed to destructors by default will never be -used again by the library, so it is safe to finalise them. This does not apply -to the customisable third parameter, which the user is responsible for handling -properly. - ------ - -## Optimisations - -ForPairs is the least restrictive of the For objects, allowing full access to -the key-value pairs being processed. This means that very little optimisation is -applied - values are always locked to the specific key they were generated for, -and any change in the input's key or value will prompt a recalculation. - -For other optimisations, consider using [ForValues](../forvalues) or -[ForKeys](../forkeys), which impose stricter restrictions to allow for less -frequent updates and greater reuse. \ No newline at end of file diff --git a/docs/api-reference/state/forvalues.md b/docs/api-reference/state/forvalues.md deleted file mode 100644 index 732b99071..000000000 --- a/docs/api-reference/state/forvalues.md +++ /dev/null @@ -1,92 +0,0 @@ - - -

- :octicons-package-24: - ForValues - - state object - -

- -Processes a table from another state object by transforming its values only. - -```Lua -( - input: CanBeState<{[K]: VI}>, - valueProcessor: (Use, VI) -> (VO, M), - valueDestructor: ((VO, M) -> ())? -) -> ForValues -``` - ------ - -## Parameters - -- `input: CanBeState<{[K]: VI}>` - the table to be processed, either as a state -object or a constant value -- `valueProcessor: (Use, VI) -> (VO, M)` - transforms input values into new values, -optionally providing metadata for the destructor alone -- `valueDestructor: ((VO, M) -> ())?` - disposes of values generated by -`valueProcessor` when they are no longer in use - ------ - -## Example Usage - -```Lua -local data = Value({ - one = 1, - two = 2, - three = 3, - four = 4 -}) - -local transformed = ForValues(data, function(use, value) - local newValue = value * 2 - return newValue -end) - -print(peek(transformed)) --> {ONE = 2, TWO = 4 ... } -``` - ------ - -## Dependency Management - -By default, ForValues runs the processor function once per value in the input, -then caches the result indefinitely. To specify the calculation should re-run -for the value when a state object changes value, the objects can be passed to -the [use callback](./use.md) passed to the processor function for that value. -The use callback will unwrap the value as normal, but any state objects will -become dependencies of that value. - ------ - -## Destructors - -The `valueDestructor` callback, if provided, is called when this object swaps -out an old value for a newly-generated one. It is called with the old value as -the first parameter, and - if provided - an extra value returned from -`valueProcessor` as a customisable second parameter. - -Destructors are required when working with data types that require destruction, -such as instances. Otherwise, they are optional, so not all calculations have to -specify destruction behaviour. - -Fusion guarantees that values passed to destructors by default will never be -used again by the library, so it is safe to finalise them. This does not apply -to the customisable second parameter, which the user is responsible for handling -properly. - ------ - -## Optimisations - -ForValues does not allow access to the keys of the input table. This guarantees -that all generated values are completely independent of the key they were -generated for. This means that values may be moved between keys instead of being -destroyed when their original key changes value. Values are only reused once - -values aren't copied when there are multiple occurences of the same input. \ No newline at end of file From 47946ae33edfa3f9ad42a1bfaaad293be9e44f81 Mon Sep 17 00:00:00 2001 From: Elttob Date: Sat, 3 Feb 2024 13:07:31 +0000 Subject: [PATCH 204/287] Make Set explicit in typedefs --- src/InternalTypes.lua | 4 +--- src/Types.lua | 6 ++---- 2 files changed, 3 insertions(+), 7 deletions(-) diff --git a/src/InternalTypes.lua b/src/InternalTypes.lua index d01b4b144..e947c9dc0 100644 --- a/src/InternalTypes.lua +++ b/src/InternalTypes.lua @@ -12,8 +12,6 @@ local Package = script.Parent local Types = require(Package.Types) -type Set = {[T]: unknown} - --[[ General use types ]] @@ -52,7 +50,7 @@ export type Value = Types.Value & { -- A state object whose value is derived from other objects using a callback. export type Computed = Types.Computed & { scope: Types.Scope?, - _oldDependencySet: Set, + _oldDependencySet: {[Types.Dependency]: unknown}, _processor: (Types.Use, Types.Scope) -> T, _value: T, _innerScope: Types.Scope? diff --git a/src/Types.lua b/src/Types.lua index e6f56d2d9..6add4af96 100644 --- a/src/Types.lua +++ b/src/Types.lua @@ -5,8 +5,6 @@ Stores common public-facing type information for Fusion APIs. ]] -type Set = {[T]: unknown} - --[[ General use types ]] @@ -73,13 +71,13 @@ type ContextualIsMethods = { -- A graph object which can have dependents. export type Dependency = ScopeLifetime & { - dependentSet: Set + dependentSet: {[Dependent]: unknown} } -- A graph object which can have dependencies. export type Dependent = ScopeLifetime & { update: (Dependent) -> boolean, - dependencySet: Set + dependencySet: {[Dependency]: unknown} } -- An object which stores a piece of reactive state. From 9330b11aa098c17bddcf8390c36fdea6ecbbfa00 Mon Sep 17 00:00:00 2001 From: Elttob Date: Sat, 3 Feb 2024 13:32:37 +0000 Subject: [PATCH 205/287] Dependency API ref --- docs/api-reference/state/types/dependency.md | 36 ++++++++++++++++++++ 1 file changed, 36 insertions(+) create mode 100644 docs/api-reference/state/types/dependency.md diff --git a/docs/api-reference/state/types/dependency.md b/docs/api-reference/state/types/dependency.md new file mode 100644 index 000000000..2a5325a74 --- /dev/null +++ b/docs/api-reference/state/types/dependency.md @@ -0,0 +1,36 @@ + + +

+ :octicons-note-24: + Dependency +

+ +```Lua +export type Dependency = ScopeLifetime & { + dependentSet: Set +} +``` + +A reactive graph object which can broadcast updates to other reactive graph +objects. + +This type includes [`ScopeLifetime`](../../../memory/types/scopelifetime), which +allows the lifetime and destruction order of the reactive graph to be analysed. + +----- + +## Members + +

+ dependentSet + + : Scope<unknown>? + +

+ +The reactive graph objects which declare themselves as dependent upon this +object. \ No newline at end of file From dcea5458a361921090608f46b4e2f6159eef93d0 Mon Sep 17 00:00:00 2001 From: Elttob Date: Sat, 3 Feb 2024 13:33:00 +0000 Subject: [PATCH 206/287] Fix Scope link on Dependency API ref --- docs/api-reference/state/types/dependency.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/api-reference/state/types/dependency.md b/docs/api-reference/state/types/dependency.md index 2a5325a74..56de47ab6 100644 --- a/docs/api-reference/state/types/dependency.md +++ b/docs/api-reference/state/types/dependency.md @@ -28,7 +28,7 @@ allows the lifetime and destruction order of the reactive graph to be analysed.

dependentSet - : Scope<unknown>? + : Scope<unknown>?

From 4a5dc14ec1cbd6ed9a3f37d3436c4a803d814162 Mon Sep 17 00:00:00 2001 From: Elttob Date: Sat, 3 Feb 2024 13:35:36 +0000 Subject: [PATCH 207/287] Reworded Dependency API ref --- docs/api-reference/state/types/dependency.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/docs/api-reference/state/types/dependency.md b/docs/api-reference/state/types/dependency.md index 56de47ab6..be9e97157 100644 --- a/docs/api-reference/state/types/dependency.md +++ b/docs/api-reference/state/types/dependency.md @@ -15,8 +15,9 @@ export type Dependency = ScopeLifetime & { } ``` -A reactive graph object which can broadcast updates to other reactive graph -objects. +A reactive graph object which can broadcast updates. Other graph objects can +declare themselves as [dependent](../dependent) upon this object to receive +updates. This type includes [`ScopeLifetime`](../../../memory/types/scopelifetime), which allows the lifetime and destruction order of the reactive graph to be analysed. From e909b38c0528e1308105e0e0ec5e6395535e04c9 Mon Sep 17 00:00:00 2001 From: Elttob Date: Sat, 3 Feb 2024 13:57:44 +0000 Subject: [PATCH 208/287] Fix typedef in Dependency API ref --- docs/api-reference/state/types/dependency.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/api-reference/state/types/dependency.md b/docs/api-reference/state/types/dependency.md index be9e97157..70343585a 100644 --- a/docs/api-reference/state/types/dependency.md +++ b/docs/api-reference/state/types/dependency.md @@ -11,7 +11,7 @@ ```Lua export type Dependency = ScopeLifetime & { - dependentSet: Set + dependentSet: {[Dependent]: unknown} } ``` @@ -29,7 +29,7 @@ allows the lifetime and destruction order of the reactive graph to be analysed.

dependentSet - : Scope<unknown>? + : {[Dependent]: unknown}

From 1c1b582c1275ac9a68178470c6703e2465e171bd Mon Sep 17 00:00:00 2001 From: Elttob Date: Sat, 3 Feb 2024 13:58:22 +0000 Subject: [PATCH 209/287] Fix links in Dependency page --- docs/api-reference/state/types/dependency.md | 2 +- docs/api-reference/state/types/dependent.md | 36 ++++++++++++++++++++ 2 files changed, 37 insertions(+), 1 deletion(-) create mode 100644 docs/api-reference/state/types/dependent.md diff --git a/docs/api-reference/state/types/dependency.md b/docs/api-reference/state/types/dependency.md index 70343585a..cceaac29a 100644 --- a/docs/api-reference/state/types/dependency.md +++ b/docs/api-reference/state/types/dependency.md @@ -29,7 +29,7 @@ allows the lifetime and destruction order of the reactive graph to be analysed.

dependentSet - : {[Dependent]: unknown} + : {[Dependent]: unknown}

diff --git a/docs/api-reference/state/types/dependent.md b/docs/api-reference/state/types/dependent.md new file mode 100644 index 000000000..654136091 --- /dev/null +++ b/docs/api-reference/state/types/dependent.md @@ -0,0 +1,36 @@ + + +

+ :octicons-note-24: + Dependent +

+ +```Lua +export type Dependent = ScopeLifetime & { + update: (Dependent) -> boolean, + dependencySet: {[Dependency]: unknown} +} +``` + +A reactive graph object which can receive updates by adding [dependencies](../dependency). + +This type includes [`ScopeLifetime`](../../../memory/types/scopelifetime), which +allows the lifetime and destruction order of the reactive graph to be analysed. + +----- + +## Members + +

+ dependencySet + + : Scope<unknown>? + +

+ +The reactive graph objects which declare themselves as dependent upon this +object. \ No newline at end of file From cb1c0da23251b3644a8836fcb2aa8652710a3c30 Mon Sep 17 00:00:00 2001 From: Elttob Date: Sat, 3 Feb 2024 14:05:52 +0000 Subject: [PATCH 210/287] Dependent API ref --- docs/api-reference/state/types/dependent.md | 39 ++++++++++++++++++--- 1 file changed, 34 insertions(+), 5 deletions(-) diff --git a/docs/api-reference/state/types/dependent.md b/docs/api-reference/state/types/dependent.md index 654136091..e6e4c6296 100644 --- a/docs/api-reference/state/types/dependent.md +++ b/docs/api-reference/state/types/dependent.md @@ -11,16 +11,21 @@ ```Lua export type Dependent = ScopeLifetime & { - update: (Dependent) -> boolean, + update: (self) -> boolean, dependencySet: {[Dependency]: unknown} } ``` -A reactive graph object which can receive updates by adding [dependencies](../dependency). +A reactive graph object which can add itself to [dependencies](../dependency) +and receive updates. This type includes [`ScopeLifetime`](../../../memory/types/scopelifetime), which allows the lifetime and destruction order of the reactive graph to be analysed. +!!! note "Non-standard type syntax" + The above type definition uses `self` to denote methods. At time of writing, + Luau does not interpret `self` specially. + ----- ## Members @@ -28,9 +33,33 @@ allows the lifetime and destruction order of the reactive graph to be analysed.

dependencySet - : Scope<unknown>? + : {[Dependency]: unknown} + +

+ +Everything this reactive graph object currently declares itself as dependent +upon. + +----- + +## Methods + +

+ update + + -> boolean

-The reactive graph objects which declare themselves as dependent upon this -object. \ No newline at end of file +```Lua +function Dependent:update(): boolean +``` + +Called from a dependency when a change occurs. Returns `true` if the update +should propagate transitively through this object, or `false` if the update +should not continue through this object specifically. + +!!! note "Return value ignored for non-dependencies" + If this `Dependent` is not also a `Dependency`, the return value does + nothing, as an object must be declarable as a dependency for other objects + to receive updates from it. From 1b226f153876481e64a4ffc6ff3f14fae84adecb Mon Sep 17 00:00:00 2001 From: Elttob Date: Sat, 3 Feb 2024 17:10:16 +0000 Subject: [PATCH 211/287] Add scope pooling --- src/InternalTypes.lua | 2 +- src/Memory/deriveScope.lua | 6 +++++- src/Memory/scopePool.lua | 35 +++++++++++++++++++++++++++++++++++ src/Memory/scoped.lua | 6 +++++- src/State/Computed.lua | 6 +++++- src/State/For.lua | 6 ++++-- 6 files changed, 55 insertions(+), 6 deletions(-) create mode 100644 src/Memory/scopePool.lua diff --git a/src/InternalTypes.lua b/src/InternalTypes.lua index e947c9dc0..b23b50d80 100644 --- a/src/InternalTypes.lua +++ b/src/InternalTypes.lua @@ -74,7 +74,7 @@ export type For = Types.For & { type ForProcessor = { inputPair: Types.Value<{key: unknown, value: unknown}>, outputPair: Types.StateObject<{key: unknown, value: unknown}>, - cleanupTask: unknown + scope: Types.Scope? } -- A state object which follows another state object using tweens. diff --git a/src/Memory/deriveScope.lua b/src/Memory/deriveScope.lua index dce483dff..97ca4b38f 100644 --- a/src/Memory/deriveScope.lua +++ b/src/Memory/deriveScope.lua @@ -7,13 +7,17 @@ ]] local Package = script.Parent.Parent local Types = require(Package.Types) +local scopePool = require(Package.Memory.scopePool) -- This return type is technically a lie, but it's required for useful type -- checking behaviour. local function deriveScope( existing: Types.Scope ): Types.Scope - return setmetatable({}, getmetatable(existing)) :: any + return setmetatable( + scopePool.reuseAny() or {}, + getmetatable(existing) + ) :: any end return deriveScope \ No newline at end of file diff --git a/src/Memory/scopePool.lua b/src/Memory/scopePool.lua new file mode 100644 index 000000000..64eb5a852 --- /dev/null +++ b/src/Memory/scopePool.lua @@ -0,0 +1,35 @@ +--!strict +--!nolint LocalShadow + +local Package = script.Parent.Parent +local Types = require(Package.Types) + +local MAX_POOL_SIZE = 16 -- TODO: need to test what an ideal number for this is + +local pool = {} +local poolSize = 0 + +return { + giveIfEmpty = function( + scope: Types.Scope + ): Types.Scope? + if next(scope) == nil then + if poolSize < MAX_POOL_SIZE then + poolSize += 1 + pool[poolSize] = scope + end + return nil + else + return scope + end + end, + reuseAny = function(): Types.Scope + if poolSize == 0 then + return nil + else + local scope = pool[poolSize] + poolSize -= 1 + return scope + end + end +} \ No newline at end of file diff --git a/src/Memory/scoped.lua b/src/Memory/scoped.lua index f661f376c..8270f832b 100644 --- a/src/Memory/scoped.lua +++ b/src/Memory/scoped.lua @@ -8,6 +8,7 @@ local Package = script.Parent.Parent local Types = require(Package.Types) local logError = require(Package.Logging.logError) +local scopePool = require(Package.Memory.scopePool) local function merge( into: {[unknown]: unknown}, @@ -31,7 +32,10 @@ end local function scoped( ...: {[unknown]: unknown} ): {[unknown]: unknown} - return setmetatable({}, {__index = merge({}, ...)}) :: any + return setmetatable( + scopePool.reuseAny() or {}, + {__index = merge({}, ...)} + ) :: any end return (scoped :: any) :: Types.ScopedConstructor \ No newline at end of file diff --git a/src/State/Computed.lua b/src/State/Computed.lua index 87950e683..c0cc996c6 100644 --- a/src/State/Computed.lua +++ b/src/State/Computed.lua @@ -22,6 +22,7 @@ local isState = require(Package.State.isState) local doCleanup = require(Package.Memory.doCleanup) local deriveScope = require(Package.Memory.deriveScope) local whichLivesLonger = require(Package.Memory.whichLivesLonger) +local scopePool = require(Package.Memory.scopePool) local class = {} class.type = "State" @@ -69,6 +70,7 @@ function class:update(): boolean end end local ok, newValue = xpcall(self._processor, parseError, use, innerScope) + local innerScope = scopePool.giveIfEmpty(innerScope) if ok then local oldValue = self._value @@ -91,7 +93,9 @@ function class:update(): boolean -- update process logErrorNonFatal("callbackError", errorObj) - doCleanup(innerScope) + if innerScope ~= nil then + doCleanup(innerScope) + end -- restore old dependencies, because the new dependencies may be corrupt self._oldDependencySet, self.dependencySet = self.dependencySet, self._oldDependencySet diff --git a/src/State/For.lua b/src/State/For.lua index 4bafc01e6..edc18a57c 100644 --- a/src/State/For.lua +++ b/src/State/For.lua @@ -19,6 +19,7 @@ local Value = require(Package.State.Value) -- Memory local doCleanup = require(Package.Memory.doCleanup) local deriveScope = require(Package.Memory.deriveScope) +local scopePool = require(Package.Memory.scopePool) local class = {} class.type = "State" @@ -141,7 +142,7 @@ function class:update(): boolean -- So, existing processors should be destroyed, and remaining pairs should -- be created. This accomodates for table growth and shrinking. for unusedProcessor in existingProcessors do - doCleanup(unusedProcessor.cleanupTask) + doCleanup(unusedProcessor.scope) end for key, remainingValues in remainingPairs do @@ -149,11 +150,12 @@ function class:update(): boolean local innerScope = deriveScope(outerScope) local inputPair = Value(innerScope, {key = key, value = value}) local processOK, outputPair = xpcall(self._processor, parseError, innerScope, inputPair) + local innerScope = scopePool.giveIfEmpty(innerScope) if processOK then local processor = { inputPair = inputPair, outputPair = outputPair, - cleanupTask = innerScope + scope = innerScope } newProcessors[processor] = true else From 7ec7fa30cc0e774158f7db0a65a50ded70efbe4b Mon Sep 17 00:00:00 2001 From: Elttob Date: Sat, 3 Feb 2024 17:27:40 +0000 Subject: [PATCH 212/287] More scope pooling --- src/Instances/Children.lua | 2 ++ src/Memory/scopePool.lua | 9 +++++++++ src/State/Computed.lua | 3 +++ src/State/For.lua | 2 ++ 4 files changed, 16 insertions(+) diff --git a/src/Instances/Children.lua b/src/Instances/Children.lua index c88c83c79..c1d05a1fa 100644 --- a/src/Instances/Children.lua +++ b/src/Instances/Children.lua @@ -14,6 +14,7 @@ local Observer = require(Package.State.Observer) local peek = require(Package.State.peek) local isState = require(Package.State.isState) local doCleanup = require(Package.Memory.doCleanup) +local scopePool = require(Package.Memory.scopePool) type Set = {[T]: unknown} @@ -141,6 +142,7 @@ return { -- disconnect observers which weren't reused for oldState, childScope in pairs(oldScopes) do doCleanup(childScope) + scopePool.clearAndGive(childScope) end end diff --git a/src/Memory/scopePool.lua b/src/Memory/scopePool.lua index 64eb5a852..817de559f 100644 --- a/src/Memory/scopePool.lua +++ b/src/Memory/scopePool.lua @@ -23,6 +23,15 @@ return { return scope end end, + clearAndGive = function( + scope: Types.Scope + ) + if poolSize < MAX_POOL_SIZE then + table.clear(scope) + poolSize += 1 + pool[poolSize] = scope + end + end, reuseAny = function(): Types.Scope if poolSize == 0 then return nil diff --git a/src/State/Computed.lua b/src/State/Computed.lua index c0cc996c6..e37331d6f 100644 --- a/src/State/Computed.lua +++ b/src/State/Computed.lua @@ -77,6 +77,7 @@ function class:update(): boolean local similar = isSimilar(oldValue, newValue) if self._innerScope ~= nil then doCleanup(self._innerScope) + scopePool.clearAndGive(self._innerScope) end self._value = newValue self._innerScope = innerScope @@ -95,6 +96,7 @@ function class:update(): boolean if innerScope ~= nil then doCleanup(innerScope) + scopePool.clearAndGive(self._innerScope) end -- restore old dependencies, because the new dependencies may be corrupt @@ -132,6 +134,7 @@ function class:destroy() end if self._innerScope ~= nil then doCleanup(self._innerScope) + scopePool.clearAndGive(self._innerScope) end end diff --git a/src/State/For.lua b/src/State/For.lua index edc18a57c..ad3bca59b 100644 --- a/src/State/For.lua +++ b/src/State/For.lua @@ -143,6 +143,7 @@ function class:update(): boolean -- be created. This accomodates for table growth and shrinking. for unusedProcessor in existingProcessors do doCleanup(unusedProcessor.scope) + scopePool.clearAndGive(unusedProcessor.scope) end for key, remainingValues in remainingPairs do @@ -215,6 +216,7 @@ function class:destroy() end for unusedProcessor in self._existingProcessors do doCleanup(unusedProcessor.cleanupTask) + scopePool.clearAndGive(unusedProcessor.cleanupTask) end end From aead5000327cf2b57a34b3b3b06a568d4d4e8786 Mon Sep 17 00:00:00 2001 From: Elttob Date: Sat, 3 Feb 2024 17:37:09 +0000 Subject: [PATCH 213/287] Update API ref to mention pooling --- docs/api-reference/memory/members/derivescope.md | 9 ++++++++- docs/api-reference/memory/members/docleanup.md | 5 +++++ docs/api-reference/memory/members/scoped.md | 11 +++++++++-- docs/api-reference/memory/types/scope.md | 9 ++++++++- 4 files changed, 30 insertions(+), 4 deletions(-) diff --git a/docs/api-reference/memory/members/derivescope.md b/docs/api-reference/memory/members/derivescope.md index 12d471f07..e3f7788a8 100644 --- a/docs/api-reference/memory/members/derivescope.md +++ b/docs/api-reference/memory/members/derivescope.md @@ -18,9 +18,16 @@ function Fusion.deriveScope( ): Scope ``` -Creates a new [scope](../../types/scope) with the same methods as an existing +Returns a blank [scope](../../types/scope) with the same methods as an existing scope. +!!! warning "Scopes are not unique" + Fusion can recycle old unused scopes and return them from this function. + This reduces wasted memory while your program is running. + + However, it means this function doesn't always return a completely new + scope, so you shouldn't save the scope anywhere or use it as an identifier. + ----- ## Parameters diff --git a/docs/api-reference/memory/members/docleanup.md b/docs/api-reference/memory/members/docleanup.md index 0e0d3f436..25eb41c40 100644 --- a/docs/api-reference/memory/members/docleanup.md +++ b/docs/api-reference/memory/members/docleanup.md @@ -20,6 +20,11 @@ function Fusion.doCleanup( Attempts to destroy all arguments based on their runtime type. +!!! warning "This is a black hole!" + Any values you pass into `doCleanup` should be treated as completely gone. + Make sure you remove all references to those values, and ensure your code + never uses them again. + ----- ## Parameters diff --git a/docs/api-reference/memory/members/scoped.md b/docs/api-reference/memory/members/scoped.md index 294223ccd..4551a0865 100644 --- a/docs/api-reference/memory/members/scoped.md +++ b/docs/api-reference/memory/members/scoped.md @@ -18,8 +18,15 @@ function Fusion.scoped( ): Scope ``` -Creates and returns a blank [scope](../../types/scope), with the `__index` -metatable pointing at the given list of constructors for syntax convenience. +Returns a blank [scope](../../types/scope), with the `__index` metatable +pointing at the given list of constructors for syntax convenience. + +!!! warning "Scopes are not unique" + Fusion can recycle old unused scopes and return them from this function. + This reduces wasted memory while your program is running. + + However, it means this function doesn't always return a completely new + scope, so you shouldn't save the scope anywhere or use it as an identifier. ----- diff --git a/docs/api-reference/memory/types/scope.md b/docs/api-reference/memory/types/scope.md index 80d7d9259..74e81f08e 100644 --- a/docs/api-reference/memory/types/scope.md +++ b/docs/api-reference/memory/types/scope.md @@ -18,4 +18,11 @@ with optional `Constructors` as methods which can be called. !!! note "Approximated type" Luau does not yet have syntax for annotating metatables, so scopes created - with constructor methods cannot be represented in text. \ No newline at end of file + with constructor methods cannot be represented in text. + +!!! warning "Scopes are not unique" + Fusion can recycle old unused scopes and return them from other functions. + This reduces wasted memory while your program is running. + + However, it means this function doesn't always return a completely new + scope, so you shouldn't save the scope anywhere or use it as an identifier. \ No newline at end of file From e331839c1c35b7dc3dc4cea934006e0e45185516 Mon Sep 17 00:00:00 2001 From: Elttob Date: Sat, 3 Feb 2024 18:25:23 +0000 Subject: [PATCH 214/287] Better scope pooling callout wording --- docs/api-reference/memory/members/derivescope.md | 11 ++++++----- docs/api-reference/memory/members/scoped.md | 13 +++++++------ docs/api-reference/memory/types/scope.md | 9 +++++---- 3 files changed, 18 insertions(+), 15 deletions(-) diff --git a/docs/api-reference/memory/members/derivescope.md b/docs/api-reference/memory/members/derivescope.md index e3f7788a8..3fd25b52b 100644 --- a/docs/api-reference/memory/members/derivescope.md +++ b/docs/api-reference/memory/members/derivescope.md @@ -22,11 +22,12 @@ Returns a blank [scope](../../types/scope) with the same methods as an existing scope. !!! warning "Scopes are not unique" - Fusion can recycle old unused scopes and return them from this function. - This reduces wasted memory while your program is running. + Fusion can recycle old unused scopes. This helps make scopes more + lightweight, but it also means they don't uniquely belong to any part of + your program. - However, it means this function doesn't always return a completely new - scope, so you shouldn't save the scope anywhere or use it as an identifier. + As a result, you shouldn't hold on to scopes after they've been cleaned up, + and you shouldn't use them as unique identifiers anywhere. ----- @@ -50,7 +51,7 @@ An existing scope, whose methods should be re-used for the new scope. -A freshly-made, blank scope with the same methods. +A blank scope with the same methods as the existing scope. ----- diff --git a/docs/api-reference/memory/members/scoped.md b/docs/api-reference/memory/members/scoped.md index 4551a0865..9d8aaf987 100644 --- a/docs/api-reference/memory/members/scoped.md +++ b/docs/api-reference/memory/members/scoped.md @@ -22,11 +22,12 @@ Returns a blank [scope](../../types/scope), with the `__index` metatable pointing at the given list of constructors for syntax convenience. !!! warning "Scopes are not unique" - Fusion can recycle old unused scopes and return them from this function. - This reduces wasted memory while your program is running. + Fusion can recycle old unused scopes. This helps make scopes more + lightweight, but it also means they don't uniquely belong to any part of + your program. - However, it means this function doesn't always return a completely new - scope, so you shouldn't save the scope anywhere or use it as an identifier. + As a result, you shouldn't hold on to scopes after they've been cleaned up, + and you shouldn't use them as unique identifiers anywhere. ----- @@ -40,7 +41,7 @@ pointing at the given list of constructors for syntax convenience. A table, ideally including functions which take a scope as their first -parameter. +parameter. Those functions will turn into methods. ----- @@ -51,7 +52,7 @@ parameter. -A freshly created, blank scope. +A blank scope with the specified methods. ----- diff --git a/docs/api-reference/memory/types/scope.md b/docs/api-reference/memory/types/scope.md index 74e81f08e..6de7c087d 100644 --- a/docs/api-reference/memory/types/scope.md +++ b/docs/api-reference/memory/types/scope.md @@ -21,8 +21,9 @@ with optional `Constructors` as methods which can be called. with constructor methods cannot be represented in text. !!! warning "Scopes are not unique" - Fusion can recycle old unused scopes and return them from other functions. - This reduces wasted memory while your program is running. + Fusion can recycle old unused scopes. This helps make scopes more + lightweight, but it also means they don't uniquely belong to any part of + your program. - However, it means this function doesn't always return a completely new - scope, so you shouldn't save the scope anywhere or use it as an identifier. \ No newline at end of file + As a result, you shouldn't hold on to scopes after they've been cleaned up, + and you shouldn't use them as unique identifiers anywhere. \ No newline at end of file From 03057d84c251772048eb1c49b2510bc8de39b137 Mon Sep 17 00:00:00 2001 From: Elttob Date: Sat, 3 Feb 2024 18:44:26 +0000 Subject: [PATCH 215/287] Update scopes tutorial to mention pooling --- docs/tutorials/fundamentals/scopes.md | 51 ++++++++++++--------------- 1 file changed, 22 insertions(+), 29 deletions(-) diff --git a/docs/tutorials/fundamentals/scopes.md b/docs/tutorials/fundamentals/scopes.md index 6886926d5..100f14f58 100644 --- a/docs/tutorials/fundamentals/scopes.md +++ b/docs/tutorials/fundamentals/scopes.md @@ -92,20 +92,15 @@ to a cleanup function. ----- -## Improved Syntax +## Improved Scopes !!! success "This syntax is recommended" - It is recommended to use this syntax. However, it is technically optional; - if it does not fit your technical requirements, the barebones syntax will - always be available. - From now on, you'll see this syntax used throughout the tutorials. -Fusion provides alternate syntax for scopes, which improves readability and -maintainability. +Fusion can help manage your scopes for you. This unlocks convenient syntax, and +allows Fusion to optimise your code. -To use this syntax, call `scoped()` to create your scopes. This creates a normal -empty array. +You can call `scoped()` to obtain a new scope. ```Lua linenums="2" hl_lines="2 4" local Fusion = require(ReplicatedStorage.Fusion) @@ -119,10 +114,13 @@ local thing3 = Fusion.Value(scope, "i am thing 3") doCleanup(scope) ``` -`scoped` can then add methods to that array for you. This is most useful for -constructor functions. +Unlike `{}` (which always creates a new array), `scoped` can re-use old arrays. +This helps keep your program running smoothly. + +Beyond making your code more efficient, you can also use `scoped` for convenient +syntax. -Name some constructors in a table, and pass it to `scoped()`. +You can pass a table of constructor functions into `scoped`: ```Lua linenums="2" hl_lines="4-6" local Fusion = require(ReplicatedStorage.Fusion) @@ -138,8 +136,7 @@ local thing3 = Fusion.Value(scope, "i am thing 3") doCleanup(scope) ``` -You can now rewrite those constructors as method calls. The `scope` argument is -inferred for you. +You can now use those constructors as methods on `scope`. ```Lua linenums="2" hl_lines="7-9" local Fusion = require(ReplicatedStorage.Fusion) @@ -155,13 +152,12 @@ local thing3 = scope:Value("i am thing 3") doCleanup(scope) ``` -This strongly ties your constructors to your scopes, which makes it much harder -to mess up or circumvent them. It also makes code read much more naturally. +This makes it harder to mess up writing scopes. Your code reads more naturally, +too. ### Adding Methods In Bulk -Try passing `Fusion` to `scoped()` rather than listing functions manually. -Because `Fusion` is a table containing functions, it works too. +Try passing `Fusion` to `scoped()` - it's a table with functions. ```Lua linenums="2" hl_lines="4" local Fusion = require(ReplicatedStorage.Fusion) @@ -178,20 +174,17 @@ doCleanup(scope) This gives you access to all of Fusion's constructors without having to import each one manually. -You can merge in as many extras as you'd like by adding them as arguments. - -```Lua hl_lines="4" -local LibraryA = { foo = ..., bar = ... } -local LibraryB = { frob = ..., garb = ... } - -local scope = scoped(Fusion, LibraryA, LibraryB) - -print(scope.Value == Fusion.Value) --> true -print(scope.foo == LibraryA.foo) --> true -print(scope.garb == LibraryB.garb) --> true +If you need to mix in other things, you can pass in another table. +```Lua +local scope = scoped(Fusion, { + Foo = ..., + Bar = ... +}) ``` +You can do this for as many tables as you need. + !!! fail "Conflicting names" If you pass in two tables that contain things with the same name, `scoped()` will error. From 9f8f91ce2b6e118c645a8b278849f7ab558d7323 Mon Sep 17 00:00:00 2001 From: Elttob Date: Sat, 3 Feb 2024 19:06:07 +0000 Subject: [PATCH 216/287] Remove approximated type notice --- docs/api-reference/memory/types/scope.md | 4 ---- 1 file changed, 4 deletions(-) diff --git a/docs/api-reference/memory/types/scope.md b/docs/api-reference/memory/types/scope.md index 6de7c087d..77c23aaf2 100644 --- a/docs/api-reference/memory/types/scope.md +++ b/docs/api-reference/memory/types/scope.md @@ -16,10 +16,6 @@ export type Scope = {unknown} & Constructors A table collecting all objects created as part of an independent unit of code, with optional `Constructors` as methods which can be called. -!!! note "Approximated type" - Luau does not yet have syntax for annotating metatables, so scopes created - with constructor methods cannot be represented in text. - !!! warning "Scopes are not unique" Fusion can recycle old unused scopes. This helps make scopes more lightweight, but it also means they don't uniquely belong to any part of From 361a09287f334725546ff6da6f6f66c9762ee9f7 Mon Sep 17 00:00:00 2001 From: Elttob Date: Sat, 3 Feb 2024 19:12:25 +0000 Subject: [PATCH 217/287] StateObject type API ref --- docs/api-reference/state/types/stateobject.md | 23 +++++++++++++++++++ 1 file changed, 23 insertions(+) create mode 100644 docs/api-reference/state/types/stateobject.md diff --git a/docs/api-reference/state/types/stateobject.md b/docs/api-reference/state/types/stateobject.md new file mode 100644 index 000000000..5326ad38b --- /dev/null +++ b/docs/api-reference/state/types/stateobject.md @@ -0,0 +1,23 @@ + + +

+ :octicons-note-24: + StateObject +

+ +```Lua +export type StateObject = Dependency & { + type: "State", + kind: string +} +``` + +Stores a value of `T` which can change over time. As a +[dependency](../dependency), it can broadcast updates when its value changes. + +This type isn't generally useful outside of Fusion itself; you should prefer to +work with [`CanBeState`](../canbestate) in your own code. \ No newline at end of file From 477ff7995afc6a0e5b0bf10be4d28aa912e676ca Mon Sep 17 00:00:00 2001 From: Elttob Date: Sat, 3 Feb 2024 19:20:46 +0000 Subject: [PATCH 218/287] Move destroy() to ScopeLifetime --- .../memory/types/scopelifetime.md | 33 ++++++++++++++++--- src/Types.lua | 21 +++++------- 2 files changed, 36 insertions(+), 18 deletions(-) diff --git a/docs/api-reference/memory/types/scopelifetime.md b/docs/api-reference/memory/types/scopelifetime.md index 51a7fdf77..9d8a31a8e 100644 --- a/docs/api-reference/memory/types/scopelifetime.md +++ b/docs/api-reference/memory/types/scopelifetime.md @@ -11,14 +11,18 @@ ```Lua export type ScopeLifetime = { - scope: Scope? + scope: Scope?, + destroy: () -> () } ``` -An object which uses a [scope](../../types/scope) to dictate how long it lives. +An object designed for use with [scopes](../../types/scope). + Objects satisfying this interface can be probed for information about their lifetime and how long they live relative to other objects satisfying this -interface. +interface. + +These objects are also recognised by [`doCleanup`](../../members/docleanup). ----- @@ -31,7 +35,7 @@ interface. -The scope which this object was constructed within, or `nil` if the object has +The scope which this object was constructed with, or `nil` if the object has been destroyed. !!! note "Unchanged until destruction" @@ -47,4 +51,23 @@ been destroyed. It's strongly recommended that you emulate this behaviour if you're implementing your own objects, as this protects against double-destruction - and exposes potential scoping issues further ahead of time. \ No newline at end of file + and exposes potential scoping issues further ahead of time. + +----- + +## Methods + +

+ destroy + + -> () + +

+ +```Lua +function ScopeLifetime:destroy(): () +``` + +Called from a dependency when a change occurs. Returns `true` if the update +should propagate transitively through this object, or `false` if the update +should not continue through this object specifically. \ No newline at end of file diff --git a/src/Types.lua b/src/Types.lua index 6add4af96..3794ee5ec 100644 --- a/src/Types.lua +++ b/src/Types.lua @@ -44,7 +44,8 @@ export type Scope = {unknown} & Constructors -- An object which uses a scope to dictate how long it lives. export type ScopeLifetime = { - scope: Scope? + scope: Scope?, + destroy: () -> () } -- Script-readable version information. @@ -100,8 +101,7 @@ export type Use = (target: CanBeState) -> T -- A state object whose value can be set at any time by the user. export type Value = StateObject & { kind: "State", - set: (Value, newValue: T, force: boolean?) -> (), - destroy: () -> () + set: (Value, newValue: T, force: boolean?) -> () } export type ValueConstructor = ( scope: Scope, @@ -110,8 +110,7 @@ export type ValueConstructor = ( -- A state object whose value is derived from other objects using a callback. export type Computed = StateObject & Dependent & { - kind: "Computed", - destroy: () -> () + kind: "Computed" } export type ComputedConstructor = ( scope: Scope, @@ -120,8 +119,7 @@ export type ComputedConstructor = ( -- A state object which maps over keys and/or values in another table. export type For = StateObject<{[KO]: VO}> & Dependent & { - kind: "For", - destroy: () -> () + kind: "For" } export type ForPairsConstructor = ( scope: Scope, @@ -143,8 +141,7 @@ export type ForValuesConstructor = ( export type Observer = Dependent & { kind: "Observer", onChange: (Observer, callback: () -> ()) -> (() -> ()), - onBind: (Observer, callback: () -> ()) -> (() -> ()), - destroy: () -> () + onBind: (Observer, callback: () -> ()) -> (() -> ()) } export type ObserverConstructor = ( scope: Scope, @@ -153,8 +150,7 @@ export type ObserverConstructor = ( -- A state object which follows another state object using tweens. export type Tween = StateObject & Dependent & { - kind: "Tween", - destroy: () -> () + kind: "Tween" } export type TweenConstructor = ( scope: Scope, @@ -167,8 +163,7 @@ export type Spring = StateObject & Dependent & { kind: "Spring", setPosition: (Spring, newPosition: Animatable) -> (), setVelocity: (Spring, newVelocity: Animatable) -> (), - addVelocity: (Spring, deltaVelocity: Animatable) -> (), - destroy: () -> () + addVelocity: (Spring, deltaVelocity: Animatable) -> () } export type SpringConstructor = ( scope: Scope, From b77f2a4d8034145d7b520dc6587013d12eaffdf2 Mon Sep 17 00:00:00 2001 From: Elttob Date: Sat, 3 Feb 2024 19:24:36 +0000 Subject: [PATCH 219/287] Rename ScopeLifetime to ScopedObject --- .../{scopelifetime.md => scopedobject.md} | 8 +++---- docs/api-reference/state/types/computed.md | 23 +++++++++++++++++++ docs/api-reference/state/types/dependency.md | 4 ++-- docs/api-reference/state/types/dependent.md | 4 ++-- mkdocs.yml | 2 +- src/Types.lua | 6 ++--- src/init.lua | 2 +- 7 files changed, 36 insertions(+), 13 deletions(-) rename docs/api-reference/memory/types/{scopelifetime.md => scopedobject.md} (92%) create mode 100644 docs/api-reference/state/types/computed.md diff --git a/docs/api-reference/memory/types/scopelifetime.md b/docs/api-reference/memory/types/scopedobject.md similarity index 92% rename from docs/api-reference/memory/types/scopelifetime.md rename to docs/api-reference/memory/types/scopedobject.md index 9d8a31a8e..6f4ee7543 100644 --- a/docs/api-reference/memory/types/scopelifetime.md +++ b/docs/api-reference/memory/types/scopedobject.md @@ -1,16 +1,16 @@

:octicons-note-24: - ScopeLifetime + ScopedObject

```Lua -export type ScopeLifetime = { +export type ScopedObject = { scope: Scope?, destroy: () -> () } @@ -65,7 +65,7 @@ been destroyed. ```Lua -function ScopeLifetime:destroy(): () +function ScopedObject:destroy(): () ``` Called from a dependency when a change occurs. Returns `true` if the update diff --git a/docs/api-reference/state/types/computed.md b/docs/api-reference/state/types/computed.md new file mode 100644 index 000000000..5326ad38b --- /dev/null +++ b/docs/api-reference/state/types/computed.md @@ -0,0 +1,23 @@ + + +

+ :octicons-note-24: + StateObject +

+ +```Lua +export type StateObject = Dependency & { + type: "State", + kind: string +} +``` + +Stores a value of `T` which can change over time. As a +[dependency](../dependency), it can broadcast updates when its value changes. + +This type isn't generally useful outside of Fusion itself; you should prefer to +work with [`CanBeState`](../canbestate) in your own code. \ No newline at end of file diff --git a/docs/api-reference/state/types/dependency.md b/docs/api-reference/state/types/dependency.md index cceaac29a..26d67f707 100644 --- a/docs/api-reference/state/types/dependency.md +++ b/docs/api-reference/state/types/dependency.md @@ -10,7 +10,7 @@ ```Lua -export type Dependency = ScopeLifetime & { +export type Dependency = ScopedObject & { dependentSet: {[Dependent]: unknown} } ``` @@ -19,7 +19,7 @@ A reactive graph object which can broadcast updates. Other graph objects can declare themselves as [dependent](../dependent) upon this object to receive updates. -This type includes [`ScopeLifetime`](../../../memory/types/scopelifetime), which +This type includes [`ScopedObject`](../../../memory/types/scopedobject), which allows the lifetime and destruction order of the reactive graph to be analysed. ----- diff --git a/docs/api-reference/state/types/dependent.md b/docs/api-reference/state/types/dependent.md index e6e4c6296..9bdbe0acf 100644 --- a/docs/api-reference/state/types/dependent.md +++ b/docs/api-reference/state/types/dependent.md @@ -10,7 +10,7 @@ ```Lua -export type Dependent = ScopeLifetime & { +export type Dependent = ScopedObject & { update: (self) -> boolean, dependencySet: {[Dependency]: unknown} } @@ -19,7 +19,7 @@ export type Dependent = ScopeLifetime & { A reactive graph object which can add itself to [dependencies](../dependency) and receive updates. -This type includes [`ScopeLifetime`](../../../memory/types/scopelifetime), which +This type includes [`ScopedObject`](../../../memory/types/scopedobject), which allows the lifetime and destruction order of the reactive graph to be analysed. !!! note "Non-standard type syntax" diff --git a/mkdocs.yml b/mkdocs.yml index d108bdfb7..449fededb 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -105,7 +105,7 @@ nav: - Memory: - Types: - Scope: api-reference/memory/types/scope.md - - ScopeLifetime: api-reference/memory/types/scopelifetime.md + - ScopedObject: api-reference/memory/types/scopedobject.md - Task: api-reference/memory/types/task.md - Members: - deriveScope: api-reference/memory/members/derivescope.md diff --git a/src/Types.lua b/src/Types.lua index 3794ee5ec..ffee47047 100644 --- a/src/Types.lua +++ b/src/Types.lua @@ -43,7 +43,7 @@ export type Task = export type Scope = {unknown} & Constructors -- An object which uses a scope to dictate how long it lives. -export type ScopeLifetime = { +export type ScopedObject = { scope: Scope?, destroy: () -> () } @@ -71,12 +71,12 @@ type ContextualIsMethods = { ]] -- A graph object which can have dependents. -export type Dependency = ScopeLifetime & { +export type Dependency = ScopedObject & { dependentSet: {[Dependent]: unknown} } -- A graph object which can have dependencies. -export type Dependent = ScopeLifetime & { +export type Dependent = ScopedObject & { update: (Dependent) -> boolean, dependencySet: {[Dependency]: unknown} } diff --git a/src/init.lua b/src/init.lua index 47aebddda..317afca71 100644 --- a/src/init.lua +++ b/src/init.lua @@ -19,7 +19,7 @@ export type For = Types.For export type Observer = Types.Observer export type PropertyTable = Types.PropertyTable export type Scope = Types.Scope -export type ScopeLifetime = Types.ScopeLifetime +export type ScopedObject = Types.ScopedObject export type SpecialKey = Types.SpecialKey export type Spring = Types.Spring export type StateObject = Types.StateObject From 89341fd4a16e810204642fd05a0d419d5dd12695 Mon Sep 17 00:00:00 2001 From: Elttob Date: Sat, 3 Feb 2024 19:26:29 +0000 Subject: [PATCH 220/287] Add description for destroy() --- docs/api-reference/memory/types/scopedobject.md | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/docs/api-reference/memory/types/scopedobject.md b/docs/api-reference/memory/types/scopedobject.md index 6f4ee7543..61fc6b251 100644 --- a/docs/api-reference/memory/types/scopedobject.md +++ b/docs/api-reference/memory/types/scopedobject.md @@ -68,6 +68,5 @@ been destroyed. function ScopedObject:destroy(): () ``` -Called from a dependency when a change occurs. Returns `true` if the update -should propagate transitively through this object, or `false` if the update -should not continue through this object specifically. \ No newline at end of file +Called by `doCleanup` to destroy this object. User code should generally not +call this; instead, destroy the scope as a whole. \ No newline at end of file From 9120ae0058da05f6a82d821952e37736d830e4f1 Mon Sep 17 00:00:00 2001 From: Elttob Date: Sat, 3 Feb 2024 19:36:38 +0000 Subject: [PATCH 221/287] Computed API ref --- docs/api-reference/state/types/computed.md | 40 +++++++++++++++++----- 1 file changed, 31 insertions(+), 9 deletions(-) diff --git a/docs/api-reference/state/types/computed.md b/docs/api-reference/state/types/computed.md index 5326ad38b..c6d81f297 100644 --- a/docs/api-reference/state/types/computed.md +++ b/docs/api-reference/state/types/computed.md @@ -1,23 +1,45 @@

:octicons-note-24: - StateObject + Computed

```Lua -export type StateObject = Dependency & { - type: "State", - kind: string +export type Computed = StateObject & Dependent & { + kind: "Computed" } ``` -Stores a value of `T` which can change over time. As a -[dependency](../dependency), it can broadcast updates when its value changes. +A specialised [state object](../stateobject) for tracking single values computed +from a user-defined computation. -This type isn't generally useful outside of Fusion itself; you should prefer to -work with [`CanBeState`](../canbestate) in your own code. \ No newline at end of file +In addition to the standard state object interfaces, this object is a +[dependent](../dependent) so it can receive updates from the objects used as +part of the computation. + +This type isn't generally useful outside of Fusion itself. + +----- + +## Members + +

+ kind + + : "Computed" + +

+ +A more specific type string which can be used for runtime type checking. This +can be used to tell types of state object apart. + +----- + +## Learn More + +- [Computeds tutorial](../../../../tutorials/fundamentals/computeds) \ No newline at end of file From 743a70819da569d67d93f168a6bf99a7fe2223e5 Mon Sep 17 00:00:00 2001 From: Elttob Date: Sat, 3 Feb 2024 19:36:53 +0000 Subject: [PATCH 222/287] Fix ScopedObject h3 formatting --- docs/api-reference/memory/types/scopedobject.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/api-reference/memory/types/scopedobject.md b/docs/api-reference/memory/types/scopedobject.md index 61fc6b251..1d5f34b25 100644 --- a/docs/api-reference/memory/types/scopedobject.md +++ b/docs/api-reference/memory/types/scopedobject.md @@ -57,12 +57,12 @@ been destroyed. ## Methods -

+

destroy -> () -

+ ```Lua function ScopedObject:destroy(): () From 223075388ccead1d3f1487945027ff5c76df9efb Mon Sep 17 00:00:00 2001 From: Elttob Date: Sat, 3 Feb 2024 19:37:01 +0000 Subject: [PATCH 223/287] Update StateObject with member API --- docs/api-reference/state/types/stateobject.md | 25 ++++++++++++++++++- 1 file changed, 24 insertions(+), 1 deletion(-) diff --git a/docs/api-reference/state/types/stateobject.md b/docs/api-reference/state/types/stateobject.md index 5326ad38b..d0dbd6cdb 100644 --- a/docs/api-reference/state/types/stateobject.md +++ b/docs/api-reference/state/types/stateobject.md @@ -20,4 +20,27 @@ Stores a value of `T` which can change over time. As a [dependency](../dependency), it can broadcast updates when its value changes. This type isn't generally useful outside of Fusion itself; you should prefer to -work with [`CanBeState`](../canbestate) in your own code. \ No newline at end of file +work with [`CanBeState`](../canbestate) in your own code. + +----- + +## Members + +

+ type + + : "State" + +

+ +A type string which can be used for runtime type checking. + +

+ kind + + : string + +

+ +A more specific type string which can be used for runtime type checking. This +can be used to tell types of state object apart. \ No newline at end of file From 3f15deeac2548c1b51411c15263c0aa80bc369f2 Mon Sep 17 00:00:00 2001 From: Elttob Date: Sat, 3 Feb 2024 19:43:25 +0000 Subject: [PATCH 224/287] For type API ref --- docs/api-reference/state/types/for.md | 47 +++++++++++++++++++++++++++ 1 file changed, 47 insertions(+) create mode 100644 docs/api-reference/state/types/for.md diff --git a/docs/api-reference/state/types/for.md b/docs/api-reference/state/types/for.md new file mode 100644 index 000000000..ea85c8b44 --- /dev/null +++ b/docs/api-reference/state/types/for.md @@ -0,0 +1,47 @@ + + +

+ :octicons-note-24: + For +

+ +```Lua +export type For = StateObject<{[KO]: VO}> & Dependent & { + kind: "For" +} +``` + +A specialised [state object](../stateobject) for tracking multiple values +computed from user-defined computations, which are merged into an output table. + +In addition to the standard state object interfaces, this object is a +[dependent](../dependent) so it can receive updates from objects used as +part of any of the computations. + +This type isn't generally useful outside of Fusion itself. + +----- + +## Members + +

+ kind + + : "For" + +

+ +A more specific type string which can be used for runtime type checking. This +can be used to tell types of state object apart. + +----- + +## Learn More + +- [ForValues tutorial](../../../../tutorials/tables/forvalues) +- [ForKeys tutorial](../../../../tutorials/tables/forkeys) +- [ForPairs tutorial](../../../../tutorials/tables/forpairs) \ No newline at end of file From 5bef97bfe499ac33afecb1da9c1143151e6d15d2 Mon Sep 17 00:00:00 2001 From: Elttob Date: Sat, 3 Feb 2024 19:56:46 +0000 Subject: [PATCH 225/287] Value API ref --- docs/api-reference/state/types/value.md | 65 +++++++++++++++++++++++++ 1 file changed, 65 insertions(+) create mode 100644 docs/api-reference/state/types/value.md diff --git a/docs/api-reference/state/types/value.md b/docs/api-reference/state/types/value.md new file mode 100644 index 000000000..65cfa665f --- /dev/null +++ b/docs/api-reference/state/types/value.md @@ -0,0 +1,65 @@ + + +

+ :octicons-note-24: + Value +

+ +```Lua +export type Value = StateObject & { + kind: "State", + set: (self, newValue: T) -> () +} +``` + +A specialised [state object](../stateobject) which allows regular Luau code to +control its value. + +!!! note "Non-standard type syntax" + The above type definition uses `self` to denote methods. At time of writing, + Luau does not interpret `self` specially. + +----- + +## Members + +

+ kind + + : "Value" + +

+ +A more specific type string which can be used for runtime type checking. This +can be used to tell types of state object apart. + +----- + +## Methods + +

+ set + + -> () + +

+ +```Lua +function Value:set( + newValue: T +): () +``` + +Updates the value of this state object. + +Other objects using the value are notified immediately of the change. + +----- + +## Learn More + +- [Values tutorial](../../../../tutorials/fundamentals/values) \ No newline at end of file From e751f5eecdcf134f3a9264b0a14e8619a56b1de8 Mon Sep 17 00:00:00 2001 From: Elttob Date: Sat, 3 Feb 2024 19:58:48 +0000 Subject: [PATCH 226/287] Move :destroy() admonition to the right place --- .../memory/types/scopedobject.md | 20 +++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/docs/api-reference/memory/types/scopedobject.md b/docs/api-reference/memory/types/scopedobject.md index 1d5f34b25..9a6ea5918 100644 --- a/docs/api-reference/memory/types/scopedobject.md +++ b/docs/api-reference/memory/types/scopedobject.md @@ -44,15 +44,6 @@ been destroyed. should be set to `nil` to indicate that it no longer exists inside of a scope. This is typically done inside the `:destroy()` method, if it exists. -!!! tip "Double-destruction prevention" - Fusion's objects throw - [`destroyedTwice`](../../../general/errors/#destroyedtwice) if they detect - a `nil` scope during`:destroy()`. - - It's strongly recommended that you emulate this behaviour if you're - implementing your own objects, as this protects against double-destruction - and exposes potential scoping issues further ahead of time. - ----- ## Methods @@ -69,4 +60,13 @@ function ScopedObject:destroy(): () ``` Called by `doCleanup` to destroy this object. User code should generally not -call this; instead, destroy the scope as a whole. \ No newline at end of file +call this; instead, destroy the scope as a whole. + +!!! tip "Double-destruction prevention" + Fusion's objects throw + [`destroyedTwice`](../../../general/errors/#destroyedtwice) if they detect + a `nil` scope during`:destroy()`. + + It's strongly recommended that you emulate this behaviour if you're + implementing your own objects, as this protects against double-destruction + and exposes potential scoping issues further ahead of time. \ No newline at end of file From dffdbf11eceb53ae319e11a46e9c427bec968d81 Mon Sep 17 00:00:00 2001 From: Elttob Date: Sat, 3 Feb 2024 20:10:36 +0000 Subject: [PATCH 227/287] Correct Observer runtime typestrings --- src/State/Observer.lua | 3 +-- src/Types.lua | 2 +- test/State/Observer.spec.lua | 3 +-- 3 files changed, 3 insertions(+), 5 deletions(-) diff --git a/src/State/Observer.lua b/src/State/Observer.lua index f63faf133..78ea747d4 100644 --- a/src/State/Observer.lua +++ b/src/State/Observer.lua @@ -15,8 +15,7 @@ local logWarn = require(Package.Logging.logWarn) local logError = require(Package.Logging.logError) local class = {} -class.type = "State" -class.kind = "Observer" +class.type = "Observer" local CLASS_METATABLE = {__index = class} diff --git a/src/Types.lua b/src/Types.lua index ffee47047..f45e99884 100644 --- a/src/Types.lua +++ b/src/Types.lua @@ -139,7 +139,7 @@ export type ForValuesConstructor = ( -- An object which can listen for updates on another state object. export type Observer = Dependent & { - kind: "Observer", + type: "Observer", onChange: (Observer, callback: () -> ()) -> (() -> ()), onBind: (Observer, callback: () -> ()) -> (() -> ()) } diff --git a/test/State/Observer.spec.lua b/test/State/Observer.spec.lua index 6e232308a..f1eaa498d 100644 --- a/test/State/Observer.spec.lua +++ b/test/State/Observer.spec.lua @@ -11,8 +11,7 @@ return function() local observer = Observer(scope, dependency) expect(observer).to.be.a("table") - expect(observer.type).to.equal("State") - expect(observer.kind).to.equal("Observer") + expect(observer.type).to.equal("Observer") expect(scope[2]).to.equal(observer) doCleanup(scope) From 23676be9bc2fd27b32960595ade444f18779e5b2 Mon Sep 17 00:00:00 2001 From: Elttob Date: Sat, 3 Feb 2024 20:13:09 +0000 Subject: [PATCH 228/287] Make Observer generic over all dependencies --- src/State/Observer.lua | 15 ++++++++++++--- src/Types.lua | 2 +- 2 files changed, 13 insertions(+), 4 deletions(-) diff --git a/src/State/Observer.lua b/src/State/Observer.lua index 78ea747d4..f624cb711 100644 --- a/src/State/Observer.lua +++ b/src/State/Observer.lua @@ -74,7 +74,7 @@ end local function Observer( scope: Types.Scope, - watchedState: Types.StateObject + watchedState: Types.Dependency ): Types.Observer if watchedState == nil then logError("scopeMissing", nil, "Observers", "myScope:Observer(watchedState)") @@ -91,9 +91,18 @@ local function Observer( table.insert(scope, self) if watchedState.scope == nil then - logError("useAfterDestroy", nil, `The {watchedState.kind} object`, `the Observer that is watching it`) + logError( + "useAfterDestroy", + nil, + `The {watchedState.kind or watchedState.type or "watched"} object`, + `the Observer that is watching it` + ) elseif whichLivesLonger(scope, self, watchedState.scope, watchedState) == "definitely-a" then - logWarn("possiblyOutlives", `The {watchedState.kind} object`, `the Observer that is watching it`) + logWarn( + "possiblyOutlives", + `The {watchedState.kind or watchedState.type or "watched"} object`, + `the Observer that is watching it` + ) end -- add this object to the watched state's dependent set diff --git a/src/Types.lua b/src/Types.lua index f45e99884..582cd806d 100644 --- a/src/Types.lua +++ b/src/Types.lua @@ -145,7 +145,7 @@ export type Observer = Dependent & { } export type ObserverConstructor = ( scope: Scope, - watchedState: StateObject + watchedState: Dependency ) -> Observer -- A state object which follows another state object using tweens. From 78e79f5ba17c977d73863fdfeb7643aa2026a517 Mon Sep 17 00:00:00 2001 From: Elttob Date: Sat, 3 Feb 2024 20:15:54 +0000 Subject: [PATCH 229/287] Update Observer API naming --- docs/api-reference/state/members/observer.md | 11 ++-- docs/api-reference/state/types/observer.md | 66 ++++++++++++++++++++ src/State/Observer.lua | 20 +++--- src/Types.lua | 2 +- 4 files changed, 82 insertions(+), 17 deletions(-) create mode 100644 docs/api-reference/state/types/observer.md diff --git a/docs/api-reference/state/members/observer.md b/docs/api-reference/state/members/observer.md index fed2d1e36..6323d1c71 100644 --- a/docs/api-reference/state/members/observer.md +++ b/docs/api-reference/state/members/observer.md @@ -15,7 +15,7 @@ ```Lua function Fusion.Observer( scope: Scope, - watchedState: StateObject + watching: Dependency ) -> Observer ``` @@ -24,7 +24,7 @@ Constructs and returns a new [observer](../../types/observer). !!! success "Use scoped() method syntax" This function is intended to be accessed as a method on a scope: ```Lua - local observer = scope:Observer(watchedState) + local observer = scope:Observer(watching) ``` ----- @@ -42,14 +42,13 @@ The [scope](../../../memory/types/scope) which should be used to store destruction tasks for this object.

- watchedState + watching - : StateObject<unknown> + : Dependency

-The state object which this -object should respond to. +The object which the observer should receive updates from. ----- diff --git a/docs/api-reference/state/types/observer.md b/docs/api-reference/state/types/observer.md new file mode 100644 index 000000000..b82bf5893 --- /dev/null +++ b/docs/api-reference/state/types/observer.md @@ -0,0 +1,66 @@ + + +

+ :octicons-note-24: + Observer +

+ +```Lua +export type Observer = Dependent & { + type: "Observer", + onChange: (self, callback: () -> ()) -> (() -> ()), + onBind: (self, callback: () -> ()) -> (() -> ()) +} +``` + +A user-constructed [dependent](../dependent) that runs user code when its +[dependency](../dependency) is updated. + +!!! note "Non-standard type syntax" + The above type definition uses `self` to denote methods. At time of writing, + Luau does not interpret `self` specially. + +----- + +## Members + +

+ kind + + : "Value" + +

+ +A more specific type string which can be used for runtime type checking. This +can be used to tell types of state object apart. + +----- + +## Methods + +

+ set + + -> () + +

+ +```Lua +function Value:set( + newValue: T +): () +``` + +Updates the value of this state object. + +Other objects using the value are notified immediately of the change. + +----- + +## Learn More + +- [Values tutorial](../../../../tutorials/fundamentals/values) \ No newline at end of file diff --git a/src/State/Observer.lua b/src/State/Observer.lua index f624cb711..18d562be0 100644 --- a/src/State/Observer.lua +++ b/src/State/Observer.lua @@ -74,15 +74,15 @@ end local function Observer( scope: Types.Scope, - watchedState: Types.Dependency + watching: Types.Dependency ): Types.Observer - if watchedState == nil then - logError("scopeMissing", nil, "Observers", "myScope:Observer(watchedState)") + if watching == nil then + logError("scopeMissing", nil, "Observers", "myScope:Observer(watching)") end local self = setmetatable({ scope = scope, - dependencySet = {[watchedState] = true}, + dependencySet = {[watching] = true}, dependentSet = {}, _changeListeners = {} }, CLASS_METATABLE) @@ -90,23 +90,23 @@ local function Observer( table.insert(scope, self) - if watchedState.scope == nil then + if watching.scope == nil then logError( "useAfterDestroy", nil, - `The {watchedState.kind or watchedState.type or "watched"} object`, + `The {watching.kind or watching.type or "watched"} object`, `the Observer that is watching it` ) - elseif whichLivesLonger(scope, self, watchedState.scope, watchedState) == "definitely-a" then + elseif whichLivesLonger(scope, self, watching.scope, watching) == "definitely-a" then logWarn( "possiblyOutlives", - `The {watchedState.kind or watchedState.type or "watched"} object`, + `The {watching.kind or watching.type or "watched"} object`, `the Observer that is watching it` ) end - -- add this object to the watched state's dependent set - watchedState.dependentSet[self] = true + -- add this object to the watched object's dependent set + watching.dependentSet[self] = true return self end diff --git a/src/Types.lua b/src/Types.lua index 582cd806d..bdfe3f17e 100644 --- a/src/Types.lua +++ b/src/Types.lua @@ -145,7 +145,7 @@ export type Observer = Dependent & { } export type ObserverConstructor = ( scope: Scope, - watchedState: Dependency + watching: Dependency ) -> Observer -- A state object which follows another state object using tweens. From 1ce4a6b853a7c33feda33d9517350b81da79c3ce Mon Sep 17 00:00:00 2001 From: Elttob Date: Sat, 3 Feb 2024 20:19:57 +0000 Subject: [PATCH 230/287] Observer API ref --- docs/api-reference/state/types/observer.md | 41 +++++++++++++++------- 1 file changed, 29 insertions(+), 12 deletions(-) diff --git a/docs/api-reference/state/types/observer.md b/docs/api-reference/state/types/observer.md index b82bf5893..e133ed33d 100644 --- a/docs/api-reference/state/types/observer.md +++ b/docs/api-reference/state/types/observer.md @@ -29,38 +29,55 @@ A user-constructed [dependent](../dependent) that runs user code when its ## Members

- kind + type - : "Value" + : "Observer"

-A more specific type string which can be used for runtime type checking. This -can be used to tell types of state object apart. +A type string which can be used for runtime type checking. ----- ## Methods

- set + onChange - -> () + -> (() -> ())

```Lua -function Value:set( - newValue: T -): () +function Observer:onChange( + callback: () -> () +): (() -> ()) ``` -Updates the value of this state object. +Registers the callback to run when an update is received. -Other objects using the value are notified immediately of the change. +The returned function will unregister the callback. + +

+ onBind + + -> (() -> ()) + +

+ +```Lua +function Observer:onBind( + callback: () -> () +): (() -> ()) +``` + +Runs the callback immediately, and registers the callback to run when an update +is received. + +The returned function will unregister the callback. ----- ## Learn More -- [Values tutorial](../../../../tutorials/fundamentals/values) \ No newline at end of file +- [Observers tutorial](../../../../tutorials/fundamentals/observers) \ No newline at end of file From 603a24b9b94d11d04eaf2f5a9f41c9bf07ac5eda Mon Sep 17 00:00:00 2001 From: Elttob Date: Sun, 4 Feb 2024 12:00:42 +0000 Subject: [PATCH 231/287] Remove leftover md files --- docs/api-reference/state/computed.md | 77 ----------------- docs/api-reference/state/dependency.md | 39 --------- docs/api-reference/state/dependent.md | 61 -------------- docs/api-reference/state/observer.md | 106 ------------------------ docs/api-reference/state/stateobject.md | 45 ---------- docs/api-reference/state/value.md | 79 ------------------ 6 files changed, 407 deletions(-) delete mode 100644 docs/api-reference/state/computed.md delete mode 100644 docs/api-reference/state/dependency.md delete mode 100644 docs/api-reference/state/dependent.md delete mode 100644 docs/api-reference/state/observer.md delete mode 100644 docs/api-reference/state/stateobject.md delete mode 100644 docs/api-reference/state/value.md diff --git a/docs/api-reference/state/computed.md b/docs/api-reference/state/computed.md deleted file mode 100644 index 39706dda1..000000000 --- a/docs/api-reference/state/computed.md +++ /dev/null @@ -1,77 +0,0 @@ - - -

- :octicons-package-24: - Computed - - state object - -

- -Calculates a single value based on the returned values from other state objects. - -```Lua -( - processor: (Use) -> (T, M), - destructor: ((T, M) -> ())? -) -> Computed -``` - ------ - -## Parameters - -- `processor: (Use) -> (T, M)` - computes and returns values to be returned from -the computed object, optionally returning extra values for the destructor alone - -- `destructor: ((T, M) -> ())?` - disposes of values generated by `processor` -when they are no longer in use - ------ - -## Example Usage - -```Lua -local numCoins = Value(50) - -local doubleCoins = Computed(function(use) - return use(numCoins) * 2 -end) - -print(peek(doubleCoins)) --> 100 - -numCoins:set(2) -print(peek(doubleCoins)) --> 4 -``` - ------ - -## Dependency Management - -By default, computed objects run their processor function once during -construction, then cache the result indefinitely. To specify the calculation -should re-run when a state object changes value, the objects can be passed -to the [use callback](./use.md) passed to the processor function. The use -callback will unwrap the value as normal, but any state objects will become -dependencies of the computed object. - ------ - -## Destructors - -The `destructor` callback, if provided, is called when the computed object swaps -out an old value for a newly-generated one. It is called with the old value as -the first parameter, and - if provided - an extra value returned from `processor` -as a customisable second parameter. - -Destructors are required when working with data types that require destruction, -such as instances. Otherwise, they are optional, so not all calculations have to -specify destruction behaviour. - -Fusion guarantees that values passed to destructors by default will never be -used again by the library, so it is safe to finalise them. This does not apply -to the customisable second parameter, which the user is responsible for handling -properly. \ No newline at end of file diff --git a/docs/api-reference/state/dependency.md b/docs/api-reference/state/dependency.md deleted file mode 100644 index 3e2f5a849..000000000 --- a/docs/api-reference/state/dependency.md +++ /dev/null @@ -1,39 +0,0 @@ - - -

- :octicons-checklist-24: - Dependency - - type - -

- -A graph object which can send updates to [dependents](../dependent) on the -reactive graph. - -Most often used with [state objects](../stateobject), though the reactive graph -does not require objects to store state. - -```Lua -{ - dependentSet: Set -} -``` - ------ - -## Example Usage - -```Lua --- these are examples of objects which are dependencies -local value: Dependency = Value(2) -local computed: Dependency = Computed(function(use) - return use(value) * 2 -end) - --- dependencies can be used with some internal functions such as updateAll() -updateAll(value) -``` \ No newline at end of file diff --git a/docs/api-reference/state/dependent.md b/docs/api-reference/state/dependent.md deleted file mode 100644 index 5b27bb9cd..000000000 --- a/docs/api-reference/state/dependent.md +++ /dev/null @@ -1,61 +0,0 @@ - - -

- :octicons-checklist-24: - Dependent - - type - -

- -A graph object which can receive updates from [dependecies](../dependency) on -the reactive graph. - -Most often used with [state objects](../stateobject), though the reactive graph -does not require objects to store state. - -```Lua -{ - dependencySet: Set, - update: (self) -> boolean -} -``` - ------ - -## Fields - -- `dependencySet` - stores the graph objects which this object can receive -updates from - ------ - -## Methods - -### :octicons-code-24: Dependent:update() - -Called when this object receives an update from one or more dependencies. - -If this object is a dependency, and updates should be propagated to further -dependencies, this method should return true. Otherwise, to block further -updates from occuring (for example, because this object did not change value), -this method should return false. - -```Lua -() -> boolean -``` - ------ - -## Example Usage - -```Lua --- these are examples of objects which are dependents -local computed: Dependent = Computed(function(use) - return "foo" -end) -local observer: Dependent = Observer(computed) -``` \ No newline at end of file diff --git a/docs/api-reference/state/observer.md b/docs/api-reference/state/observer.md deleted file mode 100644 index a435f2fa6..000000000 --- a/docs/api-reference/state/observer.md +++ /dev/null @@ -1,106 +0,0 @@ - - -

- :octicons-package-24: - Observer - - graph object - -

- -Observes various updates and events on a given dependency. - -```Lua -( - observe: Dependency -) -> Observer -``` - ------ - -## Parameters - -- `observe: Dependency` - the dependency this observer should respond to - ------ - -## Object Methods - -### :octicons-code-24: Observer:onChange() - -Connects the given callback as a change handler, and returns a function which -will disconnect the callback. The callback will run whenever the observed -dependency is updated. - -```Lua -(callback: () -> ()) -> (() -> ()) -``` -#### Parameters - -- `callback` - The function to call when a change is observed - -!!! caution "Connection memory leaks" - Make sure to disconnect any change handlers made using this function once - you're done using them. - - As long as a change handler is connected, this observer and the dependency - it observes will be held in memory in case further changes occur. This means, - if you don't call the disconnect function, you may end up accidentally - holding your state objects in memory forever after you're done using them. - ------ - -### :octicons-code-24: Observer:onBind() - -Connects the given callback as a change handler, and returns a function which will disconnect the callback. The callback is run immediately, and re-run whenever the observed dependency is updated. - -```Lua -(callback: () -> ()) -> (() -> ()) -``` -#### Parameters - -- `callback` - The function to call when a change is observed **and** when the observer is created - -!!! caution "Connection memory leaks" - Make sure to disconnect any change handlers made using this function once - you're done using them. - - As long as a change handler is connected, this observer and the dependency - it observes will be held in memory in case further changes occur. This means, - if you don't call the disconnect function, you may end up accidentally - holding your state objects in memory forever after you're done using them. - ------ - -## Example Usage - -```Lua -local numCoins = Value(50) - -local coinObserver = Observer(numCoins) - -local disconnect = coinObserver:onChange(function() - print("coins is now:", peek(numCoins)) -end) - -numCoins:set(25) -- prints 'coins is now: 25' - --- always clean up your connections! -disconnect() -``` - -```Lua -local someValue = Value("") - -function update() - someObject.Text = peek(someValue) -end - -local disconnect = Observer(someValue):onBind(update) - --- always clean up your connections! -disconnect() -``` \ No newline at end of file diff --git a/docs/api-reference/state/stateobject.md b/docs/api-reference/state/stateobject.md deleted file mode 100644 index f0957df49..000000000 --- a/docs/api-reference/state/stateobject.md +++ /dev/null @@ -1,45 +0,0 @@ - - -

- :octicons-checklist-24: - StateObject - - type - -

- -A dependency that provides a single stateful value; the dependency updates when -the value changes state. - -Note that state objects do not expose a public interface for accessing their -interior value - the standard way of doing this is by using a -[use callback](./use.md) such as the [peek function](./peek.md). - -```Lua -Dependency & { - type: "State", - kind: string -} -``` - ------ - -## Fields - -- `type` - uniquely identifies state objects for runtime type checks -- `kind` - holds a more specific type name for different kinds of state object - ------ - -## Example Usage - -```Lua --- these are examples of objects which are state objects -local value: StateObject = Value(5) -local computed: StateObject = Computed(function(use) - return "foo" -end) -``` \ No newline at end of file diff --git a/docs/api-reference/state/value.md b/docs/api-reference/state/value.md deleted file mode 100644 index 0d9fc6cef..000000000 --- a/docs/api-reference/state/value.md +++ /dev/null @@ -1,79 +0,0 @@ - - -

- :octicons-package-24: - Value - - state object - -

- -Stores a single value which can be updated at any time. - -```Lua -( - initialValue: T -) -> Value -``` - ------ - -## Parameters - -- `initialValue` - The value that should be initially stored after construction. - ------ - -## Methods - -### :octicons-code-24: Value:set() - -Replaces the currently stored value, updating any other state objects that -depend on this value object. The value is stored directly, and no cloning or -alteration is done. - -If the new value is the same as the old value, other state objects won't be -updated. - -```Lua -(newValue: T) -> () -``` - -#### Parameters - -- `newValue` - The new value to be stored. - -??? note "Table sameness" - Updates are always sent out when setting a table value, because it's much - more difficult to evaluate if two tables are the same. Therefore, this - method is conservative and labels all tables as different, even - compared to themselves. - -??? caution "Legacy parameter: force" - Originally, a second `force` parameter was available in Fusion 0.1 so that - updates could forcibly be sent out, even when the new value was the same as - the old value. This is because Fusion 0.1 used equality to evaluate sameness - for all data types, including tables. This was problematic as many users - attempted to get the table value, modify it, and `:set()` it back into the - object, which would not cause an update as the table reference did not - change. - - Fusion 0.2 uses a different sameness definition for tables to alleviate this - problem. As such, there is no longer a good reason to use this parameter, - and so it is not currently recommended for use. For backwards compatibility, - it will remain for the time being, but do not depend on it for new work. - ------ - -## Example Usage - -```Lua -local numCoins = Value(50) -- start off with 50 coins -print(peek(numCoins)) --> 50 - -numCoins:set(10) -print(peek(numCoins)) --> 10 -``` \ No newline at end of file From f8028d4dfd56a993ca9b817f9161e9892bbe70a9 Mon Sep 17 00:00:00 2001 From: Elttob Date: Sun, 4 Feb 2024 12:12:43 +0000 Subject: [PATCH 232/287] Child API ref --- docs/api-reference/instances/child.md | 45 ------------------- .../{instances => roblox}/attribute.md | 0 .../{instances => roblox}/attributechange.md | 0 .../{instances => roblox}/attributeout.md | 0 .../{instances => roblox}/children.md | 0 .../{instances => roblox}/cleanup.md | 0 .../{instances => roblox}/component.md | 0 .../{instances => roblox}/hydrate.md | 0 .../{instances => roblox}/new.md | 0 .../{instances => roblox}/onchange.md | 0 .../{instances => roblox}/onevent.md | 0 .../{instances => roblox}/out.md | 0 .../{instances => roblox}/ref.md | 0 .../{instances => roblox}/specialkey.md | 0 docs/api-reference/roblox/types/child.md | 24 ++++++++++ 15 files changed, 24 insertions(+), 45 deletions(-) delete mode 100644 docs/api-reference/instances/child.md rename docs/api-reference/{instances => roblox}/attribute.md (100%) rename docs/api-reference/{instances => roblox}/attributechange.md (100%) rename docs/api-reference/{instances => roblox}/attributeout.md (100%) rename docs/api-reference/{instances => roblox}/children.md (100%) rename docs/api-reference/{instances => roblox}/cleanup.md (100%) rename docs/api-reference/{instances => roblox}/component.md (100%) rename docs/api-reference/{instances => roblox}/hydrate.md (100%) rename docs/api-reference/{instances => roblox}/new.md (100%) rename docs/api-reference/{instances => roblox}/onchange.md (100%) rename docs/api-reference/{instances => roblox}/onevent.md (100%) rename docs/api-reference/{instances => roblox}/out.md (100%) rename docs/api-reference/{instances => roblox}/ref.md (100%) rename docs/api-reference/{instances => roblox}/specialkey.md (100%) create mode 100644 docs/api-reference/roblox/types/child.md diff --git a/docs/api-reference/instances/child.md b/docs/api-reference/instances/child.md deleted file mode 100644 index 392ac1c5b..000000000 --- a/docs/api-reference/instances/child.md +++ /dev/null @@ -1,45 +0,0 @@ - - -

- :octicons-checklist-24: - Child - - type - -

- -Represents some UI which can be parented to an ancestor, usually via [Children](./children.md). -The most simple kind of child is a single instance, though arrays can be used -to parent multiple instances at once and state objects can be used to make the -children dynamic. - -```Lua -Instance | {[any]: Child} | StateObject -``` - ------ - -## Example Usage - -```Lua --- all of the following fit the definition of Child - -local child1: Child = New "Folder" {} -local child2: Child = { - New "Folder" {}, - New "Folder" {}, - New "Folder" {} -} -local child3: Child = Computed(function() - return New "Folder" {} -end) -local child4: Child = { - Computed(function() - return New "Folder" {} - end), - {New "Folder" {}, New "Folder" {}} -} -``` \ No newline at end of file diff --git a/docs/api-reference/instances/attribute.md b/docs/api-reference/roblox/attribute.md similarity index 100% rename from docs/api-reference/instances/attribute.md rename to docs/api-reference/roblox/attribute.md diff --git a/docs/api-reference/instances/attributechange.md b/docs/api-reference/roblox/attributechange.md similarity index 100% rename from docs/api-reference/instances/attributechange.md rename to docs/api-reference/roblox/attributechange.md diff --git a/docs/api-reference/instances/attributeout.md b/docs/api-reference/roblox/attributeout.md similarity index 100% rename from docs/api-reference/instances/attributeout.md rename to docs/api-reference/roblox/attributeout.md diff --git a/docs/api-reference/instances/children.md b/docs/api-reference/roblox/children.md similarity index 100% rename from docs/api-reference/instances/children.md rename to docs/api-reference/roblox/children.md diff --git a/docs/api-reference/instances/cleanup.md b/docs/api-reference/roblox/cleanup.md similarity index 100% rename from docs/api-reference/instances/cleanup.md rename to docs/api-reference/roblox/cleanup.md diff --git a/docs/api-reference/instances/component.md b/docs/api-reference/roblox/component.md similarity index 100% rename from docs/api-reference/instances/component.md rename to docs/api-reference/roblox/component.md diff --git a/docs/api-reference/instances/hydrate.md b/docs/api-reference/roblox/hydrate.md similarity index 100% rename from docs/api-reference/instances/hydrate.md rename to docs/api-reference/roblox/hydrate.md diff --git a/docs/api-reference/instances/new.md b/docs/api-reference/roblox/new.md similarity index 100% rename from docs/api-reference/instances/new.md rename to docs/api-reference/roblox/new.md diff --git a/docs/api-reference/instances/onchange.md b/docs/api-reference/roblox/onchange.md similarity index 100% rename from docs/api-reference/instances/onchange.md rename to docs/api-reference/roblox/onchange.md diff --git a/docs/api-reference/instances/onevent.md b/docs/api-reference/roblox/onevent.md similarity index 100% rename from docs/api-reference/instances/onevent.md rename to docs/api-reference/roblox/onevent.md diff --git a/docs/api-reference/instances/out.md b/docs/api-reference/roblox/out.md similarity index 100% rename from docs/api-reference/instances/out.md rename to docs/api-reference/roblox/out.md diff --git a/docs/api-reference/instances/ref.md b/docs/api-reference/roblox/ref.md similarity index 100% rename from docs/api-reference/instances/ref.md rename to docs/api-reference/roblox/ref.md diff --git a/docs/api-reference/instances/specialkey.md b/docs/api-reference/roblox/specialkey.md similarity index 100% rename from docs/api-reference/instances/specialkey.md rename to docs/api-reference/roblox/specialkey.md diff --git a/docs/api-reference/roblox/types/child.md b/docs/api-reference/roblox/types/child.md new file mode 100644 index 000000000..67d14d21b --- /dev/null +++ b/docs/api-reference/roblox/types/child.md @@ -0,0 +1,24 @@ + + +

+ :octicons-note-24: + Child +

+ +```Lua +export type Child = Instance | StateObject | {[unknown]: Child} +``` + +All of the types understood by the [`[Children]`](../../members/children) +special key. + +----- + +## Learn More + +- [Parenting tutorial](../../../../tutorials/roblox/parenting/) +- [Instance Handling tutorial](../../../../tutorials/best-practices/instance-handling/) \ No newline at end of file From 26b295c9df90fb3f9caf7ce75447d4f64d490b8e Mon Sep 17 00:00:00 2001 From: Elttob Date: Sun, 4 Feb 2024 12:36:31 +0000 Subject: [PATCH 233/287] PropertyTable API ref --- .../roblox/types/propertytable.md | 25 +++++++++++++++++++ 1 file changed, 25 insertions(+) create mode 100644 docs/api-reference/roblox/types/propertytable.md diff --git a/docs/api-reference/roblox/types/propertytable.md b/docs/api-reference/roblox/types/propertytable.md new file mode 100644 index 000000000..d3fc62bcc --- /dev/null +++ b/docs/api-reference/roblox/types/propertytable.md @@ -0,0 +1,25 @@ + + +

+ :octicons-note-24: + PropertyTable +

+ +```Lua +export type PropertyTable = {[string | SpecialKey]: unknown} +``` + +A table of named instance properties and [special keys](../specialkey), which +can be passed to [`New`](../../members/new) to create an instance. + +!!! warning "This type can be overly generic" + In most cases, you should know what properties your code is looking for. In + those cases, you should prefer to list out the properties explicitly, to + document what your code needs. + + You should only use this type if you don't know what properties your code + will accept. From 719048330621a2b1d6f56943c7f1ac418140ffde Mon Sep 17 00:00:00 2001 From: Elttob Date: Sun, 4 Feb 2024 23:40:47 +0000 Subject: [PATCH 234/287] API reference consistency pass --- docs/api-reference/general/types/version.md | 12 ++++++------ docs/api-reference/memory/types/scope.md | 8 +++++++- docs/api-reference/memory/types/scopedobject.md | 12 +++++++++--- docs/api-reference/memory/types/task.md | 8 +++++++- docs/api-reference/state/types/dependency.md | 4 ++-- docs/api-reference/state/types/dependent.md | 8 ++++---- docs/api-reference/state/types/stateobject.md | 8 ++++---- 7 files changed, 39 insertions(+), 21 deletions(-) diff --git a/docs/api-reference/general/types/version.md b/docs/api-reference/general/types/version.md index f80ae760c..5c4385359 100644 --- a/docs/api-reference/general/types/version.md +++ b/docs/api-reference/general/types/version.md @@ -23,33 +23,33 @@ Describes a version of Fusion's source code. ## Members -

+

major : number -

+ The major version number. If this is greater than `0`, then two versions sharing the same major version number are not expected to be incompatible or have breaking changes. -

+

minor : number -

+ The minor version number. Describes version updates that are not enumerated by the major version number, such as versions prior to 1.0, or versions which are non-breaking. -

+

isRelease : boolean -

+ Describes whether the version was sourced from an official release package. \ No newline at end of file diff --git a/docs/api-reference/memory/types/scope.md b/docs/api-reference/memory/types/scope.md index 77c23aaf2..3cf13a4ae 100644 --- a/docs/api-reference/memory/types/scope.md +++ b/docs/api-reference/memory/types/scope.md @@ -22,4 +22,10 @@ with optional `Constructors` as methods which can be called. your program. As a result, you shouldn't hold on to scopes after they've been cleaned up, - and you shouldn't use them as unique identifiers anywhere. \ No newline at end of file + and you shouldn't use them as unique identifiers anywhere. + +----- + +## Learn More + +- [Scopes tutorial](../../../../tutorials/fundamentals/scopes) \ No newline at end of file diff --git a/docs/api-reference/memory/types/scopedobject.md b/docs/api-reference/memory/types/scopedobject.md index 9a6ea5918..d2f995ebf 100644 --- a/docs/api-reference/memory/types/scopedobject.md +++ b/docs/api-reference/memory/types/scopedobject.md @@ -28,12 +28,12 @@ These objects are also recognised by [`doCleanup`](../../members/docleanup). ## Members -

+

scope : Scope<unknown>? -

+ The scope which this object was constructed with, or `nil` if the object has been destroyed. @@ -69,4 +69,10 @@ call this; instead, destroy the scope as a whole. It's strongly recommended that you emulate this behaviour if you're implementing your own objects, as this protects against double-destruction - and exposes potential scoping issues further ahead of time. \ No newline at end of file + and exposes potential scoping issues further ahead of time. + +----- + +## Learn More + +- [Scopes tutorial](../../../../tutorials/fundamentals/scopes) \ No newline at end of file diff --git a/docs/api-reference/memory/types/task.md b/docs/api-reference/memory/types/task.md index 61cae4b2a..3e34c1785 100644 --- a/docs/api-reference/memory/types/task.md +++ b/docs/api-reference/memory/types/task.md @@ -24,4 +24,10 @@ Types which [`doCleanup`](../../members/docleanup) has defined behaviour for. Fusion does not use static types to enforce that `doCleanup` is given a type which it can process. - This type is only exposed for your own use. \ No newline at end of file + This type is only exposed for your own use. + +----- + +## Learn More + +- [Scopes tutorial](../../../../tutorials/fundamentals/scopes) \ No newline at end of file diff --git a/docs/api-reference/state/types/dependency.md b/docs/api-reference/state/types/dependency.md index 26d67f707..fd4d7969a 100644 --- a/docs/api-reference/state/types/dependency.md +++ b/docs/api-reference/state/types/dependency.md @@ -26,12 +26,12 @@ allows the lifetime and destruction order of the reactive graph to be analysed. ## Members -

+

dependentSet : {[Dependent]: unknown} -

+ The reactive graph objects which declare themselves as dependent upon this object. \ No newline at end of file diff --git a/docs/api-reference/state/types/dependent.md b/docs/api-reference/state/types/dependent.md index 9bdbe0acf..613d1c6b7 100644 --- a/docs/api-reference/state/types/dependent.md +++ b/docs/api-reference/state/types/dependent.md @@ -30,12 +30,12 @@ allows the lifetime and destruction order of the reactive graph to be analysed. ## Members -

+

dependencySet : {[Dependency]: unknown} -

+ Everything this reactive graph object currently declares itself as dependent upon. @@ -44,12 +44,12 @@ upon. ## Methods -

+

update -> boolean -

+ ```Lua function Dependent:update(): boolean diff --git a/docs/api-reference/state/types/stateobject.md b/docs/api-reference/state/types/stateobject.md index d0dbd6cdb..bfb90860c 100644 --- a/docs/api-reference/state/types/stateobject.md +++ b/docs/api-reference/state/types/stateobject.md @@ -26,21 +26,21 @@ work with [`CanBeState`](../canbestate) in your own code. ## Members -

+

type : "State" -

+ A type string which can be used for runtime type checking. -

+

kind : string -

+ A more specific type string which can be used for runtime type checking. This can be used to tell types of state object apart. \ No newline at end of file From 936c3006ac4af55df6f80b9a3aa68aa0543ac828 Mon Sep 17 00:00:00 2001 From: Elttob Date: Sun, 4 Feb 2024 23:40:52 +0000 Subject: [PATCH 235/287] SpecialKey API ref --- docs/api-reference/roblox/types/specialkey.md | 99 +++++++++++++++++++ 1 file changed, 99 insertions(+) create mode 100644 docs/api-reference/roblox/types/specialkey.md diff --git a/docs/api-reference/roblox/types/specialkey.md b/docs/api-reference/roblox/types/specialkey.md new file mode 100644 index 000000000..f6dd3cbe6 --- /dev/null +++ b/docs/api-reference/roblox/types/specialkey.md @@ -0,0 +1,99 @@ + + +

+ :octicons-note-24: + SpecialKey +

+ +```Lua +export type SpecialKey = { + type: "SpecialKey", + kind: string, + stage: "self" | "descendants" | "ancestor" | "observer", + apply: ( + self, + scope: Scope, + value: unknown, + applyTo: Instance + ) -> () +} +``` + +When used as the key in a [property table](../propertytable), defines a custom +operation to apply to the created Roblox instance. + +!!! note "Non-standard type syntax" + The above type definition uses `self` to denote methods. At time of writing, + Luau does not interpret `self` specially. + +----- + +## Members + +

+ type + + : "SpecialKey" + +

+ +A type string which can be used for runtime type checking. + + +

+ kind + + : string + +

+ +A more specific type string which can be used for runtime type checking. This +can be used to tell types of special key apart. + +

+ stage + + : "self" | "descendants" | "ancestor" | "observer" + +

+ +Describes the type of operation, which subsequently determines when it's applied +relative to other operations. + +- `self` runs before parenting any instances +- `descendants` runs once descendants are parented, but before this instance is +parented to its ancestor +- `ancestor` runs after all parenting operations are complete +- `observer` runs after all other operations, so the final state of the instance +can be observed + +----- + +## Methods + +

+ apply + + -> () + +

+ +```Lua +function SpecialKey:apply( + self, + scope: Scope, + value: unknown, + applyTo: Instance +): () +``` + +Called to apply this operation to an instance. `value` is the value from the +property table, and `applyTo` is the instance to apply the operation to. + +The given `scope` is cleaned up when the operation is being unapplied, including +when the instance is destroyed. Operations should use the scope to clean up any +connections or undo any changes they cause. \ No newline at end of file From 40a0f1c1d15119bf7546bf6a3b23daafd85c9d1e Mon Sep 17 00:00:00 2001 From: Elttob Date: Mon, 5 Feb 2024 15:13:43 +0000 Subject: [PATCH 236/287] Attribute API ref --- .../api-reference/roblox/members/attribute.md | 49 +++++++++++++++++++ 1 file changed, 49 insertions(+) create mode 100644 docs/api-reference/roblox/members/attribute.md diff --git a/docs/api-reference/roblox/members/attribute.md b/docs/api-reference/roblox/members/attribute.md new file mode 100644 index 000000000..f05d5eee7 --- /dev/null +++ b/docs/api-reference/roblox/members/attribute.md @@ -0,0 +1,49 @@ + + +

+ :octicons-workflow-24: + Attribute + + -> SpecialKey + +

+ +```Lua +function Fusion.Attribute( + attributeName: string +): SpecialKey +``` + +Given an attribute name, returns a [special key](../../types/specialkey) which +can modify attributes of that name. + +When paired with a value in a [property table](../../types/propertytable), the +special key sets the attribute to that value. + +----- + +## Parameters + +

+ attributeName + + : string + +

+ +The name of the attribute that the special key should target. + +----- + +

+ Returns + + -> SpecialKey + +

+ +A special key for modifying attributes of that name. \ No newline at end of file From 172ec8246cf24caa51c68b849de7f208f02b5fdf Mon Sep 17 00:00:00 2001 From: Elttob Date: Mon, 5 Feb 2024 16:58:47 +0000 Subject: [PATCH 237/287] AttributeChange API ref --- .../roblox/members/attributechange.md | 49 +++++++++++++++++++ 1 file changed, 49 insertions(+) create mode 100644 docs/api-reference/roblox/members/attributechange.md diff --git a/docs/api-reference/roblox/members/attributechange.md b/docs/api-reference/roblox/members/attributechange.md new file mode 100644 index 000000000..7b0994833 --- /dev/null +++ b/docs/api-reference/roblox/members/attributechange.md @@ -0,0 +1,49 @@ + + +

+ :octicons-workflow-24: + AttributeChange + + -> SpecialKey + +

+ +```Lua +function Fusion.AttributeChange( + attributeName: string +): SpecialKey +``` + +Given an attribute name, returns a [special key](../../types/specialkey) which +can listen to changes for attributes of that name. + +When paired with a callback in a [property table](../../types/propertytable), +the special key connects the callback to the attribute's change event. + +----- + +## Parameters + +

+ attributeName + + : string + +

+ +The name of the attribute that the special key should target. + +----- + +

+ Returns + + -> SpecialKey + +

+ +A special key for listening to changes for attributes of that name. \ No newline at end of file From 81cf02ada397e3c9cd3407c6bd72433f3dca00f4 Mon Sep 17 00:00:00 2001 From: Elttob Date: Mon, 5 Feb 2024 17:01:45 +0000 Subject: [PATCH 238/287] AttributeOut API ref --- .../roblox/members/attributeout.md | 50 +++++++++++++++++++ 1 file changed, 50 insertions(+) create mode 100644 docs/api-reference/roblox/members/attributeout.md diff --git a/docs/api-reference/roblox/members/attributeout.md b/docs/api-reference/roblox/members/attributeout.md new file mode 100644 index 000000000..8ec480811 --- /dev/null +++ b/docs/api-reference/roblox/members/attributeout.md @@ -0,0 +1,50 @@ + + +

+ :octicons-workflow-24: + AttributeOut + + -> SpecialKey + +

+ +```Lua +function Fusion.AttributeOut( + attributeName: string +): SpecialKey +``` + +Given an attribute name, returns a [special key](../../types/specialkey) which +can output values from attributes of that name. + +When paired with a [value object](../../../state/types/value) in a +[property table](../../types/propertytable), the special key sets the value when +the attribute changes. + +----- + +## Parameters + +

+ attributeName + + : string + +

+ +The name of the attribute that the special key should target. + +----- + +

+ Returns + + -> SpecialKey + +

+ +A special key for outputting values from attributes of that name. \ No newline at end of file From c8bd7b4542275813df4a164cbd7515eff8889a6b Mon Sep 17 00:00:00 2001 From: Elttob Date: Mon, 5 Feb 2024 17:05:50 +0000 Subject: [PATCH 239/287] OnEvent API ref --- docs/api-reference/roblox/members/onevent.md | 49 ++++++++++++++++++++ 1 file changed, 49 insertions(+) create mode 100644 docs/api-reference/roblox/members/onevent.md diff --git a/docs/api-reference/roblox/members/onevent.md b/docs/api-reference/roblox/members/onevent.md new file mode 100644 index 000000000..d35a98b29 --- /dev/null +++ b/docs/api-reference/roblox/members/onevent.md @@ -0,0 +1,49 @@ + + +

+ :octicons-workflow-24: + OnEvent + + -> SpecialKey + +

+ +```Lua +function Fusion.OnEvent( + eventName: string +): SpecialKey +``` + +Given an event name, returns a [special key](../../types/specialkey) which +can listen for events of that name. + +When paired with a callback in a [property table](../../types/propertytable), +the special key connects the callback to the event. + +----- + +## Parameters + +

+ eventName + + : string + +

+ +The name of the event that the special key should target. + +----- + +

+ Returns + + -> SpecialKey + +

+ +A special key for listening to events of that name. \ No newline at end of file From c1fa9c2bfb60b079c74a4c71e99c3ea10684d9a6 Mon Sep 17 00:00:00 2001 From: Elttob Date: Mon, 5 Feb 2024 17:07:20 +0000 Subject: [PATCH 240/287] OnChange API ref --- docs/api-reference/roblox/members/onchange.md | 49 +++++++++++++++++++ 1 file changed, 49 insertions(+) create mode 100644 docs/api-reference/roblox/members/onchange.md diff --git a/docs/api-reference/roblox/members/onchange.md b/docs/api-reference/roblox/members/onchange.md new file mode 100644 index 000000000..fb680f62e --- /dev/null +++ b/docs/api-reference/roblox/members/onchange.md @@ -0,0 +1,49 @@ + + +

+ :octicons-workflow-24: + OnChange + + -> SpecialKey + +

+ +```Lua +function Fusion.OnChange( + propertyName: string +): SpecialKey +``` + +Given an property name, returns a [special key](../../types/specialkey) which +can listen to changes for properties of that name. + +When paired with a callback in a [property table](../../types/propertytable), +the special key connects the callback to the property's change event. + +----- + +## Parameters + +

+ propertyName + + : string + +

+ +The name of the property that the special key should target. + +----- + +

+ Returns + + -> SpecialKey + +

+ +A special key for listening to changes for properties of that name. \ No newline at end of file From 68a5c9be720824501dce4f7ac746fd7c22d57e93 Mon Sep 17 00:00:00 2001 From: Elttob Date: Mon, 5 Feb 2024 17:08:54 +0000 Subject: [PATCH 241/287] Add tutorial links to OnEvent/Change API --- docs/api-reference/roblox/members/onchange.md | 8 +++++++- docs/api-reference/roblox/members/onevent.md | 8 +++++++- 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/docs/api-reference/roblox/members/onchange.md b/docs/api-reference/roblox/members/onchange.md index fb680f62e..341d4a854 100644 --- a/docs/api-reference/roblox/members/onchange.md +++ b/docs/api-reference/roblox/members/onchange.md @@ -46,4 +46,10 @@ The name of the property that the special key should target. -A special key for listening to changes for properties of that name. \ No newline at end of file +A special key for listening to changes for properties of that name. + +----- + +## Learn More + +- [Change Events tutorial](../../../../tutorials/roblox/change-events) \ No newline at end of file diff --git a/docs/api-reference/roblox/members/onevent.md b/docs/api-reference/roblox/members/onevent.md index d35a98b29..1ed718768 100644 --- a/docs/api-reference/roblox/members/onevent.md +++ b/docs/api-reference/roblox/members/onevent.md @@ -46,4 +46,10 @@ The name of the event that the special key should target. -A special key for listening to events of that name. \ No newline at end of file +A special key for listening to events of that name. + +----- + +## Learn More + +- [Events tutorial](../../../../tutorials/roblox/events) \ No newline at end of file From 8b312ea0fbb5a0f54a6005bf01487988e2fcfc74 Mon Sep 17 00:00:00 2001 From: Elttob Date: Mon, 5 Feb 2024 17:15:05 +0000 Subject: [PATCH 242/287] Out API ref --- docs/api-reference/roblox/members/out.md | 56 ++++++++++++++++++++++++ 1 file changed, 56 insertions(+) create mode 100644 docs/api-reference/roblox/members/out.md diff --git a/docs/api-reference/roblox/members/out.md b/docs/api-reference/roblox/members/out.md new file mode 100644 index 000000000..9dfa5c9e9 --- /dev/null +++ b/docs/api-reference/roblox/members/out.md @@ -0,0 +1,56 @@ + + +

+ :octicons-workflow-24: + Out + + -> SpecialKey + +

+ +```Lua +function Fusion.Out( + propertyName: string +): SpecialKey +``` + +Given an property name, returns a [special key](../../types/specialkey) which +can output values from properties of that name. + +When paired with a [value object](../../../state/types/value) in a +[property table](../../types/propertytable), the special key sets the value when +the property changes. + +----- + +## Parameters + +

+ propertyName + + : string + +

+ +The name of the property that the special key should target. + +----- + +

+ Returns + + -> SpecialKey + +

+ +A special key for outputting values from properties of that name. + +----- + +## Learn More + +- [Outputs tutorial](../../../../tutorials/roblox/outputs) \ No newline at end of file From 59b181a049ffa286ea8d9de17f2da9c84a689727 Mon Sep 17 00:00:00 2001 From: Elttob Date: Mon, 5 Feb 2024 17:19:27 +0000 Subject: [PATCH 243/287] Update syntax for constant values --- docs/api-reference/general/members/version.md | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/docs/api-reference/general/members/version.md b/docs/api-reference/general/members/version.md index 0dd5f55b1..aa8ba3d0d 100644 --- a/docs/api-reference/general/members/version.md +++ b/docs/api-reference/general/members/version.md @@ -13,11 +13,7 @@ ```Lua -Fusion.version = { - major = 0, - minor = 3, - isRelease = false -} +Fusion.version: Version ``` The version of the Fusion source code. From 0fddc961e277a00699224bd29583319ba7462eb6 Mon Sep 17 00:00:00 2001 From: Elttob Date: Mon, 5 Feb 2024 17:19:33 +0000 Subject: [PATCH 244/287] Ref API ref --- docs/api-reference/roblox/members/ref.md | 30 ++++++++++++++++++++++++ 1 file changed, 30 insertions(+) create mode 100644 docs/api-reference/roblox/members/ref.md diff --git a/docs/api-reference/roblox/members/ref.md b/docs/api-reference/roblox/members/ref.md new file mode 100644 index 000000000..83f9e449e --- /dev/null +++ b/docs/api-reference/roblox/members/ref.md @@ -0,0 +1,30 @@ + + +

+ :octicons-workflow-24: + Ref + + : SpecialKey + +

+ +```Lua +Fusion.Ref: SpecialKey +``` + +A [special key](../../types/specialkey) which outputs the instance it's being +applied to. + +When paired with a [value object](../../../state/types/value) in a +[property table](../../types/propertytable), the special key sets the value to +the instance. + +----- + +## Learn More + +- [References tutorial](../../../../tutorials/roblox/references) \ No newline at end of file From 4396ec10ea3ca99387ff5b656c05ecc12e23be86 Mon Sep 17 00:00:00 2001 From: Elttob Date: Mon, 5 Feb 2024 17:24:54 +0000 Subject: [PATCH 245/287] Children API ref --- docs/api-reference/roblox/members/children.md | 34 +++++++++++++++++++ 1 file changed, 34 insertions(+) create mode 100644 docs/api-reference/roblox/members/children.md diff --git a/docs/api-reference/roblox/members/children.md b/docs/api-reference/roblox/members/children.md new file mode 100644 index 000000000..859e77286 --- /dev/null +++ b/docs/api-reference/roblox/members/children.md @@ -0,0 +1,34 @@ + + +

+ :octicons-workflow-24: + Children + + : SpecialKey + +

+ +```Lua +Fusion.Children: SpecialKey +``` + +A [special key](../../types/specialkey) which parents other instances into this +instance. + +When paired with a [`Child`](../../types/child) in a +[property table](../../types/propertytable), the special key explores the +`Child` to find every `Instance` nested inside. It then parents those instances +under the instance which the special key was applied to. + +In particular, this special key will recursively explore arrays and bind to any +[state objects](../../../state/types/stateobject). + +----- + +## Learn More + +- [Parenting tutorial](../../../../tutorials/roblox/parenting) \ No newline at end of file From a50ad46545df8ddc1da7374b8e8176fb2759f846 Mon Sep 17 00:00:00 2001 From: Elttob Date: Mon, 5 Feb 2024 17:30:34 +0000 Subject: [PATCH 246/287] Clean up unused markdown --- docs/api-reference/roblox/attribute.md | 43 ------- docs/api-reference/roblox/attributechange.md | 45 ------- docs/api-reference/roblox/attributeout.md | 50 -------- docs/api-reference/roblox/children.md | 126 ------------------- docs/api-reference/roblox/cleanup.md | 49 -------- docs/api-reference/roblox/component.md | 35 ------ docs/api-reference/roblox/onchange.md | 46 ------- docs/api-reference/roblox/onevent.md | 46 ------- docs/api-reference/roblox/out.md | 49 -------- docs/api-reference/roblox/ref.md | 38 ------ docs/api-reference/roblox/specialkey.md | 109 ---------------- 11 files changed, 636 deletions(-) delete mode 100644 docs/api-reference/roblox/attribute.md delete mode 100644 docs/api-reference/roblox/attributechange.md delete mode 100644 docs/api-reference/roblox/attributeout.md delete mode 100644 docs/api-reference/roblox/children.md delete mode 100644 docs/api-reference/roblox/cleanup.md delete mode 100644 docs/api-reference/roblox/component.md delete mode 100644 docs/api-reference/roblox/onchange.md delete mode 100644 docs/api-reference/roblox/onevent.md delete mode 100644 docs/api-reference/roblox/out.md delete mode 100644 docs/api-reference/roblox/ref.md delete mode 100644 docs/api-reference/roblox/specialkey.md diff --git a/docs/api-reference/roblox/attribute.md b/docs/api-reference/roblox/attribute.md deleted file mode 100644 index ee6f82a63..000000000 --- a/docs/api-reference/roblox/attribute.md +++ /dev/null @@ -1,43 +0,0 @@ - - -

- :octicons-key-24: - Attribute - - special key - -

- -A [special key](./specialkey.md) for adding attributes to instances. - ------ - -## Parameters - -```lua -(attributeName: string) -> SpecialKey -``` - ------ - -## Example Usage - -```lua -local ammoValue = Value(10) -local label = New "TextLabel" { - [Attribute "Ammo"] = ammoValue -} - -print(label:GetAttribute("Ammo")) -- 10 -``` - ------ - -## Technical Details - -This special key runs at the `self` stage. - ------ diff --git a/docs/api-reference/roblox/attributechange.md b/docs/api-reference/roblox/attributechange.md deleted file mode 100644 index 6f715ddec..000000000 --- a/docs/api-reference/roblox/attributechange.md +++ /dev/null @@ -1,45 +0,0 @@ - - -

- :octicons-code-24: - AttributeChange - - function - -

- -Given an attribute name, returns a [special key](./specialkey.md) which connects -to that attribute's change events. - -```Lua -(attributeName: string) -> SpecialKey -``` - ------ - -## Parameters - -- `attributeName` - The name of the attribute to listen for changes to. - ------ - -## Returns - -A special key which runs at the `observer` stage. When applied to an instance, -it connects to the attribute change signal on the instance for the given property. -The handler is run with the attribute's value after every change. - ------ - -## Example Usage - -```Lua -New "TextBox" { - [AttributeChange "State"] = function(newValue) - print("The state attribute changed to:", newValue) - end -} -``` diff --git a/docs/api-reference/roblox/attributeout.md b/docs/api-reference/roblox/attributeout.md deleted file mode 100644 index b4a2a9967..000000000 --- a/docs/api-reference/roblox/attributeout.md +++ /dev/null @@ -1,50 +0,0 @@ - - -

- :octicons-code-24: - AttributeOut - - function - -

- -Given an attribute name, returns a [special key](./specialkey.md) which outputs the value of -attribute's with that name. It should be used with a [value](../state/value.md) object. - -```Lua -(attributeName: string) -> SpecialKey -``` - ------ - -## Parameters - -- `attributeName` - The name of the attribute to output the value of. - ------ - -## Returns - -A special key which runs at the `observer` stage. When applied to an instance, -it sets the value object equal to the attribute with the given name. It then -listens for further changes and updates the value object accordingly. - ------ - -## Example Usage - -```Lua -local ammo = Value() - -New "Configuration" { - [Attribute "Ammo"] = ammo, - [AttributeOut "Ammo"] = ammo -} - -Observer(ammo):onChange(function() - print("Current ammo:", peek(ammo)) -end) -``` diff --git a/docs/api-reference/roblox/children.md b/docs/api-reference/roblox/children.md deleted file mode 100644 index 5b9061350..000000000 --- a/docs/api-reference/roblox/children.md +++ /dev/null @@ -1,126 +0,0 @@ - - -

- :octicons-key-24: - Children - - special key - -

- -Allows parenting children to an instance, both statically and dynamically. - ------ - -## Example Usage - -```Lua -local example = New "Folder" { - [Children] = New "StringValue" { - Value = "I'm parented to the Folder!" - } -} -``` - ------ - -## Processing Children - -A 'child' is defined (recursively) as: - -- an instance -- a [state object](../../state/stateobject) containing children -- an array of children - -Since this definition is recursive, arrays and state objects can be nested; that -is, the following code is valid: - -```Lua -local example = New "Folder" { - [Children] = { - { - { - New "StringValue" { - Value = "I'm parented to the Folder!" - } - } - } - } -} -``` - -This behaviour is especially useful when working with components - the following -component can return multiple instances to be parented without disrupting the -code next to it: - -```Lua -local function Component(props) - return { - New "TextLabel" { - LayoutOrder = 1, - Text = "Instance one" - }, - - New "TextLabel" { - LayoutOrder = 2, - Text = "Instance two" - } - } -end - -local parent = New "Frame" { - Children = { - New "UIListLayout" { - SortOrder = "LayoutOrder" - }, - - Component {} - } -} -``` - -When using a state object as a child, `Children` will interpret the stored value -as children and watch for changes. When the value of the state object changes, -it'll unparent the old children and parent the new children. - -!!! note - As with bound properties, updates are deferred to the next render step, and - so parenting won't occur right away. - -```Lua -local child1 = New "Folder" { - Name = "Child one" -} -local child2 = New "Folder" { - Name = "Child two" -} - -local childState = Value(child1) - -local parent = New "Folder" { - [Children] = childState -} - -print(parent:GetChildren()) -- { Child one } - -childState:set(child2) -wait(1) -- wait for deferred updates to run - -print(parent:GetChildren()) -- { Child two } -``` - -!!! warning - When using state objects, note that old children *won't* be destroyed, only - unparented - it's up to you to decide if/when children need to be destroyed. - ------ - -## Technical Details - -This special key runs at the `descendants` stage. - -On cleanup, all children are unparented, as if wrapped in a state object that -has changed to nil. \ No newline at end of file diff --git a/docs/api-reference/roblox/cleanup.md b/docs/api-reference/roblox/cleanup.md deleted file mode 100644 index 63becf874..000000000 --- a/docs/api-reference/roblox/cleanup.md +++ /dev/null @@ -1,49 +0,0 @@ - - -

- :octicons-key-24: - Cleanup - - special key - -

- -Cleans up all items given to it when the instance is destroyed, equivalent to -passing the items to `Fusion.cleanup`. - ------ - -## Example Usage - -```Lua -local example1 = New "Folder" { - [Cleanup] = function() - print("I'm in danger!") - end -} - -local example2 = New "Folder" { - [Cleanup] = example1 -} - -local example3 = New "Folder" { - [Cleanup] = { - RunService.RenderStepped:Connect(print), - function() - print("I'm in danger also!") - end, - example2 - } -} - -example3:Destroy() -``` - ------ - -## Technical Details - -This special key runs at the `observer` stage. \ No newline at end of file diff --git a/docs/api-reference/roblox/component.md b/docs/api-reference/roblox/component.md deleted file mode 100644 index 60aaea991..000000000 --- a/docs/api-reference/roblox/component.md +++ /dev/null @@ -1,35 +0,0 @@ - - -

- :octicons-checklist-24: - Component - - type - -

- -The standard type signature for UI components. They accept a property table and -return a [child type](./child.md). - -```Lua -(props: {[any]: any}) -> Child -``` - ------ - -## Example Usage - -```Lua --- create a Button component -local function Button(props) - return New "TextButton" { - Text = props.Text - } -end - --- the Button component is compatible with the Component type -local myComponent: Component = Button -``` \ No newline at end of file diff --git a/docs/api-reference/roblox/onchange.md b/docs/api-reference/roblox/onchange.md deleted file mode 100644 index 67577656d..000000000 --- a/docs/api-reference/roblox/onchange.md +++ /dev/null @@ -1,46 +0,0 @@ - - -

- :octicons-code-24: - OnChange - - function - -

- -Given a property name, returns a [special key](./specialkey.md) which connects -to that property's change events. It should be used with a handler callback, -which may accept the new value of the property. - -```Lua -(propertyName: string) -> SpecialKey -``` - ------ - -## Parameters - -- `propertyName` - The name of the property to listen for changes to. - ------ - -## Returns - -A special key which runs at the `observer` stage. When applied to an instance, -it connects to the property change signal on the instance for the given property. -The handler is run with the property's value after every change. - ------ - -## Example Usage - -```Lua -New "TextBox" { - [OnChange "Text"] = function(newText) - print("You typed:", newText) - end -} -``` \ No newline at end of file diff --git a/docs/api-reference/roblox/onevent.md b/docs/api-reference/roblox/onevent.md deleted file mode 100644 index 9a8cff2aa..000000000 --- a/docs/api-reference/roblox/onevent.md +++ /dev/null @@ -1,46 +0,0 @@ - - -

- :octicons-code-24: - OnEvent - - function - -

- -Given an event name, returns a [special key](./specialkey.md) which connects to -events of that name. It should be used with a handler callback, which may accept -arguments from the event. - -```Lua -(eventName: string) -> SpecialKey -``` - ------ - -## Parameters - -- `eventName` - the name of the event to connect to - ------ - -## Returns - -A special key which runs at the `observer` stage. When applied to an instance, -it connects to the event on the instance of the given name. The handler is run -with the event's arguments after every firing. - ------ - -## Example Usage - -```Lua -New "TextButton" { - [OnEvent "Activated"] = function(...) - print("The button was clicked! Arguments:", ...) - end -} -``` \ No newline at end of file diff --git a/docs/api-reference/roblox/out.md b/docs/api-reference/roblox/out.md deleted file mode 100644 index 68eb65c08..000000000 --- a/docs/api-reference/roblox/out.md +++ /dev/null @@ -1,49 +0,0 @@ - - -

- :octicons-code-24: - Out - - function - -

- -Given a property name, returns a [special key](./specialkey.md) which outputs -the value of properties with that name. It should be used with a [value](../state/value.md). - -```Lua -(propertyName: string) -> SpecialKey -``` - ------ - -## Parameters - -- `propertyName` - The name of the property to output the value of. - ------ - -## Returns - -A special key which runs at the `observer` stage. When applied to an instance, -it sets the value object equal to the property with the given name. It then -listens for further changes and updates the value object accordingly. - ------ - -## Example Usage - -```Lua -local userText = Value() - -New "TextBox" { - [Out "Text"] = userText -} - -Observer(userText):onChange(function() - print("The user typed:", peek(userText)) -end) -``` \ No newline at end of file diff --git a/docs/api-reference/roblox/ref.md b/docs/api-reference/roblox/ref.md deleted file mode 100644 index 1e06550e6..000000000 --- a/docs/api-reference/roblox/ref.md +++ /dev/null @@ -1,38 +0,0 @@ - - -

- :octicons-key-24: - Ref - - special key - -

- -When applied to an instance, outputs the instance to a state object. It should -be used with a [value](../state/value.md). - ------ - -## Example Usage - -```Lua -local myRef = Value() - -New "Part" { - [Ref] = myRef -} - -print(peek(ref)) --> Part -``` - ------ - -## Technical Details - -This special key runs at the `observer` stage. - -On cleanup, the state object is reset to nil, in order to avoid potential -memory leaks. diff --git a/docs/api-reference/roblox/specialkey.md b/docs/api-reference/roblox/specialkey.md deleted file mode 100644 index 7d3cf528e..000000000 --- a/docs/api-reference/roblox/specialkey.md +++ /dev/null @@ -1,109 +0,0 @@ - - -

- :octicons-checklist-24: - SpecialKey - - type - -

- -The standard interface for special keys that can be used in property tables for -instance processing. Compatible with the [New](./new.md) and -[Hydrate](./hydrate.md) functions. - -```Lua -{ - type: "SpecialKey", - kind: string, - stage: "self" | "descendants" | "ancestor" | "observer", - apply: ( - self: SpecialKey, - value: any, - applyTo: Instance, - cleanupTasks: {Task} - ) -> () -} -``` - ------ - -## Fields - -- `type` - identifies this table as a special key -- `kind` - gives a developer-friendly name to the object for debugging -- `stage` - determines when -the special key should apply itself during the hydration process -- `apply` - the method that will be called to apply the special key to an -instance - ------ - -## Example Usage - -```Lua -local Example = {} -Example.type = "SpecialKey" -Example.kind = "Example" -Example.stage = "observer" - -function Example:apply(value, applyTo, cleanupTasks) - local conn = applyTo:GetAttributeChangedSignal("Foo"):Connect(function() - print("My value is", value) - end) - table.insert(cleanupTasks, conn) -end -``` - ------ - -## Stages - -When using [New](../instances/new.md) and [Hydrate](../instances/hydrate.md), -properties are applied in the following order: - -1. String keys, except Parent -2. Special keys with `stage = "self"` -3. Special keys with `stage = "descendants"` -4. Parent, if provided -5. Special keys with `stage = "ancestor"` -6. Special keys with `stage = "observer"` - -There are multiple motivations for splitting special keys into stages like these: - -- Before we parent descendants to the instance, we want to initialise all of -the instance's properties that don't depend on anything else -- Before we parent the instance to an ancestor, we want to parent and initialise -all of the instance's descendants as fully as possible -- Before we attach handlers to anything, we want to parent to and initialise -the instance's ancestor as fully as possible - -For these reasons, the roles of each stage are as follows: - -### self - -The `self` stage is used for special keys that run before descendants are -parented. This is typically used for special keys that operate on the instance -itself in a vacuum. - -### descendants - -The `descendants` stage is used for special keys that need to deal with -descendants, but which don't need to know about the ancestry. This is important -because parenting descendants after the instance is parented to an ancestor can -be more expensive in terms of performance. - -### ancestor - -The `ancestor` stage is used for special keys that deal with the ancestor of -the instance. This is the last stage that should be used for initialising the -instance, and occurs after the Parent has been set. - -### observer - -The `observer` stage is used for special keys that watch the instance for -changes or export references to the instance. This stage is where any event -handlers should be connected, as initialisation should be done by this point. \ No newline at end of file From 45c978e1d52db518294d21286561cbd543a07037 Mon Sep 17 00:00:00 2001 From: Elttob Date: Mon, 5 Feb 2024 17:54:05 +0000 Subject: [PATCH 247/287] Initial work on error descriptions --- docs/api-reference/general/errors.md | 63 ++++++++++------------------ 1 file changed, 22 insertions(+), 41 deletions(-) diff --git a/docs/api-reference/general/errors.md b/docs/api-reference/general/errors.md index 2da745658..df658f182 100644 --- a/docs/api-reference/general/errors.md +++ b/docs/api-reference/general/errors.md @@ -33,21 +33,15 @@ the details for you. The class type 'Foo' has no assignable property 'Bar'. ``` +**Related to:** +[`New`](../../instances/members/new), +[`Hydrate`](../../instances/members/hydrate) + This message means you tried to set a property on an instance, but the property can't be assigned to. This could be because the property doesn't exist, or because it's locked by Roblox to prevent edits. -This usually occurs with the [New](../instances/new) or -[Hydrate](../instances/hydrate) functions: - -```Lua -local folder = New "Folder" { - DataCost = 12345, - ThisPropertyDoesntExist = "Example" -} -``` - -!!! tip +!!! warning "Check your privileges" Different scripts may have different privileges - for example, plugins will be allowed more privileges than in-game scripts. Make sure you have the necessary privileges to assign to your properties! @@ -63,19 +57,11 @@ local folder = New "Folder" { The Frame class doesn't have a property called 'Foo'. ``` +**Related to:** +[`OnChange`](../../instances/members/onchange) + This message means you tried to connect to a property change event, but the property you specify doesn't exist on the instance. - -This usually occurs with the [New](../instances/new) or -[Hydrate](../instances/hydrate) functions: - -```Lua -local textBox = New "TextBox" { - [OnChange "ThisPropertyDoesntExist"] = function() - ... - end) -} -```
----- @@ -88,19 +74,11 @@ local textBox = New "TextBox" { The Frame class doesn't have an event called 'Foo'. ``` +**Related to:** +[`OnEvent`](../../instances/members/onevent) + This message means you tried to connect to an event on an instance, but the event you specify doesn't exist on the instance. - -This usually occurs with the [New](../instances/new) or -[Hydrate](../instances/hydrate) functions: - -```Lua -local button = New "TextButton" { - [OnEvent "ThisEventDoesntExist"] = function() - ... - end) -} -```
----- @@ -113,16 +91,19 @@ local button = New "TextButton" { Can't create a new instance of class 'Foo'. ``` -This message means you tried to create a new instance type, but the type of -instance you specify doesn't exist in Roblox. +**Related to:** +[`New`](../../instances/members/new) -This usually occurs with the [New](../instances/new) function: +This message means you tried to create a new instance type, but that type of +instance could not be created, or doesn't exist in Roblox. -```Lua -local instance = New "ThisClassTypeIsInvalid" { - ... -} -``` +Check that you spelled the class name correctly. + +!!! warning "Some instances are not available outside of testing" + Sometimes, Roblox will lock new instance types behind beta tests or FFlags, + so if you're using bleeding-edge or unreleased features, ensure you're + enrolled in the correct beta tests, using the correct Roblox Studio + update channel, and have the correct flags configured locally.
----- From 37217bdc30f3df6e9e2e4a10be6b8978dc67c096 Mon Sep 17 00:00:00 2001 From: dphfox Date: Thu, 8 Feb 2024 17:00:28 +0000 Subject: [PATCH 248/287] New API ref --- docs/api-reference/roblox/members/new.md | 66 ++++++++++++++++++++++++ 1 file changed, 66 insertions(+) create mode 100644 docs/api-reference/roblox/members/new.md diff --git a/docs/api-reference/roblox/members/new.md b/docs/api-reference/roblox/members/new.md new file mode 100644 index 000000000..5b9bf85c8 --- /dev/null +++ b/docs/api-reference/roblox/members/new.md @@ -0,0 +1,66 @@ + + +

+ :octicons-workflow-24: + New + + -> (PropertyTable) -> Instance + +

+ +```Lua +function Fusion.New( + className: string +): ( + props: PropertyTable +) -> Instance +``` + +Given a class name, returns a component for constructing instances of that +class. + +In the property table, string keys are assigned as properties on the instance. +If the value is a [state object](../../../state/types/stateobject), it is +re-assigned every time the value of the state object changes. + +Any [special keys](../../types/specialkey) present in the property table are +applied to the instance after string keys are processed, in the order specified +by their `stage`. + +A special exception is made for assigning `Parent`, which is only assigned after +the `descendants` stage. + +----- + +## Parameters + +

+ className + + : string + +

+ +The kind of instance that should be constructed. + +----- + +

+ Returns + + -> (PropertyTable) -> Instance + +

+ +A component that constructs instances of that type, accepting various properties +to customise each instance uniquely. + +----- + +## Learn More + +- [New Instances tutorial](../../../../tutorials/roblox/new-instances) \ No newline at end of file From d99aeab4fcd50c35cc9460ed2c4d0b55e3b7fb9d Mon Sep 17 00:00:00 2001 From: dphfox Date: Thu, 8 Feb 2024 17:08:49 +0000 Subject: [PATCH 249/287] Hydrate API ref --- docs/api-reference/roblox/members/hydrate.md | 72 ++++++++++++++++++++ 1 file changed, 72 insertions(+) create mode 100644 docs/api-reference/roblox/members/hydrate.md diff --git a/docs/api-reference/roblox/members/hydrate.md b/docs/api-reference/roblox/members/hydrate.md new file mode 100644 index 000000000..c6853b8aa --- /dev/null +++ b/docs/api-reference/roblox/members/hydrate.md @@ -0,0 +1,72 @@ + + +

+ :octicons-workflow-24: + Hydrate + + -> (PropertyTable) -> Instance + +

+ +```Lua +function Fusion.Hydrate( + target: Instance +): ( + props: PropertyTable +) -> Instance +``` + +Given an instance, returns a component for binding extra functionality to that +instance. + +In the property table, string keys are assigned as properties on the instance. +If the value is a [state object](../../../state/types/stateobject), it is +re-assigned every time the value of the state object changes. + +Any [special keys](../../types/specialkey) present in the property table are +applied to the instance after string keys are processed, in the order specified +by their `stage`. + +A special exception is made for assigning `Parent`, which is only assigned after +the `descendants` stage. + +!!! warning "Do not overwrite properties" + If the instance was previously created with [`New`](../new) or oreviously + hydrated, do not assign to any properties that were previously specified in + those prior calls. Duplicated assignments can interfere with each other in + unpredictable ways. + +----- + +## Parameters + +

+ target + + : Instance + +

+ +The instance which should be modified. + +----- + +

+ Returns + + -> (PropertyTable) -> Instance + +

+ +A component that hydrates that instance, accepting various properties to build +up bindings and operations applied to the instance. + +----- + +## Learn More + +- [Hydration tutorial](../../../../tutorials/roblox/hydration) \ No newline at end of file From fd84e2c0b98082cae22f4229b186b169e6851d9f Mon Sep 17 00:00:00 2001 From: dphfox Date: Sat, 24 Feb 2024 13:31:30 +0000 Subject: [PATCH 250/287] Fix typo in Hydrate docs --- docs/api-reference/roblox/members/hydrate.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/api-reference/roblox/members/hydrate.md b/docs/api-reference/roblox/members/hydrate.md index c6853b8aa..6a1ee75de 100644 --- a/docs/api-reference/roblox/members/hydrate.md +++ b/docs/api-reference/roblox/members/hydrate.md @@ -35,7 +35,7 @@ A special exception is made for assigning `Parent`, which is only assigned after the `descendants` stage. !!! warning "Do not overwrite properties" - If the instance was previously created with [`New`](../new) or oreviously + If the instance was previously created with [`New`](../new) or previously hydrated, do not assign to any properties that were previously specified in those prior calls. Duplicated assignments can interfere with each other in unpredictable ways. From f90e27c3f86c26b860bf175def393890b1b08a69 Mon Sep 17 00:00:00 2001 From: Daniel P H Fox Date: Mon, 18 Mar 2024 21:51:31 +0000 Subject: [PATCH 251/287] Fix typo in For destroy --- src/State/For.lua | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/State/For.lua b/src/State/For.lua index ad3bca59b..a2186b299 100644 --- a/src/State/For.lua +++ b/src/State/For.lua @@ -215,8 +215,8 @@ function class:destroy() dependency.dependentSet[self] = nil end for unusedProcessor in self._existingProcessors do - doCleanup(unusedProcessor.cleanupTask) - scopePool.clearAndGive(unusedProcessor.cleanupTask) + doCleanup(unusedProcessor.scope) + scopePool.clearAndGive(unusedProcessor.scope) end end From d2703e58280219d5b8b62c0cdd86b14411edcb76 Mon Sep 17 00:00:00 2001 From: dphfox Date: Sun, 14 Apr 2024 08:50:06 +0100 Subject: [PATCH 252/287] Ignore Windows desktop files --- .gitignore | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 5a28ccb03..eaccc5fd8 100644 --- a/.gitignore +++ b/.gitignore @@ -11,4 +11,6 @@ /site/ # Ignore selene auto-generated config -roblox.toml \ No newline at end of file +roblox.toml + +desktop.ini \ No newline at end of file From dd9b704974bc32f360b88b2b37902aafc62837f4 Mon Sep 17 00:00:00 2001 From: dphfox Date: Sun, 14 Apr 2024 08:57:07 +0100 Subject: [PATCH 253/287] Fix tutorial typos --- docs/tutorials/animation/springs.md | 6 +++--- docs/tutorials/animation/tweens.md | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/docs/tutorials/animation/springs.md b/docs/tutorials/animation/springs.md index 34665f1df..ed53aa3a2 100644 --- a/docs/tutorials/animation/springs.md +++ b/docs/tutorials/animation/springs.md @@ -14,7 +14,7 @@ to move towards: ```Lua local goal = scope:Value(0) -local animated = scope:Spring(target) +local animated = scope:Spring(goal) ``` The spring will smoothly follow the 'goal' state object over time. @@ -32,7 +32,7 @@ use. Both are optional, and both can be state objects if desired: local goal = scope:Value(0) local speed = 25 local damping = scope:Value(0.5) -local animated = scope:Spring(target, speed, damping) +local animated = scope:Spring(goal, speed, damping) ``` You can also set the position and velocity of the spring at any time. @@ -48,7 +48,7 @@ each number inside the type is animated individually. ```Lua local goalPosition = scope:Value(UDim2.new(0.5, 0, 0, 0)) -local animated = scope:Spring(target, 25, 0.5) +local animated = scope:Spring(goalPosition, 25, 0.5) ``` ----- diff --git a/docs/tutorials/animation/tweens.md b/docs/tutorials/animation/tweens.md index b9c0a1e56..cc7723a3b 100644 --- a/docs/tutorials/animation/tweens.md +++ b/docs/tutorials/animation/tweens.md @@ -13,7 +13,7 @@ move towards: ```Lua local goal = scope:Value(0) -local animated = scope:Tween(target) +local animated = scope:Tween(goal) ``` The tween will smoothly follow the 'goal' state object over time. @@ -31,7 +31,7 @@ desired: ```Lua local goal = scope:Value(0) local style = TweenInfo.new(0.5, Enum.EasingStyle.Quad) -local animated = scope:Tween(target, style) +local animated = scope:Tween(goal, style) ``` You can use many different kinds of values with tweens, not just numbers. @@ -40,7 +40,7 @@ each number inside the type is animated individually. ```Lua local goalPosition = scope:Value(UDim2.new(0.5, 0, 0, 0)) -local animated = scope:Tween(target, TweenInfo.new(0.5, Enum.EasingStyle.Quad)) +local animated = scope:Tween(goalPosition, TweenInfo.new(0.5, Enum.EasingStyle.Quad)) ``` ----- From c2d9be9631e3d0d3100f0bdfbd6beafa924c63bc Mon Sep 17 00:00:00 2001 From: dphfox Date: Sun, 14 Apr 2024 09:03:39 +0100 Subject: [PATCH 254/287] Animatable type API --- docs/api-reference/animation/animatable.md | 54 ------------------- .../animation/{ => members}/spring.md | 0 .../animation/{ => members}/tween.md | 0 .../animation/types/animatable.md | 46 ++++++++++++++++ 4 files changed, 46 insertions(+), 54 deletions(-) delete mode 100644 docs/api-reference/animation/animatable.md rename docs/api-reference/animation/{ => members}/spring.md (100%) rename docs/api-reference/animation/{ => members}/tween.md (100%) create mode 100644 docs/api-reference/animation/types/animatable.md diff --git a/docs/api-reference/animation/animatable.md b/docs/api-reference/animation/animatable.md deleted file mode 100644 index 8c517a207..000000000 --- a/docs/api-reference/animation/animatable.md +++ /dev/null @@ -1,54 +0,0 @@ - - -

- :octicons-checklist-24: - Animatable - - type - -

- -Represents types that can be animated component-wise. If a data type can -reasonably be represented as a fixed-length array of numbers, then it is -animatable. - -Any data type present in this type can be animated by Fusion. - -```Lua -number | CFrame | Color3 | ColorSequenceKeypoint | DateTime | NumberRange | -NumberSequenceKeypoint | PhysicalProperties | Ray | Rect | Region3 | -Region3int16 | UDim | UDim2 | Vector2 | Vector2int16 | Vector3 | Vector3int16 -``` - ------ - -## Example Usage - -```Lua -local DEFAULT_TWEEN = TweenInfo.new(0.25, Enum.EasingStyle.Quint) - -local function withDefaultTween(target: StateObject) - return Tween(target, DEFAULT_TWEEN) -end -``` - ------ - -## Animatability - -[Tween](./tween.md) and [Spring](./spring.md) work by animating the individual -components of whatever data they're working with. For example, if you tween a -Vector3, the X, Y and Z components will have the tween individually applied to -each. - -This is a very flexible definition of animatability, but it does not cover all -data types. For example, it still doesn't make sense to animate a string, a -boolean, or nil. - -By default, Tween and Spring will just snap to the goal value if you try to -smoothly animate something that is not animatable. However, if you want to try -and prevent the use of non-animatable types statically, you can use this type -definition in your own code. \ No newline at end of file diff --git a/docs/api-reference/animation/spring.md b/docs/api-reference/animation/members/spring.md similarity index 100% rename from docs/api-reference/animation/spring.md rename to docs/api-reference/animation/members/spring.md diff --git a/docs/api-reference/animation/tween.md b/docs/api-reference/animation/members/tween.md similarity index 100% rename from docs/api-reference/animation/tween.md rename to docs/api-reference/animation/members/tween.md diff --git a/docs/api-reference/animation/types/animatable.md b/docs/api-reference/animation/types/animatable.md new file mode 100644 index 000000000..6e6e9c1b1 --- /dev/null +++ b/docs/api-reference/animation/types/animatable.md @@ -0,0 +1,46 @@ + + +

+ :octicons-note-24: + Animatable +

+ +```Lua +export type Animatable = + number | + CFrame | + Color3 | + ColorSequenceKeypoint | + DateTime | + NumberRange | + NumberSequenceKeypoint | + PhysicalProperties | + Ray | + Rect | + Region3 | + Region3int16 | + UDim | + UDim2 | + Vector2 | + Vector2int16 | + Vector3 | + Vector3int16 +``` + +Any data type that Fusion can decompose into a tuple of animatable numbers. + +!!! note "Passing other types to animation objects" + Other types can be passed to `Tween` and `Spring` objects, however those + types will not animate. Instead, non-`Animatable` types will immediately + arrive at their goal value. + +----- + +## Learn More + +- [Tweens tutorial](../../../../tutorials/animations/tweens) +- [Springs tutorial](../../../../tutorials/animations/springs) \ No newline at end of file From 25af10062a53e0dee2f1218c6bd68d644e3bbcda Mon Sep 17 00:00:00 2001 From: dphfox Date: Sun, 14 Apr 2024 09:04:01 +0100 Subject: [PATCH 255/287] Fix Animatable Learn more links --- docs/api-reference/animation/types/animatable.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/api-reference/animation/types/animatable.md b/docs/api-reference/animation/types/animatable.md index 6e6e9c1b1..4745b060e 100644 --- a/docs/api-reference/animation/types/animatable.md +++ b/docs/api-reference/animation/types/animatable.md @@ -42,5 +42,5 @@ Any data type that Fusion can decompose into a tuple of animatable numbers. ## Learn More -- [Tweens tutorial](../../../../tutorials/animations/tweens) -- [Springs tutorial](../../../../tutorials/animations/springs) \ No newline at end of file +- [Tweens tutorial](../../../../tutorials/animation/tweens) +- [Springs tutorial](../../../../tutorials/animation/springs) \ No newline at end of file From 4f409e60fdf9bc71b19881856850c24984321a5a Mon Sep 17 00:00:00 2001 From: dphfox Date: Sun, 14 Apr 2024 09:04:57 +0100 Subject: [PATCH 256/287] Clarify wording of Animatable API --- docs/api-reference/animation/types/animatable.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/api-reference/animation/types/animatable.md b/docs/api-reference/animation/types/animatable.md index 4745b060e..d756033ec 100644 --- a/docs/api-reference/animation/types/animatable.md +++ b/docs/api-reference/animation/types/animatable.md @@ -31,7 +31,7 @@ export type Animatable = Vector3int16 ``` -Any data type that Fusion can decompose into a tuple of animatable numbers. +Any data type that Fusion can decompose into a tuple of animatable parameters. !!! note "Passing other types to animation objects" Other types can be passed to `Tween` and `Spring` objects, however those From 5237b6d6657f78bc7bc77664d4d4ba70a9c90fdf Mon Sep 17 00:00:00 2001 From: dphfox Date: Sun, 14 Apr 2024 09:16:13 +0100 Subject: [PATCH 257/287] API for Tween and Spring --- docs/api-reference/animation/types/spring.md | 101 +++++++++++++++++++ docs/api-reference/animation/types/tween.md | 44 ++++++++ src/Types.lua | 6 +- 3 files changed, 148 insertions(+), 3 deletions(-) create mode 100644 docs/api-reference/animation/types/spring.md create mode 100644 docs/api-reference/animation/types/tween.md diff --git a/docs/api-reference/animation/types/spring.md b/docs/api-reference/animation/types/spring.md new file mode 100644 index 000000000..98655730a --- /dev/null +++ b/docs/api-reference/animation/types/spring.md @@ -0,0 +1,101 @@ + + +

+ :octicons-note-24: + Spring +

+ +```Lua +export type Spring = StateObject & Dependent & { + kind: "Spring", + setPosition: (self, newPosition: T) -> (), + setVelocity: (self, newVelocity: T) -> (), + addVelocity: (self, deltaVelocity: T) -> () +} +``` + +A specialised [state object](../stateobject) for following a goal state smoothly +over time, using physics to shape the motion. + +In addition to the standard state object interfaces, this object is a +[dependent](../dependent) so it can receive updates from the goal state. + +The methods on this type allow for direct control over the position and velocity +of the motion. Other than that, this type is of limited utility outside of +Fusion itself. + +----- + +## Members + +

+ kind + + : "Spring" + +

+ +A more specific type string which can be used for runtime type checking. This +can be used to tell types of state object apart. + +----- + +## Methods + +

+ setPosition + + -> () + +

+ +```Lua +function Spring:setPosition( + newPosition: T +): () +``` + +Immediately snaps the spring to the given position. The position must have the +same `typeof()` as the goal state. + +

+ setVelocity + + -> () + +

+ +```Lua +function Spring:setVelocity( + newVelocity: T +): () +``` + +Overwrites the spring's velocity without changing its position. The velocity +must have the same `typeof()` as the goal state. + +

+ addVelocity + + -> () + +

+ +```Lua +function Spring:addVelocity( + deltaVelocity: T +): () +``` + +Appends to the spring's velocity without changing its position. The velocity +must have the same `typeof()` as the goal state. + +----- + +## Learn More + +- [Springs tutorial](../../../../tutorials/animation/springs) \ No newline at end of file diff --git a/docs/api-reference/animation/types/tween.md b/docs/api-reference/animation/types/tween.md new file mode 100644 index 000000000..dab6ac00b --- /dev/null +++ b/docs/api-reference/animation/types/tween.md @@ -0,0 +1,44 @@ + + +

+ :octicons-note-24: + Tween +

+ +```Lua +export type Tween = StateObject & Dependent & { + kind: "Tween" +} +``` + +A specialised [state object](../stateobject) for following a goal state smoothly +over time, using a `TweenInfo` to shape the motion. + +In addition to the standard state object interfaces, this object is a +[dependent](../dependent) so it can receive updates from the goal state. + +This type isn't generally useful outside of Fusion itself. + +----- + +## Members + +

+ kind + + : "Tween" + +

+ +A more specific type string which can be used for runtime type checking. This +can be used to tell types of state object apart. + +----- + +## Learn More + +- [Tweens tutorial](../../../../tutorials/animation/tweens) \ No newline at end of file diff --git a/src/Types.lua b/src/Types.lua index bdfe3f17e..187355bd2 100644 --- a/src/Types.lua +++ b/src/Types.lua @@ -161,9 +161,9 @@ export type TweenConstructor = ( -- A state object which follows another state object using spring simulation. export type Spring = StateObject & Dependent & { kind: "Spring", - setPosition: (Spring, newPosition: Animatable) -> (), - setVelocity: (Spring, newVelocity: Animatable) -> (), - addVelocity: (Spring, deltaVelocity: Animatable) -> () + setPosition: (Spring, newPosition: T) -> (), + setVelocity: (Spring, newVelocity: T) -> (), + addVelocity: (Spring, deltaVelocity: T) -> () } export type SpringConstructor = ( scope: Scope, From 20c86e1e03bc2b461e7b9061fbb68257427cadfe Mon Sep 17 00:00:00 2001 From: dphfox Date: Sun, 14 Apr 2024 09:30:48 +0100 Subject: [PATCH 258/287] Spring + Tween member API --- .../api-reference/animation/members/spring.md | 136 ++++++++---------- docs/api-reference/animation/members/tween.md | 88 ++++++++---- src/Types.lua | 2 +- 3 files changed, 117 insertions(+), 109 deletions(-) diff --git a/docs/api-reference/animation/members/spring.md b/docs/api-reference/animation/members/spring.md index d0e67b4a1..f0b61d8b8 100644 --- a/docs/api-reference/animation/members/spring.md +++ b/docs/api-reference/animation/members/spring.md @@ -1,112 +1,92 @@

- :octicons-package-24: + :octicons-workflow-24: Spring - - state object + + -> Spring<T>

-Follows the value of another state object, as if linked by a damped spring. - -If the state object is not [animatable](./animatable.md), the spring will -just snap to the goal value. - ```Lua -( - goal: StateObject, +function Fusion.Spring( + scope: Scope, + goalState: StateObject, speed: CanBeState?, damping: CanBeState? ) -> Spring ``` ------ +Constructs and returns a new [spring state object](../../types/spring). -## Parameters - -- `goal` - The state object whose value should be followed. -- `speed` - Scales the time it takes for the spring to move (but does not -directly correlate to a duration). Defaults to `10`. -- `damping` - Affects the friction/damping which the spring experiences. `0` -represents no friction, and `1` is just enough friction to reach the goal -without overshooting or oscillating. Defaults to `1`. +!!! success "Use scoped() method syntax" + This function is intended to be accessed as a method on a scope: + ```Lua + local spring = scope:Spring(goal, speed, damping) + ``` ----- -## Methods - -### :octicons-code-24: Spring:setPosition() - -Instantaneously moves the spring to a new position. This does not affect the -velocity of the spring. - -If the given value doesn't have the same type as the spring's current value, -the position will snap instantly to the new value. - -```Lua -(newPosition: T) -> () -``` - -#### Parameters +## Parameters -- `newPosition` - The value the spring's position should jump to. +

+ scope + + : Scope<S> + +

------ +The [scope](../../../memory/types/scope) which should be used to store +destruction tasks for this object. -### :octicons-code-24: Spring:setVelocity() +

+ goalState + + : StateObject<T> + +

-Overwrites the velocity of this spring. This does not have an immediate effect -on the position of the spring. +The goal state that this object should follow. For best results, `T` should be +[animatable](../../types/animatable). -If the given value doesn't have the same type as the spring's current value, -the velocity will snap instantly to the new value. +

+ speed + + : CanBeState<T>? + +

-```Lua -(newVelocity: T) -> () -``` +Multiplies how fast the motion should occur; doubling the `speed` exactly halves +the time it takes for the motion to complete. -#### Parameters +

+ damping + + : CanBeState<T>? + +

-- `newVelocity` - The value the spring's velocity should jump to. +The amount of resistance the motion encounters. 0 represents no resistance, +1 is just enough resistance to prevent overshoot (critical damping), and larger +values damp out inertia effects and straighten the motion. ----- -### :octicons-code-24: Spring:addVelocity() - -Adds to the velocity of this spring. This does not have an immediate effect -on the position of the spring. - -If the given value doesn't have the same type as the spring's current value, -the velocity will snap instantly to the new value. - -```Lua -(deltaVelocity: T) -> () -``` - -#### Parameters +

+ Returns + + -> Spring<T> + +

-- `deltaVelocity` - The velocity to add to the spring. +A freshly constructed spring state object. ----- -## Example Usage +## Learn More -```Lua -local position = Value(UDim2.fromOffset(25, 50)) -local smoothPosition = Spring(position, 25, 0.6) - -local ui = New "Frame" { - Parent = PlayerGui.ScreenGui, - Position = smoothPosition -} - -while true do - task.wait(5) - -- apply an impulse - smoothPosition:addVelocity(UDim2.fromOffset(-10, 10)) -end -``` \ No newline at end of file +- [Springs tutorial](../../../../tutorials/animation/springs) \ No newline at end of file diff --git a/docs/api-reference/animation/members/tween.md b/docs/api-reference/animation/members/tween.md index 869977480..c169657dc 100644 --- a/docs/api-reference/animation/members/tween.md +++ b/docs/api-reference/animation/members/tween.md @@ -1,51 +1,79 @@

- :octicons-package-24: + :octicons-workflow-24: Tween - - state object + + -> Tween<T>

-Follows the value of another state object, by tweening towards it. - -If the state object is not [animatable](./animatable.md), the tween will -just snap to the goal value. - ```Lua -( - goal: StateObject, +function Fusion.Tween( + scope: Scope, + goalState: StateObject, tweenInfo: CanBeState? ) -> Tween ``` +Constructs and returns a new [tween state object](../../types/tween). + +!!! success "Use scoped() method syntax" + This function is intended to be accessed as a method on a scope: + ```Lua + local tween = scope:Tween(goal, info) + ``` + ----- ## Parameters -- `goal` - The state object whose value should be followed. -- `tweenInfo` - The style of tween to use when moving to the goal. Defaults -to `TweenInfo.new()`. +

+ scope + + : Scope<S> + +

+ +The [scope](../../../memory/types/scope) which should be used to store +destruction tasks for this object. + +

+ goalState + + : StateObject<T> + +

+ +The goal state that this object should follow. For best results, `T` should be +[animatable](../../types/animatable). + +

+ info + + : CanBeState<TweenInfo>? + +

+ +Determines the easing curve that the motion will follow. ----- -## Example Usage +

+ Returns + + -> Tween<T> + +

-```Lua -local position = Value(UDim2.fromOffset(25, 50)) -local smoothPosition = Tween(position, TweenInfo.new(2)) - -local ui = New "Frame" { - Parent = PlayerGui.ScreenGui, - Position = smoothPosition -} - -while true do - task.wait(5) - position:set(peek(position) + UDim2.fromOffset(100, 100)) -end -``` \ No newline at end of file +A freshly constructed tween state object. + +----- + +## Learn More + +- [Tweens tutorial](../../../../tutorials/animation/tweens) \ No newline at end of file diff --git a/src/Types.lua b/src/Types.lua index 187355bd2..913249261 100644 --- a/src/Types.lua +++ b/src/Types.lua @@ -155,7 +155,7 @@ export type Tween = StateObject & Dependent & { export type TweenConstructor = ( scope: Scope, goalState: StateObject, - tweenInfo: TweenInfo? + tweenInfo: CanBeState? ) -> Tween -- A state object which follows another state object using spring simulation. From 7168f3c0f8204c052ad3ae2ee6d74ac9ad820328 Mon Sep 17 00:00:00 2001 From: dphfox Date: Sun, 14 Apr 2024 10:36:42 +0100 Subject: [PATCH 259/287] callbackError documentation --- docs/api-reference/general/errors.md | 550 +-------------------------- src/Utility/Contextual.lua | 2 +- 2 files changed, 15 insertions(+), 537 deletions(-) diff --git a/docs/api-reference/general/errors.md b/docs/api-reference/general/errors.md index df658f182..9efdcce2a 100644 --- a/docs/api-reference/general/errors.md +++ b/docs/api-reference/general/errors.md @@ -85,544 +85,22 @@ event you specify doesn't exist on the instance.
-## cannotCreateClass - -``` -Can't create a new instance of class 'Foo'. -``` - -**Related to:** -[`New`](../../instances/members/new) - -This message means you tried to create a new instance type, but that type of -instance could not be created, or doesn't exist in Roblox. - -Check that you spelled the class name correctly. - -!!! warning "Some instances are not available outside of testing" - Sometimes, Roblox will lock new instance types behind beta tests or FFlags, - so if you're using bleeding-edge or unreleased features, ensure you're - enrolled in the correct beta tests, using the correct Roblox Studio - update channel, and have the correct flags configured locally. -
- ------ - -
- ## callbackError ``` -Callback error: attempt to index a nil value -``` - -This message means you provided a callback to Fusion, but it ran into an error. -For example, a [computed object](../state/computed) might have failed to compute -a value. - -Review the stack trace that came with the error to see what part of the code -may have caused the error. - -```Lua -local example = Computed(function() - local badMath = 2 + "fish" -end) -``` -
- ------ - -
- -## forKeyCollision - -``` -ForKeys should only write to output key 'Charlie' once when processing key changes, but it wrote to it twice. Previously input key: 'Alice'; New input key: 'Bob' -``` - -This message means you returned the same value twice for two different keys in -a [ForKeys object](../state/forkeys). - -```Lua -local data = { - Alice = true, - Bob = true -} -local example = ForKeys(data, function(key) - if key == "Alice" or key == "Bob" then - return "Charlie" - end -end) -``` -
- ------ - -
- -## forProcessorError - -``` -ForKeys callback error: attempt to index a nil value -``` - -This message means the callback of a [ForKeys object](../state/forkeys) -encountered an error. - -```Lua -local example = ForKeys(array, function(key) - local badMath = 2 + "fish" -end) +Error in callback: attempt to perform arithmetic (add) on number and string ``` -
- ------ - -
-## invalidChangeHandler - -``` -The change handler for the 'Text' property must be a function. -``` - -This message means you tried to use [OnChange](../instances/onchange) on an -instance's property, but instead of passing a function callback, you passed -something else. - -```Lua -local input = New "TextBox" { - [OnChange "Text"] = "lemons" -} -``` -
- ------ - -
- -## invalidAttributeChangeHandler - -``` -The change handler for the 'Ammo' attribute must be a function. -``` - -This message means you tried to use [AttributeChange](../instances/attributechange) on an -instance's attribute, but instead of passing a function callback, you passed -something else. - -```Lua -local config = New "Configuration" { - [AttributeChange "Ammo"] = "guns" -} -``` -
- ------ - -
- -## invalidEventHandler - -``` -The handler for the 'Activated' event must be a function. -``` - -This message means you tried to use [OnEvent](../instances/onevent) on an -instance's event, but instead of passing a function callback, you passed -something else. - -```Lua -local button = New "TextButton" { - [OnEvent "Activated"] = "limes" -} -``` -
- ------ - -
- -## invalidPropertyType - -``` -'Frame.Size' expected a 'UDim2' type, but got a 'Color3' type. -``` - -This message means you tried to set a property on an instance, but you gave it -the wrong type of value. - -This usually occurs with the [New](../instances/new) or -[Hydrate](../instances/hydrate) functions: - -```Lua -local ui = New "Frame" { - Size = Computed(function() - return Color3.new(1, 0, 0) - end) -} -``` -
- ------ - -
- -## invalidRefType - -``` -Instance refs must be Value objects. -``` - -This message means you tried to use [Ref](../instances/ref), but you didn't also -give it a [value object](../state/value) to store the instance inside of. - -```Lua -local thing = New "Part" { - [Ref] = 2 -} -``` -
- ------ - -
- -## invalidOutType - -``` -[Out] properties must be given Value objects. -``` - -This message means you tried to use [Out](../instances/out), but you didn't also -give it a [value object](../state/value) to store the property's value inside -of. - -```Lua -local thing = New "Part" { - [Out "Color"] = true -} -``` -
- ------ - -
- -## invalidAttributeOutType - -``` -[AttributeOut] properties must be given Value objects. -``` - -This message means you tried to use [AttributeOut](../instances/attributeout), but you didn't also -give it a [value object](../state/value) to store the attributes's value inside -of. - -```Lua -local config = New "Configuration" { - [AttributeChange "Ammo"] = "guns" -} -``` -
- ------ - -
- -## invalidOutProperty - -``` -The Part class doesn't have a property called 'Flobulator'. -``` - -This message means you tried to read a property of an instance using -[Out](../instances/out), but the property can't be read. This could be because -the property doesn't exist, or because it's locked by Roblox to prevent reading. - -```Lua -local value = Value() - -local thing = New "Part" { - [Out "Flobulator"] = value -} -``` -
- ------ - -
- -## invalidSpringDamping - -``` -The damping ratio for a spring must be >= 0. (damping was -0.50) -``` - -This message means you gave a damping ratio to a [spring object](../animation/spring) -which is less than 0: - -```Lua -local speed = 10 -local damping = -12345 -local spring = Spring(state, speed, damping) -``` - -Damping ratio must always be between 0 and infinity for a spring to be -physically simulatable. -
- ------ - -
- -## invalidSpringSpeed - -``` -The speed of a spring must be >= 0. (speed was -2.00) -``` - -This message means you gave a speed to a [spring object](../animation/spring) -which is less than 0: - -```Lua -local speed = -12345 -local spring = Spring(state, speed) -``` - -Since a speed of 0 is equivalent to a spring that doesn't move, any slower speed -is not simulatable or physically sensible. -
- ------ - -
- -## mistypedSpringDamping - -``` -The damping ratio for a spring must be a number. (got a boolean) -``` - -This message means you gave a damping ratio to a [spring object](../animation/spring) -which isn't a number. - -```Lua -local speed = 10 -local damping = true -local spring = Spring(state, speed, damping) -``` -
- ------ - -
- -## mistypedSpringSpeed - -``` -The speed of a spring must be a number. (got a boolean) -``` - -This message means you gave a speed to a [spring object](../animation/spring) -which isn't a number. - -```Lua -local speed = true -local spring = Spring(state, speed) -``` -
- ------ - -
- -## mistypedTweenInfo - -``` -The tween info of a tween must be a TweenInfo. (got a boolean) -``` - -This message shows if you try to provide a tween info to a [tween](../animation/tween) -which isn't a TweenInfo: - -```Lua -local tweenInfo = true -local tween = Tween(state, tweenInfo) -``` -
- ------ - -
- -## noTaskScheduler - -``` -Fusion is not connected to an external task scheduler. -``` - -This message shows when Fusion attempts to schedule something for execution -without first setting a task scheduler for the library to use. - -For users of Fusion on Roblox, this generally shouldn't occur as Fusion should -automatically be bound to Roblox's task scheduler. However, when using Fusion -in other environments, the fix is to provide Fusion with all the task scheduler -callbacks necessary to schedule tasks for execution in the future. -
- ------ - -
- -## springTypeMismatch - -``` -The type 'number' doesn't match the spring's type 'Color3'. -``` - -Some methods on [spring objects](../animation/spring) require incoming values to -match the types previously being used on the spring. - -This message means you passed a value to one of those methods, but it wasn't the -same type as the type of the spring. - -```Lua -local colour = State(Color3.new(1, 0, 0)) -local colourSpring = Spring(colour) - -colourSpring:addVelocity(Vector2.new(2, 3)) -``` -
- ------ - -
- -## stateGetWasRemoved - -``` -`StateObject:get()` has been replaced by `use()` and `peek()` - see discussion #217 on GitHub. -``` - -This message means you attempted to call the now-removed `:get()` method on a -[state object](../state/stateobject.md). Starting with Fusion 0.3, this method -has been removed in favour of the [peek function](../state/peek.md) and -[use callbacks](../state/use.md). - -[Learn more by visiting this discussion on GitHub.](https://github.com/Elttob/Fusion/discussions/217) - -```Lua -local value = Value(5) -print(value:get()) -- should be print(peek(value)) -``` -
- ------ - -
- -## unknownMessage - -``` -Unknown error: attempt to index a nil value -``` - -If you see this message, it's almost certainly an internal bug, so make sure to -get in contact so the issue can be fixed. - -When Fusion code attempts to log a message, warning or error, it needs to -provide an ID. This ID is used to show the correct message, and serves as a -simple, memorable identifier if you need to look up the message later. -However, if that code provides an invalid ID, then the message will be replaced -with this one. -
- ------ - -
- -## unrecognisedChildType - -``` -'number' type children aren't accepted as children in `New`. -``` - -This message means you tried to pass something to [Children](../instances/children.md) -which isn't a valid [child](../instances/child.md). This usually means that you -passed something that isn't an instance, array or [state object](../state/stateobject.md). - -```Lua -local instance = New "Folder" { - [Children] = { - 1, 2, 3, 4, 5, - - {true, false}, - - State(Enum.Material.Grass) - } -} -``` - -!!! note - Note that state objects are allowed to store `nil` to represent the absence - of an instance, as an exception to these rules. -
- ------ - -
- -## unrecognisedPropertyKey - -``` -'number' keys aren't accepted in the property table of `New`. -``` - -This message means, while using [New](../instances/new) or [Hydrate](../instances/hydrate), -you specified something in the property table that's not a property name or -[special key](../instances/specialkey). - -Commonly, this means you accidentally specified children directly inside of -the property table, rather than using the dedicated [Children](../instances/children.md) -special key. - -```Lua -local folder = New "Folder" { - [Vector3.new()] = "Example", - - "This", "Shouldn't", "Be", "Here" -} -``` -
- ------ - -
- -## unrecognisedPropertyStage - -``` -'discombobulate' isn't a valid stage for a special key to be applied at. -``` - -Fusion provides a standard interface for defining [special keys](../instances/specialkey.md) -which can be used to extend the functionality of [New](../instances/new.md) or -[Hydrate](../instances/hydrate.md). - -Within this interface, keys can select when they run using the `stage` field. -If an unexpected value is passed as the stage, then this error will be thrown -when attempting to use the key. - -```Lua -local Example = { - type = "SpecialKey", - kind = "Example", - stage = "discombobulate", - apply = function() ... end -} - -local folder = New "Folder" { - [Example] = "foo" -} -``` \ No newline at end of file +**Related to:** +[`Computed`](../../state/members/computed), +[`ForKeys`](../../state/members/forkeys), +[`ForValues`](../../state/members/forvalues), +[`ForPairs`](../../state/members/forpairs), +[`Contextual`](../../memory/members/contextual) + +This message means that Fusion ran a function you specified, but the function +threw an error that Fusion couldn't handle. + +The error includes a more specific message which can be used to diagnose the +issue. +
\ No newline at end of file diff --git a/src/Utility/Contextual.lua b/src/Utility/Contextual.lua index 18b9d9393..750bd7088 100644 --- a/src/Utility/Contextual.lua +++ b/src/Utility/Contextual.lua @@ -58,7 +58,7 @@ function class:is( if ok then return value else - logError("contextualCallbackError", value) + logError("callbackError", value) end end From 9f088600175b7b8d79fbe08057282dfb7e8e4c87 Mon Sep 17 00:00:00 2001 From: dphfox Date: Sun, 14 Apr 2024 10:40:53 +0100 Subject: [PATCH 260/287] doCleanup error + general conciseness --- docs/api-reference/general/errors.md | 35 +++++++++++++++++++++------- 1 file changed, 26 insertions(+), 9 deletions(-) diff --git a/docs/api-reference/general/errors.md b/docs/api-reference/general/errors.md index 9efdcce2a..ee56be812 100644 --- a/docs/api-reference/general/errors.md +++ b/docs/api-reference/general/errors.md @@ -37,9 +37,9 @@ The class type 'Foo' has no assignable property 'Bar'. [`New`](../../instances/members/new), [`Hydrate`](../../instances/members/hydrate) -This message means you tried to set a property on an instance, but the property -can't be assigned to. This could be because the property doesn't exist, or -because it's locked by Roblox to prevent edits. +You tried to set a property on an instance, but the property can't be assigned +to for some reason. This could be because the property doesn't exist, or because +it's locked by Roblox to prevent edits. !!! warning "Check your privileges" Different scripts may have different privileges - for example, plugins will @@ -60,8 +60,8 @@ The Frame class doesn't have a property called 'Foo'. **Related to:** [`OnChange`](../../instances/members/onchange) -This message means you tried to connect to a property change event, but the -property you specify doesn't exist on the instance. +You tried to connect to a property change event, but the property you specify +doesn't exist on the instance.
----- @@ -77,8 +77,8 @@ The Frame class doesn't have an event called 'Foo'. **Related to:** [`OnEvent`](../../instances/members/onevent) -This message means you tried to connect to an event on an instance, but the -event you specify doesn't exist on the instance. +You tried to connect to an event on an instance, but the event you specify +doesn't exist on the instance.
----- @@ -98,9 +98,26 @@ Error in callback: attempt to perform arithmetic (add) on number and string [`ForPairs`](../../state/members/forpairs), [`Contextual`](../../memory/members/contextual) -This message means that Fusion ran a function you specified, but the function -threw an error that Fusion couldn't handle. +Fusion ran a function you specified, but the function threw an error that Fusion +couldn't handle. The error includes a more specific message which can be used to diagnose the issue. +
+ +----- + +
+ +## cleanupWasRenamed + +``` +`Fusion.cleanup` was renamed to `Fusion.doCleanup`. This will be an error in future versions of Fusion. +``` + +**Related to:** +[`doCleanup`](../../memory/members/doCleanup) + +You attempted to use `cleanup()` in Fusion 0.3, which replaces it with the +`doCleanup()` method.
\ No newline at end of file From 2a1524c85d6d76948d3a259847931e48276df7b9 Mon Sep 17 00:00:00 2001 From: dphfox Date: Sun, 14 Apr 2024 10:50:05 +0100 Subject: [PATCH 261/287] destroyedTwice error --- docs/api-reference/general/errors.md | 39 +++++++++++++++++++++++----- 1 file changed, 33 insertions(+), 6 deletions(-) diff --git a/docs/api-reference/general/errors.md b/docs/api-reference/general/errors.md index ee56be812..9f8361408 100644 --- a/docs/api-reference/general/errors.md +++ b/docs/api-reference/general/errors.md @@ -33,7 +33,7 @@ the details for you. The class type 'Foo' has no assignable property 'Bar'. ``` -**Related to:** +**Thrown by:** [`New`](../../instances/members/new), [`Hydrate`](../../instances/members/hydrate) @@ -57,7 +57,7 @@ it's locked by Roblox to prevent edits. The Frame class doesn't have a property called 'Foo'. ``` -**Related to:** +**Thrown by:** [`OnChange`](../../instances/members/onchange) You tried to connect to a property change event, but the property you specify @@ -74,7 +74,7 @@ doesn't exist on the instance. The Frame class doesn't have an event called 'Foo'. ``` -**Related to:** +**Thrown by:** [`OnEvent`](../../instances/members/onevent) You tried to connect to an event on an instance, but the event you specify @@ -91,7 +91,7 @@ doesn't exist on the instance. Error in callback: attempt to perform arithmetic (add) on number and string ``` -**Related to:** +**Thrown by:** [`Computed`](../../state/members/computed), [`ForKeys`](../../state/members/forkeys), [`ForValues`](../../state/members/forvalues), @@ -115,9 +115,36 @@ issue. `Fusion.cleanup` was renamed to `Fusion.doCleanup`. This will be an error in future versions of Fusion. ``` -**Related to:** -[`doCleanup`](../../memory/members/doCleanup) +**Thrown by:** +[`doCleanup`](../../memory/members/docleanup) You attempted to use `cleanup()` in Fusion 0.3, which replaces it with the `doCleanup()` method. +
+ +----- + +
+ +## destroyedTwice + +``` +Attempted to destroy Computed twice; ensure you're not manually calling `:destroy()` while using scopes. See discussion #292 on GitHub for advice. +``` + +**Thrown by:** +[`Value`](../../state/members/value), +[`Computed`](../../state/members/computed), +[`Observer`](../../state/members/observer), +[`For`](../../state/members/for), +[`Spring`](../../animation/members/spring), +[`Tween`](../../animation/members/tween), + +The `:destroy()` method of the object in question was called more than once. + +This usually means you called `:destroy()` manually, which is almost never +required because Fusion's constructors always link objects to +[scopes](../../../tutorials/fundamentals/scopes). When that scope is passed to +[`doCleanup()`](../../memory/members/docleanup), the `:destroy()` method is +called on every object inside.
\ No newline at end of file From 2e6d36fcd58f72029e46027e274ee7843abcf097 Mon Sep 17 00:00:00 2001 From: dphfox Date: Sun, 14 Apr 2024 10:56:01 +0100 Subject: [PATCH 262/287] destructorRedundant error --- docs/api-reference/general/errors.md | 43 +++++++++++++++++++++++++--- 1 file changed, 39 insertions(+), 4 deletions(-) diff --git a/docs/api-reference/general/errors.md b/docs/api-reference/general/errors.md index 9f8361408..84be53572 100644 --- a/docs/api-reference/general/errors.md +++ b/docs/api-reference/general/errors.md @@ -112,7 +112,8 @@ issue. ## cleanupWasRenamed ``` -`Fusion.cleanup` was renamed to `Fusion.doCleanup`. This will be an error in future versions of Fusion. +`Fusion.cleanup` was renamed to `Fusion.doCleanup`. This will be an error in +future versions of Fusion. ``` **Thrown by:** @@ -129,16 +130,22 @@ You attempted to use `cleanup()` in Fusion 0.3, which replaces it with the ## destroyedTwice ``` -Attempted to destroy Computed twice; ensure you're not manually calling `:destroy()` while using scopes. See discussion #292 on GitHub for advice. +Attempted to destroy Computed twice; ensure you're not manually calling +`:destroy()` while using scopes. See discussion #292 on GitHub for advice. ``` **Thrown by:** [`Value`](../../state/members/value), [`Computed`](../../state/members/computed), [`Observer`](../../state/members/observer), -[`For`](../../state/members/for), +[`ForKeys`](../../state/members/forkeys), +[`ForValues`](../../state/members/forvalues), +[`ForPairs`](../../state/members/forpairs), [`Spring`](../../animation/members/spring), -[`Tween`](../../animation/members/tween), +[`Tween`](../../animation/members/tween) + +**Related discussions:** +[`#292`](https://github.com/dphfox/Fusion/discussions/292) The `:destroy()` method of the object in question was called more than once. @@ -147,4 +154,32 @@ required because Fusion's constructors always link objects to [scopes](../../../tutorials/fundamentals/scopes). When that scope is passed to [`doCleanup()`](../../memory/members/docleanup), the `:destroy()` method is called on every object inside. +
+ +----- + +
+ +## destructorRedundant + +``` +Computed destructors no longer do anything. If you wish to run code on destroy, +`table.insert` a function into the `scope` argument. See discussion #292 on +GitHub for advice. +``` + +**Thrown by:** +[`Computed`](../../state/members/computed), +[`ForKeys`](../../state/members/forkeys), +[`ForValues`](../../state/members/forvalues), +[`ForPairs`](../../state/members/forpairs) + +**Related discussions:** +[`#292`](https://github.com/dphfox/Fusion/discussions/292) + +You passed an extra parameter to the constructor, which has historically been +interpreted as a function that runs when a value is cleaned up. + +This mechanism has been replaced by +[scopes](../../../tutorials/fundamentals/scopes).
\ No newline at end of file From b521a1c462913664d1165cc5828020da81af2349 Mon Sep 17 00:00:00 2001 From: dphfox Date: Sun, 14 Apr 2024 11:04:33 +0100 Subject: [PATCH 263/287] forKeyCollision error --- docs/api-reference/general/errors.md | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/docs/api-reference/general/errors.md b/docs/api-reference/general/errors.md index 84be53572..8eeda84a8 100644 --- a/docs/api-reference/general/errors.md +++ b/docs/api-reference/general/errors.md @@ -182,4 +182,23 @@ interpreted as a function that runs when a value is cleaned up. This mechanism has been replaced by [scopes](../../../tutorials/fundamentals/scopes). +
+ +----- + +
+ +## forKeyCollision + +``` +The key '6' was returned multiple times simultaneously, which is not allowed in +`For` objects. +``` + +**Thrown by:** +[`ForKeys`](../../state/members/forkeys), +[`ForPairs`](../../state/members/forpairs) + +When called with different items from the table, the same key was returned for +both of them. This is not allowed, because keys have to be unique in a table.
\ No newline at end of file From 8b459f7e9b10abc6fa6e9528474d7146d24a5501 Mon Sep 17 00:00:00 2001 From: dphfox Date: Sun, 14 Apr 2024 11:06:55 +0100 Subject: [PATCH 264/287] invalidChangeHandler --- docs/api-reference/general/errors.md | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/docs/api-reference/general/errors.md b/docs/api-reference/general/errors.md index 8eeda84a8..47995e021 100644 --- a/docs/api-reference/general/errors.md +++ b/docs/api-reference/general/errors.md @@ -201,4 +201,23 @@ The key '6' was returned multiple times simultaneously, which is not allowed in When called with different items from the table, the same key was returned for both of them. This is not allowed, because keys have to be unique in a table. +
+ +----- + +
+ +## invalidChangeHandler + +``` +The change handler for the 'AbsoluteSize' property must be a function. +``` + +**Thrown by:** +[`OnChange`](../../instances/members/onchange) + +`OnChange` expected you to provide a function for it to run when the property +changes, but you provided something other than a function. + +For example, you might have accidentally provided `nil`.
\ No newline at end of file From 88bd5aabc31a0efc454a6477ccaded212aee447a Mon Sep 17 00:00:00 2001 From: dphfox Date: Sun, 14 Apr 2024 11:08:15 +0100 Subject: [PATCH 265/287] invalidAttributeChangeHandler error --- docs/api-reference/general/errors.md | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/docs/api-reference/general/errors.md b/docs/api-reference/general/errors.md index 47995e021..f32b381d4 100644 --- a/docs/api-reference/general/errors.md +++ b/docs/api-reference/general/errors.md @@ -219,5 +219,25 @@ The change handler for the 'AbsoluteSize' property must be a function. `OnChange` expected you to provide a function for it to run when the property changes, but you provided something other than a function. +For example, you might have accidentally provided `nil`. +
+ + +----- + +
+ +## invalidAttributeChangeHandler + +``` +The change handler for the 'Active' attribute must be a function. +``` + +**Thrown by:** +[`AttributeChange`](../../instances/members/attributechange) + +`AttributeChange` expected you to provide a function for it to run when the +attribute changes, but you provided something other than a function. + For example, you might have accidentally provided `nil`.
\ No newline at end of file From c0b31547281516e9a67848d3317c48e66f3db297 Mon Sep 17 00:00:00 2001 From: dphfox Date: Sun, 14 Apr 2024 11:09:51 +0100 Subject: [PATCH 266/287] invalidEventHandler error --- docs/api-reference/general/errors.md | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/docs/api-reference/general/errors.md b/docs/api-reference/general/errors.md index f32b381d4..ab4fec406 100644 --- a/docs/api-reference/general/errors.md +++ b/docs/api-reference/general/errors.md @@ -222,7 +222,6 @@ changes, but you provided something other than a function. For example, you might have accidentally provided `nil`.
- -----
@@ -239,5 +238,24 @@ The change handler for the 'Active' attribute must be a function. `AttributeChange` expected you to provide a function for it to run when the attribute changes, but you provided something other than a function. +For example, you might have accidentally provided `nil`. +
+ +----- + +
+ +## invalidEventHandler + +``` +The handler for the 'MouseEnter' event must be a function. +``` + +**Thrown by:** +[`OnEvent`](../../instances/members/onevent) + +`OnEvent` expected you to provide a function for it to run when the event is +fired, but you provided something other than a function. + For example, you might have accidentally provided `nil`.
\ No newline at end of file From d7b96819bc02601033fd7bb1739ff14301c03a21 Mon Sep 17 00:00:00 2001 From: dphfox Date: Sun, 14 Apr 2024 11:13:01 +0100 Subject: [PATCH 267/287] invalidPropertyType error --- docs/api-reference/general/errors.md | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/docs/api-reference/general/errors.md b/docs/api-reference/general/errors.md index ab4fec406..3718df59e 100644 --- a/docs/api-reference/general/errors.md +++ b/docs/api-reference/general/errors.md @@ -258,4 +258,23 @@ The handler for the 'MouseEnter' event must be a function. fired, but you provided something other than a function. For example, you might have accidentally provided `nil`. +
+ +----- + +
+ +## invalidPropertyType + +``` +'Frame.BackgroundColor3' expected a 'Color3' type, but got a 'Vector3' type. +``` + +**Thrown by:** +[`New`](../../instances/members/new), +[`Hydrate`](../../instances/members/hydrate) + +You attempted to assign a value to a Roblox instance's property, but the +assignment threw an error because that property doesn't accept values of that +type.
\ No newline at end of file From bd33adc44be21a6740ba715b087379b5efa0e400 Mon Sep 17 00:00:00 2001 From: dphfox Date: Sun, 14 Apr 2024 11:14:08 +0100 Subject: [PATCH 268/287] invalidRefType --- docs/api-reference/general/errors.md | 34 ++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/docs/api-reference/general/errors.md b/docs/api-reference/general/errors.md index 3718df59e..322987401 100644 --- a/docs/api-reference/general/errors.md +++ b/docs/api-reference/general/errors.md @@ -277,4 +277,38 @@ For example, you might have accidentally provided `nil`. You attempted to assign a value to a Roblox instance's property, but the assignment threw an error because that property doesn't accept values of that type. +
+ +----- + +
+ +## invalidRefType + +``` +Instance refs must be Value objects. +``` + +**Thrown by:** +[`Ref`](../../instances/members/ref) + +`Ref` expected you to give it a [value](../../state/members/value), but you gave +it something else. +
+ +----- + +
+ +## invalidRefType + +``` +Instance refs must be Value objects. +``` + +**Thrown by:** +[`Ref`](../../instances/members/ref) + +`Ref` expected you to give it a [value](../../state/members/value), but you gave +it something else.
\ No newline at end of file From 5851ee1edf8eff1c07a6966357ad55384deff0f5 Mon Sep 17 00:00:00 2001 From: dphfox Date: Sun, 14 Apr 2024 11:14:37 +0100 Subject: [PATCH 269/287] invalidOutType error --- docs/api-reference/general/errors.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/api-reference/general/errors.md b/docs/api-reference/general/errors.md index 322987401..efd327f1d 100644 --- a/docs/api-reference/general/errors.md +++ b/docs/api-reference/general/errors.md @@ -300,15 +300,15 @@ it something else.
-## invalidRefType +## invalidOutType ``` -Instance refs must be Value objects. +[Out] properties must be given Value objects. ``` **Thrown by:** -[`Ref`](../../instances/members/ref) +[`Out`](../../instances/members/out) -`Ref` expected you to give it a [value](../../state/members/value), but you gave +`Out` expected you to give it a [value](../../state/members/value), but you gave it something else.
\ No newline at end of file From 44bce0bfb2799bb6c271685fcd0f1c24749f9915 Mon Sep 17 00:00:00 2001 From: dphfox Date: Sun, 14 Apr 2024 11:15:01 +0100 Subject: [PATCH 270/287] invalidAttributeOutType error --- docs/api-reference/general/errors.md | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/docs/api-reference/general/errors.md b/docs/api-reference/general/errors.md index efd327f1d..4f7e5b0aa 100644 --- a/docs/api-reference/general/errors.md +++ b/docs/api-reference/general/errors.md @@ -311,4 +311,21 @@ it something else. `Out` expected you to give it a [value](../../state/members/value), but you gave it something else. +
+ +----- + +
+ +## invalidAttributeOutType + +``` +[AttributeOut] properties must be given Value objects. +``` + +**Thrown by:** +[`AttributeOut`](../../instances/members/attributeout) + +`AttributeOut` expected you to give it a [value](../../state/members/value), but +you gave it something else.
\ No newline at end of file From b9f1e188cf1152060ef95ae827bcd6e82318ba39 Mon Sep 17 00:00:00 2001 From: dphfox Date: Sun, 14 Apr 2024 11:17:12 +0100 Subject: [PATCH 271/287] invalidOutProperty error --- docs/api-reference/general/errors.md | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/docs/api-reference/general/errors.md b/docs/api-reference/general/errors.md index 4f7e5b0aa..cb2f547f4 100644 --- a/docs/api-reference/general/errors.md +++ b/docs/api-reference/general/errors.md @@ -328,4 +328,21 @@ it something else. `AttributeOut` expected you to give it a [value](../../state/members/value), but you gave it something else. +
+ +----- + +
+ +## invalidOutProperty + +``` +The Frame class doesn't have a property called 'MouseButton1Down'. +``` + +**Thrown by:** +[`Out`](../../instances/members/out) + +The property that you tried to output doesn't exist on the instance that `Out` +was used with.
\ No newline at end of file From ff95b19c1bc3a678c91015619c0ee126b0c0cf7d Mon Sep 17 00:00:00 2001 From: dphfox Date: Sun, 14 Apr 2024 11:20:26 +0100 Subject: [PATCH 272/287] invalidSpringSpeed/Damping errors --- docs/api-reference/general/errors.md | 34 ++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/docs/api-reference/general/errors.md b/docs/api-reference/general/errors.md index cb2f547f4..15fbb9b96 100644 --- a/docs/api-reference/general/errors.md +++ b/docs/api-reference/general/errors.md @@ -345,4 +345,38 @@ The Frame class doesn't have a property called 'MouseButton1Down'. The property that you tried to output doesn't exist on the instance that `Out` was used with. +
+ +----- + +
+ +## invalidSpringDamping + +``` +The damping ratio for a spring must be >= 0. (damping was -1.00) +``` + +**Thrown by:** +[`Spring`](../../instances/members/spring) + +You provided a damping ratio that the spring doesn't support, for example `NaN`, +or a negative damping implying negative friction. +
+ +----- + +
+ +## invalidSpringSpeed + +``` +The speed of a spring must be >= 0. (speed was NaN) +``` + +**Thrown by:** +[`Spring`](../../instances/members/spring) + +You provided a speed multiplier that the spring doesn't support, for example +`NaN` or a negative speed implying the spring moves backwards through time.
\ No newline at end of file From 840c7780c45505150b531c9b3e0700ced6e6e862 Mon Sep 17 00:00:00 2001 From: dphfox Date: Sun, 14 Apr 2024 11:23:08 +0100 Subject: [PATCH 273/287] mistypedSpringDamping/Speed errors --- docs/api-reference/general/errors.md | 34 ++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/docs/api-reference/general/errors.md b/docs/api-reference/general/errors.md index 15fbb9b96..12d9a9158 100644 --- a/docs/api-reference/general/errors.md +++ b/docs/api-reference/general/errors.md @@ -379,4 +379,38 @@ The speed of a spring must be >= 0. (speed was NaN) You provided a speed multiplier that the spring doesn't support, for example `NaN` or a negative speed implying the spring moves backwards through time. +
+ +----- + +
+ +## mistypedSpringDamping + +``` +The damping ratio for a spring must be a number. (got a string) +``` + +**Thrown by:** +[`Spring`](../../instances/members/spring) + +You provided a damping ratio that the spring couldn't understand. Damping ratio +has to be a number. +
+ +----- + +
+ +## mistypedSpringSpeed + +``` +The speed of a spring must be a number. (got a string) +``` + +**Thrown by:** +[`Spring`](../../instances/members/spring) + +You provided a speed multiplier that the spring couldn't understand. Speed has +to be a number.
\ No newline at end of file From fd807a7b5d9129bc7ffca51c46decfd42446d0af Mon Sep 17 00:00:00 2001 From: dphfox Date: Sun, 14 Apr 2024 11:25:56 +0100 Subject: [PATCH 274/287] mistypedTweenInfo error --- docs/api-reference/general/errors.md | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/docs/api-reference/general/errors.md b/docs/api-reference/general/errors.md index 12d9a9158..41c263dee 100644 --- a/docs/api-reference/general/errors.md +++ b/docs/api-reference/general/errors.md @@ -413,4 +413,21 @@ The speed of a spring must be a number. (got a string) You provided a speed multiplier that the spring couldn't understand. Speed has to be a number. +
+ +----- + +
+ +## mistypedTweenInfo + +``` +The tween info of a tween must be a TweenInfo. (got a table) +``` + +**Thrown by:** +[`Tween`](../../instances/members/tween) + +You provided an easing curve that the tween couldn't understand. The easing +curve has to be specified using Roblox's `TweenInfo` data type.
\ No newline at end of file From c06a109abf789599686b6393c0ad86788c8997e0 Mon Sep 17 00:00:00 2001 From: dphfox Date: Sun, 14 Apr 2024 11:28:16 +0100 Subject: [PATCH 275/287] mergeConflict error --- docs/api-reference/general/errors.md | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/docs/api-reference/general/errors.md b/docs/api-reference/general/errors.md index 41c263dee..4bc0a2845 100644 --- a/docs/api-reference/general/errors.md +++ b/docs/api-reference/general/errors.md @@ -430,4 +430,26 @@ The tween info of a tween must be a TweenInfo. (got a table) You provided an easing curve that the tween couldn't understand. The easing curve has to be specified using Roblox's `TweenInfo` data type. +
+ +----- + +
+ +## mergeConflict + +``` +Multiple definitions for 'Observer' found while merging. +``` + +**Thrown by:** +[`scoped`](../../memory/members/scoped) + +Fusion tried to merge together multiple tables, but a key was found in more than +one of the tables, and it's unclear which one you intended to have in the final +merged result. + +This can happen subtly with methods such as +[`scoped()`](../../memory/members/scoped) which automatically merge together all +of their arguments.
\ No newline at end of file From ade36582f97cef36cecb58f2b2c8f3bf17ad04b4 Mon Sep 17 00:00:00 2001 From: dphfox Date: Sun, 14 Apr 2024 11:31:07 +0100 Subject: [PATCH 276/287] noTaskScheduler error --- docs/api-reference/general/errors.md | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/docs/api-reference/general/errors.md b/docs/api-reference/general/errors.md index 4bc0a2845..3f8c68cd6 100644 --- a/docs/api-reference/general/errors.md +++ b/docs/api-reference/general/errors.md @@ -452,4 +452,22 @@ merged result. This can happen subtly with methods such as [`scoped()`](../../memory/members/scoped) which automatically merge together all of their arguments. +
+ +----- + +
+ +## noTaskScheduler + +``` +Fusion is not connected to an external task scheduler. +``` + +Fusion depends on a task scheduler being present to perform certain time-related +tasks such as deferral, delays, or updating animations. You'll need to define a +set of standard task scheduler functions that Fusion can use for those purposes. + +Roblox users should never see this error, as Fusion automatically connects to +Roblox's task scheduling APIs.
\ No newline at end of file From 3683e050baad7d7703ba4be90008cd35c3cf1669 Mon Sep 17 00:00:00 2001 From: dphfox Date: Sun, 14 Apr 2024 11:50:03 +0100 Subject: [PATCH 277/287] possiblyOutlives error --- docs/api-reference/general/errors.md | 47 ++++++++++++++++++++++++++++ 1 file changed, 47 insertions(+) diff --git a/docs/api-reference/general/errors.md b/docs/api-reference/general/errors.md index 3f8c68cd6..b85843f2c 100644 --- a/docs/api-reference/general/errors.md +++ b/docs/api-reference/general/errors.md @@ -470,4 +470,51 @@ set of standard task scheduler functions that Fusion can use for those purposes. Roblox users should never see this error, as Fusion automatically connects to Roblox's task scheduling APIs. +
+ +----- + +
+ +## possiblyOutlives + +``` +The Value object could be destroyed before the Computed that is use()-ing it; +review the order they're created in, and what scopes they belong to. See +discussion #292 on GitHub for advice. +``` + +**Thrown by:** +[`Spring`](../../animation/members/spring), +[`Tween`](../../animation/members/tween), +[`New`](../../instances/members/new), +[`Hydrate`](../../instances/members/hydrate), +[`Attribute`](../../instances/members/attribute), +[`AttributeOut`](../../instances/members/attributeout), +[`Out`](../../instances/members/out), +[`Ref`](../../instances/members/ref), +[`Computed`](../../state/members/computed), +[`Observer`](../../state/members/observer) + +**Related discussions:** +[`#292`](https://github.com/dphfox/Fusion/discussions/292) + +If you use an object after it's been destroyed, then your code can break. This +mainly happens when one object 'outlives' another object that it's using. + +Because [scopes](../../../tutorials/fundamentals/scopes) clean up the newest +objects first, this can happen when an old object depends on something much +newer that itself. During cleanup, a situation could arise where the newer +object is destroyed, then the older object runs code of some kind that needed +the newer object to be there. + +Fusion can check for situations like this by analysing the scopes. This message +is shown when Fusion can prove one of these situations will occur. + +There are two typical solutions: + +- If the objects should always be created and destroyed at the exact same time, +then ensure they're created in the correct order. +- Otherwise, move the objects into separate scopes, and ensure that both scopes +can exist without the other scope.
\ No newline at end of file From cc8504b2a904ad89ca7cfe955e480cf190de2fc6 Mon Sep 17 00:00:00 2001 From: dphfox Date: Sun, 14 Apr 2024 12:01:47 +0100 Subject: [PATCH 278/287] scopeMissing error --- docs/api-reference/general/errors.md | 35 +++++++++++++++++++++++++++- 1 file changed, 34 insertions(+), 1 deletion(-) diff --git a/docs/api-reference/general/errors.md b/docs/api-reference/general/errors.md index b85843f2c..aa730f6a9 100644 --- a/docs/api-reference/general/errors.md +++ b/docs/api-reference/general/errors.md @@ -517,4 +517,37 @@ There are two typical solutions: then ensure they're created in the correct order. - Otherwise, move the objects into separate scopes, and ensure that both scopes can exist without the other scope. -
\ No newline at end of file +
+ +----- + +
+ +## scopeMissing + +``` +To create Observers, provide a scope. (e.g. `myScope:Observer(watching)`). See +discussion #292 on GitHub for advice. +``` + +**Thrown by:** +[`New`](../../instances/members/new), +[`Hydrate`](../../instances/members/hydrate), +[`Value`](../../state/members/value), +[`Computed`](../../state/members/computed), +[`Observer`](../../state/members/observer), +[`ForKeys`](../../state/members/forkeys), +[`ForValues`](../../state/members/forvalues), +[`ForPairs`](../../state/members/forpairs), +[`Spring`](../../animation/members/spring), +[`Tween`](../../animation/members/tween) + +**Related discussions:** +[`#292`](https://github.com/dphfox/Fusion/discussions/292) + +You attempted to create an object without providing a +[scope](../../../tutorials/fundamentals/scopes) as the first parameter. + +Scopes are mandatory for all Fusion constructors so that Fusion knows when the +object should be destroyed. +
From 62d3b6ab2f6c8fa3dfca6a9df0795a922650d89c Mon Sep 17 00:00:00 2001 From: dphfox Date: Sun, 14 Apr 2024 12:05:18 +0100 Subject: [PATCH 279/287] springTypeMismatch error --- docs/api-reference/general/errors.md | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/docs/api-reference/general/errors.md b/docs/api-reference/general/errors.md index aa730f6a9..c33187976 100644 --- a/docs/api-reference/general/errors.md +++ b/docs/api-reference/general/errors.md @@ -551,3 +551,20 @@ You attempted to create an object without providing a Scopes are mandatory for all Fusion constructors so that Fusion knows when the object should be destroyed.
+ +----- + +
+ +## springTypeMismatch + +``` +The type 'Vector3' doesn't match the spring's type 'Color3'. +``` + +**Thrown by:** +[`Spring`](../../animation/members/spring) + +The spring expected you to provide a type matching the data type that the spring +is currently outputting. However, you provided a different data type. +
From 4abb9d9003a7f5605202c6a99693c038bbbd074d Mon Sep 17 00:00:00 2001 From: dphfox Date: Sun, 14 Apr 2024 12:08:59 +0100 Subject: [PATCH 280/287] stateGetWasRemoved errors --- docs/api-reference/general/errors.md | 30 ++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/docs/api-reference/general/errors.md b/docs/api-reference/general/errors.md index c33187976..98899bef1 100644 --- a/docs/api-reference/general/errors.md +++ b/docs/api-reference/general/errors.md @@ -568,3 +568,33 @@ The type 'Vector3' doesn't match the spring's type 'Color3'. The spring expected you to provide a type matching the data type that the spring is currently outputting. However, you provided a different data type.
+ +----- + +
+ +## stateGetWasRemoved + +``` +`StateObject:get()` has been replaced by `use()` and `peek()` - see discussion +#217 on GitHub. +``` + +**Thrown by:** +[`Value`](../../state/members/value), +[`Computed`](../../state/members/computed), +[`ForKeys`](../../state/members/forkeys), +[`ForValues`](../../state/members/forvalues), +[`ForPairs`](../../state/members/forpairs), +[`Spring`](../../animation/members/spring), +[`Tween`](../../animation/members/tween) + +**Related discussions:** +[`#217`](https://github.com/dphfox/Fusion/discussions/217) + +Older versions of Fusion let you call `:get()` directly on state objects to read +their current value and attempt to infer dependencies. + +This has been replaced by [use functions](../../state/types/use) in Fusion 0.3 +for more predictable behaviour and better support for constant values. +
From a19172bac22a0bffe8ce51bb8ca1c5c51950bb61 Mon Sep 17 00:00:00 2001 From: dphfox Date: Sun, 14 Apr 2024 12:11:04 +0100 Subject: [PATCH 281/287] unknownMessage error --- docs/api-reference/general/errors.md | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/docs/api-reference/general/errors.md b/docs/api-reference/general/errors.md index 98899bef1..b448c0bac 100644 --- a/docs/api-reference/general/errors.md +++ b/docs/api-reference/general/errors.md @@ -598,3 +598,18 @@ their current value and attempt to infer dependencies. This has been replaced by [use functions](../../state/types/use) in Fusion 0.3 for more predictable behaviour and better support for constant values.
+ +----- + +
+ +## unknownMessage + +``` +Unknown error: attempt to call a nil value +``` + +Fusion ran into a problem, but couldn't associate it with a valid type of error. +This is a fallback error type which shouldn't be seen by end users, because it +indicates that Fusion code isn't reporting errors correctly. +
From 8db6094f3d67eb7bd2b30653b4d08062dbee37d1 Mon Sep 17 00:00:00 2001 From: dphfox Date: Sun, 14 Apr 2024 12:14:09 +0100 Subject: [PATCH 282/287] unrecognisedChildType error --- docs/api-reference/general/errors.md | 71 ++++++++++++++++++---------- 1 file changed, 45 insertions(+), 26 deletions(-) diff --git a/docs/api-reference/general/errors.md b/docs/api-reference/general/errors.md index b448c0bac..cca8ecaca 100644 --- a/docs/api-reference/general/errors.md +++ b/docs/api-reference/general/errors.md @@ -34,8 +34,8 @@ The class type 'Foo' has no assignable property 'Bar'. ``` **Thrown by:** -[`New`](../../instances/members/new), -[`Hydrate`](../../instances/members/hydrate) +[`New`](../../roblox/members/new), +[`Hydrate`](../../roblox/members/hydrate) You tried to set a property on an instance, but the property can't be assigned to for some reason. This could be because the property doesn't exist, or because @@ -58,7 +58,7 @@ The Frame class doesn't have a property called 'Foo'. ``` **Thrown by:** -[`OnChange`](../../instances/members/onchange) +[`OnChange`](../../roblox/members/onchange) You tried to connect to a property change event, but the property you specify doesn't exist on the instance. @@ -75,7 +75,7 @@ The Frame class doesn't have an event called 'Foo'. ``` **Thrown by:** -[`OnEvent`](../../instances/members/onevent) +[`OnEvent`](../../roblox/members/onevent) You tried to connect to an event on an instance, but the event you specify doesn't exist on the instance. @@ -214,7 +214,7 @@ The change handler for the 'AbsoluteSize' property must be a function. ``` **Thrown by:** -[`OnChange`](../../instances/members/onchange) +[`OnChange`](../../roblox/members/onchange) `OnChange` expected you to provide a function for it to run when the property changes, but you provided something other than a function. @@ -233,7 +233,7 @@ The change handler for the 'Active' attribute must be a function. ``` **Thrown by:** -[`AttributeChange`](../../instances/members/attributechange) +[`AttributeChange`](../../roblox/members/attributechange) `AttributeChange` expected you to provide a function for it to run when the attribute changes, but you provided something other than a function. @@ -252,7 +252,7 @@ The handler for the 'MouseEnter' event must be a function. ``` **Thrown by:** -[`OnEvent`](../../instances/members/onevent) +[`OnEvent`](../../roblox/members/onevent) `OnEvent` expected you to provide a function for it to run when the event is fired, but you provided something other than a function. @@ -271,8 +271,8 @@ For example, you might have accidentally provided `nil`. ``` **Thrown by:** -[`New`](../../instances/members/new), -[`Hydrate`](../../instances/members/hydrate) +[`New`](../../roblox/members/new), +[`Hydrate`](../../roblox/members/hydrate) You attempted to assign a value to a Roblox instance's property, but the assignment threw an error because that property doesn't accept values of that @@ -290,7 +290,7 @@ Instance refs must be Value objects. ``` **Thrown by:** -[`Ref`](../../instances/members/ref) +[`Ref`](../../roblox/members/ref) `Ref` expected you to give it a [value](../../state/members/value), but you gave it something else. @@ -307,7 +307,7 @@ it something else. ``` **Thrown by:** -[`Out`](../../instances/members/out) +[`Out`](../../roblox/members/out) `Out` expected you to give it a [value](../../state/members/value), but you gave it something else. @@ -324,7 +324,7 @@ it something else. ``` **Thrown by:** -[`AttributeOut`](../../instances/members/attributeout) +[`AttributeOut`](../../roblox/members/attributeout) `AttributeOut` expected you to give it a [value](../../state/members/value), but you gave it something else. @@ -341,7 +341,7 @@ The Frame class doesn't have a property called 'MouseButton1Down'. ``` **Thrown by:** -[`Out`](../../instances/members/out) +[`Out`](../../roblox/members/out) The property that you tried to output doesn't exist on the instance that `Out` was used with. @@ -358,7 +358,7 @@ The damping ratio for a spring must be >= 0. (damping was -1.00) ``` **Thrown by:** -[`Spring`](../../instances/members/spring) +[`Spring`](../../roblox/members/spring) You provided a damping ratio that the spring doesn't support, for example `NaN`, or a negative damping implying negative friction. @@ -375,7 +375,7 @@ The speed of a spring must be >= 0. (speed was NaN) ``` **Thrown by:** -[`Spring`](../../instances/members/spring) +[`Spring`](../../roblox/members/spring) You provided a speed multiplier that the spring doesn't support, for example `NaN` or a negative speed implying the spring moves backwards through time. @@ -392,7 +392,7 @@ The damping ratio for a spring must be a number. (got a string) ``` **Thrown by:** -[`Spring`](../../instances/members/spring) +[`Spring`](../../roblox/members/spring) You provided a damping ratio that the spring couldn't understand. Damping ratio has to be a number. @@ -409,7 +409,7 @@ The speed of a spring must be a number. (got a string) ``` **Thrown by:** -[`Spring`](../../instances/members/spring) +[`Spring`](../../roblox/members/spring) You provided a speed multiplier that the spring couldn't understand. Speed has to be a number. @@ -426,7 +426,7 @@ The tween info of a tween must be a TweenInfo. (got a table) ``` **Thrown by:** -[`Tween`](../../instances/members/tween) +[`Tween`](../../roblox/members/tween) You provided an easing curve that the tween couldn't understand. The easing curve has to be specified using Roblox's `TweenInfo` data type. @@ -487,12 +487,12 @@ discussion #292 on GitHub for advice. **Thrown by:** [`Spring`](../../animation/members/spring), [`Tween`](../../animation/members/tween), -[`New`](../../instances/members/new), -[`Hydrate`](../../instances/members/hydrate), -[`Attribute`](../../instances/members/attribute), -[`AttributeOut`](../../instances/members/attributeout), -[`Out`](../../instances/members/out), -[`Ref`](../../instances/members/ref), +[`New`](../../roblox/members/new), +[`Hydrate`](../../roblox/members/hydrate), +[`Attribute`](../../roblox/members/attribute), +[`AttributeOut`](../../roblox/members/attributeout), +[`Out`](../../roblox/members/out), +[`Ref`](../../roblox/members/ref), [`Computed`](../../state/members/computed), [`Observer`](../../state/members/observer) @@ -531,8 +531,8 @@ discussion #292 on GitHub for advice. ``` **Thrown by:** -[`New`](../../instances/members/new), -[`Hydrate`](../../instances/members/hydrate), +[`New`](../../roblox/members/new), +[`Hydrate`](../../roblox/members/hydrate), [`Value`](../../state/members/value), [`Computed`](../../state/members/computed), [`Observer`](../../state/members/observer), @@ -613,3 +613,22 @@ Fusion ran into a problem, but couldn't associate it with a valid type of error. This is a fallback error type which shouldn't be seen by end users, because it indicates that Fusion code isn't reporting errors correctly.
+ +----- + +
+ +## unrecognisedChildType + +``` +'string' type children aren't accepted by `[Children]`. +``` + +**Thrown by:** +[`Children`](../../roblox/members/children) + +You provided a value inside of `[Children]` which didn't meet the definition of +a [child](../../roblox/types/child) value. Check that you're only passing +instances, arrays and state objects. +
+ From cc303d14ddfb44bfac264679b25a761504607056 Mon Sep 17 00:00:00 2001 From: dphfox Date: Sun, 14 Apr 2024 12:17:55 +0100 Subject: [PATCH 283/287] unrecognisedPropertyKey error --- docs/api-reference/general/errors.md | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/docs/api-reference/general/errors.md b/docs/api-reference/general/errors.md index cca8ecaca..c4608441b 100644 --- a/docs/api-reference/general/errors.md +++ b/docs/api-reference/general/errors.md @@ -632,3 +632,25 @@ a [child](../../roblox/types/child) value. Check that you're only passing instances, arrays and state objects.
+----- + +
+ +## unrecognisedPropertyKey + +``` +'number' keys aren't accepted in property tables. +``` + +**Thrown by:** +[`New`](../../roblox/members/new), +[`Hydrate`](../../roblox/members/hydrate) + +You provided something other than a property assignment (`Property = Value`) or +[special key](../../roblox/types/specialkey) in your property table. + +Most commonly, this means you tried to add child instances directly into the +property table, rather than passing them into the +[`[Children]`](../../roblox/members/children) special key. +
+ From 37190ca56c4c2c21e3516bb3293b147bfc893761 Mon Sep 17 00:00:00 2001 From: dphfox Date: Sun, 14 Apr 2024 12:20:26 +0100 Subject: [PATCH 284/287] unrecognisedPropertyStage error --- docs/api-reference/general/errors.md | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/docs/api-reference/general/errors.md b/docs/api-reference/general/errors.md index c4608441b..d9a20c8a6 100644 --- a/docs/api-reference/general/errors.md +++ b/docs/api-reference/general/errors.md @@ -654,3 +654,23 @@ property table, rather than passing them into the [`[Children]`](../../roblox/members/children) special key.
+ +----- + +
+ +## unrecognisedPropertyStage + +``` +'children' isn't a valid stage for a special key to be applied at. +``` + +**Thrown by:** +[`New`](../../roblox/members/new), +[`Hydrate`](../../roblox/members/hydrate) + +You attempted to use a [special key](../../roblox/types/specialkey) which has a +misconfigured `stage`, so Fusion didn't know when to apply it during instance +construction. +
+ From 45b6598416118874c12bfb04b4d5c95816a380ca Mon Sep 17 00:00:00 2001 From: dphfox Date: Sun, 14 Apr 2024 12:25:04 +0100 Subject: [PATCH 285/287] useAfterDestroy error --- docs/api-reference/general/errors.md | 34 ++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/docs/api-reference/general/errors.md b/docs/api-reference/general/errors.md index d9a20c8a6..4e50f1460 100644 --- a/docs/api-reference/general/errors.md +++ b/docs/api-reference/general/errors.md @@ -674,3 +674,37 @@ misconfigured `stage`, so Fusion didn't know when to apply it during instance construction.
+----- + +
+ +## useAfterDestroy + +``` +The Value object is no longer valid - it was destroyed before the Computed that +is use()-ing. See discussion #292 on GitHub for advice. +``` + +**Thrown by:** +[`Spring`](../../animation/members/spring), +[`Tween`](../../animation/members/tween), +[`New`](../../roblox/members/new), +[`Hydrate`](../../roblox/members/hydrate), +[`Attribute`](../../roblox/members/attribute), +[`AttributeOut`](../../roblox/members/attributeout), +[`Out`](../../roblox/members/out), +[`Ref`](../../roblox/members/ref), +[`Computed`](../../state/members/computed), +[`Observer`](../../state/members/observer) + +**Related discussions:** +[`#292`](https://github.com/dphfox/Fusion/discussions/292) + +Your code attempted to access an object after that object was destroyed, either +because its `:destroy()` method was called manually, or because the object's +[scope](../../../tutorials/fundamentals/scope) was cleaned up. + +Make sure your objects are being added to the correct scopes according to when +you expect them to be destroyed. Additionally, make sure your code can detect +and deal with situations where other objects are no longer available. +
From 6fc2abb2eabe050acbf1d1469e96903fe7d7f81c Mon Sep 17 00:00:00 2001 From: dphfox Date: Sun, 14 Apr 2024 13:42:59 +0100 Subject: [PATCH 286/287] Last API bits --- docs/api-reference/general/errors.md | 2 +- docs/api-reference/index.md | 166 ++++++++++++++------------- docs/api-reference/roblox/hydrate.md | 97 ---------------- docs/api-reference/roblox/new.md | 110 ------------------ docs/assets/theme/api-reference.css | 48 ++++++-- docs/assets/theme/paragraph.css | 9 -- 6 files changed, 125 insertions(+), 307 deletions(-) delete mode 100644 docs/api-reference/roblox/hydrate.md delete mode 100644 docs/api-reference/roblox/new.md diff --git a/docs/api-reference/general/errors.md b/docs/api-reference/general/errors.md index 4e50f1460..ea6edd81b 100644 --- a/docs/api-reference/general/errors.md +++ b/docs/api-reference/general/errors.md @@ -21,7 +21,7 @@ the details for you. placeholder="Type or paste an error ID here..." /> - + ----- diff --git a/docs/api-reference/index.md b/docs/api-reference/index.md index ef6d39cec..c2c1756f5 100644 --- a/docs/api-reference/index.md +++ b/docs/api-reference/index.md @@ -7,118 +7,124 @@ title: API Reference Welcome to the API Reference! This is where you can find more technical documentation about what the Fusion library provides. -This isn't a beginner's guide. For a guided experience, [try the tutorials!](../tutorials/) +For a beginner-friendly experience, [try the tutorials.](../tutorials/) ------ +## Most Popular -## Navigation +
-Using the sidebar on the left, you can find API members grouped by category. -Alternatively, you can search for APIs using the search box at the top of the -page. - ------ - -## Commonly Used - -
- -
+ -
-### Memory - - :octicons-code-16: - doCleanup - :octicons-chevron-right-16: + -
-### State - - :octicons-package-16: - Computed - :octicons-chevron-right-16: - - - - :octicons-package-16: - ForKeys - :octicons-chevron-right-16: + -
-### Instances - - :octicons-code-16: + -
-### Animation - - :octicons-package-16: + diff --git a/docs/api-reference/roblox/hydrate.md b/docs/api-reference/roblox/hydrate.md deleted file mode 100644 index 50d3445f3..000000000 --- a/docs/api-reference/roblox/hydrate.md +++ /dev/null @@ -1,97 +0,0 @@ - - -

- :octicons-code-24: - Hydrate - - function - -

- -Given an instance, returns a [component](./component.md) which modifies that -instance. The property table may specify properties to set on the instance, or -include [special keys](./specialkey.md) for more advanced operations. - -```Lua -(target: Instance) -> Component -``` - ------ - -## Parameters - -- `target` - the instance which the component should modify - ------ - -## Returns - -A component function. When called, it populates the target instance using the -property table, then returns the target instance. - ------ - -## Example Usage - -```Lua -local myButton: TextButton = Hydrate(PlayerGui.ScreenGui.TextButton) { - Position = UDim2.fromScale(.5, .5), - AnchorPoint = Vector2.new(.5, .5), - Size = UDim2.fromOffset(200, 50), - - Text = "Hello, world!", - - [OnEvent "Activated"] = function() - print("The button was clicked!") - end, - - [OnChange "Name"] = function(newName) - print("The button was renamed to:", newName) - end, - - [Children] = New "UICorner" { - CornerRadius = UDim.new(0, 8) - } -} -``` - ------ - -## Property Table Processing - -The `props` table uses a mix of string and special keys to specify attributes of -the instance which should be set. - -String keys are treated as property declarations - values passed in will be set -upon the instance: - -```Lua -local example = Hydrate(workspace.Part) { - -- sets the Position property - Position = Vector3.new(1, 2, 3) -} -``` - -Passing a state object to a string key will bind the property value; when the -value of the object changes, the property will update to match on the next -resumption step: - -```Lua -local myName = Value("Bob") - -local example = Hydrate(workspace.Part) { - -- initially, the Name will be set to Bob - Name = myName -} - --- change the state object to store "John" --- on the next resumption step, the part's Name will change to John -myName:set("John") -``` - -Special keys, such as [Children](./children.md) or [OnEvent](./onevent.md), may -also be used as keys in the property table. For more information about how -special keys work, [see the SpecialKey page.](./specialkey.md) \ No newline at end of file diff --git a/docs/api-reference/roblox/new.md b/docs/api-reference/roblox/new.md deleted file mode 100644 index a1b40fa3e..000000000 --- a/docs/api-reference/roblox/new.md +++ /dev/null @@ -1,110 +0,0 @@ - - -

- :octicons-code-24: - New - - function - -

- -Given a class name, returns a [component](./component.md) which creates -instances of that class. The property table may specify properties to set on the -instance, or include [special keys](./specialkey.md) for more advanced -operations. - -```Lua -(className: string) -> Component -``` - ------ - -## Parameters - -- `className` - the instance class that should be created - ------ - -## Returns - -A component function. When called, it creates a new instance of the given class, -populates it using the property table, and returns it. - ------ - -## Example Usage - -```Lua -local myButton: TextButton = New "TextButton" { - Parent = Players.LocalPlayer.PlayerGui, - - Position = UDim2.fromScale(.5, .5), - AnchorPoint = Vector2.new(.5, .5), - Size = UDim2.fromOffset(200, 50), - - Text = "Hello, world!", - - [OnEvent "Activated"] = function() - print("The button was clicked!") - end, - - [OnChange "Name"] = function(newName) - print("The button was renamed to:", newName) - end, - - [Children] = New "UICorner" { - CornerRadius = UDim.new(0, 8) - } -} -``` - ------ - -## Property Table Processing - -The `props` table uses a mix of string and special keys to specify attributes of -the instance which should be set. - -String keys are treated as property declarations - values passed in will be set -upon the instance: - -```Lua -local example = New "Part" { - -- sets the Position property - Position = Vector3.new(1, 2, 3) -} -``` - -Passing a state object to a string key will bind the property value; when the -value of the object changes, the property will update to match on the next -resumption step: - -```Lua -local myName = State("Bob") - -local example = New "Part" { - -- initially, the Name will be set to Bob - Name = myName -} - --- change the state object to store "John" --- on the next resumption step, the part's Name will change to John -myName:set("John") -``` - -Special keys, such as [Children](./children.md) or [OnEvent](./onevent.md), may -also be used as keys in the property table. For more information about how -special keys work, [see the SpecialKey page.](./specialkey.md) - ------ - -## Default Properties - -The `New` function provides its own set of 'sensible default' property values -for some class types, which will be used in place of Roblox defaults. This is -done to opt out of some legacy features and unhelpful defaults. - -[You can see the default properties Fusion uses here.](https://github.com/Elttob/Fusion/blob/main/src/Instances/defaultProps.lua) \ No newline at end of file diff --git a/docs/assets/theme/api-reference.css b/docs/assets/theme/api-reference.css index ab8a5cc30..df32bd7f2 100644 --- a/docs/assets/theme/api-reference.css +++ b/docs/assets/theme/api-reference.css @@ -43,6 +43,43 @@ margin: 0 0.25rem; } +.fusiondoc-error-api-section { + opacity: 1; + transition: opacity 0.4s ease, filter 0.4s ease; +} + +.fusiondoc-error-api-section.fusiondoc-error-api-section-defocus { + opacity: 0.4; + filter: blur(0.25rem); + pointer-events: none; +} + +.fusiondoc-api-bento { + column-width: 12rem; + column-gap: 0.5rem; +} + +.fusiondoc-api-bento > * { + break-inside: avoid; + background-color: var(--fusiondoc-bg-2); + box-shadow: var(--md-shadow-z1); + border-radius: 0.25rem; + padding: 0.75rem 1rem; + padding-bottom: 0.01rem; + margin: 0.5rem 0; + margin-top: 0; +} + +.fusiondoc-api-bento > * :first-child { + margin-top: 0 !important; +} + +.fusiondoc-api-bento > * > h3 { + font-size: 0.7rem; + color: var(--fusiondoc-fg-3); + font-weight: 700; +} + .fusiondoc-api-index-header { color: var(--fusiondoc-fg-1) !important; } @@ -65,20 +102,11 @@ } .fusiondoc-api-index-link > .fusiondoc-api-index-arrow { + margin-bottom: -0.3em; transform: translateX(0rem); transition: transform 0.2s ease; } .fusiondoc-api-index-link:hover > .fusiondoc-api-index-arrow { transform: translateX(0.5rem); -} - -.fusiondoc-error-api-section { - opacity: 1; - transition: opacity 0.4s ease; -} - -.fusiondoc-error-api-section.fusiondoc-error-api-section-defocus { - opacity: 0.4; - transition: opacity 0.4s ease; } \ No newline at end of file diff --git a/docs/assets/theme/paragraph.css b/docs/assets/theme/paragraph.css index a853eab0e..a3937e819 100644 --- a/docs/assets/theme/paragraph.css +++ b/docs/assets/theme/paragraph.css @@ -86,13 +86,4 @@ h6 > .twemoji:first-child { font-style: normal; font-size: 0.8em; letter-spacing: 0.02em; -} - -.fusiondoc-index-multicol { - column-width: 15em; -} - -.fusiondoc-index-multicol .fusiondoc-index-multicol-section { - break-inside: avoid; - padding-top: 0.001em; /* prevent margins collapsing */ } \ No newline at end of file From 41da7bc8f6982888446400f0e68f147ece8514b1 Mon Sep 17 00:00:00 2001 From: dphfox Date: Sun, 14 Apr 2024 13:57:50 +0100 Subject: [PATCH 287/287] Final few errors --- docs/api-reference/general/errors.md | 186 ++++++++++++++++----------- 1 file changed, 114 insertions(+), 72 deletions(-) diff --git a/docs/api-reference/general/errors.md b/docs/api-reference/general/errors.md index ea6edd81b..d72ae735c 100644 --- a/docs/api-reference/general/errors.md +++ b/docs/api-reference/general/errors.md @@ -27,6 +27,30 @@ the details for you.
+## callbackError + +``` +Error in callback: attempt to perform arithmetic (add) on number and string +``` + +**Thrown by:** +[`Computed`](../../state/members/computed), +[`ForKeys`](../../state/members/forkeys), +[`ForValues`](../../state/members/forvalues), +[`ForPairs`](../../state/members/forpairs), +[`Contextual`](../../memory/members/contextual) + +Fusion ran a function you specified, but the function threw an error that Fusion +couldn't handle. + +The error includes a more specific message which can be used to diagnose the +issue. +
+ +----- + +
+ ## cannotAssignProperty ``` @@ -85,24 +109,21 @@ doesn't exist on the instance.
-## callbackError +## cannotCreateClass ``` -Error in callback: attempt to perform arithmetic (add) on number and string +Can't create a new instance of class 'EditableImage'. ``` **Thrown by:** -[`Computed`](../../state/members/computed), -[`ForKeys`](../../state/members/forkeys), -[`ForValues`](../../state/members/forvalues), -[`ForPairs`](../../state/members/forpairs), -[`Contextual`](../../memory/members/contextual) +[`New`](../../roblox/members/new) -Fusion ran a function you specified, but the function threw an error that Fusion -couldn't handle. +You attempted to create a type of instance that Fusion can't create. -The error includes a more specific message which can be used to diagnose the -issue. +!!! warning "Beta features" + Some instances are only creatable when you have certain Studio betas + enabled. Check your Beta Features tab to ensure that beta features aren't + causing the issue.
----- @@ -207,17 +228,17 @@ both of them. This is not allowed, because keys have to be unique in a table.
-## invalidChangeHandler +## invalidAttributeChangeHandler ``` -The change handler for the 'AbsoluteSize' property must be a function. +The change handler for the 'Active' attribute must be a function. ``` **Thrown by:** -[`OnChange`](../../roblox/members/onchange) +[`AttributeChange`](../../roblox/members/attributechange) -`OnChange` expected you to provide a function for it to run when the property -changes, but you provided something other than a function. +`AttributeChange` expected you to provide a function for it to run when the +attribute changes, but you provided something other than a function. For example, you might have accidentally provided `nil`.
@@ -226,36 +247,34 @@ For example, you might have accidentally provided `nil`.
-## invalidAttributeChangeHandler +## invalidAttributeOutType ``` -The change handler for the 'Active' attribute must be a function. +[AttributeOut] properties must be given Value objects. ``` **Thrown by:** -[`AttributeChange`](../../roblox/members/attributechange) - -`AttributeChange` expected you to provide a function for it to run when the -attribute changes, but you provided something other than a function. +[`AttributeOut`](../../roblox/members/attributeout) -For example, you might have accidentally provided `nil`. +`AttributeOut` expected you to give it a [value](../../state/members/value), but +you gave it something else.
-----
-## invalidEventHandler +## invalidChangeHandler ``` -The handler for the 'MouseEnter' event must be a function. +The change handler for the 'AbsoluteSize' property must be a function. ``` **Thrown by:** -[`OnEvent`](../../roblox/members/onevent) +[`OnChange`](../../roblox/members/onchange) -`OnEvent` expected you to provide a function for it to run when the event is -fired, but you provided something other than a function. +`OnChange` expected you to provide a function for it to run when the property +changes, but you provided something other than a function. For example, you might have accidentally provided `nil`.
@@ -264,36 +283,36 @@ For example, you might have accidentally provided `nil`.
-## invalidPropertyType +## invalidEventHandler ``` -'Frame.BackgroundColor3' expected a 'Color3' type, but got a 'Vector3' type. +The handler for the 'MouseEnter' event must be a function. ``` **Thrown by:** -[`New`](../../roblox/members/new), -[`Hydrate`](../../roblox/members/hydrate) +[`OnEvent`](../../roblox/members/onevent) -You attempted to assign a value to a Roblox instance's property, but the -assignment threw an error because that property doesn't accept values of that -type. +`OnEvent` expected you to provide a function for it to run when the event is +fired, but you provided something other than a function. + +For example, you might have accidentally provided `nil`.
-----
-## invalidRefType +## invalidOutProperty ``` -Instance refs must be Value objects. +The Frame class doesn't have a property called 'MouseButton1Down'. ``` **Thrown by:** -[`Ref`](../../roblox/members/ref) +[`Out`](../../roblox/members/out) -`Ref` expected you to give it a [value](../../state/members/value), but you gave -it something else. +The property that you tried to output doesn't exist on the instance that `Out` +was used with.
----- @@ -317,34 +336,36 @@ it something else.
-## invalidAttributeOutType +## invalidPropertyType ``` -[AttributeOut] properties must be given Value objects. +'Frame.BackgroundColor3' expected a 'Color3' type, but got a 'Vector3' type. ``` **Thrown by:** -[`AttributeOut`](../../roblox/members/attributeout) +[`New`](../../roblox/members/new), +[`Hydrate`](../../roblox/members/hydrate) -`AttributeOut` expected you to give it a [value](../../state/members/value), but -you gave it something else. +You attempted to assign a value to a Roblox instance's property, but the +assignment threw an error because that property doesn't accept values of that +type.
-----
-## invalidOutProperty +## invalidRefType ``` -The Frame class doesn't have a property called 'MouseButton1Down'. +Instance refs must be Value objects. ``` **Thrown by:** -[`Out`](../../roblox/members/out) +[`Ref`](../../roblox/members/ref) -The property that you tried to output doesn't exist on the instance that `Out` -was used with. +`Ref` expected you to give it a [value](../../state/members/value), but you gave +it something else.
----- @@ -385,6 +406,28 @@ You provided a speed multiplier that the spring doesn't support, for example
+## mergeConflict + +``` +Multiple definitions for 'Observer' found while merging. +``` + +**Thrown by:** +[`scoped`](../../memory/members/scoped) + +Fusion tried to merge together multiple tables, but a key was found in more than +one of the tables, and it's unclear which one you intended to have in the final +merged result. + +This can happen subtly with methods such as +[`scoped()`](../../memory/members/scoped) which automatically merge together all +of their arguments. +
+ +----- + +
+ ## mistypedSpringDamping ``` @@ -436,28 +479,6 @@ curve has to be specified using Roblox's `TweenInfo` data type.
-## mergeConflict - -``` -Multiple definitions for 'Observer' found while merging. -``` - -**Thrown by:** -[`scoped`](../../memory/members/scoped) - -Fusion tried to merge together multiple tables, but a key was found in more than -one of the tables, and it's unclear which one you intended to have in the final -merged result. - -This can happen subtly with methods such as -[`scoped()`](../../memory/members/scoped) which automatically merge together all -of their arguments. -
- ------ - -
- ## noTaskScheduler ``` @@ -523,6 +544,27 @@ can exist without the other scope.
+## propertySetError + +``` +Error setting property: UIAspectRatioConstraint.AspectRatio set to a +non-positive value. Value must be a positive. +``` + +**Thrown by:** +[`New`](../../roblox/members/new), +[`Hydrate`](../../roblox/members/hydrate) + +You attempted to set a property, but Roblox threw an error in response. + +The error includes a more specific message which can be used to diagnose the +issue. +
+ +----- + +
+ ## scopeMissing ```