Forest Logo
search
package_2

wire

By @mrkirdid

Roblox

Mirrored from Wally

Wire

Typed networking for Roblox. You write each event, function or replicated value once, with schemas. From that you get:

  • typed Fire, Connect, Call and Get, in both the old and new Luau type solvers
  • buffer encoding, sized to fit each value
  • one batched remote call per player per frame
  • validation of every value a client sends

There's no build step and no dependencies.

[dependencies]
Wire = "mrkirdid/wire@^1.0.0"

Define

One module per feature, somewhere both sides can require it (e.g. Shared/Net/Shop.luau):

local ReplicatedStorage = game:GetService("ReplicatedStorage")

local Wire = require(ReplicatedStorage.Shared.Packages.Wire)

local T = Wire.T

return Wire.Namespace("Shop", {
	Buy = Wire.FromClient(T.String(40), T.Int(1, 99)),
	Bought = Wire.FromServer(T.String(40)),
	Price = Wire.Function(T.String(40)):Returns(T.Int(0, 1000000)),
	SaleActive = Wire.Value(T.Boolean(), false),
	Cash = Wire.PlayerValue(T.Int(0, 1000000), 0),
})

Use

-- server
local Shop = require(Shared.Net.Shop)

Shop.Buy.Server:Connect(function(Player, ItemId, Amount)
	-- ItemId is a string of at most 40 chars and Amount a whole number from 1 to 99, already checked
	Shop.Bought.Server:Fire(Player, ItemId)
end)

Shop.Price.Server:SetHandler(function(Player, ItemId)
	return 250
end)

Shop.SaleActive.Server:Set(true)
Shop.Cash.Server:Set(Player, 1200)
-- client
local Shop = require(Shared.Net.Shop)

Shop.Buy.Client:Fire("Sword", 1)
Shop.Bought.Client:Connect(function(ItemId) end)

local Price = Shop.Price.Client:Call("Sword") -- yields, errors on failure or timeout

Shop.Cash.Client:Observe(function(Cash) end) -- runs now, then on every change

Every definition has a .Server half and a .Client half. Each side only sees what it can call, and touching the wrong half errors with a message saying so.

Definitions

ServerClient
Wire.FromClient(...)Connect(fn(Player, ...)), OnceFire(...)
Wire.FromServer(...)Fire(Player, ...), FireAll, FireExcept(Player, ...), FireList({Player}, ...)Connect(fn(...)), Once
Wire.Function(...):Returns(Schema)SetHandler(fn(Player, ...) -> value)Call(...) -> value
Wire.Value(Schema, Default)Get(), Set(value)Get(), Observe(fn)
Wire.PlayerValue(Schema, Default)Get(Player), Set(Player, value)Get(), Observe(fn) (its own value)

Options chain off a definition:

  • :RateLimit(PerSecond, Burst?): per player, on FromClient and Function. The default is 100/s. Anything over the limit is dropped before it's decoded.
  • :Unreliable(): on events. It uses UnreliableRemoteEvent, has a 900-byte limit (instance references count toward it) and is never batched. It can carry Instances but not T.Any. Roblox doesn't hold unreliable messages for clients that aren't listening yet.
  • :Timeout(Seconds): on Function. The default is 10s.

Connect, Once and Observe return a connection with :Disconnect().

Schemas (Wire.T)

SchemaBytesNotes
T.Boolean()1
T.Int(Min?, Max?)1–4 (8 past 32 bits)Picks the smallest fit: Int(0, 255) is 1 byte. Whole numbers only.
T.Number(Min?, Max?)8Full precision.
T.Float(Min?, Max?)4About 7 digits. Use it for positions, never for ids or money.
T.String(Max = 4096)length + 1Max is in bytes. Must be valid UTF-8.
T.Literal("A", "B", ...)1One of a fixed set of strings.
T.Buffer(Max = 32768)length + 1
T.Vector3() / T.Vector2()12 / 8f32
T.CFrame()24Position plus axis-angle; always reads back as a valid rotation.
T.Color3()3
T.UDim2()16
T.EnumItem(Enum.X)1–2
T.Instance(Class?), T.Player()0The instance goes as a reference. If the receiver can't see it (not replicated or streamed out), it arrives as nil, so wrap it in Optional.
T.Optional(S)1 + S
T.Array(S, Max = 1024)count + items
T.Map(K, V, Max = 1024)count + pairs
T.Struct({ Field = S, ... })sum of fieldsCast it for the type: T.Struct({...}) :: Wire.Schema<{ Name: string }>.
T.Any()0Roblox serialization, not validated. Treat anything a client sends this way as hostile.

NaN and infinity are refused everywhere. A bad value fails at the sender's own line, and the error names the route and the argument (Shop.Buy: argument 2: expected integer in 1..99, got number 120). A bad value from a client means the message is dropped and the server warns once per 10 seconds per player.

What it fixes compared to KirNet

  • Typed per side, from the schemas. No casts or type-gen extension needed.
  • Validated on the server. Each type is checked, along with ranges, string and array limits, NaN, unknown routes, lengths that run past the end, and leftover bytes. Spam is dropped before decoding.
  • Connections can be disconnected, and only your own listener is removed.
  • No dropped messages at client startup. Messages the server fires before the client connects are held and delivered when it does. Messages for a module the client hasn't required yet are replayed when it does.
  • Call never returns a silent nil. It errors on a timeout, a server error or a missing handler. The server's error details stay on the server.
  • Values catch late joiners up and use one serializer.
  • Order is kept. A FireAll followed by a Fire arrives in the same order.
  • Nothing is lossy unless you ask for it. Floats are only rounded where you chose T.Float.

Good to know

  • The module that defines a namespace has to be required on the server too. It always is if a service uses it. Otherwise the client warns after 10s.
  • Arguments are typed up to six per event. Past that, send one Struct.
  • The server does not hold client messages: connect your .Server listeners in Init. A client message that arrives with nothing connected is dropped, and the server warns once.
  • One frame's traffic is split into more than one packet when it passes 64KB or 256 instance references. A single message over those limits is refused at the call.
  • A namespace name must be an identifier (letters, digits and _), and each definition can be registered only once.
  • Both sides must run the same definitions. If they don't, messages fail their "used up exactly" check and get dropped loudly.

Developing

rojo build test.project.json -o WireTest.rbxlx

Open it in Studio and press Play. The output should end with [WireTest] client: 22 passed, 0 failed and [WireTest] server: 7 passed, 0 failed.

Package Details

Install command (Click to copy)


Version

1.0.0

License

MIT

check_circle

Safe for commercial use

Automated license review — not legal advice.