-- Compiled with roblox-ts v2.0.4 local TS = _G[script] local Signal = TS.import(script, TS.getModule(script, "@rbxts", "beacon").out).Signal local CollectionService = TS.import(script, TS.getModule(script, "@rbxts", "services")).CollectionService local usedTags = {} local componentClassToRunner = {} --[[ * * Base class for components. All components should * extend from this class. * * ```ts * class MyComponent extends BaseComponent { * onStart() {} * onStop() {} * } * ``` ]] local BaseComponent do BaseComponent = {} function BaseComponent:constructor() end end local ComponentRunner do ComponentRunner = setmetatable({}, { __tostring = function() return "ComponentRunner" end, }) ComponentRunner.__index = ComponentRunner function ComponentRunner.new(...) local self = setmetatable({}, ComponentRunner) return self:constructor(...) or self end function ComponentRunner:constructor(config, componentClass) self.config = config self.componentClass = componentClass self.compInstances = {} self.addQueue = {} self.componentStarted = Signal.new() self.componentStopped = Signal.new() if config.tag ~= nil then CollectionService:GetInstanceAddedSignal(config.tag):Connect(function(instance) return self:queueOnInstanceAdded(instance) end) CollectionService:GetInstanceRemovedSignal(config.tag):Connect(function(instance) return self:onInstanceRemoved(instance) end) for _, instance in CollectionService:GetTagged(config.tag) do task.spawn(function() return self:queueOnInstanceAdded(instance) end) end end end function ComponentRunner:checkParent(instance) if self.config.blacklistDescendants ~= nil then for _, descendant in self.config.blacklistDescendants do if instance:IsDescendantOf(descendant) then return false end end end if self.config.whitelistDescendants ~= nil then for _, descendant in self.config.whitelistDescendants do if instance:IsDescendantOf(descendant) then return true end end return false end return true end function ComponentRunner:onInstanceAdded(instance) local comp = self.componentClass.new() comp.instance = instance local _condition = self.config.tag if _condition == nil then _condition = "" end comp.tag = _condition local compItem = { comp = comp, started = false, connections = {}, } local _compInstances = self.compInstances local _instance = instance _compInstances[_instance] = compItem if self:checkParent(instance) then compItem.started = true task.spawn(function() return comp:onStart() end) self.componentStarted:Fire(comp) end if self.config.whitelistDescendants ~= nil or self.config.blacklistDescendants ~= nil then local ancestryConnection = instance.AncestryChanged:Connect(function(_, parent) if parent == nil then return nil end if self:checkParent(instance) then if not compItem.started then compItem.started = true task.spawn(function() return comp:onStart() end) self.componentStarted:Fire(comp) end else if compItem.started then compItem.started = false task.spawn(function() return comp:onStop() end) self.componentStopped:Fire(comp) end end end) table.insert(compItem.connections, ancestryConnection) end local _addQueue = self.addQueue local _instance_1 = instance _addQueue[_instance_1] = nil return comp end function ComponentRunner:queueOnInstanceAdded(instance) local _addQueue = self.addQueue local _instance = instance local _condition = _addQueue[_instance] ~= nil if not _condition then local _compInstances = self.compInstances local _instance_1 = instance _condition = _compInstances[_instance_1] ~= nil end if _condition then return nil end local _addQueue_1 = self.addQueue local _instance_1 = instance local _arg1 = task.defer(function() return self:onInstanceAdded(instance) end) _addQueue_1[_instance_1] = _arg1 end function ComponentRunner:onInstanceRemoved(instance) local _addQueue = self.addQueue local _instance = instance if _addQueue[_instance] ~= nil then local _fn = task local _addQueue_1 = self.addQueue local _instance_1 = instance _fn.cancel(_addQueue_1[_instance_1]) local _addQueue_2 = self.addQueue local _instance_2 = instance _addQueue_2[_instance_2] = nil end local _compInstances = self.compInstances local _instance_1 = instance local compItem = _compInstances[_instance_1] if compItem ~= nil then local _compInstances_1 = self.compInstances local _instance_2 = instance _compInstances_1[_instance_2] = nil if compItem.started then task.spawn(function() return compItem.comp:onStop() end) self.componentStopped:Fire(compItem.comp) end for _, connection in compItem.connections do connection:Disconnect() end end end function ComponentRunner:getFromInstance(instance) local _compInstances = self.compInstances local _instance = instance local compItem = _compInstances[_instance] if compItem ~= nil and compItem.started then return compItem.comp end return nil end function ComponentRunner:getAll() local all = {} for _, compItem in self.compInstances do local _comp = compItem.comp table.insert(all, _comp) end return all end function ComponentRunner:forceSpawn(instance) if self.config.tag ~= nil then error("[Proton]: Component with a configured tag cannot be spawned", 2) end return self:getFromInstance(instance) or self:onInstanceAdded(instance) end function ComponentRunner:forceDespawn(instance) if self.config.tag ~= nil then error("[Proton]: Component with a configured tag cannot be despawned", 2) end self:onInstanceRemoved(instance) end end --[[ * * Component decorator. * @param config Component configuration ]] local function Component(config) return function(componentClass) local _condition = config.tag ~= nil if _condition then local _tag = config.tag _condition = usedTags[_tag] ~= nil end if _condition then error('[Proton]: Cannot have more than one component with the same tag (tag: "' .. (config.tag .. '")'), 2) end local runner = ComponentRunner.new(config, componentClass) local _componentClass = componentClass componentClassToRunner[_componentClass] = runner if config.tag ~= nil then local _tag = config.tag usedTags[_tag] = true end end end --[[ * * Get a component attached to the given instance. Returns * `undefined` if nothing is found. * * ```ts * const component = getComponent(MyComponent, someInstance); * ``` * * @param componentClass Component class * @param instance Roblox instance * @returns Component or undefined ]] local function getComponent(componentClass, instance) local _componentClass = componentClass local _result = componentClassToRunner[_componentClass] if _result ~= nil then _result = _result:getFromInstance(instance) end return _result end --[[ * * Get all component instances for a given component class. * @param componentClass Component class * @returns Component instances ]] local function getAllComponents(componentClass) local _componentClass = componentClass local runner = componentClassToRunner[_componentClass] if runner == nil then error("[Proton]: Invalid component class", 2) end return runner:getAll() end --[[ * * Get a signal for the given component class that will be fired any time * a new component instance for the given class is started. * * ```ts * getComponentStartedSignal(MyComponent).Connect((myComponent) => {}); * ``` * * @param componentClass Component class * @returns Signal ]] local function getComponentStartedSignal(componentClass) local _componentClass = componentClass local runner = componentClassToRunner[_componentClass] if runner == nil then error("[Proton]: Invalid component class", 2) end return runner.componentStarted end --[[ * * Get a signal for the given component class that will be fired any time * a new component instance for the given class is stopped. * * ```ts * getComponentStoppedSignal(MyComponent).Connect((myComponent) => {}); * ``` * * @param componentClass Component class * @returns Signal ]] local function getComponentStoppedSignal(componentClass) local _componentClass = componentClass local runner = componentClassToRunner[_componentClass] if runner == nil then error("[Proton]: Invalid component class", 2) end return runner.componentStopped end --[[ * * Observe each component instance during its lifetime. The `observer` function * will be called for each component instance that starts. The observer should * return a cleanup function, which will then be called when the component stops. * * A root cleanup function is returned from this function too, which will stop * all observations and call all current cleanup functions from your observer. * * ```ts * const stopObserving = observeComponent(MyComponent, (myComponent) => { * print("myComponent instance started"); * return () => { * print("myComponent instance stopped"); * }; * }); * * // If observations should stop, call the returned cleanup function: * stopObserving(); * ``` * * @param componentClass Component class * @param observer Observer function * @returns Root cleanup ]] local function observeComponent(componentClass, observer) local _componentClass = componentClass local runner = componentClassToRunner[_componentClass] if runner == nil then error("[Proton]: Invalid component class", 2) end local cleanups = {} local onStopped = function(component) local _component = component local cleanup = cleanups[_component] if cleanup == nil then return nil end local _component_1 = component cleanups[_component_1] = nil cleanup() end local onStarted = function(component) onStopped(component) local cleanup = observer(component) local _component = component cleanups[_component] = cleanup end local startedConnection = runner.componentStarted:Connect(onStarted) local stoppedConnection = runner.componentStopped:Connect(onStopped) for _, component in runner:getAll() do task.spawn(onStarted, component) end return function() startedConnection:Disconnect() stoppedConnection:Disconnect() for _, cleanup in cleanups do task.spawn(cleanup) end end end --[[ * * Add a component manually (bypass CollectionService). * @param componentClass Component class * @param instance Instance ]] local function addComponent(componentClass, instance) local _componentClass = componentClass local runner = componentClassToRunner[_componentClass] if runner == nil then error("[Proton]: Component class not set up") end return runner:forceSpawn(instance) end --[[ * * Remove a component manually. * @param componentClass Component class * @param instance Instance ]] local function removeComponent(componentClass, instance) local _componentClass = componentClass local runner = componentClassToRunner[_componentClass] if runner == nil then error("[Proton]: Component class not set up") end runner:forceDespawn(instance) end return { Component = Component, getComponent = getComponent, getAllComponents = getAllComponents, getComponentStartedSignal = getComponentStartedSignal, getComponentStoppedSignal = getComponentStoppedSignal, observeComponent = observeComponent, addComponent = addComponent, removeComponent = removeComponent, BaseComponent = BaseComponent, }