{"id":"iamkleon/bindable","name":"bindable","scope":"iamkleon","platform":"roblox","description":"Mirrored from the Wally registry.","version":"1.0.1","latest":"1.0.1","versions":["1.0.0","1.0.1"],"license":"MIT","licenseRating":"safe","licenseCaveats":["License identified from the packaged LICENSE file; the manifest declared none."],"licenseVerified":true,"dependencies":{},"integrity":"9dcaa22daeec7dfc5d0e43f6472a82a334caf973ea9863557820f62d342d6b40","likes":0,"downloads":0,"install":"forest install iamkleon/bindable","url":"https://forest.dev/p/roblox/iamkleon/bindable","files":"https://api.forest.dev/ai/package/roblox/iamkleon/bindable/files","readme":"# Bindable\r\n\r\nA high-performance, pure-Luau event and callback system. It provides two primitives — EventBindable and FunctionBindable — that operate entirely in Luau, with strict runtime guards and aggressive performance optimizations.\r\n\r\n## Features\r\n\r\n- Pure Luau: No Instance objects, no hidden BindableEvents, no memory leaks.\r\n- Zero-Allocation Fire: Event:Fire(...) iterates handlers backwards (LIFO) with deferred in-place compaction and no temporary tables or thread creation.\r\n- Instant Wait: Event:Wait() resumes waiting threads directly via coroutine.resume() without scheduler indirection, while still surfacing errors thrown by the resumed thread.\r\n- Synchronous Invoke: Function:Invoke(...) calls the assigned callback on the same thread and returns results immediately.\r\n- O(1) Lazy Disconnect: :Disconnect() writes a single boolean flag. Disconnected handlers are swept and compacted on the next outermost :Fire().\r\n- Error Isolation: A crashing handler does not stop the rest of the handlers from running; stack traces are preserved and re-thrown via task.defer. Errors raised after :Wait() returns are likewise reported, not swallowed.\r\n- Re-entrancy Guard: Nested :Fire() calls are detected and capped at a depth of 100. Compaction is deferred to the outermost fire so re-entrant calls never corrupt the parent iteration.\r\n- Once Safety: :Once() connections are disconnected before their callback runs, so a re-entrant :Fire() cannot fire them a second time.\r\n- Waiter Safety: :Destroy() automatically resumes any threads waiting with :Wait() so they never hang.\r\n- Strict Metatables: Accessing or assigning invalid members throws immediately, catching typos at development time. Internal state is protected via rawset whitelisting.\r\n\r\n## Installation\r\n\r\n### Wally\r\n\r\nAdd the package to your wally.toml (https://wally.run/package/iamkleon/bindable), then require it:\r\n\r\nlocal Bindable = require(game.ReplicatedStorage.Packages.Bindable)\r\n\r\n### Manual\r\n\r\n1. Download the latest .rbxm model from the Releases page.\r\n2. Insert the model into your place (e.g., ReplicatedStorage.Bindable).\r\n3. Require the ModuleScript:\r\n\r\nlocal Bindable = require(game.ReplicatedStorage.Bindable)\r\n\r\n## API Reference\r\n\r\n### Constructors\r\n\r\nBindable.Event() -> EventBindable\r\nCreates a new event bindable.\r\n\r\nBindable.Function() -> FunctionBindable\r\nCreates a new function bindable.\r\n\r\n### EventBindable\r\n\r\n:Connect(fn: (...any) -> ()) -> Connection\r\nConnects a function that will be called every time the event fires.\r\n\r\n:Once(fn: (...any) -> ()) -> Connection\r\nConnects a function that will be called only on the next fire, then automatically disconnects. The disconnect happens before the callback runs, so re-entrant fires cannot trigger it twice.\r\n\r\n:Wait() -> ...any\r\nYields the current coroutine until the next fire, then returns the arguments passed to :Fire(...). The waiting thread is resumed directly via coroutine.resume; any error raised after :Wait() returns is reported through task.defer rather than swallowed. If the event is destroyed while waiting, it throws \"EventBindable was destroyed while waiting\".\r\n\r\n:Fire(...any)\r\nSynchronously invokes all connected handlers in LIFO (Last-In, First-Out) order. Disconnected handlers are skipped. Compaction of dead handlers is deferred to the outermost fire so re-entrant calls are safe. If a handler errors, the error is captured and deferred so the remaining handlers still run.\r\n\r\n:FireDeferred(...any)\r\nSchedules :Fire(...) on the next resumption cycle using task.defer.\r\n\r\n:DisconnectAll()\r\nDisconnects every handler but leaves the event usable for future connections.\r\n\r\n:Destroy()\r\nDisconnects all handlers, clears internal state, resumes any waiting threads, and marks the event as destroyed. Future :Connect calls return dead connections.\r\n\r\n:IsDestroyed() -> boolean\r\nReturns whether the event has been destroyed.\r\n\r\n### FunctionBindable\r\n\r\n:OnInvoke(fn: ((...any) -> ...any)?) -> ((...any) -> ...any)?\r\nAssigns the callback invoked by :Invoke(...). Returns the previous callback, if any.\r\n\r\n:Invoke(...any) -> ...any\r\nCalls the set callback synchronously and returns its results directly. Callback errors propagate to the caller.\r\n\r\n:Destroy()\r\nClears the callback and marks the bindable as destroyed. Future :Invoke calls will error.\r\n\r\n:IsDestroyed() -> boolean\r\nReturns whether the function bindable has been destroyed.\r\n\r\n### Connection\r\n\r\n.Connected: boolean (read-only)\r\nWhether the connection is still active. Writing to this property will throw.\r\n\r\n:Disconnect()\r\nLazily disconnects the handler. O(1) and safe to call from inside a handler.\r\n\r\n:IsConnected() -> boolean\r\nReturns the value of .Connected.\r\n\r\n## Usage Examples\r\n\r\n### 1. Basic Event Wiring\r\nConnecting a function to an event and firing it with arguments.\r\n\r\nlocal Bindable = require(game.ReplicatedStorage.Bindable)\r\n\r\nlocal OnScoreChanged = Bindable.Event()\r\n\r\nlocal conn = OnScoreChanged:Connect(function(newScore, playerName)\r\n    print(playerName .. \" scored! New total: \" .. newScore)\r\nend)\r\n\r\nOnScoreChanged:Fire(100, \"Player1\") -- Output: Player1 scored! New total: 100\r\n\r\n### 2. One-Shot Events (Once)\r\nFiring an event that should only be handled a single time, such as an initialization or loading phase.\r\n\r\nlocal OnGameLoaded = Bindable.Event()\r\n\r\nOnGameLoaded:Once(function(mapName)\r\n    print(\"Game loaded on map: \" .. mapName)\r\nend)\r\n\r\nOnGameLoaded:Fire(\"Desert\") -- Output: Game loaded on map: Desert\r\nOnGameLoaded:Fire(\"Forest\") -- Does nothing, the connection was already removed.\r\n\r\n### 3. Waiting for an Event (Wait)\r\nYielding a thread until an event fires. This is useful for creating asynchronous flows without polling.\r\n\r\nlocal OnDoorOpened = Bindable.Event()\r\n\r\ntask.spawn(function()\r\n    print(\"Waiting for door to open...\")\r\n    local doorId, openedBy = OnDoorOpened:Wait()\r\n    print(openedBy .. \" opened door #\" .. doorId)\r\nend)\r\n\r\ntask.wait(2) -- Simulate some time passing\r\nOnDoorOpened:Fire(42, \"Alice\") -- Output: Alice opened door #42\r\n\r\n### 4. Synchronous Function Binding (FunctionBindable)\r\nUsing a FunctionBindable to synchronously request and receive data from another system.\r\n\r\nlocal GetPlayerData = Bindable.Function()\r\n\r\n-- Set the callback that will handle the request\r\nGetPlayerData:OnInvoke(function(userId)\r\n    -- Simulate fetching data\r\n    if userId == 1 then\r\n        return \"Alice\", 100\r\n    end\r\n    return \"Unknown\", 0\r\nend)\r\n\r\n-- Invoke the callback and get results immediately\r\nlocal name, score = GetPlayerData:Invoke(1)\r\nprint(name, score) -- Output: Alice 100\r\n\r\n### 5. Disconnecting and Cleanup\r\nProperly managing memory and preventing memory leaks by disconnecting when done.\r\n\r\nlocal OnRoundEnd = Bindable.Event()\r\nlocal connection = OnRoundEnd:Connect(function()\r\n    print(\"Round ended!\")\r\nend)\r\n\r\n-- Later, when you no longer need to listen:\r\nconnection:Disconnect()\r\n\r\n-- Or disconnect all listeners at once:\r\nOnRoundEnd:DisconnectAll()\r\n\r\n-- When the event is completely done being used, destroy it:\r\nOnRoundEnd:Destroy()\r\n\r\n## Design Notes & Best Practices\r\n\r\n### Synchronous Fire and Yielding\r\nBecause :Fire() runs handlers on the calling thread, if a handler yields, the entire fire loop yields with it. If you need to perform yielding work inside a handler without blocking the caller, move that work into task.spawn or task.defer inside the handler.\r\n\r\n### Re-entrancy\r\nCalling :Fire() from inside a handler is allowed and recurses synchronously. The handler array is not compacted during nested fires — compaction runs only when the outermost fire unwinds (_fireDepth == 0) — so a parent loop's array positions stay stable while a child fire iterates. A depth limit of 100 prevents accidental infinite loops. Use :FireDeferred() to break a synchronous chain.\r\n\r\n### Once Semantics\r\n:Once() disconnects the connection before invoking its callback. This guarantees exactly one invocation even if the callback (or a sibling handler) triggers a re-entrant :Fire().\r\n\r\n### Error Isolation\r\nEach handler is wrapped in pcall; failures are captured with a stack trace and re-thrown via task.defer so the remaining handlers still run. The same error-reporting path is used for threads resumed by :Wait(): if code executed after :Wait() returns throws, the error is reported rather than silently captured by coroutine.resume.\r\n\r\n### Thread Safety\r\n- Disconnecting during :Fire() is always safe; the flag flip is visible to the current and any nested fire.\r\n- Connecting during :Fire() appends to the end of the handler array; the new handler is not invoked until the next :Fire() due to backward iteration.\r\n- Destroying during :Fire() is safe: the array is cleared and the loop stops at the next nil entry; the destroyed flag prevents new connections.\r\n- Re-entrant :Fire() does not double-invoke :Once() handlers and does not skip or duplicate handlers due to compaction.\r\n\r\n### Memory\r\n- :Destroy() clears the internal handler table and wakes up any waiters. Connection objects held elsewhere become harmless dead objects.\r\n- Always :Destroy() bindables when done to release references.","readmeTruncated":false}