Skip to content

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

5 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

luaukit

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.

Overview

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.

Modules

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.

Install

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.

Usage

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)

Example

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 automatically

DataStore and Net guarantees

These 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:

  • Get distinguishes "no data" from "the read failed." It returns data, err. If GetAsync fails on every retry attempt, you get nil, err (err is non-nil). If it succeeds and there's genuinely nothing stored yet (a new player), you get nil, nil. Check err before treating nil data as "new player" -- a caller that doesn't will eventually autosave a fresh profile over data that was actually there, just unreadable that moment.
  • Set is last-write-wins by design -- it is not a compare-and-swap. Its UpdateAsync transform 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, Set will overwrite that write. This is intentional (it's what "set" means), but it means Set is the wrong tool when two sessions might write the same key concurrently.
  • Update is 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. Use Update(key, fn), not Get + 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. Every Set/Update call refreshes lockExpiry to os.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. Each Set/Update attempt -- including retries -- opens a fresh UpdateAsync transform that reads whatever's currently stored and checks lockOwner against this session's JobId before writing. So a server that stalls, loses its lock to another server, then wakes up and calls Set/Update, doesn't clobber the new owner: its write sees the other session's lockOwner in 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:UseRateLimit is per-player, per-endpoint, not a single bucket shared across every player hitting one remote. Each Endpoint:UseRateLimit(...) call creates its own RateLimiter, keyed internally by player.UserId -- one player spamming a remote can't exhaust another player's budget on that same remote.

Testing

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

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

Notes

  • 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 -> RetryPolicy and Net -> Middleware/RateLimiter, both internal to their own module.
  • MIT licensed.

License

MIT, see LICENSE.

About

Eight tested Luau/Roblox utility modules (Trove, Signal, Spring, StateMachine, Promise, DataStore, Net, Bench) in one Wally-installable kit.

Topics

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages