Changelog
Changelog
Every release of BlinkBlox since it forked from upstream Blink at v0.18.8. The GitHub release notes
tell the longer story of each one, with the measurements and the reports that led to it.
A line marked Recompile both modules means a client and a server must be generated by the same release to talk to each other; the schema signature added in 0.23.0 makes a mismatch refuse at startup instead of misreading packets.
0.33.0 — 2026-09-24
A limit on the calls a server runs at once, and tests for the seams between events. The wire format does not change.
Added
Concurrency: Non a function caps how many of one player’s calls the server runs at once.Ratelimits how many calls start each second. A listener that waits (a DataStore save, an HTTP request, an invocation back to the client) holds each call open while it waits, so calls arriving at an allowed rate still piled up into suspended threads. A client sending its own requests is not bound by the 32 outstanding calls an honest client module keeps to.- A call past the limit is answered with a failure at once.
- It is reported through
SetRateLimitHandlerand a warning, once a second per player. - The place is given back when the call is answered: returned, thrown, or failed to serialise.
- A call queued for a listener that has not connected counts as running.
- The counts go when the player leaves.
- A fractional value is refused (
E2003), andConcurrencyon aFrom: Serverfunction warns (W3020).
Tests
-
test/Composition.luaufires random sequences of client events into one packet, using a schema of its own built on isolated remotes (test/Isolated.luau). It checks four things:- the listeners receive exactly the sequence that was fired;
- the packet is the events’ standalone packets laid end to end;
- a fire the sender refuses leaves the packet byte for byte unchanged;
- a packet cut short delivers every event that ended before the cut, then reports once, naming the event it was cut inside.
It catches the 0.24 phantom event and the 0.29 closed-up holes when either is put back.
-
test/EventIndices.luausends every one of the 256 index bytes, alone, on both channels of every test schema’s server. A declared index must be taken as its declaration, and any other index must be reported exactly once as unknown. It catches the 0.26 channel that never refused when that bug is put back. -
test/Concurrency.luaucovers the limit, and checks that a failing call gives its place back.
0.32.0 — 2026-09-24
The generated modules pass their own --!strict line. The wire format does not change.
Fixed
- Every generated module declares
--!strict, and none had been type-checked. The test schemas’ output carried about 190 strict errors, which a game’s editor showed in a file its owner cannot edit. They came from a few repeated lines:- a
pcallof a writer that returns nothing, destructured into two locals; - a polled event’s iterator ending in a bare
return; - an
Instancecompared with nil where its type does not allow nil; - an exported enum’s
Readreturning a value thatpcallhad widened tostring.
- a
- An empty
FutureLibraryorPromiseLibraryemittedrequire(), which fails to load. An empty path now counts as no path. - A Future or Promise library is required only by a module that invokes through it. The side that answers such a call never uses it, so the require was an unused import there.
Language
- A type named after a built-in Luau type (
number,string,boolean,buffer,thread,any,unknown,never) is refused,E3005. It was exported asexport type number = number, which Luau does not allow. - A top-level type named after a Roblox type the module uses is refused,
E3005. That coversPlayer,Instance,RemoteEvent,UnreliableRemoteEvent,CFrame,Vector3,Color3,DateTime,BrickColor, and any class the schema names inInstance(...).type Player = Instance(Player)exported a type that referred to itself and shadowed everyPlayerparameter in the module. A type inside ascopeis exported with the scope’s name in front, so it is not affected.
Tests
- The type gate analyses
test/Golden, the output of every test schema, as a third contour. test/HalfFloats.luaureads all 65536 f16 bit patterns and checks each against an IEEE decoder written independently. It also writes every value midway between two neighbours and checks it lands on one of them. Putting back the 0.29 carry bug or the 0.31 signed-zero bug makes it fail.
0.31.0 — 2026-09-24
Property-based tests for every serialiser, and what they found. The wire format does not change: modules from 0.27.0 onward still talk to each other.
Fixed
- A 257th declaration on a channel was sent as the first. Each channel numbers its declarations
with one byte, and nothing refused the 257th, so the receiver decoded it as a different event. It
is now a compile error,
E3030. Imports count toward the limit, and declarations a profile leaves out do not. f16lost the sign of zero: -0 arrived as +0. It is now written as0x8000, which an older reader still decodes as 0.- Under
WriteValidations, a fixed-length array longer than its length was cut short on send instead of refused. WithoutWriteValidationsit is still cut short, as an exact-length string is. - A function or event whose data is an empty type pack,
Data: (), read an undeclared global in its reader: nil at runtime, but a type error in the--!strictgenerated module.
Tests
- Every exported type of the test schema is drawn at random, 100 times, into two builds: one with
WriteValidationson and one with the defaults. Draws lean on the edges: a range’s own bounds, lengths of 255 and 256, f16 subnormals, NaN and -0. - Each draw must be written into the size the compiler’s analysis allows, read back as itself, and re-written to the same bytes. The encoding must also be impossible to decode one byte short.
- Corrupted bytes must either fail to decode, or decode to a value the schema admits and the sender would write.
- A value the receiver would not accept, such as a length past its bound or a float that needs
rounding, may be refused. What is written anyway must be read to its last byte, and under
WriteValidationsmust arrive as what was sent. - The tests catch each of three earlier serialiser bugs when it is put back into the compiler: the
0.28.0 length prefix that wrapped, the 0.29.0 f16 carry, and the 0.29.0 holes in optional arrays.
BLINKBLOX_SEEDreplays or varies the draws.
0.30.0 — 2026-09-24
Tooling for the programs that read the compiler’s output: editor tasks, CI steps and coding assistants. The wire format does not change.
Added
--checkruns the whole compile and writes nothing: no modules, no output directories, no prompt. The diagnostics and exit code are those of a real compile. It combines with--watch.--jsonprints the result as one JSON document on standard output and nothing else: every diagnostic with its code, name, file, line, column and byte offset, its labels and its notes, plus the paths written. A failure that is not a diagnostic, such as a missing schema file, is reported in the document too. It cannot be combined with--watch.
Fixed
- A diagnostic named the schema by its bare file name, and one inside an import by the string the
importwrote, relative to whichever file wrote it. Both now name the file by its path, normalised. - The lexer’s one diagnostic, an unexpected character, named the file
input.blinkwhatever it was called. - A schema without
ServerOutputorClientOutputwas reported with the compiler’s own file and line in front of the message.
Documentation
- The syntax highlighting knows
OrderedUnreliable,quat, attributes such as@profile, and decimal numbers. - The promise of an MCP server is withdrawn.
--check --jsongives an assistant what such a server would have.
0.29.0 — 2026-09-24
A runtime audit, made possible once 0.28.0 had split the runtime into files small enough to read whole. The wire format does not change.
Fixed
- A refused or failed invocation reply desynchronised everything batched after it. The caller read the success flag inside the payload’s block, so the cursor had already moved past a payload that was never sent, and a pcall around the read hid the error.
- An invocation is now identified by its id and its function. Ids go round the whole u8 before one is reused, and each call’s timer is cancelled when the call settles. Previously an earlier call’s timer could fail a later one, a late reply could resume the next call, and on the server a client could answer one function’s call with another function’s value type.
- A failed server fire left its Instance in the player’s batch, shifting every instance after it.
- A failed exported
Writelost the queued batch. - A queued event lost every value after a trailing nil.
- A listener that disconnected itself cost the next listener that event.
- A
Synclistener’s error was reported as the sender’s decode failure. OrderedUnreliableaccepted a stale packet across the wrap, starved a player left out of the server’s sends (the server now counts per player), and never freed what it kept.- An optional array’s holes closed up.
- f16 NaN decoded as -65600, and a subnormal lost its carry.
- A float range refused its own bounds once narrowed on receipt.
- A type-pack element named
Lengthshadowed the string writer’s local. - An enum with more than 256 values wrapped.
Hardened
- A polled event’s queue is capped on the server.
- A client can no longer put a line in the output, or a call into the game’s decode handler, for every packet it sends. Both are rate-bounded.
Language
- Repeated flags, values and variants are refused.
- The TypeScript tag is quoted.
- A trailing comma is accepted in every list.
0.28.0 — 2026-09-24
The fork is renamed BlinkBlox. This changes what the tools print and what the release artifacts
are called, not what a game depends on. The remotes, _G._BLINK, the plugin’s Blink output folder,
ServerStorage.BLINK_CONFIGURATION_FILES and the .blink extension all keep their names, so builds
from either side of the rename still talk to each other.
Fixed
- A length prefix wrapped on send. With a lower bound of 0 and
WriteValidationsoff (the default), a 300-byte value instring(0..64)went out with a length of 44 and all 300 bytes behind it. The receiver then decoded the rest of the packet from the wrong offset. The same held for buffers, arrays, unbounded lengths and a map’s count. The upper bound is now checked on send, whatever the options say. Predicton a reliableManyevent dropped the event when no listener was bound, where the network would have queued it.- A field, flag or tag named after a Luau keyword produced a module that did not load.
- The Studio plugin’s editor never showed a warning, because the same parse set it and cleared it. It also printed every warning to Output on every keystroke.
- A map’s size diagnostic had nothing to underline.
- The plugin’s file search treated the query as a Lua pattern.
- Exact-bound errors read “to equal to”.
Changed
- Luau files are capped at 500 lines with no exceptions. The parser, the generator, the prefabs and the plugin editor were split to fit.
0.27.0 — 2026-09-24
Ideas taken from reading ByteNet-Max, Warp and satset. Recompile both modules (WIRE_VERSION 2).
Added
- An inbound byte budget per player. Set it with
option InboundBytesPerSecondandoption InboundBurst. Each server connection charges a per-player token bucket before it decodes anything. A packet costs its size, and never less than 128 bytes. The burst is a whole second, because after a hitch Roblox delivers the backlog at once, and a refused reliable packet takes every event in it along. Refusals go to the rate-limit handler withEventnil. boolean[]is packed eight to a byte.CFrame<quat>encodes a rotation in 7 bytes instead of 12. It is opt-in, because it is lossy.
Fixed
- Color3 wrapped HDR channels: 2.0 arrived as 254/255. Channels are now clamped.
- The docs had described CFrame’s two components backwards since upstream.
Performance
- Each flush threw away the buffer it had just grown. Keeping it measured 35 to 52 percent faster on the flush path.
0.26.0 — 2026-09-22
Recompile both modules. The rate-limit handler now takes (Player, Event, Refused).
Fixed
- A channel the server receives nothing on (every event
From: Server, or no unreliable client event) never got the unknown-index guard. A client’s junk packet was read one byte at a time up toMaxEventsPerPacket, and nothing was reported. The channel now fails at the first byte, once, throughSetDecodeErrorHandlerwith the eventnil. - The rate-limit handler was spawned on every refusal. It now shares the warning’s schedule of once
a second per player per event, and receives
Refused, the number of refusals that call stands for.
0.25.0 — 2026-09-19
Recompile both modules if a schema has an open float range.
Added
@profile("dev" | "debug" | "test" | "release")keeps a declaration out of every build that did not ask for it. The default isrelease. Choose a profile with--profileon the CLI, or with aProfileattribute in the Studio plugin. New codes: E3025, E3026, E3027, E3028.
Fixed
- An open side of a float range was filled with the exact-integer limit (2^24 for
f32), sof32(0..)refused 2e7. Open sides are now unbounded. - A vector range has bounds of its own,
0..inf. A negative lower bound on the magnitude is an error. - The vector magnitude check is now tested. Lune has no global
Vector3, so no test had ever run it. lune run init -- file.blinkno longer treats--as the config path.
0.24.2 — 2026-09-18
A hardening patch. Generated output changes; the wire format and the schema signature do not.
Hardened
- The server’s queue for an event with no listener stops at 256 and drops the rest silently. An invocation past the cap is answered with a failure.
- Ranges refuse NaN.
x < Minpasses NaN, so the checks are now writtennot (x >= Min).
Added
SetDecodeErrorHandlertells the game which player sent a packet that failed to decode, and which event the packet claimed to be (upstream #102).
Fixed
- Invoking a player who had already left fails immediately instead of waiting out the timeout.
0.24.1 — 2026-09-17
Toolchain only (lefthook 2.1.14). The compiler is unchanged.
0.24.0 — 2026-09-17
No wire-format or signature change.
Fixed
-
A write that threw left its bytes behind:
- a reliable fire delivered a phantom event;
- an unreliable fire left its scratch buffer installed and dropped the pending reliable batch;
- an invocation stranded its slot.
Upstream has these open as #91 and #107.
-
A reply that could not be serialised answered nobody, and the caller waited out the full timeout. It now fails the call.
-
Two files importing each other recursed until the path outgrew the filesystem. This is now E3015, “Cyclic import”.
-
A type naming itself is reported as E3022, not as “Unknown reference”.
-
An unknown
Casingis caught at parse time (E2003). -
Component errors now have their own code (E3023), and so do duplicate options (E3024).
-
Poll: truewarns (W3017). -
The Studio plugin’s version had drifted five minor releases behind the compiler’s.
-
--versionworks, and--quietsilences the banner.
0.23.0 — 2026-09-11
The devforum release. Recompile both modules.
Added
- A schema signature on both modules. A client and a server built from different schemas refuse each other at startup instead of decoding one event as another.
Fromon functions, so the server can invoke a client.option InvocationTimeout(10 seconds by default). Invocations had no timeout, and a lost call leaked a slot.- Named type-pack elements:
Data: (chefId: f64, dish: string). - A thread pool for Async dispatch, kept because it measured faster.
Hardened
- The client’s decode loop is guarded the way the server’s already was.
- Map keys that can never be looked up (tables) are refused at compile time.
Changed
SyncValidationsays that it discards the rest of the packet, which it always did silently.- A second
.Onon aSingleevent warns, because the first listener’s disconnect stops working.
Studio plugin
- The editor no longer rebuilds one frame per line on every keystroke (39 ms at line 800, now constant).
- It no longer paints the document twice.
- It no longer crashes past 2000 lines.
- It no longer deletes other contents of the output folder.
- It remembers the output location.
0.22.0 — 2026-09-11
Compatible with 0.21.0 on the wire.
Hardened
- A decoded length is checked before the read and the allocation it authorises.
- Every variable-length read is bounded by the bytes left in the packet.
- Decode loops are bounded by what the packet paid for:
unknownchecks the value it takes;- an element that costs nothing to decode is refused where no bound was written (E3021);
- a count is checked before
table.create.
0.21.0 — 2026-09-10
A wire-format release. Recompile both modules.
Added
- Booleans and optional flags share a bitfield. Seventeen booleans went from 17 bytes to 3.
- Lengths are encoded relative to their minimum.
u8[300..400]spends one byte on its length instead of two. Type: OrderedUnreliablediscards an unreliable packet that arrives after a newer one.
0.20.0 — 2026-09-10
A security release.
Added
- Per-player, per-event rate limits. Use
RateandBurston each event, oroption DefaultRatefor all of them, andoption RequireRatesto make a missing rate an error.SetRateLimitHandlerreceives refusals. Nobody is kicked automatically. - Unreliable size analysis. An unreliable event that can never fit is refused (E3018). One that
might not fit is warned about (W3019). A runtime check catches the rest. The limit is set with
option MaxUnreliableSize, 900 by default.
Hardened
- Remote arguments are type-checked before they are read.
- Decoding stops when the player leaves.
- Both remotes are class-checked when the module is required.
- Invocation slots are recycled from a 32-slot bitset.
- Types carrying an
Instanceorunknowncannot be exported.
Fixed
- Numeric options never lexed.
- Only the first seventeen
optionstatements were read. Color3(1..2)silently dropped its range, andunknown?parsed.- The diagnostics renderer crashed on a blank line.
Upgrading
- Schemas that used to compile may now be refused.
- The concurrent invocation ceiling is 32, down from 256.
0.19.0 — 2026-09-10
The first release of the fork, continuing from upstream v0.18.8.
Hardened
- Inbound packets are bounded:
MaxPacketSize(8192),MaxEventsPerPacket(64) andMaxInstancesPerPacket(256);- an unknown event id stops the parse;
- a truncated packet is contained.
- A player who leaves mid-call no longer lingers in the replication map.
Fixed
- Instance classes containing a digit parse.
- Edit-mode stubs keep the shape of the real API.
- The CLI works unattended.
TypesOutputresolves against the output path.
Added
option Predictdelivers an event to local listeners without a remote.
Full release notes, with the measurements behind each change, are on GitHub Releases.
