-
Notifications
You must be signed in to change notification settings - Fork 143
Refactor bindings, introducing joinBindings #208
Changes from 1 commit
8a76308
3505253
9a99374
2b1a4f6
69852df
bff1e23
7202c15
783c896
ef40422
35b5ec4
9d72e74
fa8be88
ac32d5d
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -4,146 +4,156 @@ local Type = require(script.Parent.Type) | |
|
||
local config = require(script.Parent.GlobalConfig).get() | ||
|
||
--[[ | ||
Default mapping function used for non-mapped bindings | ||
]] | ||
local function identity(value) | ||
return value | ||
local BindingImpl = Symbol.named("BindingImpl") | ||
|
||
local BindingInternalApi = {} | ||
|
||
local BindingPublicMethods = {} | ||
|
||
function BindingPublicMethods:getValue() | ||
return BindingInternalApi.getValue(self) | ||
end | ||
|
||
local Binding = {} | ||
function BindingPublicMethods:map(predicate) | ||
return BindingInternalApi.map(self, predicate) | ||
end | ||
|
||
--[[ | ||
Set of keys for fields that are internal to Bindings | ||
]] | ||
local InternalData = Symbol.named("InternalData") | ||
local BindingPublicMeta = { | ||
__index = BindingPublicMethods, | ||
__tostring = function(self) | ||
return string.format("RoactBinding(%s)", tostring(self:getValue())) | ||
end, | ||
} | ||
|
||
local bindingPrototype = {} | ||
bindingPrototype.__index = bindingPrototype | ||
bindingPrototype.__tostring = function(self) | ||
return ("RoactBinding(%s)"):format(tostring(self[InternalData].value)) | ||
function BindingInternalApi.update(binding, newValue) | ||
return binding[BindingImpl].update(newValue) | ||
end | ||
|
||
--[[ | ||
Get the current value from a binding | ||
]] | ||
function bindingPrototype:getValue() | ||
local internalData = self[InternalData] | ||
|
||
--[[ | ||
If our source is another binding but we're not subscribed, we'll | ||
return the mapped value from our upstream binding. | ||
|
||
This allows us to avoid subscribing to our source until someone | ||
has subscribed to us, and avoid creating dangling connections. | ||
]] | ||
if internalData.upstreamBinding ~= nil and internalData.upstreamDisconnect == nil then | ||
return internalData.valueTransform(internalData.upstreamBinding:getValue()) | ||
end | ||
function BindingInternalApi.subscribe(binding, callback) | ||
return binding[BindingImpl].subscribe(callback) | ||
end | ||
|
||
return internalData.value | ||
function BindingInternalApi.getValue(binding) | ||
return binding[BindingImpl].getValue() | ||
end | ||
|
||
--[[ | ||
Creates a new binding from this one with the given mapping. | ||
]] | ||
function bindingPrototype:map(valueTransform) | ||
if config.typeChecks then | ||
assert(typeof(valueTransform) == "function", "Bad arg #1 to binding:map: expected function") | ||
function BindingInternalApi.create(initialValue) | ||
local impl = { | ||
value = initialValue, | ||
changeSignal = createSignal(), | ||
} | ||
|
||
function impl.subscribe(callback) | ||
return impl.changeSignal:subscribe(callback) | ||
end | ||
|
||
local binding = Binding.create(valueTransform(self:getValue())) | ||
function impl.update(newValue) | ||
impl.value = newValue | ||
impl.changeSignal:fire(newValue) | ||
end | ||
|
||
binding[InternalData].valueTransform = valueTransform | ||
binding[InternalData].upstreamBinding = self | ||
function impl.getValue() | ||
return impl.value | ||
end | ||
|
||
return binding | ||
return setmetatable({ | ||
[Type] = Type.Binding, | ||
[BindingImpl] = impl, | ||
}, BindingPublicMeta), impl.update | ||
end | ||
|
||
--[[ | ||
Update a binding's value. This is only accessible by Roact. | ||
]] | ||
function Binding.update(binding, newValue) | ||
local internalData = binding[InternalData] | ||
function BindingInternalApi.map(upstreamBinding, predicate) | ||
if config.typeChecks then | ||
assert(Type.of(upstreamBinding) == Type.Binding, "Expected arg #1 to be a binding") | ||
assert(typeof(predicate) == "function", "Expected arg #1 to be a function") | ||
end | ||
|
||
newValue = internalData.valueTransform(newValue) | ||
local impl = {} | ||
|
||
internalData.value = newValue | ||
internalData.changeSignal:fire(newValue) | ||
end | ||
|
||
--[[ | ||
Subscribe to a binding's change signal. This is only accessible by Roact. | ||
]] | ||
function Binding.subscribe(binding, handler) | ||
local internalData = binding[InternalData] | ||
|
||
--[[ | ||
If this binding is mapped to another and does not have any subscribers, | ||
we need to create a subscription to our source binding so that updates | ||
get passed along to us | ||
]] | ||
if internalData.upstreamBinding ~= nil and internalData.subscriberCount == 0 then | ||
internalData.upstreamDisconnect = Binding.subscribe(internalData.upstreamBinding, function(value) | ||
Binding.update(binding, value) | ||
function impl.subscribe(callback) | ||
return BindingInternalApi.subscribe(upstreamBinding, function(newValue) | ||
callback(predicate(newValue)) | ||
end) | ||
end | ||
|
||
local disconnect = internalData.changeSignal:subscribe(handler) | ||
internalData.subscriberCount = internalData.subscriberCount + 1 | ||
function impl.update(newValue) | ||
-- This operation doesn't make sense; we can leave it as a no-op | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Let's throw here to catch weird cases (like using a mapped binding as a ref) |
||
end | ||
|
||
local disconnected = false | ||
function impl.getValue() | ||
return predicate(upstreamBinding:getValue()) | ||
end | ||
|
||
--[[ | ||
We wrap the disconnect function so that we can manage our subscriptions | ||
when the disconnect is triggered | ||
]] | ||
return function() | ||
if disconnected then | ||
return | ||
return setmetatable({ | ||
[Type] = Type.Binding, | ||
[BindingImpl] = impl, | ||
}, BindingPublicMeta) | ||
end | ||
|
||
function BindingInternalApi.join(upstreamBindings) | ||
if config.typeChecks then | ||
assert(typeof(upstreamBindings) == "table", "Expected arg #1 to be of type table") | ||
|
||
for key, value in pairs(upstreamBindings) do | ||
if Type.of(value) ~= Type.Binding then | ||
local message = ( | ||
"Expected arg #1 to contain only bindings, but key %s had a non-binding value" | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Should we use |
||
):format( | ||
tostring(key) | ||
) | ||
error(message, 2) | ||
end | ||
end | ||
end | ||
|
||
disconnected = true | ||
disconnect() | ||
internalData.subscriberCount = internalData.subscriberCount - 1 | ||
|
||
--[[ | ||
If our subscribers count drops to 0, we can safely unsubscribe from | ||
our source binding | ||
]] | ||
if internalData.subscriberCount == 0 and internalData.upstreamDisconnect ~= nil then | ||
internalData.upstreamDisconnect() | ||
internalData.upstreamDisconnect = nil | ||
local impl = { | ||
disconnectMethods = nil, | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This variable is unused. |
||
} | ||
|
||
local function getValue() | ||
local value = {} | ||
|
||
for key, upstream in pairs(upstreamBindings) do | ||
value[key] = upstream:getValue() | ||
end | ||
|
||
return value | ||
end | ||
end | ||
|
||
--[[ | ||
Create a new binding object with the given starting value. This | ||
function will be exposed to users of Roact. | ||
]] | ||
function Binding.create(initialValue) | ||
local binding = { | ||
[Type] = Type.Binding, | ||
function impl.subscribe(callback) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Would it make more sense to only subscribe to Upstream connections could easily add up, considering bindings mapped from this one will invoke There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Just got back into working on this! I don't think it's a big deal to pass subscriptions up through to their parents. At the end of the day, someone is going to have to track the connection, and consolidating that at the top is the simplest approach! |
||
local disconnects = {} | ||
|
||
[InternalData] = { | ||
value = initialValue, | ||
changeSignal = createSignal(), | ||
subscriberCount = 0, | ||
for key, upstream in pairs(upstreamBindings) do | ||
disconnects[key] = BindingInternalApi.subscribe(upstream, function(newValue) | ||
callback(getValue()) | ||
end) | ||
end | ||
|
||
valueTransform = identity, | ||
upstreamBinding = nil, | ||
upstreamDisconnect = nil, | ||
}, | ||
} | ||
return function() | ||
if disconnects == nil then | ||
return | ||
end | ||
|
||
setmetatable(binding, bindingPrototype) | ||
for _, disconnect in pairs(disconnects) do | ||
disconnect() | ||
end | ||
|
||
local setter = function(newValue) | ||
Binding.update(binding, newValue) | ||
disconnects = nil | ||
end | ||
end | ||
|
||
return binding, setter | ||
function impl.update(newValue) | ||
-- This operation doesn't make sense; we can leave it as a no-op | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Should throw here too. |
||
end | ||
|
||
function impl.getValue() | ||
return getValue() | ||
end | ||
|
||
return setmetatable({ | ||
[Type] = Type.Binding, | ||
[BindingImpl] = impl, | ||
}, BindingPublicMeta) | ||
end | ||
|
||
return Binding | ||
return BindingInternalApi |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should this be changed to
bindingPrototype
to be congruent with other classes?