Eight small, focused Luau/Roblox utility modules in one Wally-installable package -- the primitives you'd otherwise stitch together from five different authors' repos, tested, with one consistent style.
Roblox game dev has no shortage of frameworks (Fusion for UI, Knit for
server/client architecture), but no small, coherent utility layer sits
between them -- the stuff every project ends up needing regardless of which
framework it's built on: cleanup, custom events, easing, a state machine, a
promise, a DataStore wrapper that doesn't silently drop writes, a real
networking layer, a way to benchmark any of it. luaukit is that layer:
eight modules, each independently require-able, each already used and
tested on its own before being brought in here.
Nothing here is new code written for this package -- every module already existed as its own repo, already had a test suite, and is just getting packaged and documented properly for the first time.
| Module | What it does |
|---|---|
Trove |
Cleanup/janitor for connections, instances, and callbacks -- the pattern usually called a "Maid" in the Roblox community. |
Signal |
An RBXScriptSignal-like event primitive. Roblox's built-in signals can't be constructed outside the engine; this is a drop-in stand-in with the same Connect/Once/Wait/Fire/Disconnect interface. |
Spring |
Critically-damped spring value for smooth UI/animation, using the closed-form solution to the spring-damper ODE (the same approach behind Unity's SmoothDamp). |
StateMachine |
A small finite state machine with guards and transitions, declarative config. |
Promise |
A minimal Promise: resolve/reject, andThen/catch/finally, Promise.all. |
DataStore |
DataStore wrapper with retry/backoff and session locking, so a flaky write doesn't silently lose player data. |
Net |
A networking layer over RemoteEvent/RemoteFunction: middleware pipeline, token-bucket rate limiter, request/response, and a buffered event for high-frequency traffic. |
Bench |
Microbenchmark harness: warmup, then run for a minimum duration (or fixed iteration count), reporting ns/op. Uses os.clock(), not a Roblox-only timer, so it works standalone too. |
Add to your wally.toml:
[dependencies]
luaukit = "hinikaa/luaukit@0.1.0"Or copy src/ directly into your project — each module is self-contained,
so you don't need the others to use one.
local luaukit = require(path.to.luaukit)
local trove = luaukit.Trove.new()
local signal = luaukit.Signal.new()Or require a single module directly, without pulling in the rest:
local Trove = require(path.to.luaukit.Trove.Trove)local luaukit = require(path.to.luaukit)
local trove = luaukit.Trove.new()
local onDamage = luaukit.Signal.new()
trove:Add(onDamage:Connect(function(amount)
print("took", amount, "damage")
end))
onDamage:Fire(10)
trove:Clean() -- disconnects onDamage automaticallyThese two modules wrap real failure modes (a flaky write, a lost ack, a crashed server) where "retry/backoff" and "rate limited" can sound safer than they actually are if left unexplained. Specifics, not just adjectives:
Getdistinguishes "no data" from "the read failed." It returnsdata, err. IfGetAsyncfails on every retry attempt, you getnil, err(err is non-nil). If it succeeds and there's genuinely nothing stored yet (a new player), you getnil, nil. Checkerrbefore treatingnildata as "new player" -- a caller that doesn't will eventually autosave a fresh profile over data that was actually there, just unreadable that moment.Setis last-write-wins by design -- it is not a compare-and-swap. ItsUpdateAsynctransform ignores whatever's currently stored (past the lock check) and writes the value you gave it, every attempt, including retries. If a write's ack is lost and it retries after another session wrote in between,Setwill overwrite that write. This is intentional (it's what "set" means), but it meansSetis the wrong tool when two sessions might write the same key concurrently.Updateis the safe primitive for that case. Its transform reads whatever's actually stored at the moment each attempt runs -- including a retry, which re-reads current state rather than reapplying a stale value -- and derives the new value from it. UseUpdate(key, fn), notGet+Set, whenever the new value depends on the old one.- Session locks self-expire in
LOCK_DURATION(30s) from the last write, not on a separate heartbeat. EverySet/Updatecall refresheslockExpirytoos.time() + 30. A session that crashes stops refreshing it, so any other session can take the lock within 30 seconds of the crash -- there's no scenario where a dead session locks a player out indefinitely. lockOwner(game.JobId) is compared against the live stored value on every write, not just at acquisition. EachSet/Updateattempt -- including retries -- opens a freshUpdateAsynctransform that reads whatever's currently stored and checkslockOwneragainst this session'sJobIdbefore writing. So a server that stalls, loses its lock to another server, then wakes up and callsSet/Update, doesn't clobber the new owner: its write sees the other session'slockOwnerin the live value and gets rejected (false, "locked by another session") instead of silently going through. Nothing about this check is cached from when the lock was first taken.- The rate limiter in
Net:UseRateLimitis per-player, per-endpoint, not a single bucket shared across every player hitting one remote. EachEndpoint:UseRateLimit(...)call creates its ownRateLimiter, keyed internally byplayer.UserId-- one player spamming a remote can't exhaust another player's budget on that same remote.
Each module ships its own test.lua, runnable standalone with a plain Lua
5.1 interpreter (no Roblox required) from inside that module's folder:
cd src/Trove && lua5.1 test.luaDataStore and Net's tests only cover their engine-independent pieces
(RetryPolicy, Middleware, RateLimiter) -- the parts that touch
DataStoreService/Instance.new can only be exercised inside Roblox
Studio.
- Each module was built and tested independently before being packaged here -- this repo is packaging and documentation, not new engineering.
- No cross-module dependencies except
DataStore->RetryPolicyandNet->Middleware/RateLimiter, both internal to their own module. - MIT licensed.
MIT, see LICENSE.