{"id":"hurreman/roblox-net","name":"roblox-net","scope":"hurreman","platform":"roblox","description":"Declared remotes for hurreman's Roblox games: one config shared by server and client, sends queued until a client is ready or broadcast to ready clients only, per-player throttles and inbound token-bucket limits, late-joiner replay","version":"0.1.0","latest":"0.1.0","versions":["0.1.0"],"license":"MIT","licenseRating":"safe","licenseCaveats":[],"licenseVerified":true,"dependencies":{},"integrity":"928354b462722cd0d8385ca918b936dd40eabab68fa835a720f87eb5de6647c1","likes":0,"downloads":0,"install":"forest install hurreman/roblox-net","url":"https://forest.dev/p/roblox/hurreman/roblox-net","files":"https://api.forest.dev/ai/package/roblox/hurreman/roblox-net/files","readme":"# roblox-net\n\nOne declared set of remotes per game, shared by server and client. It merges two existing approaches:\n- Slime Nebula's networking layer: EventsConfig, RemoteBinder, the player and global event queues, and TokenBucket.\n- The `Net` module from Magic Neighborhood and Steal an Egg: one list of remotes, and \"only fire to clients that are listening\".\n\n```toml\n# wally.toml\n[dependencies]\nNet = \"hurreman/roblox-net@0.1.0\"\n```\n\n```lua\n-- src/shared/Remotes.luau: required by server and client alike\nlocal Net = require(ReplicatedStorage.Packages.Net)\n\nreturn Net.new({\n\tremotes = {\n\t\tDataSync = { kind = \"event\" },\n\t\tNotify = { kind = \"event\", throttle = 10 },                    -- s->c: at most 10/s per player\n\t\tSwing = { kind = \"event\", limit = { burst = 3, perSecond = 2 } }, -- c->s: token bucket per player\n\t\tEventStream = { kind = \"unreliable\" },\n\t\tAdminList = { kind = \"function\" },\n\t},\n\t-- layout = \"folder\" (default: ReplicatedStorage.Remotes) | \"byType\" (RemoteEvents / RemoteFunctions / UnreliableRemoteEvents)\n\t-- readyRemote = \"NetReady\" (default) | false\n\t-- onLimited = function(player, name) end   -- a refused client request\n})\n```\n\nThe list form works too: `remotes = Net.fromLists({ events = {...}, unreliable = {...}, functions = {...} })`.\n\nOn the server, requiring the module creates every remote at once, in one replication step. On the client, the accessors wait for them.\n\n## Server → client\n\n| Call | Delivery |\n|---|---|\n| `net:SendTo(player, name, ...)` | Queued until that client is ready, then delivered in order; trailing `nil`s are kept. Returns `false` if the remote's `throttle` dropped it, or if the name is unknown or is a function remote. |\n| `net:SendToAll(name, ...)` | `SendTo` for every player. |\n| `net:Broadcast(name, ...)` | Ready clients only, immediately, never queued. Use it for cosmetic cues a loading client should simply miss. |\n| `net:QueueGlobal(name, { args }, expiresIn?)` / `net:ClearGlobal(name)` | World state replayed to each client when it becomes ready (default lifetime 30 s). Clear it when a newer state replaces it. |\n| `net:InvokeOn(player, name, ...)` | Yields for the client's answer (after it's ready); raises if the client's handler errors. |\n\nA client becomes ready in one of two ways:\n- it calls `net:SignalReady()` once its listeners are connected;\n- the server calls `net:MarkReady(player)` itself, for example from a data hello.\n\n`net:IsReady(player)`, `net:MarkNotReady(player)` and `net:Queued(player)` round this out.\n\nA player's queue, readiness, throttle windows and buckets are dropped when they leave, and a late `MarkReady` for someone who already left is ignored.\n\n## Client → server\n\n```lua\nnet:OnServerEvent(\"Swing\", function(player, targetId) ... end)    -- refused calls never reach the handler\nnet:OnServerInvoke(\"AdminList\", function(player) return list end)  -- a refused call returns nil\n```\n\n`limit` is a token bucket per player and remote: up to `burst` requests at once, refilling at `perSecond`. For limits keyed by something else, such as per target or per action, use `Net.TokenBucket` directly.\n\nOn the client, use `net:OnClientEvent(name, fn)`, `net:FireServer(name, ...)` and `net:InvokeServer(name, ...)`. The raw instances are available from `net:Event(name)`, `net:Unreliable(name)` and `net:Function(name)`; each checks the name and kind.\n\n## Remotes that aren't declared\n\n`net:Get(name, className, waitForServer?)` creates a remote on demand on the server, or waits for it on the client, in the same folders. This is Slime Nebula's `RemoteBinder.get`. A remote that exists with the wrong class is replaced on the server.\n\n`Net.Binder.new({ clientFallback = true })` keeps RemoteBinder's old client behaviour: after a timeout it creates a local stand-in with no server behind it. This is off by default.\n\n## Studio Edit mode\n\n`RunService:IsServer()` is false in Edit mode, so a plain `Net.new` there acts as a client. Edit-mode tools and plugins pass `server = true` to create the remotes. Handlers can't be connected in Edit mode: Roblox refuses `OnServerEvent` and `OnServerInvoke` there.\n\n## Moving the games over\n\n- **Steal an Egg and Magic Neighborhood:**\n  1. `shared/Net.luau` becomes `return Net.new({ remotes = Net.fromLists({ events = EVENTS, unreliable = UNRELIABLE, functions = FUNCTIONS }) })`, with the same `Remotes` folder.\n  2. Rename the calls: `Net.event(x)` to `net:Event(x)`, `Net.unreliable(x)` to `net:Unreliable(x)`, `Net.func(x)` to `net:Function(x)`, `Net.markReady(p)` to `net:MarkReady(p)`, and `Net.fireReady(Net.event(x), ...)` to `net:Broadcast(x, ...)`.\n  3. Handlers that validate rate by hand (ProductService's prompt cooldown, for example) can move to a `limit`.\n- **Slime Nebula:**\n  1. `EventsConfig` entries become remote specs: `{ type = \"RemoteEvent\", throttle = n }` maps to `{ kind = \"event\", throttle = n }`.\n  2. Use `layout = \"byType\"` so instance paths stay as they are. Pass `readyRemote = false` while GameInit still calls `MarkPlayerReady`.\n  3. `EventsManager.SendTo`, `SendToAll`, `MarkPlayerReady` and `InvokeOn` map to the same-named `net` calls.\n  4. `QueueGlobalEvent(name, ...)` maps to `net:QueueGlobal(name, { ... }, expiresIn)`.\n  5. `RemoteBinder.get` (325 call sites) maps to `net:Get`, or keep `RemoteBinder.lua` as a one-line shim over a `Net.Binder.new({ layout = \"byType\", clientFallback = true })`.\n  6. `ThrottlingManager` goes away (`throttle` covers it), and `TokenBucket` is `Net.TokenBucket`.\n\n## Behaviour fixed on the way\n\n- **InvokeOn:** Slime Nebula's version, for an already-ready client, resumed its own running coroutine and then yielded forever. It now invokes directly.\n- **Trailing nils:** queued sends kept only the arguments up to the first trailing `nil` (`{...}`). They now keep them all (`table.pack`).\n- **Throttle state:** throttle windows were never cleared when a player left. They are now.\n- **Debug prints:** the prints on every ready and flush are gone.\n\n## Developing\n\n```bash\nlune run tests/run    # specs on hurreman/luau-spec (DevPackages, else ../luau-spec); fake Roblox in tests/fixtures\n```\n\nThe Lune specs cover queueing, broadcast, throttle, limits, global replay, InvokeOn, leaving players and the client path, against fake Instances.\n\nIn Studio (Slime Nebula's Edit DataModel, under a throwaway folder), these were checked: real remote creation in both layouts, replacement of a wrong-class remote, on-demand `Get`, and the accessors' classes.\n\nClient-to-server delivery between a real server and client in Play mode has not been run yet.\n","readmeTruncated":false}