Skip to content

Instantly share code, notes, and snippets.

@Sleitnick
Created November 2, 2025 16:12
Show Gist options
  • Select an option

  • Save Sleitnick/c14be94ae7503b82e87f40eb53734965 to your computer and use it in GitHub Desktop.

Select an option

Save Sleitnick/c14be94ae7503b82e87f40eb53734965 to your computer and use it in GitHub Desktop.
Observer Test
--!strict
-- NOTE: This is an early prototype. It is incredibly messy. It's probably buggy too.
-- There is a lot of code duplication and weird type-casting. Just making it work.
-- There are a lot of things that I need/want to change. It is not ready for production.
local CollectionService = game:GetService("CollectionService")
type InstanceContext = {
Type: "instance",
In: {},
Out: {
Instance: Instance,
},
}
type TagContext = {
Type: "tag",
In: {
ExclusiveInstance: Instance?,
},
Out: {
Callback: (instance: Instance) -> (() -> ())?,
},
}
type AttrContext = {
Type: "attr",
In: {
Instance: Instance,
},
Out: {
Callback: (value: any) -> (() -> ())?,
},
}
type Context = TagContext | AttrContext | InstanceContext
type Generator = {
Type: string,
}
local function tag(name: string, ancestors: { Instance }?)
local function tagObserver(ctx: TagContext)
local tracking: { [Instance]: boolean } = {}
local cleanups: { [Instance]: () -> () } = {}
local ancestryConns: { [Instance]: RBXScriptConnection } = {}
local ids: { [Instance]: number } = {}
local function onInstanceAdded(instance: Instance)
if tracking[instance] then
return
end
ids[instance] = 0
tracking[instance] = true
local init = false
local function onInstanceInit()
ids[instance] += 1
local id = ids[instance]
local cleanup = ctx.Out.Callback(instance)
if typeof(cleanup) == "function" then
if id == ids[instance] then
cleanups[instance] = cleanup
else
task.spawn(cleanup)
end
end
end
local function onInstanceDeinit()
ids[instance] += 1
local cleanup = cleanups[instance]
if cleanup then
task.spawn(cleanup)
cleanups[instance] = nil
end
end
local function onAncestryChanged()
local allowed = true
if ctx.In.ExclusiveInstance ~= nil then
allowed = instance:IsDescendantOf(ctx.In.ExclusiveInstance)
end
if allowed and ancestors ~= nil then
allowed = false
for _, ancestor in ancestors do
if instance:IsDescendantOf(ancestor) then
allowed = true
break
end
end
end
if allowed then
if not init then
init = true
onInstanceInit()
end
else
if init then
init = false
onInstanceDeinit()
end
end
end
task.spawn(onAncestryChanged)
ancestryConns[instance] = instance.AncestryChanged:Connect(onAncestryChanged)
end
local function onInstanceRemoved(instance: Instance)
tracking[instance] = nil
local ancestryConn = ancestryConns[instance]
if ancestryConn then
ancestryConn:Disconnect()
end
end
local instanceAddedConn = CollectionService:GetInstanceAddedSignal(name):Connect(onInstanceAdded)
local instanceRemovedConn = CollectionService:GetInstanceRemovedSignal(name):Connect(onInstanceRemoved)
for _, instance in CollectionService:GetTagged(name) do
task.spawn(onInstanceAdded, instance)
end
return function()
instanceAddedConn:Disconnect()
instanceRemovedConn:Disconnect()
table.clear(tracking)
for _, conn in ancestryConns do
conn:Disconnect()
end
table.clear(ancestryConns)
table.clear(ids)
for _, cleanup in cleanups do
task.spawn(cleanup)
end
table.clear(cleanups)
end
end
return {
Type = "tag",
Observer = tagObserver,
}
end
local function attr(name: string, guard: ((value: any) -> boolean)?)
local function attrObserver(ctx: AttrContext)
local instance = ctx.In.Instance
local cleanup: (() -> ())? = nil
local id = 0
local function onValueChanged()
local newId = id + 1
id = newId
if cleanup ~= nil then
task.spawn(cleanup)
cleanup = nil
end
local value = instance:GetAttribute(name)
if value == nil then
return
end
if guard ~= nil and not guard(value) then
print("guard failed", value)
return
end
local cleanupFn = ctx.Out.Callback(value)
if typeof(cleanupFn) == "function" then
if newId ~= id then
task.spawn(cleanupFn)
else
cleanup = cleanupFn
end
end
end
local conn = instance:GetAttributeChangedSignal(name):Connect(onValueChanged)
task.spawn(onValueChanged)
return function()
conn:Disconnect()
if cleanup ~= nil then
task.spawn(cleanup)
cleanup = nil
end
end
end
return {
Type = "attr",
Observer = attrObserver,
}
end
type AttrGenerator = typeof(attr(""))
type TagGenerator = typeof(tag(""))
local function observer(first: Instance | Generator, ...: Generator)
local generators = { ... }
local allGenerators = false
if typeof(first) == "table" and typeof(first.Type) == "string" then
allGenerators = true
table.insert(generators, 1, first)
end
if #generators == 0 then
error("no observers provided", 2)
end
return function(callback: (...any) -> (() -> ())?)
local callbacks: { (...any) -> any } = {}
local instances: { Instance | "no" } = table.create(#generators, "no") :: any
local values: { any } = {}
for i = #generators, 1, -1 do
local gen = generators[i]
if gen.Type == "tag" then
local cb = function(instance): (() -> ())?
instances[i] = instance
values[i] = instance
if i == #generators then
return callback(table.unpack(values))
end
local nextGen = generators[i + 1]
if nextGen.Type == "tag" then
return (nextGen :: TagGenerator).Observer({
Type = "tag",
In = {
ExclusiveInstance = instance,
},
Out = {
Callback = callbacks[i + 1],
},
})
end
if nextGen.Type == "attr" then
return (nextGen :: AttrGenerator).Observer({
Type = "attr",
In = {
Instance = instance,
},
Out = {
Callback = callbacks[i + 1],
},
})
end
error(`unhandled type for {gen.Type}: {nextGen.Type}`)
end
callbacks[i] = cb :: any
elseif gen.Type == "attr" then
local cb = function(value: any): (() -> ())?
values[i] = value
if i == #generators then
return callback(table.unpack(values))
end
local nextGen = generators[i + 1]
local instance: Instance? = nil
for j = i - 1, 1, -1 do
local inst = instances[j]
if inst ~= "no" then
instance = inst
end
end
if instance == nil and not allGenerators then
instance = first :: Instance
end
if nextGen.Type == "tag" then
return (nextGen :: TagGenerator).Observer({
Type = "tag",
In = {
ExclusiveInstance = instance,
},
Out = {
Callback = callbacks[i + 1],
},
})
end
if nextGen.Type == "attr" then
return (nextGen :: AttrGenerator).Observer({
Type = "attr",
In = {
Instance = instance :: Instance,
},
Out = {
Callback = callbacks[i + 1],
},
})
end
error(`unhandled type for {gen.Type}: {nextGen.Type}`)
end
callbacks[i] = cb :: any
end
end
local firstGen = generators[1]
if firstGen.Type == "tag" then
return (firstGen :: TagGenerator).Observer({
Type = "tag",
In = {
ExclusiveInstance = if allGenerators then nil else first :: Instance,
},
Out = {
Callback = callbacks[1],
},
})
elseif firstGen.Type == "attr" then
return (firstGen :: AttrGenerator).Observer({
Type = "attr",
In = {
Instance = first :: Instance,
},
Out = {
Callback = callbacks[1],
},
})
else
error(`unhandled type: {firstGen.Type}`)
end
end
end
return {
observer = observer,
tag = tag,
attr = attr,
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment