Created
November 2, 2025 16:12
-
-
Save Sleitnick/c14be94ae7503b82e87f40eb53734965 to your computer and use it in GitHub Desktop.
Observer Test
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| --!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