Utilities
Riptide bundles five essential utility modules so you don’t need separate dependencies for the most common patterns:
| Utility | Access | Purpose |
|---|---|---|
Async |
Riptide.Async |
Timeouts, retries, parallel execution |
Signal |
Riptide.Signal |
Typed events with independent listeners |
Trove |
Riptide.Trove |
Resource cleanup tracking |
EventBus |
Riptide.EventBus |
Pub/sub messaging between modules |
Guard |
Riptide.Guard |
Runtime type validation |
A coroutine orchestration utility for timeouts, retries, and parallel execution.
type AsyncModule = { Run: (fn: (...any) -> ...any, timeout: number, ...any) -> ...any, Retry: (fn: (...any) -> ...any, maxAttempts: number, delay: number?, ...any) -> ...any, Parallel: (fns: { () -> any }, timeout: number?) -> { AsyncResult<any> },}Async.Run
Section titled “Async.Run”Riptide.Async.Run(fn, timeout, ...fallback) -> ...anyExecutes a (potentially yielding) function with a timeout. If the function does not complete within timeout seconds, the fallback values are returned instead.
-- Wait up to 5 seconds for data, return nil on timeoutlocal data = Riptide.Async.Run(function() return DataStoreService:GetAsync("key")end, 5, nil)If the function throws an error (before timeout), the error is re-thrown.
When the timeout expires, Run returns the fallback and ignores any eventual result. The user function may continue running; a timeout does not cancel its side effects.
Async.Retry
Section titled “Async.Retry”Riptide.Async.Retry(fn, maxAttempts, delay?, ...args) -> ...anyRetries a function up to maxAttempts times. If an attempt throws, it waits delay seconds (default 0) before the next attempt. Returns the result on the first success; re-throws the last error if all attempts fail.
| Parameter | Type | Description |
|---|---|---|
fn |
function | The function to retry. |
maxAttempts |
integer | Maximum attempts (must be ≥ 1). |
delay |
number? | Seconds between retries (default 0). |
... |
any | Arguments passed to fn on each call. |
local data = Riptide.Async.Retry(function() return DataStoreService:GetAsync("player_123")end, 3, 1) -- up to 3 attempts, 1 second between retriesAsync.Parallel
Section titled “Async.Parallel”Riptide.Async.Parallel(fns, timeout?) -> { AsyncResult<any> }Runs an array of zero-argument functions in parallel via task.spawn and waits for all to complete. Returns explicit result objects in the same order as fns.
type AsyncResult<T> = { ok: boolean, value: T?, error: string?, timedOut: boolean?,}| Parameter | Type | Description |
|---|---|---|
fns |
{ () -> any } |
Array of functions to run. |
timeout |
number? |
Max wait time in seconds. Defaults to 30. |
If timeout expires before all functions complete, unfinished entries return { ok = false, timedOut = true, error = "..." }.
local results = Riptide.Async.Parallel({ function() return fetchPlayerData() end, function() return fetchInventory() end, function() return fetchFriendsList() end,}, 10)
if results[1].ok then local data = results[1].valueendSignal
Section titled “Signal”A fast, strictly typed, custom event implementation using a linked-list of connections.
type Connection = { Connected: boolean, Disconnect: (self: Connection) -> (),}
type Event<T...> = { Connect: (self: Event<T...>, fn: (T...) -> ()) -> Connection, Once: (self: Event<T...>, fn: (T...) -> ()) -> Connection, Wait: (self: Event<T...>) -> T...,}
type Signal<T...> = Event<T...> & { Fire: (self: Signal<T...>, T...) -> (), DisconnectAll: (self: Signal<T...>) -> (), Destroy: (self: Signal<T...>) -> (),}Signal.new
Section titled “Signal.new”Riptide.Signal.new() -> SignalCreates a new signal instance.
local onScoreChanged = Riptide.Signal.new()Signal:Connect
Section titled “Signal:Connect”Signal:Connect(fn: (...any) -> ()) -> ConnectionConnects a callback to the signal. Fire schedules listeners independently through reusable threads. A listener may yield or error without blocking the other listeners. A callback is not guaranteed to finish before Fire returns.
Returns a Connection object.
local connection = onScoreChanged:Connect(function(newScore) print("Score is now:", newScore)end)Signal:Once
Section titled “Signal:Once”Signal:Once(fn: (...any) -> ()) -> ConnectionLike Connect, but the callback fires only once and then automatically disconnects.
onScoreChanged:Once(function(newScore) print("First score change:", newScore)end)Signal:Fire
Section titled “Signal:Fire”Signal:Fire(...any) -> ()Fires the signal with the provided arguments. Dispatch begins with the newest connection. Listener completion order is not guaranteed, and Fire does not wait for a yielding listener.
onScoreChanged:Fire(150)Signal:Wait
Section titled “Signal:Wait”Signal:Wait() -> ...anyYields the current thread until the signal is fired. Returns all arguments passed to Fire, including trailing nil values. If DisconnectAll or Destroy cancels the wait, it raises an error.
local score = onScoreChanged:Wait()print("Received score:", score)Signal:DisconnectAll
Section titled “Signal:DisconnectAll”Signal:DisconnectAll() -> ()Disconnects all active connections immediately. Clears all internal references.
onScoreChanged:DisconnectAll()Signal:Destroy
Section titled “Signal:Destroy”Signal:Destroy() -> ()Disconnects all connections and marks the signal destroyed. New connections are rejected after Destroy.
onScoreChanged:Destroy()Connection:Disconnect
Section titled “Connection:Disconnect”Connection:Disconnect() -> ()Disconnects a single connection from its signal. Safe to call multiple times.
local conn = signal:Connect(function() end)conn:Disconnect()
print(conn.Connected) -- falseAfter disconnecting, the connection releases its references to the signal and listener node.
A resource cleanup tracking helper based on Sleitnick’s Trove pattern. Exposes scoped tracking for signals, connections, instances, and generic functions.
Access via Riptide.Trove.
Trove.new
Section titled “Trove.new”Riptide.Trove.new() -> TroveCreates a new resource tracking Trove instance.
local trove = Riptide.Trove.new()Trove:Add
Section titled “Trove:Add”trove:Add(object: any, cleanupMethod: string?) -> anyTracks any cleanable object. Automatically infers the cleanup method (checks for :Destroy(), :Disconnect(), or if the object is a function, executes it). A custom cleanup method name can optionally be passed.
-- Track a functiontrove:Add(function() print("Cleaned up function!")end)
-- Track a custom objecttrove:Add(myCustomObject, "CustomCleanup")Trove:Connect
Section titled “Trove:Connect”trove:Connect(signal: any, callback: (...any) -> ()) -> ConnectionConnects a callback to a signal and tracks the resulting connection within the trove.
trove:Connect(game.Players.PlayerAdded, function(player) print("Player added:", player.Name)end)Trove:Clean
Section titled “Trove:Clean”trove:Clean() -> ()Cleans up all tracked resources in the reverse order that they were added (LIFO) and clears the trove container.
trove:Clean()EventBus
Section titled “EventBus”A lightweight, isolated pub/sub messenger for inter-module communication. Wrapped in error boundaries so crashing listeners do not block others.
Access via Riptide.EventBus.
EventBus.new
Section titled “EventBus.new”Riptide.EventBus.new(label: string?) -> EventBusCreates a new EventBus instance. label is optional and is used in listener error warnings.
local bus = Riptide.EventBus.new("RoundBus")EventBus:On
Section titled “EventBus:On”bus:On(eventName: string, callback: (...any) -> ()) -> () -> ()Registers a callback for eventName. Returns an unsubscribe function that disconnects the listener when invoked.
local unsub = bus:On("ScoreUpdated", function(score) print("New score:", score)end)
-- Unsubscribe later:unsub()EventBus:Once
Section titled “EventBus:Once”bus:Once(eventName: string, callback: (...any) -> ()) -> () -> ()Registers a callback that runs only on the next emission for eventName, then unsubscribes itself. Returns an unsubscribe function so the one-shot listener can be cancelled before it fires.
local cancel = bus:Once("RoundStarted", function(roundId) print("First round observed:", roundId)end)
-- Optional cancellation before the event fires:cancel()EventBus:Emit
Section titled “EventBus:Emit”bus:Emit(eventName: string, ...: any) -> ()Fires the event, calling all registered listeners synchronously with the provided arguments. Uses xpcall to isolate crashing listeners.
bus:Emit("ScoreUpdated", 1500)EventBus:Clear
Section titled “EventBus:Clear”bus:Clear() -> ()Removes all registered listeners from the EventBus instance.
bus:Clear()EventBus:Destroy
Section titled “EventBus:Destroy”bus:Destroy() -> ()Cleanup-compatible alias for Clear. This lets an EventBus instance be passed to cleanup utilities that look for Destroy.
trove:Add(bus)A lightweight, zero-dependency type verification utility library for securing network boundaries and runtime argument validation.
Access via Riptide.Guard.
Validators
Section titled “Validators”Guard.Number(min: number?, max: number?): Validates a number, supporting optional boundaries (e.g.Guard.Number(0, 100)). RejectsNaN.Guard.String(maxLength: number?): Validates a string, supporting an optional length boundary.Guard.Boolean(): Validates a boolean value (trueorfalse).Guard.Enum(values: { any }): Validates that a value is contained in the whitelist array.Guard.Table(schema: { [string]: Validator }): Validates structured tables against schema mapping rules.Guard.Optional(validator: Validator): Allows the value to benilor match the nested validator.Guard.Instance(className: string): Validates that the instance matches the Roblox engine class name.
Example Usage
Section titled “Example Usage”-- Validate a table payloadlocal isUserSchema = Guard.Table({ name = Guard.String(50), age = Guard.Number(0, 150), isAdmin = Guard.Optional(Guard.Boolean()),})
local ok, err = isUserSchema({ name = "Bob", age = 25 })if not ok then warn("Validation error: " .. tostring(err))end