Introduction
What are Signals?
A Signal is an implementation of the Observer Pattern. They're used to manage communication between different scripts, promoting loose coupling and readability, resulting in a more modular and maintainable codebase.
Roblox Signals vs. Web Signals
Not to be confused with modern web development terminology, Signals here refer to Event Emitters, not reactive state primitives!
Why use NamedSignal?
Rather than go all out on a single selling point, NamedSignal balances performance, developer experience, and correctness.
Most notably, we support named parameters (hence the name, NamedSignal), which lends you more useful information in lambdas than the type alone.
We mirror the standard signal API, along with additional features seen in some other libraries like Connection:Reconnect(), so switching to NamedSignal is as simple as swapping modules out!
Comparisons
See how NamedSignal compares to alternatives!
vs. BindableEvent
The BindableEvent with the RBXScriptSignal and RBXScriptConnection it provides access to form the Roblox engine's implementation of the event emitter pattern.
Bindables established the convention that many signal libraries continue to follow and take inspiration from, however they suffer from shortfalls regarding data marshalling, and lack modern amenities such as type-checking.
| Feature | NamedSignal | BindableEvent |
|---|---|---|
| Argument Limitations | ✅ No Limitations | ❌ C++ Marshalling |
| Values are passed directly and untouched. Passes arguments directly, preserving tables, references, metatables, and other values. | Engine deep-copies and serializes values. Mixed tables break; arbitrary keys are unsupported; cyclic tables cause an error; metatables, special objects, identities are lost, and other limitations apply | |
| Type-checking | ✅ Full Support | ❌ No Support |
| Type-checking for signal arguments in all relevant contexts, and parameter names[1] in function auto-fill. | Lacks any interface to define types or parameter names. | |
| Object Lifecycle | 📏 Simple | 🍝 Messy |
Single-line creation with Signal.new().Simple clean-up with convenient methods, or just 'forget and GC'. | Boilerplate Instance.new() creation for every event.May cause memory leaks if not explicitly cleaned-up, which requires tracking connections, or destruction of the object. | |
| Performance | 🪶 Low overhead Luau | 🪨 Heavy engine overhead |
| Luau implementation with several optimizations and low overhead. | C++ engine marshalling deep-copies and serializes values, degrading performance with larger payloads. |
[1] Parameter names are available when using the standard Signal type, which uses function signatures (...) -> () to enable parameter naming.
vs. GoodSignal/RbxUtil Signal
Stravant's GoodSignal is the de facto standard of Roblox signal libraries, with sleitnick's RbxUtil fork being a direct extension of it.
As sleitnick's fork is extremely similar, just with some added methods and types, we'll compare with only RbxUtil Signal for simplicity.
| Feature | NamedSignal | RbxUtil Signal |
|---|---|---|
| Type-checking | Named Parameters and Wrap Inference | Standard Generic Types |
Uses type functions to facilitate enhanced type information, supporting parameter names and Signal.wrap() inference.Reduces the need to refer to documentation to identify parameters. | Has type-checking but lacks parameter names. Necessitates referral to documentation when types alone are insufficient. | |
| Thread Edge Cases | ✅ Safely caught and handled | ⚠️ No coroutine.status check |
| Cached threads are checked before reuse, ensuring listeners are always dispatched correctly. | Doesn't check thread status before reuse, may cause dispatch to fail if a thread is closed externally. | |
| Performance[1] | ||
| Always O(1) disconnects | O(n) worst-case disconnects | |
| Uses a doubly-linked list structure to maintain order, while allowing for quick constant-time disconnects in all cases. | Uses a singly-linked list structure that does not track the previous node, requiring iteration which degrades performance. | |
| Multi Thread Reuse | Single Thread Reuse | |
| Reuses all created threads to reduce overhead and churn for asynchronous listeners. | Reuses a single thread, faster than naive (no cache) for synchronous listeners, but slow churn for async. | |
| Minimized Resumption (Per Async) | Naive Resumption (Per Listener) | |
| Resumes threads only when invocation becomes asynchronous (i.e. a listener yields), greatly improving performance for large non-yielding signals. | Always resumes a thread per listener, incurring the overhead for each one, which results in poor scaling for multiple connections. |
[1] Additional implementation details and benchmarks are covered in the dedicated section.
vs. FastSignal
| Feature | NamedSignal | FastSignal |
|---|---|---|
| Type-checking | Named Parameters and Wrap Inference | Standard Generic Types |
Uses type functions to facilitate enhanced type information, supporting parameter names and Signal.wrap() inference.Reduces the need to refer to documentation to identify parameters. | Has type-checking but lacks parameter names. Necessitates referral to documentation when types alone are insufficient. | |
| Thread Edge Cases | ✅ Safely caught and handled | ⚠️ No coroutine.status check |
| Cached threads are checked before reuse, ensuring listeners are always dispatched correctly. | Doesn't check thread status before reuse, may cause dispatch to fail if a thread is closed externally. | |
| Performance[1] | ||
| Multi Thread Reuse | Single Thread Reuse | |
| Reuses all created threads to reduce overhead and churn for asynchronous listeners. | Reuses a single thread, faster than naive (no cache) for synchronous listeners, but slow churn for async. | |
| Minimized Resumption (Per Async) | Naive Resumption (Per Listener) | |
| Resumes threads only when invocation becomes asynchronous (i.e. a listener yields), greatly improving performance for large non-yielding signals. | Always resumes a thread per listener, incurring the overhead for each one, which results in poor scaling for multiple connections. |
[1] Additional implementation details and benchmarks are covered in the dedicated section.
vs. Signal+
| Feature | NamedSignal | Signal+ |
|---|---|---|
| Type-checking | Named Parameters and Wrap Inference | Standard Generic Types |
Uses type functions to facilitate enhanced type information, supporting parameter names and Signal.wrap() inference.Reduces the need to refer to documentation to identify parameters. | Has type-checking but lacks parameter names. Necessitates referral to documentation when types alone are insufficient. | |
| Thread Edge Cases | ✅ Safely caught and handled | ⚠️ No coroutine.status check |
| Cached threads are checked before reuse, ensuring listeners are always dispatched correctly. | Doesn't check thread status before reuse, may cause dispatch to fail if a thread is closed externally. | |
| Performance[1] | ||
| Minimized Resumption (Per Async) | Naive Resumption (Per Listener) | |
| Resumes threads only when invocation becomes asynchronous (i.e. a listener yields), greatly improving performance for large non-yielding signals. | Always resumes a thread per listener, incurring the overhead for each one, which results in poor scaling for multiple connections. |
[1] Additional implementation details and benchmarks are covered in the dedicated section.
vs. LemonSignal
LemonSignal is a fairly competent Signal library, featuring the standard and extended signal API of most other libraries.
| Feature | NamedSignal | LemonSignal |
|---|---|---|
| Type-checking | Named Parameters and Wrap Inference | Standard Generic Types |
Uses type functions to facilitate enhanced type information, supporting parameter names and Signal.wrap() inference.Reduces the need to refer to documentation to identify parameters. | Has type-checking but lacks parameter names. Necessitates referral to documentation when types alone are insufficient. | |
| Performance[1] | ||
| Minimized Resumption (Per Async) | Naive Resumption (Per Listener) | |
| Resumes threads only when invocation becomes asynchronous (i.e. a listener yields), greatly improving performance for large non-yielding signals. | Always resumes a thread per listener, incurring the overhead for each one, which results in poor scaling for multiple connections. |
[1] Additional implementation details and benchmarks are covered in the dedicated section.
