From f395e0db7363883ddbb0cad1d6411f65a0c442fe Mon Sep 17 00:00:00 2001 From: gaymeowing <62822174+gaymeowing@users.noreply.github.com> Date: Fri, 26 Sep 2025 13:04:52 -0400 Subject: [PATCH 01/14] initial commit --- modules/react/src/ReactBinding.roblox.lua | 287 +++++++++++----------- 1 file changed, 141 insertions(+), 146 deletions(-) diff --git a/modules/react/src/ReactBinding.roblox.lua b/modules/react/src/ReactBinding.roblox.lua index db15415c..fdab32db 100644 --- a/modules/react/src/ReactBinding.roblox.lua +++ b/modules/react/src/ReactBinding.roblox.lua @@ -16,218 +16,213 @@ local Packages = script.Parent.Parent +local createSignal = require(script.Parent["createSignal.roblox"]) local ReactGlobals = require(Packages.ReactGlobals) local LuauPolyfill = require(Packages.LuauPolyfill) local ReactSymbols = require(Packages.Shared).ReactSymbols - local ReactTypes = require(Packages.Shared) -type Binding = ReactTypes.ReactBinding -type BindingUpdater = ReactTypes.ReactBindingUpdater - -local Symbol = LuauPolyfill.Symbol -local createSignal = require(script.Parent["createSignal.roblox"]) - -local BindingImpl = Symbol("BindingImpl") - -type BindingInternal = { - ["$$typeof"]: typeof(ReactSymbols.REACT_BINDING_TYPE), - value: T, - getValue: (BindingInternal) -> T, - -- FIXME Luau: can't define recursive types with different parameters - map: (BindingInternal, (T) -> U) -> any, +local BASE_BINDING_PROTOTYPE = {} +do + BASE_BINDING_PROTOTYPE["$$typeof"] = ReactSymbols.REACT_BINDING_TYPE + BASE_BINDING_PROTOTYPE.__index = BASE_BINDING_PROTOTYPE - update: (T) -> (), - subscribe: ((T) -> ()) -> () -> (), -} - -local BindingInternalApi = {} - -local bindingPrototype = {} - -function bindingPrototype.getValue(binding: BindingInternal): T - return BindingInternalApi.getValue(binding) + function BASE_BINDING_PROTOTYPE.__tostring(baseBinding) + return `RoactBinding({baseBinding:getValue()})` + end end -function bindingPrototype.map( - binding: BindingInternal, - predicate: (T) -> U -): Binding - return BindingInternalApi.map(binding, predicate) +local function IS_BINDING(value: unknown): boolean + -- stylua: ignore + return type(value) == "table" + and value["$$typeof"] == ReactSymbols.REACT_BINDING_TYPE end -local BindingPublicMeta = { - __index = bindingPrototype, - __tostring = function(self) - return string.format("RoactBinding(%s)", tostring(self:getValue())) - end, -} +local ReactBinding = {} -function BindingInternalApi.update(binding: any, newValue: T) - return (binding[BindingImpl] :: BindingInternal).update(newValue) -end +do -- create + local bindingPrototype = setmetatable({}, BASE_BINDING_PROTOTYPE) + bindingPrototype.__index = bindingPrototype -function BindingInternalApi.subscribe(binding: any, callback: (T) -> ()) - return (binding[BindingImpl] :: BindingInternal).subscribe(callback) -end - -function BindingInternalApi.getValue(binding: any): T - return (binding[BindingImpl] :: BindingInternal):getValue() -end - -function BindingInternalApi.create(initialValue: T): (Binding, BindingUpdater) - local subscribe, fire = createSignal() - local impl = { - value = initialValue, - subscribe = subscribe, - } + function bindingPrototype.update(binding, newValue) + binding.value = newValue + binding._fire(newValue) + end - function impl.update(newValue: T) - impl.value = newValue - fire(newValue) + function bindingPrototype.getValue(binding) + return binding.value end - function impl.getValue() - return impl.value + function bindingPrototype.subscribe(binding, callback) + return binding._subscribe(callback) end - local source - if ReactGlobals.__DEV__ then - -- ROBLOX TODO: LUAFDN-619 - improve debug stacktraces for bindings - source = debug.traceback("Binding created at:", 3) + function ReactBinding.create(initialValue: T): (Binding, BindingUpdater) + local subscribe, fire = createSignal() + local source + + if ReactGlobals.__DEV__ then + -- ROBLOX TODO: LUAFDN-619 - improve debug stacktraces for bindings + source = debug.traceback("Binding created at:", 3) + end + + local binding = setmetatable({ + _subscribe = subscribe, + value = initialValue, + _source = source, + _fire = fire, + }, bindingPrototype) + + local function setAndFire(newValue) + binding.value = newValue + fire(newValue) + end + + return binding, setAndFire end - return (setmetatable({ - ["$$typeof"] = ReactSymbols.REACT_BINDING_TYPE, - [BindingImpl] = impl, - _source = source, - }, BindingPublicMeta) :: any) :: Binding, - impl.update + table.freeze(bindingPrototype) end -function BindingInternalApi.map( - upstreamBinding: BindingInternal, - predicate: (T) -> U -): Binding - if ReactGlobals.__DEV__ then - -- ROBLOX TODO: More informative error messages here - assert( - typeof(upstreamBinding) == "table" - and upstreamBinding["$$typeof"] == ReactSymbols.REACT_BINDING_TYPE, - "Expected `self` to be a binding" - ) - assert(typeof(predicate) == "function", "Expected arg #1 to be a function") +do -- map binding + local mapBindingPrototype = setmetatable({}, BASE_BINDING_PROTOTYPE) + mapBindingPrototype.__index = mapBindingPrototype + + function mapBindingPrototype.getValue(mapBinding) + return mapBinding._predicate(mapBinding._upstreamBinding:getValue()) end - local impl = {} + function mapBindingPrototype.subscribe(mapBinding, callback) + local predicate = mapBinding._predicate - function impl.subscribe(callback) - return BindingInternalApi.subscribe(upstreamBinding, function(newValue) + return mapBinding._upstreamBinding:subscribe(function(newValue) callback(predicate(newValue)) end) end - function impl.update(newValue) - error("Bindings created by Binding:map(fn) cannot be updated directly", 2) - end - - function impl.getValue() - return predicate(upstreamBinding:getValue()) + function mapBindingPrototype.update() + error("Bindings created by Binding:map() cannot be updated directly", 2) end - local source - if ReactGlobals.__DEV__ then - -- ROBLOX TODO: LUAFDN-619 - improve debug stacktraces for bindings - source = debug.traceback("Mapped binding created at:", 3) - end + local function mapBinding( + upstreamBinding: BindingInternal, + predicate: (T) -> U + ): MapBinding + local source + + if ReactGlobals.__DEV__ then + -- ROBLOX TODO: More informative error messages here + assert( + IS_BINDING(upstreamBinding), + "Expected 'upstreamBinding' to be of type 'Binding'" + ) + assert(type(predicate) == "function", "Expected 'predicate' to be a function") + + -- ROBLOX TODO: LUAFDN-619 - improve debug stacktraces for bindings + source = debug.traceback("Mapped binding created at:", 3) + end - return ( - setmetatable({ - ["$$typeof"] = ReactSymbols.REACT_BINDING_TYPE, - [BindingImpl] = impl, + return setmetatable({ + _upstreamBinding = upstreamBinding, + _predicate = predicate, _source = source, - }, BindingPublicMeta) :: any - ) :: Binding -end + }, mapBindingPrototype) + end --- The `join` API is used statically, so the input will be a table with values --- typed as the public Binding type -function BindingInternalApi.join( - upstreamBindings: { [string | number]: Binding } -): Binding - if ReactGlobals.__DEV__ then - assert(typeof(upstreamBindings) == "table", "Expected arg #1 to be of type table") - - for key, value in upstreamBindings do - if - typeof(value) ~= "table" - or (value :: any)["$$typeof"] ~= ReactSymbols.REACT_BINDING_TYPE - then - local message = ("Expected arg #1 to contain only bindings, but key %q had a non-binding value"):format( - tostring(key) - ) - error(message, 2) - end - end + function BASE_BINDING_PROTOTYPE.map(baseBinding, predicate) + return mapBinding(baseBinding, predicate) end - local impl = {} + ReactBinding.map = mapBinding + table.freeze(mapBindingPrototype) + table.freeze(BASE_BINDING_PROTOTYPE) +end - local function getValue() +do -- join + local function getValueJoin( + upstreamBindings: { [string | number]: Binding } + ): { [string | number]: any } local value = {} -- ROBLOX FIXME Luau: needs CLI-56711 resolved to eliminate ipairs() - for key, upstream in pairs(upstreamBindings) do + for key, upstream in upstreamBindings do value[key] = upstream:getValue() end return value end - function impl.subscribe(callback) - -- ROBLOX FIXME: type refinements + local joinBindingPrototype = setmetatable({}, BASE_BINDING_PROTOTYPE) + joinBindingPrototype.__index = joinBindingPrototype + + function joinBindingPrototype.getValue(joinBinding) + return getValueJoin(joinBinding._upstreamBindings) + end + + function joinBindingPrototype.subscribe(joinBinding, callback) + local upstreamBindings = joinBinding._upstreamBindings local disconnects: any = {} for key, upstream in upstreamBindings do - disconnects[key] = BindingInternalApi.subscribe(upstream, function(newValue) - callback(getValue()) + disconnects[key] = upstream:subscribe(function(newValue) + callback(getValueJoin(upstreamBindings)) end) end return function() - if disconnects == nil then + if not disconnects then return end for _, disconnect in disconnects do disconnect() end - disconnects = nil end end - function impl.update(newValue) - error("Bindings created by joinBindings(...) cannot be updated directly", 2) + function joinBindingPrototype.update() + error("Bindings created by React.joinBindings() cannot be updated directly", 2) end - function impl.getValue() - return getValue() - end + -- The `join` API is used statically, so the input will be a table with values + -- typed as the public Binding type + function ReactBinding.join( + upstreamBindings: { [string | number]: Binding } + ): Binding + local source + + if ReactGlobals.__DEV__ then + assert( + type(upstreamBindings) == "table", + "Expected arg #1 to be of type table" + ) + + for key, value in upstreamBindings do + if + type(value) == "table" + and value["$$typeof"] == ReactSymbols.REACT_BINDING_TYPE + then + continue + end + + error( + `Expected table 'upstreamBindings' to contain only bindings, but key "{key}" had a non-binding value`, + 2 + ) + end - local source - if ReactGlobals.__DEV__ then - -- ROBLOX TODO: LUAFDN-619 - improve debug stacktraces for bindings - source = debug.traceback("Joined binding created at:", 2) - end + -- ROBLOX TODO: LUAFDN-619 - improve debug stacktraces for bindings + source = debug.traceback("Joined binding created at:", 2) + end - return ( - setmetatable({ - ["$$typeof"] = ReactSymbols.REACT_BINDING_TYPE, - [BindingImpl] = impl, + return setmetatable({ + _upstreamBindings = upstreamBindings, _source = source, - }, BindingPublicMeta) :: any - ) :: Binding + }, joinBindingPrototype) + end + + table.freeze(joinBindingPrototype) end -return BindingInternalApi +return table.freeze(ReactBinding) From 97dd0d8e66fd533980008c5ba3fa0e6a377dddf9 Mon Sep 17 00:00:00 2001 From: gaymeowing <62822174+gaymeowing@users.noreply.github.com> Date: Fri, 26 Sep 2025 13:27:37 -0400 Subject: [PATCH 02/14] formatting + remove useless comment --- modules/react/src/ReactBinding.roblox.lua | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/modules/react/src/ReactBinding.roblox.lua b/modules/react/src/ReactBinding.roblox.lua index fdab32db..8a492df2 100644 --- a/modules/react/src/ReactBinding.roblox.lua +++ b/modules/react/src/ReactBinding.roblox.lua @@ -22,8 +22,8 @@ local LuauPolyfill = require(Packages.LuauPolyfill) local ReactSymbols = require(Packages.Shared).ReactSymbols local ReactTypes = require(Packages.Shared) -local BASE_BINDING_PROTOTYPE = {} -do +-- stylua: ignore +local BASE_BINDING_PROTOTYPE = {} do BASE_BINDING_PROTOTYPE["$$typeof"] = ReactSymbols.REACT_BINDING_TYPE BASE_BINDING_PROTOTYPE.__index = BASE_BINDING_PROTOTYPE @@ -144,7 +144,6 @@ do -- join ): { [string | number]: any } local value = {} - -- ROBLOX FIXME Luau: needs CLI-56711 resolved to eliminate ipairs() for key, upstream in upstreamBindings do value[key] = upstream:getValue() end From 418558668f9a25854c78527b34f80a8c4a323121 Mon Sep 17 00:00:00 2001 From: gaymeowing <62822174+gaymeowing@users.noreply.github.com> Date: Sat, 27 Sep 2025 12:51:50 -0400 Subject: [PATCH 03/14] use IS_BINDING in ReactBinding.join --- modules/react/src/ReactBinding.roblox.lua | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/modules/react/src/ReactBinding.roblox.lua b/modules/react/src/ReactBinding.roblox.lua index 8a492df2..bcc91e36 100644 --- a/modules/react/src/ReactBinding.roblox.lua +++ b/modules/react/src/ReactBinding.roblox.lua @@ -198,10 +198,7 @@ do -- join ) for key, value in upstreamBindings do - if - type(value) == "table" - and value["$$typeof"] == ReactSymbols.REACT_BINDING_TYPE - then + if IS_BINDING(value) then continue end From 0a57d7eed0979d371795a6fb2670eac990dbf6b5 Mon Sep 17 00:00:00 2001 From: gaymeowing <62822174+gaymeowing@users.noreply.github.com> Date: Sat, 27 Sep 2025 12:52:53 -0400 Subject: [PATCH 04/14] remove LuauPolyfill from imports --- modules/react/src/ReactBinding.roblox.lua | 1 - 1 file changed, 1 deletion(-) diff --git a/modules/react/src/ReactBinding.roblox.lua b/modules/react/src/ReactBinding.roblox.lua index bcc91e36..cd61e251 100644 --- a/modules/react/src/ReactBinding.roblox.lua +++ b/modules/react/src/ReactBinding.roblox.lua @@ -18,7 +18,6 @@ local Packages = script.Parent.Parent local createSignal = require(script.Parent["createSignal.roblox"]) local ReactGlobals = require(Packages.ReactGlobals) -local LuauPolyfill = require(Packages.LuauPolyfill) local ReactSymbols = require(Packages.Shared).ReactSymbols local ReactTypes = require(Packages.Shared) From 0a39b7d282ee07f261227a9988090e3858263135 Mon Sep 17 00:00:00 2001 From: gaymeowing <62822174+gaymeowing@users.noreply.github.com> Date: Sat, 27 Sep 2025 23:52:48 -0400 Subject: [PATCH 05/14] sync wip types --- modules/react/src/ReactBinding.roblox.lua | 105 ++++++++++++++++------ 1 file changed, 78 insertions(+), 27 deletions(-) diff --git a/modules/react/src/ReactBinding.roblox.lua b/modules/react/src/ReactBinding.roblox.lua index cd61e251..fddbc6ce 100644 --- a/modules/react/src/ReactBinding.roblox.lua +++ b/modules/react/src/ReactBinding.roblox.lua @@ -21,6 +21,53 @@ local ReactGlobals = require(Packages.ReactGlobals) local ReactSymbols = require(Packages.Shared).ReactSymbols local ReactTypes = require(Packages.Shared) +type AnyBinding = Binding | MapBinding + +type BindingPrototype = { + __subscribe: (binding: Binding, f: (value: T) -> ()) -> (() -> ()), + __tostring: (binding: Binding) -> string, + update: (binding: Binding, newValue: T) -> (), + getValue: (binding: Binding) -> T, + + ["$$typeof"]: typeof(ReactSymbols.REACT_BINDING_TYPE), + __index: BindingPrototype, +} + +type Binding = typeof(setmetatable({} :: { + __source: string?, + value: T, +}, {} :: BindingPrototype)) + +-- ROBLOX FIXME: correct MapBindingPrototype type that currently doesn't work because of recursive type restriction +-- type MapBindingPrototype = typeof(setmetatable({} :: { +-- update: (mapBinding: MapBinding) -> never, +-- getValue: (MapBinding: MapBinding) -> U, +-- }, {} :: BindingPrototype)) +type MapBindingPrototype = typeof(setmetatable({} :: { + update: (mapBinding: MapBinding) -> never, + getValue: (MapBinding: MapBinding) -> U, +}, {} :: BindingPrototype)) + +-- ROBLOX FIXME: correct MapBinding type that currently doesn't work because of recursive type restriction +-- type MapBinding = typeof(setmetatable({} :: { +-- __upstreamBinding: Binding | MapBinding, +-- __predicate: (value: T) -> U, +-- __source: string?, +-- }, {} :: MapBindingPrototype)) +type MapBinding = typeof(setmetatable({} :: { + __upstreamBinding: Binding | MapBinding | JoinBinding, + __predicate: (value: any) -> U, + __source: string?, +}, {} :: MapBindingPrototype)) + +type JoinBindingPrototype = typeof(setmetatable({} :: { + update: (mapBinding: JoinBinding) -> never, +}, {} :: BindingPrototype)) + +type JoinBinding = typeof(setmetatable({} :: { + __upstreamBindings: { [string | number]: Binding | JoinBinding | MapBinding } +}, {} :: JoinBindingPrototype)) + -- stylua: ignore local BASE_BINDING_PROTOTYPE = {} do BASE_BINDING_PROTOTYPE["$$typeof"] = ReactSymbols.REACT_BINDING_TYPE @@ -39,23 +86,28 @@ end local ReactBinding = {} -do -- create - local bindingPrototype = setmetatable({}, BASE_BINDING_PROTOTYPE) +-- stylua: ignore +local bindingPrototype = {} do + bindingPrototype["$$typeof"] = ReactSymbols.REACT_BINDING_TYPE bindingPrototype.__index = bindingPrototype + function bindingPrototype.__tostring(binding) + return `RoactBinding({binding:getValue()})` + end + + function bindingPrototype.__subscribe(binding, callback) + return binding.__subscribe(callback) + end + function bindingPrototype.update(binding, newValue) binding.value = newValue - binding._fire(newValue) + binding.__fire(newValue) end function bindingPrototype.getValue(binding) return binding.value end - function bindingPrototype.subscribe(binding, callback) - return binding._subscribe(callback) - end - function ReactBinding.create(initialValue: T): (Binding, BindingUpdater) local subscribe, fire = createSignal() local source @@ -66,10 +118,10 @@ do -- create end local binding = setmetatable({ - _subscribe = subscribe, + __subscribe = subscribe, value = initialValue, - _source = source, - _fire = fire, + __source = source, + __fire = fire, }, bindingPrototype) local function setAndFire(newValue) @@ -80,21 +132,20 @@ do -- create return binding, setAndFire end - table.freeze(bindingPrototype) end do -- map binding - local mapBindingPrototype = setmetatable({}, BASE_BINDING_PROTOTYPE) + local mapBindingPrototype = setmetatable({}, bindingPrototype) mapBindingPrototype.__index = mapBindingPrototype function mapBindingPrototype.getValue(mapBinding) - return mapBinding._predicate(mapBinding._upstreamBinding:getValue()) + return mapBinding.__predicate(mapBinding.__upstreamBinding:getValue()) end - function mapBindingPrototype.subscribe(mapBinding, callback) - local predicate = mapBinding._predicate + function mapBindingPrototype.__subscribe(mapBinding, callback) + local predicate = mapBinding.__predicate - return mapBinding._upstreamBinding:subscribe(function(newValue) + return mapBinding.__upstreamBinding:__subscribe(function(newValue) callback(predicate(newValue)) end) end @@ -122,9 +173,9 @@ do -- map binding end return setmetatable({ - _upstreamBinding = upstreamBinding, - _predicate = predicate, - _source = source, + __upstreamBinding = upstreamBinding, + __predicate = predicate, + __source = source, }, mapBindingPrototype) end @@ -134,7 +185,7 @@ do -- map binding ReactBinding.map = mapBinding table.freeze(mapBindingPrototype) - table.freeze(BASE_BINDING_PROTOTYPE) + table.freeze(bindingPrototype) end do -- join @@ -150,19 +201,19 @@ do -- join return value end - local joinBindingPrototype = setmetatable({}, BASE_BINDING_PROTOTYPE) + local joinBindingPrototype = setmetatable({}, bindingPrototype) joinBindingPrototype.__index = joinBindingPrototype function joinBindingPrototype.getValue(joinBinding) - return getValueJoin(joinBinding._upstreamBindings) + return getValueJoin(joinBinding.__upstreamBindings) end - function joinBindingPrototype.subscribe(joinBinding, callback) - local upstreamBindings = joinBinding._upstreamBindings + function joinBindingPrototype.__subscribe(joinBinding, callback) + local upstreamBindings = joinBinding.__upstreamBindings local disconnects: any = {} for key, upstream in upstreamBindings do - disconnects[key] = upstream:subscribe(function(newValue) + disconnects[key] = upstream:__subscribe(function(newValue) callback(getValueJoin(upstreamBindings)) end) end @@ -212,8 +263,8 @@ do -- join end return setmetatable({ - _upstreamBindings = upstreamBindings, - _source = source, + __upstreamBindings = upstreamBindings, + __source = source, }, joinBindingPrototype) end From 8d5e57909693cfd7087a6dcf6257e9f8f419e6f1 Mon Sep 17 00:00:00 2001 From: gaymeowing <62822174+gaymeowing@users.noreply.github.com> Date: Sun, 28 Sep 2025 00:29:14 -0400 Subject: [PATCH 06/14] sync before i possibly break and need to revert --- modules/react/src/ReactBinding.roblox.lua | 30 +++++++++++++++++------ 1 file changed, 22 insertions(+), 8 deletions(-) diff --git a/modules/react/src/ReactBinding.roblox.lua b/modules/react/src/ReactBinding.roblox.lua index fddbc6ce..45f53a6c 100644 --- a/modules/react/src/ReactBinding.roblox.lua +++ b/modules/react/src/ReactBinding.roblox.lua @@ -23,20 +23,34 @@ local ReactTypes = require(Packages.Shared) type AnyBinding = Binding | MapBinding -type BindingPrototype = { - __subscribe: (binding: Binding, f: (value: T) -> ()) -> (() -> ()), - __tostring: (binding: Binding) -> string, - update: (binding: Binding, newValue: T) -> (), - getValue: (binding: Binding) -> T, +type BindingPrototype = { + __subscribe: (binding: B, f: (value: T) -> ()) -> (() -> ()), + __tostring: (binding: B) -> string, + update: (binding: B, newValue: T) -> (), + getValue: (binding: B) -> T, ["$$typeof"]: typeof(ReactSymbols.REACT_BINDING_TYPE), - __index: BindingPrototype, + __index: BindingPrototype, } +type BaseInheritedBindingPrototype = typeof(setmetatable({} :: { + getValue: GV, + update: U, +}, {} :: BindingPrototype)) + +type BaseInheritedBinding = typeof(setmetatable({} :: { + __upstreamBindings: UB, + __source: string?, + __predicate: P, + value: V, +}, ({} :: any) :: BaseInheritedBindingPrototype)) + + + type Binding = typeof(setmetatable({} :: { __source: string?, value: T, -}, {} :: BindingPrototype)) +}, {} :: BindingPrototype, T>)) -- ROBLOX FIXME: correct MapBindingPrototype type that currently doesn't work because of recursive type restriction -- type MapBindingPrototype = typeof(setmetatable({} :: { @@ -66,7 +80,7 @@ type JoinBindingPrototype = typeof(setmetatable({} :: { type JoinBinding = typeof(setmetatable({} :: { __upstreamBindings: { [string | number]: Binding | JoinBinding | MapBinding } -}, {} :: JoinBindingPrototype)) +}, {} :: BindingPrototype)) -- stylua: ignore local BASE_BINDING_PROTOTYPE = {} do From a9c3df0dc3d2ac84329dbfbaa69349f98025663c Mon Sep 17 00:00:00 2001 From: gaymeowing <62822174+gaymeowing@users.noreply.github.com> Date: Mon, 29 Sep 2025 12:53:27 -0400 Subject: [PATCH 07/14] remove createSignal * nolonger create a connection table for each connection to a binding * use a boolean for indicating if a callback is suspended * only use one callbacks table, instead of one for suspended callbacks and one for all callbacks --- modules/react/src/ReactBinding.roblox.lua | 126 +++++------------- .../react/src/__tests__/createSignal.spec.lua | 101 -------------- modules/react/src/createSignal.roblox.lua | 91 ------------- 3 files changed, 36 insertions(+), 282 deletions(-) delete mode 100644 modules/react/src/__tests__/createSignal.spec.lua delete mode 100644 modules/react/src/createSignal.roblox.lua diff --git a/modules/react/src/ReactBinding.roblox.lua b/modules/react/src/ReactBinding.roblox.lua index 45f53a6c..fcdae9f9 100644 --- a/modules/react/src/ReactBinding.roblox.lua +++ b/modules/react/src/ReactBinding.roblox.lua @@ -16,81 +16,10 @@ local Packages = script.Parent.Parent -local createSignal = require(script.Parent["createSignal.roblox"]) local ReactGlobals = require(Packages.ReactGlobals) -local ReactSymbols = require(Packages.Shared).ReactSymbols -local ReactTypes = require(Packages.Shared) - -type AnyBinding = Binding | MapBinding - -type BindingPrototype = { - __subscribe: (binding: B, f: (value: T) -> ()) -> (() -> ()), - __tostring: (binding: B) -> string, - update: (binding: B, newValue: T) -> (), - getValue: (binding: B) -> T, - - ["$$typeof"]: typeof(ReactSymbols.REACT_BINDING_TYPE), - __index: BindingPrototype, -} - -type BaseInheritedBindingPrototype = typeof(setmetatable({} :: { - getValue: GV, - update: U, -}, {} :: BindingPrototype)) - -type BaseInheritedBinding = typeof(setmetatable({} :: { - __upstreamBindings: UB, - __source: string?, - __predicate: P, - value: V, -}, ({} :: any) :: BaseInheritedBindingPrototype)) - - - -type Binding = typeof(setmetatable({} :: { - __source: string?, - value: T, -}, {} :: BindingPrototype, T>)) - --- ROBLOX FIXME: correct MapBindingPrototype type that currently doesn't work because of recursive type restriction --- type MapBindingPrototype = typeof(setmetatable({} :: { --- update: (mapBinding: MapBinding) -> never, --- getValue: (MapBinding: MapBinding) -> U, --- }, {} :: BindingPrototype)) -type MapBindingPrototype = typeof(setmetatable({} :: { - update: (mapBinding: MapBinding) -> never, - getValue: (MapBinding: MapBinding) -> U, -}, {} :: BindingPrototype)) - --- ROBLOX FIXME: correct MapBinding type that currently doesn't work because of recursive type restriction --- type MapBinding = typeof(setmetatable({} :: { --- __upstreamBinding: Binding | MapBinding, --- __predicate: (value: T) -> U, --- __source: string?, --- }, {} :: MapBindingPrototype)) -type MapBinding = typeof(setmetatable({} :: { - __upstreamBinding: Binding | MapBinding | JoinBinding, - __predicate: (value: any) -> U, - __source: string?, -}, {} :: MapBindingPrototype)) - -type JoinBindingPrototype = typeof(setmetatable({} :: { - update: (mapBinding: JoinBinding) -> never, -}, {} :: BindingPrototype)) - -type JoinBinding = typeof(setmetatable({} :: { - __upstreamBindings: { [string | number]: Binding | JoinBinding | MapBinding } -}, {} :: BindingPrototype)) - --- stylua: ignore -local BASE_BINDING_PROTOTYPE = {} do - BASE_BINDING_PROTOTYPE["$$typeof"] = ReactSymbols.REACT_BINDING_TYPE - BASE_BINDING_PROTOTYPE.__index = BASE_BINDING_PROTOTYPE - - function BASE_BINDING_PROTOTYPE.__tostring(baseBinding) - return `RoactBinding({baseBinding:getValue()})` - end -end +local Shared = require(Packages.Shared) +local ReactSymbols = Shared.ReactSymbols +local ReactTypes = Shared local function IS_BINDING(value: unknown): boolean -- stylua: ignore @@ -110,20 +39,26 @@ local bindingPrototype = {} do end function bindingPrototype.__subscribe(binding, callback) - return binding.__subscribe(callback) - end + local callbacks = binding.__callbacks + local callbackState = callbacks[callback] + + if binding.__firing and callbackState then + callbacks[callback] = false + elseif callbackState == nil then + callbacks[callback] = true + end - function bindingPrototype.update(binding, newValue) - binding.value = newValue - binding.__fire(newValue) + return function() + callbacks[callback] = nil + end end function bindingPrototype.getValue(binding) return binding.value end - function ReactBinding.create(initialValue: T): (Binding, BindingUpdater) - local subscribe, fire = createSignal() + function ReactBinding.create(initialValue: T): (Binding, (newValue: T) -> ()) + local callbacks = {} local source if ReactGlobals.__DEV__ then @@ -131,19 +66,30 @@ local bindingPrototype = {} do source = debug.traceback("Binding created at:", 3) end - local binding = setmetatable({ - __subscribe = subscribe, + local binding = { + __callbacks = callbacks, value = initialValue, + __firing = false, __source = source, - __fire = fire, - }, bindingPrototype) + } - local function setAndFire(newValue) + local function update(newValue: T) binding.value = newValue - fire(newValue) + + binding.__firing = true + for callback, notSuspended in callbacks do + if notSuspended then + callback(newValue) + else + callbacks[callback] = false + end + end + binding.__firing = false end - return binding, setAndFire + binding.update = update + + return setmetatable(binding, bindingPrototype), update end end @@ -193,8 +139,8 @@ do -- map binding }, mapBindingPrototype) end - function BASE_BINDING_PROTOTYPE.map(baseBinding, predicate) - return mapBinding(baseBinding, predicate) + function bindingPrototype.map(binding, predicate) + return mapBinding(binding, predicate) end ReactBinding.map = mapBinding diff --git a/modules/react/src/__tests__/createSignal.spec.lua b/modules/react/src/__tests__/createSignal.spec.lua deleted file mode 100644 index 1072a581..00000000 --- a/modules/react/src/__tests__/createSignal.spec.lua +++ /dev/null @@ -1,101 +0,0 @@ -local createSignal = require(script.Parent.Parent["createSignal.roblox"]) - -local Packages = script.Parent.Parent.Parent -local JestGlobals = require(Packages.Dev.JestGlobals) -local jestExpect = JestGlobals.expect -local jest = JestGlobals.jest - -local it = JestGlobals.it - -it("should fire subscribers and disconnect them", function() - local subscribe, fire = createSignal() - - local spy = jest.fn() - local disconnect = subscribe(function(...) - spy(...) - end) - - jestExpect(spy).never.toBeCalled() - - local a = 1 - local b = {} - local c = "hello" - fire(a, b, c) - - jestExpect(spy).toBeCalledTimes(1) - jestExpect(spy).toBeCalledWith(a, b, c) - - disconnect() - - fire() - - jestExpect(spy).toBeCalledTimes(1) -end) - -it("should handle multiple subscribers", function() - local subscribe, fire = createSignal() - - local spyA = jest.fn() - local spyB = jest.fn() - - local disconnectA = subscribe(function(...) - spyA(...) - end) - local disconnectB = subscribe(function(...) - spyB(...) - end) - - jestExpect(spyA).never.toBeCalled() - jestExpect(spyB).never.toBeCalled() - - local a = {} - local b = 67 - fire(a, b) - - jestExpect(spyA).toBeCalledTimes(1) - jestExpect(spyA).toBeCalledWith(a, b) - - jestExpect(spyB).toBeCalledTimes(1) - jestExpect(spyB).toBeCalledWith(a, b) - - disconnectA() - - fire(b, a) - - jestExpect(spyA).toBeCalledTimes(1) - - jestExpect(spyB).toBeCalledTimes(2) - jestExpect(spyB).toBeCalledWith(b, a) - - disconnectB() -end) - -it("should stop firing a connection if disconnected mid-fire", function() - local subscribe, fire = createSignal() - - -- In this test, we'll connect two listeners that each try to disconnect - -- the other. Because the order of listeners firing isn't defined, we - -- have to be careful to handle either case. - - local disconnectA - local disconnectB - - local spyA = jest.fn(function() - disconnectB() - end) - - local spyB = jest.fn(function() - disconnectA() - end) - - disconnectA = subscribe(function(...) - spyA(...) - end) - disconnectB = subscribe(function(...) - spyB(...) - end) - - fire() - - jestExpect(#spyA.mock.calls + #spyB.mock.calls).toBe(1) -end) diff --git a/modules/react/src/createSignal.roblox.lua b/modules/react/src/createSignal.roblox.lua deleted file mode 100644 index 3580a2e2..00000000 --- a/modules/react/src/createSignal.roblox.lua +++ /dev/null @@ -1,91 +0,0 @@ ---!strict --- ROBLOX upstream: https://github.com/Roblox/roact/blob/master/src/createSignal.lua ---[[ - * Copyright (c) Roblox Corporation. All rights reserved. - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. -]] - -type Function = (...any) -> ...any ---[[ - This is a simple signal implementation that has a dead-simple API. - - local signal = createSignal() - - local disconnect = signal:subscribe(function(foo) - print("Cool foo:", foo) - end) - - signal:fire("something") - - disconnect() -]] - -type Connection = { callback: Function, disconnected: boolean } -type Map = { [K]: V } - -local function createSignal(): ((Function) -> () -> (), (...any) -> ()) - local connections: Map = {} - local suspendedConnections = {} - local firing = false - - local function subscribe(callback) - assert( - typeof(callback) == "function", - "Can only subscribe to signals with a function." - ) - - local connection = { - callback = callback, - disconnected = false, - } - - -- If the callback is already registered, don't add to the suspendedConnection. Otherwise, this will disable - -- the existing one. - if firing and not connections[callback] then - suspendedConnections[callback] = connection - end - - connections[callback] = connection - - local function disconnect() - assert( - not connection.disconnected, - "Listeners can only be disconnected once." - ) - - connection.disconnected = true - connections[callback] = nil - suspendedConnections[callback] = nil - end - - return disconnect - end - - local function fire(...) - firing = true - for callback, connection in connections do - if not connection.disconnected and not suspendedConnections[callback] then - callback(...) - end - end - - firing = false - - -- ROBLOX performance: use table.clear - table.clear(suspendedConnections) - end - - return subscribe, fire -end - -return createSignal From a3520c03d2f2bc6b48b66514adfacf39fcce279f Mon Sep 17 00:00:00 2001 From: gaymeowing <62822174+gaymeowing@users.noreply.github.com> Date: Mon, 29 Sep 2025 17:47:30 -0400 Subject: [PATCH 08/14] fix __subscribeToBinding export from React.lua and mark it as deprecated --- modules/react/src/React.lua | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/modules/react/src/React.lua b/modules/react/src/React.lua index 26722c44..ddf1f124 100644 --- a/modules/react/src/React.lua +++ b/modules/react/src/React.lua @@ -133,18 +133,19 @@ return { -- ROBLOX TODO: REACT_SCOPE_TYPE as unstable_Scope, -- ROBLOX TODO: useOpaqueIdentifier as unstable_useOpaqueIdentifier, - -- ROBLOX deviation START: bindings support + -- ROBLOX DEVIATION START: bindings support + __subscribeToBinding = @[deprecated{ use = "Binding:_subscribe()" }] function( + binding: ReactTypes.ReactBinding, + f: (value: T) -> () + ): () -> () + return binding:_subscribe() + end, createBinding = ReactBinding.create, joinBindings = ReactBinding.join, - -- ROBLOX deviation END + -- ROBLOX DEVIATION END -- ROBLOX DEVIATION: export the `None` placeholder for use with setState None = ReactNone, - - -- ROBLOX FIXME: These aren't supposed to be exposed, but they're needed by - -- the renderer in order to update properly - __subscribeToBinding = ReactBinding.subscribe, - -- ROBLOX DEVIATION: export Change, Event, and Tag from React Event = require(Packages.Shared).Event, Change = require(Packages.Shared).Change, From 5cb914aa70191952338e1b4ce6e2b3edc4ac541d Mon Sep 17 00:00:00 2001 From: gaymeowing <62822174+gaymeowing@users.noreply.github.com> Date: Mon, 29 Sep 2025 17:49:05 -0400 Subject: [PATCH 09/14] make RobloxComponentProps.lua use :_subscribe() --- .../react-roblox/src/client/roblox/RobloxComponentProps.lua | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/modules/react-roblox/src/client/roblox/RobloxComponentProps.lua b/modules/react-roblox/src/client/roblox/RobloxComponentProps.lua index 5fa246e5..5b46ad5c 100644 --- a/modules/react-roblox/src/client/roblox/RobloxComponentProps.lua +++ b/modules/react-roblox/src/client/roblox/RobloxComponentProps.lua @@ -125,9 +125,7 @@ local function attachBinding(hostInstance, key, newBinding): () instanceToBindings[hostInstance] = {} end - instanceToBindings[hostInstance][key] = - React.__subscribeToBinding(newBinding, updateBoundProperty) - + instanceToBindings[hostInstance][key] = newBinding:_subscribe(updateBoundProperty) updateBoundProperty(newBinding:getValue()) end From 737bf27914c0d9d24c9c0e99456602ba0445450a Mon Sep 17 00:00:00 2001 From: gaymeowing <62822174+gaymeowing@users.noreply.github.com> Date: Mon, 29 Sep 2025 17:50:37 -0400 Subject: [PATCH 10/14] *remove double dashes from some stuff because _source has one dash * fix types --- modules/react/src/ReactBinding.roblox.lua | 79 +++++++++++------------ 1 file changed, 38 insertions(+), 41 deletions(-) diff --git a/modules/react/src/ReactBinding.roblox.lua b/modules/react/src/ReactBinding.roblox.lua index fcdae9f9..483d15f7 100644 --- a/modules/react/src/ReactBinding.roblox.lua +++ b/modules/react/src/ReactBinding.roblox.lua @@ -38,11 +38,11 @@ local bindingPrototype = {} do return `RoactBinding({binding:getValue()})` end - function bindingPrototype.__subscribe(binding, callback) - local callbacks = binding.__callbacks + function bindingPrototype._subscribe(binding, callback) + local callbacks = binding._callbacks local callbackState = callbacks[callback] - if binding.__firing and callbackState then + if binding._firing and callbackState then callbacks[callback] = false elseif callbackState == nil then callbacks[callback] = true @@ -54,10 +54,13 @@ local bindingPrototype = {} do end function bindingPrototype.getValue(binding) - return binding.value + return binding._value end - function ReactBinding.create(initialValue: T): (Binding, (newValue: T) -> ()) + function ReactBinding.create(initialValue: T): ( + ReactTypes.ReactBinding, + ReactTypes.ReactBindingUpdater + ) local callbacks = {} local source @@ -67,16 +70,16 @@ local bindingPrototype = {} do end local binding = { - __callbacks = callbacks, - value = initialValue, - __firing = false, - __source = source, + _callbacks = callbacks, + _value = initialValue, + _source = source, + _firing = false, } local function update(newValue: T) - binding.value = newValue + binding._value = newValue - binding.__firing = true + binding._firing = true for callback, notSuspended in callbacks do if notSuspended then callback(newValue) @@ -84,14 +87,13 @@ local bindingPrototype = {} do callbacks[callback] = false end end - binding.__firing = false + binding._firing = false end binding.update = update - return setmetatable(binding, bindingPrototype), update + return setmetatable(binding, bindingPrototype) :: any, update end - end do -- map binding @@ -99,13 +101,13 @@ do -- map binding mapBindingPrototype.__index = mapBindingPrototype function mapBindingPrototype.getValue(mapBinding) - return mapBinding.__predicate(mapBinding.__upstreamBinding:getValue()) + return mapBinding._predicate(mapBinding._upstreamBinding:getValue()) end - function mapBindingPrototype.__subscribe(mapBinding, callback) - local predicate = mapBinding.__predicate + function mapBindingPrototype._subscribe(mapBinding, callback) + local predicate = mapBinding._predicate - return mapBinding.__upstreamBinding:__subscribe(function(newValue) + return mapBinding._upstreamBinding:_subscribe(function(newValue) callback(predicate(newValue)) end) end @@ -115,9 +117,9 @@ do -- map binding end local function mapBinding( - upstreamBinding: BindingInternal, + upstreamBinding: ReactTypes.ReactBinding, predicate: (T) -> U - ): MapBinding + ): ReactTypes.ReactBinding local source if ReactGlobals.__DEV__ then @@ -133,10 +135,10 @@ do -- map binding end return setmetatable({ - __upstreamBinding = upstreamBinding, - __predicate = predicate, - __source = source, - }, mapBindingPrototype) + _upstreamBinding = upstreamBinding, + _predicate = predicate, + _source = source, + }, mapBindingPrototype) :: any end function bindingPrototype.map(binding, predicate) @@ -150,7 +152,7 @@ end do -- join local function getValueJoin( - upstreamBindings: { [string | number]: Binding } + upstreamBindings: { [string | number]: ReactTypes.ReactBinding } ): { [string | number]: any } local value = {} @@ -165,28 +167,23 @@ do -- join joinBindingPrototype.__index = joinBindingPrototype function joinBindingPrototype.getValue(joinBinding) - return getValueJoin(joinBinding.__upstreamBindings) + return getValueJoin(joinBinding._upstreamBindings) end - function joinBindingPrototype.__subscribe(joinBinding, callback) - local upstreamBindings = joinBinding.__upstreamBindings - local disconnects: any = {} + function joinBindingPrototype._subscribe(joinBinding, callback) + local upstreamBindings = joinBinding._upstreamBindings + local disconnects = {} :: { () -> () } for key, upstream in upstreamBindings do - disconnects[key] = upstream:__subscribe(function(newValue) + table.insert(disconnects, upstream:_subscribe(function(newValue) callback(getValueJoin(upstreamBindings)) - end) + end)) end return function() - if not disconnects then - return - end - for _, disconnect in disconnects do disconnect() end - disconnects = nil end end @@ -197,8 +194,8 @@ do -- join -- The `join` API is used statically, so the input will be a table with values -- typed as the public Binding type function ReactBinding.join( - upstreamBindings: { [string | number]: Binding } - ): Binding + upstreamBindings: { [string | number]: ReactTypes.ReactBinding } + ): ReactTypes.ReactBinding local source if ReactGlobals.__DEV__ then @@ -223,9 +220,9 @@ do -- join end return setmetatable({ - __upstreamBindings = upstreamBindings, - __source = source, - }, joinBindingPrototype) + _upstreamBindings = upstreamBindings, + _source = source, + }, joinBindingPrototype) :: any end table.freeze(joinBindingPrototype) From ec30c610e63b3099a3c4505acbbd59e85b1d5b61 Mon Sep 17 00:00:00 2001 From: gaymeowing <62822174+gaymeowing@users.noreply.github.com> Date: Mon, 29 Sep 2025 17:51:59 -0400 Subject: [PATCH 11/14] improve ReactBinding type --- modules/shared/src/ReactTypes.lua | 31 ++++++++++++++++++++++--------- 1 file changed, 22 insertions(+), 9 deletions(-) diff --git a/modules/shared/src/ReactTypes.lua b/modules/shared/src/ReactTypes.lua index 32509407..c25c8a83 100644 --- a/modules/shared/src/ReactTypes.lua +++ b/modules/shared/src/ReactTypes.lua @@ -174,19 +174,32 @@ export type ReactScopeInstance = { -- FIXME Luau: can't create recursive type with different parameters, so we -- need to split the generic `map` method into a different type and then -- re-combine those types together -type CoreReactBinding = { - getValue: (self: CoreReactBinding) -> T, - _source: string?, +type ReactBindingPrototype = { + getValue: (self: ReactBindingPrototype) -> T, + _subscribe: ( + self: ReactBindingPrototype, + callback: (newValue: T) -> () + ) -> () -> (), + __tostring: (self: ReactBindingPrototype) -> string, + __index: ReactBindingPrototype, + ["$$typeof"]: any, -- userdatas from newproxy() are typed any } -type ReactBindingMap = { + +type ReactBindingPrototypeMap = { map: ( - self: CoreReactBinding & ReactBindingMap, - (T) -> U - ) -> ReactBindingMap & CoreReactBinding, + self: ReactBindingPrototypeMap & ReactBindingPrototype, + predicate: (value: T) -> U + ) -> ReactBindingPrototypeMap & ReactBindingPrototype, } -export type ReactBinding = CoreReactBinding & ReactBindingMap -export type ReactBindingUpdater = (T) -> () +-- stylua: ignore +export type ReactBinding = + & { + _source: string?, + } + & ReactBindingPrototypeMap + & ReactBindingPrototype +export type ReactBindingUpdater = (newValue: T) -> () -- ROBLOX deviation END -- Mutable source version can be anything (e.g. number, string, immutable data structure) From cbec5496a56186d7db21ab731b2665f58912af4e Mon Sep 17 00:00:00 2001 From: gaymeowing <62822174+gaymeowing@users.noreply.github.com> Date: Mon, 29 Sep 2025 18:04:48 -0400 Subject: [PATCH 12/14] don't create wrapper function for bindingPrototype.map --- modules/react/src/ReactBinding.roblox.lua | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/modules/react/src/ReactBinding.roblox.lua b/modules/react/src/ReactBinding.roblox.lua index 483d15f7..f0b7b6a0 100644 --- a/modules/react/src/ReactBinding.roblox.lua +++ b/modules/react/src/ReactBinding.roblox.lua @@ -141,10 +141,8 @@ do -- map binding }, mapBindingPrototype) :: any end - function bindingPrototype.map(binding, predicate) - return mapBinding(binding, predicate) - end - + + bindingPrototype.map = mapBinding ReactBinding.map = mapBinding table.freeze(mapBindingPrototype) table.freeze(bindingPrototype) From 8906fd4093a7482ebbd3c237278fad9b53a33362 Mon Sep 17 00:00:00 2001 From: gaymeowing <62822174+gaymeowing@users.noreply.github.com> Date: Mon, 29 Sep 2025 20:04:53 -0400 Subject: [PATCH 13/14] update subscribeToBinding type and deprecated use --- modules/react/src/React.lua | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/modules/react/src/React.lua b/modules/react/src/React.lua index ddf1f124..bce665f5 100644 --- a/modules/react/src/React.lua +++ b/modules/react/src/React.lua @@ -134,8 +134,8 @@ return { -- ROBLOX TODO: useOpaqueIdentifier as unstable_useOpaqueIdentifier, -- ROBLOX DEVIATION START: bindings support - __subscribeToBinding = @[deprecated{ use = "Binding:_subscribe()" }] function( - binding: ReactTypes.ReactBinding, + __subscribeToBinding = @[deprecated{ use = "ReactBinding:_subscribe()" }] function( + binding: ReactBinding, f: (value: T) -> () ): () -> () return binding:_subscribe() From 3ba710f7187a5b28f147b5acc57d41678df162ec Mon Sep 17 00:00:00 2001 From: gaymeowing <62822174+gaymeowing@users.noreply.github.com> Date: Tue, 30 Sep 2025 12:14:03 -0400 Subject: [PATCH 14/14] * fix type for update function * make naming of bindings consistent * improve error messages --- modules/react/src/ReactBinding.roblox.lua | 58 +++++++++++------------ 1 file changed, 29 insertions(+), 29 deletions(-) diff --git a/modules/react/src/ReactBinding.roblox.lua b/modules/react/src/ReactBinding.roblox.lua index f0b7b6a0..e26ad5f6 100644 --- a/modules/react/src/ReactBinding.roblox.lua +++ b/modules/react/src/ReactBinding.roblox.lua @@ -76,7 +76,7 @@ local bindingPrototype = {} do _firing = false, } - local function update(newValue: T) + local function update(newValue: T) binding._value = newValue binding._firing = true @@ -97,23 +97,23 @@ local bindingPrototype = {} do end do -- map binding - local mapBindingPrototype = setmetatable({}, bindingPrototype) - mapBindingPrototype.__index = mapBindingPrototype + local mappedBindingPrototype = setmetatable({}, bindingPrototype) + mappedBindingPrototype.__index = mappedBindingPrototype - function mapBindingPrototype.getValue(mapBinding) - return mapBinding._predicate(mapBinding._upstreamBinding:getValue()) + function mappedBindingPrototype.getValue(mappedBinding) + return mappedBinding._predicate(mappedBinding._upstreamBinding:getValue()) end - function mapBindingPrototype._subscribe(mapBinding, callback) - local predicate = mapBinding._predicate + function mappedBindingPrototype._subscribe(mappedBinding, callback) + local predicate = mappedBinding._predicate - return mapBinding._upstreamBinding:_subscribe(function(newValue) + return mappedBinding._upstreamBinding:_subscribe(function(newValue) callback(predicate(newValue)) end) end - function mapBindingPrototype.update() - error("Bindings created by Binding:map() cannot be updated directly", 2) + function mappedBindingPrototype.update() + error("Bindings created by ReactBinding:map() cannot be updated directly", 2) end local function mapBinding( @@ -126,30 +126,30 @@ do -- map binding -- ROBLOX TODO: More informative error messages here assert( IS_BINDING(upstreamBinding), - "Expected 'upstreamBinding' to be of type 'Binding'" + "Expected 'upstreamBinding' to be of type 'ReactBinding'" ) - assert(type(predicate) == "function", "Expected 'predicate' to be a function") + assert(type(predicate) == "function", "Expected 'predicate' to be of type function") -- ROBLOX TODO: LUAFDN-619 - improve debug stacktraces for bindings - source = debug.traceback("Mapped binding created at:", 3) + source = debug.traceback("Mapped Binding created at:", 3) end return setmetatable({ _upstreamBinding = upstreamBinding, _predicate = predicate, _source = source, - }, mapBindingPrototype) :: any + }, mappedBindingPrototype) :: any end - + bindingPrototype.map = mapBinding ReactBinding.map = mapBinding - table.freeze(mapBindingPrototype) + table.freeze(mappedBindingPrototype) table.freeze(bindingPrototype) end do -- join - local function getValueJoin( + local function getValueJoined( upstreamBindings: { [string | number]: ReactTypes.ReactBinding } ): { [string | number]: any } local value = {} @@ -161,20 +161,20 @@ do -- join return value end - local joinBindingPrototype = setmetatable({}, bindingPrototype) - joinBindingPrototype.__index = joinBindingPrototype + local joinedBindingPrototype = setmetatable({}, bindingPrototype) + joinedBindingPrototype.__index = joinedBindingPrototype - function joinBindingPrototype.getValue(joinBinding) - return getValueJoin(joinBinding._upstreamBindings) + function joinedBindingPrototype.getValue(joinedBinding) + return getValueJoined(joinedBinding._upstreamBindings) end - function joinBindingPrototype._subscribe(joinBinding, callback) - local upstreamBindings = joinBinding._upstreamBindings + function joinedBindingPrototype._subscribe(joinedBinding, callback) + local upstreamBindings = joinedBinding._upstreamBindings local disconnects = {} :: { () -> () } for key, upstream in upstreamBindings do table.insert(disconnects, upstream:_subscribe(function(newValue) - callback(getValueJoin(upstreamBindings)) + callback(getValueJoined(upstreamBindings)) end)) end @@ -185,7 +185,7 @@ do -- join end end - function joinBindingPrototype.update() + function joinedBindingPrototype.update() error("Bindings created by React.joinBindings() cannot be updated directly", 2) end @@ -199,7 +199,7 @@ do -- join if ReactGlobals.__DEV__ then assert( type(upstreamBindings) == "table", - "Expected arg #1 to be of type table" + "Expected 'upstreamBindings' to be of type table" ) for key, value in upstreamBindings do @@ -214,16 +214,16 @@ do -- join end -- ROBLOX TODO: LUAFDN-619 - improve debug stacktraces for bindings - source = debug.traceback("Joined binding created at:", 2) + source = debug.traceback("Joined Binding created at:", 2) end return setmetatable({ _upstreamBindings = upstreamBindings, _source = source, - }, joinBindingPrototype) :: any + }, joinedBindingPrototype) :: any end - table.freeze(joinBindingPrototype) + table.freeze(joinedBindingPrototype) end return table.freeze(ReactBinding)