Pulse.Remote
Resolves RemoteEvent / RemoteFunction instances by a slash-separated path under ReplicatedStorage, caches the result, and exposes clean fire / invoke helpers. Components never hold WaitForChild chains or raw Remote references.
Quick start — game's remotes.lua
-- src/misc/remotes.lua
-- Prefetch so Heartbeat handlers never block on first use.
task.spawn(function()
Pulse.Remote.prefetch(
"Remotes/Building",
"Remotes/Storage",
"Remotes/Gathering",
"CombatRemotes/Server/Attack"
)
end)
-- Named wrappers — call these from components instead of raw FireServer.
func.Remote_Build = Pulse.Remote.bind("Remotes/Building")
func.Remote_Storage = Pulse.Remote.bind("Remotes/Storage")
func.Remote_Gather = Pulse.Remote.bind("Remotes/Gathering")
func.Remote_Attack = Pulse.Remote.bind("CombatRemotes/Server/Attack")
func.Remote_GetStats = Pulse.Remote.bindi("Remotes/GetStats")
-- Inside any component
func.Remote_Build("Place", someCFrame)
local stats = func.Remote_GetStats()
API
Pulse.Remote.fire(path, ...)
Resolves the RemoteEvent at path and calls FireServer(...). Safe no-op if the path is missing.
Pulse.Remote.fire("Remotes/Gathering", targetInstance)
Pulse.Remote.fire("CombatRemotes/Server/Attack", weapon, "Hit", targetChar)
Pulse.Remote.invoke(path, ...) → any
Resolves the RemoteFunction at path and calls InvokeServer(...). Returns the result, or nil on failure.
local inventory = Pulse.Remote.invoke("Remotes/GetInventory")
Pulse.Remote.connect(path, fn) → RBXScriptConnection
Connects fn to the RemoteEvent's OnClientEvent.
Pulse.Remote.connect("Remotes/Notify", function(message)
Pulse.Notify(message)
end)
Pulse.Remote.bind(path) → fn(...)
Returns a pre-bound fire function. Call it anywhere as if calling FireServer directly.
func.Remote_Build = Pulse.Remote.bind("Remotes/Building")
-- later, in a component:
func.Remote_Build("Place", cf)
func.Remote_Build("Cancel", siteName)
Pulse.Remote.bindi(path) → fn(...) → any
Returns a pre-bound invoke function.
func.Remote_GetStats = Pulse.Remote.bindi("Remotes/GetStats")
local stats = func.Remote_GetStats()
Pulse.Remote.wrap(path) → object
Returns an object with :fire(...), :invoke(...), :connect(fn), :get(). Use when you need both fire and listen on the same remote.
local notify = Pulse.Remote.wrap("Remotes/Notify")
notify:connect(function(msg) Pulse.Notify(msg) end)
notify:fire("Hello from client") -- (unusual but possible)
Pulse.Remote.prefetch(...)
Resolves paths eagerly in background threads so Heartbeat handlers don't incur the WaitForChild latency on their first call.
task.spawn(function()
Pulse.Remote.prefetch(
"Remotes/Building",
"Remotes/Storage",
"CombatRemotes/Server/Attack"
)
end)
How paths work
Paths are resolved segment-by-segment under ReplicatedStorage using WaitForChild (10 s timeout per segment). The resolved instance is cached — every subsequent call is an O(1) table lookup.
| Path | Resolves to |
|---|---|
"Remotes/Building" | ReplicatedStorage.Remotes.Building |
"CombatRemotes/Server/Attack" | ReplicatedStorage.CombatRemotes.Server.Attack |
"Remotes/Nested/Deep/Remote" | walks each segment in order |
If any segment times out, Pulse.Remote logs a warning and all operations on that path are silent no-ops.
Design notes
- One cache per process — all components share the same resolved instances.
- Race-safe — if two coroutines resolve the same path simultaneously, the second waits for the first using a
_PENDINGsentinel. - Component-friendly — call
Pulse.Remote.fire(...)directly insideon Heartbeathandlers with no performance concern; the resolution only yields once.