Skip to content

Utilities

Riptide bundles two essential utility modules — Async and Signal — so you don’t need external dependencies like Promise or GoodSignal.

Access via Riptide.Async and Riptide.Signal.


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?) -> { any },
}

Riptide.Async.Run(fn, timeout, ...fallback) -> ...any

Executes 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 timeout
local 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.


Riptide.Async.Retry(fn, maxAttempts, delay?, ...args) -> ...any

Retries 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.

ParameterTypeDescription
fnfunctionThe function to retry.
maxAttemptsintegerMaximum attempts (must be ≥ 1).
delaynumber?Seconds between retries (default 0).
...anyArguments 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 retries

Riptide.Async.Parallel(fns, timeout?) -> { any }

Runs an array of zero-argument functions in parallel via task.spawn and waits for all to complete. Returns results in the same order as fns.

ParameterTypeDescription
fns{ () -> any }Array of functions to run.
timeoutnumber?Max wait time in seconds. Defaults to 30.

If timeout expires before all functions complete, unfinished entries are nil.

local results = Riptide.Async.Parallel({
function() return fetchPlayerData() end,
function() return fetchInventory() end,
function() return fetchFriendsList() end,
}, 10)
local data, inventory, friends = results[1], results[2], results[3]

A fast, strictly typed, custom event implementation using a linked-list of connections.

type Connection = {
Connected: boolean,
Disconnect: (self: Connection) -> (),
}
type Signal = {
Connect: (self: Signal, fn: (...any) -> ()) -> Connection,
Once: (self: Signal, fn: (...any) -> ()) -> Connection,
Fire: (self: Signal, ...any) -> (),
Wait: (self: Signal) -> ...any,
DisconnectAll: (self: Signal) -> (),
Destroy: (self: Signal) -> (),
}

Riptide.Signal.new() -> Signal

Creates a new signal instance.

local onScoreChanged = Riptide.Signal.new()

Signal:Connect(fn: (...any) -> ()) -> Connection

Connects a callback to the signal. The callback is invoked via task.spawn whenever Fire is called.

Returns a Connection object.

local connection = onScoreChanged:Connect(function(newScore)
print("Score is now:", newScore)
end)

Signal:Once(fn: (...any) -> ()) -> Connection

Like Connect, but the callback fires only once and then automatically disconnects.

onScoreChanged:Once(function(newScore)
print("First score change:", newScore)
end)

Signal:Fire(...any) -> ()

Fires the signal, calling all connected callbacks with the provided arguments. Each callback runs in its own coroutine via task.spawn, so one yielding callback does not block others.

onScoreChanged:Fire(150)

Signal:Wait() -> ...any

Yields the current thread until the signal is fired. Returns the arguments passed to Fire.

local score = onScoreChanged:Wait()
print("Received score:", score)

Signal:DisconnectAll() -> ()

Disconnects all active connections immediately. Clears all internal references.

onScoreChanged:DisconnectAll()

Signal:Destroy() -> ()

Disconnects all connections and invalidates the signal by removing its metatable. After Destroy, the signal should not be used.

onScoreChanged:Destroy()

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) -- false

After disconnecting, all internal references (_signal, _fn, _next) are cleared to prevent memory leaks.