local Stream = {
Event = {},
Attribute = {},
Changed = {}
}
setmetatable(Stream.Event, {__index = function(s,i)
return "Event/"..i
end})
setmetatable(Stream.Attribute, {__index = function(s,i)
return "Attribute/"..i
end})
setmetatable(Stream.Changed, {__index = function(s,i)
return "Changed/"..i
end})
--
local CustomListeners = setmetatable({}, { __mode = "k" })
local WeakMap = setmetatable({}, { __mode = "kv" })
local ChangePushRequested = false
local RunningComputed = nil
local UniqueCounter = 1000
local IsListenBind = false
local PendingChanges2 = {}
local PendingChanges = {}
local StreamValue = { _type = "StreamValue" }
StreamValue.__index = StreamValue
local StreamComputedValue = { _type = "StreamComputedValue" }
StreamComputedValue.__index = StreamComputedValue
--
local function isValueObject(value)
return typeof(value) == "table" and (value._type == "StreamValue" or value._type == "StreamComputedValue")
end
local function onValueChanged(value, callback)
local uniqueId = UniqueCounter
UniqueCounter += 1
value._listeners[uniqueId] = callback
WeakMap[uniqueId] = value
return function()
local value = WeakMap[uniqueId]
if value then
value._listeners[uniqueId] = nil
WeakMap[uniqueId] = nil
end
end
end
local function applyValue(target, propertyName, oldValue, value)
target[propertyName] = value
local targetListeners = CustomListeners[target]
if targetListeners then
local list = targetListeners[propertyName]
if list then
for _,fn in list do
task.defer(fn, oldValue, value)
end
end
end
end
local function pushChanges()
ChangePushRequested = false
PendingChanges, PendingChanges2 = PendingChanges2, PendingChanges
for target,oldValue in PendingChanges2 do
PendingChanges2[target] = nil
if target._value ~= oldValue then
for listener,arg in target._listeners do
if typeof(listener) == "table" then
task.spawn(listener._update, listener)
else
task.spawn(arg, oldValue, target._value)
end
end
end
end
end
local function requestPushChanges()
if not ChangePushRequested then
ChangePushRequested = true
task.defer(pushChanges)
end
end
--
function StreamValue:get()
if RunningComputed then
RunningComputed._listening[self] = RunningComputed._evalCounter
end
return self._value
end
function StreamValue:set(value)
if self._value ~= value then
if not PendingChanges[self] then
PendingChanges[self] = self._value
requestPushChanges()
end
self._value = value
end
end
--
function StreamComputedValue:get()
if RunningComputed then
RunningComputed._listening[self] = RunningComputed._evalCounter
end
return self._value
end
function StreamComputedValue:_update(firstCall)
self._evalCounter = UniqueCounter
UniqueCounter += 1
RunningComputed = self
local value = self._callback()
RunningComputed = nil
for target,counter in self._listening do
if counter == self._evalCounter then
target._listeners[self] = true
else
target._listeners[self] = nil
end
end
self._evalCounter = nil
if self._value ~= value then
if not firstCall and not PendingChanges[self] then
PendingChanges[self] = self._value
requestPushChanges()
end
self._value = value
end
end
--
function Stream.Value(value)
return setmetatable({ _value = value, _listeners = {} }, StreamValue)
end
function Stream.Computed(callback)
local computed = setmetatable({ _callback = callback, _listeners = {}, _listening = {} }, StreamComputedValue)
computed:_update(true)
return computed
end
function Stream.Listen(target, args)
if isValueObject(target) then
return onValueChanged(target, args)
end
local bound = Stream.Bind(target)
return function(propertiesTable)
IsListenBind = true
return bound(propertiesTable)
end
end
function Stream.New(instance, ...)
local newClass
if typeof(instance) == "function" then
newClass = instance(...)
assert(typeof(newClass) == "table", "The constructed class from Stream.New must be a table!")
elseif typeof(instance) == "string" then
newClass = Instance.new(instance, ...)
else
error("Stream.New only takes in an instance string or a constructor!")
end
return function(propertiesTable)
local cleanup = Stream.Bind(newClass)(propertiesTable)
return newClass, cleanup
end
end
function Stream.Bind(target)
local isListenBind = IsListenBind
IsListenBind = nil
local isTable = typeof(target) == "table"
local isInstance = typeof(target) == "Instance"
assert(isInstance or isTable, "Stream.Bind only accepts instances or tables!")
return function(propertiesTable)
local cleanup = {}
local targetListeners
local listeners
local uniqueId
for propertyName,value in propertiesTable do
if isValueObject(propertyName) then
table.insert(cleanup, (onValueChanged(propertyName, value)))
continue
end
if isInstance then
if propertyName:sub(1, 6) == "Event/" then
table.insert(cleanup, target[propertyName:sub(7)]:Connect(value))
continue
elseif propertyName:sub(1, 10) == "Attribute/" then
table.insert(cleanup, target:GetAttributeChangedSignal(propertyName:sub(11)):Connect(value))
continue
elseif propertyName:sub(1, 8) == "Changed/" then
table.insert(cleanup, target:GetPropertyChangedSignal(propertyName:sub(9)):Connect(value))
continue
end
end
if isListenBind then
if isInstance then
table.insert(cleanup, target:GetPropertyChangedSignal(propertyName):Connect(value))
continue
end
print(propertyName,value,typeof(value))
if type(value) == "function" then
if not targetListeners then
targetListeners = CustomListeners[target]
if not targetListeners then
targetListeners = {}
CustomListeners[target] = targetListeners
end
listeners = {}
uniqueId = UniqueCounter
UniqueCounter += 1
WeakMap[uniqueId] = target
end
local list = targetListeners[propertyName]
if not list then
list = { count = 0 }
targetListeners[propertyName] = list
end
list[uniqueId] = value
list.count += 1
table.insert(listeners, propertyName)
end
else
if isValueObject(value) then
applyValue(target, propertyName, nil, value:get())
table.insert(cleanup, (onValueChanged(value, function(oldValue, newValue)
applyValue(target, propertyName, oldValue, newValue)
end)))
else
print(target,propertyName,value)
applyValue(target, propertyName, nil, value)
end
end
end
return function()
if cleanup then
local _cleanup = cleanup
cleanup = nil
for _,thing in _cleanup do
if type(thing) == "function" then
task.spawn(thing)
else
thing:Disconnect()
end
end
if uniqueId then
local target = WeakMap[uniqueId]
WeakMap[uniqueId] = nil
if target then
local targetListeners = CustomListeners[target]
if targetListeners then
for _,key in listeners do
local list = targetListeners[key]
if list and list[uniqueId] then
list[uniqueId] = nil
list.count -= 1
if list.count == 0 then
targetListeners[key] = nil
if next(targetListeners) == nil then
CustomListeners[target] = nil
end
end
end
end
end
end
listeners = nil
end
end
end
end
end
--
return Stream