Fusion icon indicating copy to clipboard operation
Fusion copied to clipboard

Allows developers to specify limits on internal consistency

Open dphfox opened this issue 1 year ago • 1 comments

Fusion currently mandates that the entire reactive graph is kept internally consistent, and always updates in a single atomic step. This is great for developer experience, and should absolutely remain default behaviour, but sometimes it can be okay to sacrifice consistency to allow work to be split across multiple update cycles.

One way we could explore this is by introducing wrapper state objects that are allowed to become inconsistent with the rest of the graph, retaining their previous value when they're evaluated as part of an update step. If there's time to spare within the update step, these objects can pull a new value from the state object they wrap and update their dependents.

A simple example:

local a = scope:Value(2)
local b = scope:Value(5)
local sum = scope:Computed(function(use)
    return use(a) + use(b)
end)
local sum = scope:Inconsistent(sum)
local sumIsEven = scope:Computed(function(use)
    return use(sum) % 2 == 0
end
print(peek(sumIsEven)) --> false

If the update cycle is taking too long, the Inconsistent object is allowed to fall out of lockstep with the rest of the reactive graph, meaning graph objects dependent on it won't run:

doLotsOfWork()
a:set(10)
b:set(12)
print(peek(sumIsEven)) --> false
task.wait(1) -- wait for intensity to die down
print(peek(sumIsEven)) --> true

dphfox avatar Jun 21 '24 18:06 dphfox

We must be incredibly careful around object lifetimes.

  • For the first version of this feature (or any related feature looking to add "memory" to the reactive graph), we should limit the acceptable inputs only to things that we can guarantee won't ever end up with accessible dangling references.
    • OK: Primitives with value semantics (nil, boolean, string, number, vector)
    • OK: Roblox atomics built from primitives with value semantics (Vector2, Color3, UDim2, RbxScriptSignal, RbxScriptConnection, etc...)
    • Not OK: Tables (both frozen and unfrozen) (can point to dangling references via pairs or metatable)
    • Not OK: Arbitrary userdatas (can point to dangling references via metatable)
    • Not OK: Roblox instances (entire thing becomes a dangling reference after :Destroy())
    • Not OK: Functions (dangling references accessible inside the function chunk via upvalues)
  • Concerns about how to handle destructible inputs are left until a later date for now.

This opt-in design does mean that a subgraph must be completely isolated for it to be effectively severed from instant updates. You also can't guarantee that the whole subgraph update in one go, because the objects aren't in sync with each other.

dphblox avatar Oct 01 '24 00:10 dphblox