Skip to content

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: N on a function caps how many of one player’s calls the server runs at once. Rate limits 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 SetRateLimitHandler and 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), and Concurrency on a From: Server function warns (W3020).

Tests

  • test/Composition.luau fires 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.luau sends 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.luau covers 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 pcall of a writer that returns nothing, destructured into two locals;
    • a polled event’s iterator ending in a bare return;
    • an Instance compared with nil where its type does not allow nil;
    • an exported enum’s Read returning a value that pcall had widened to string.
  • An empty FutureLibrary or PromiseLibrary emitted require(), 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 as export type number = number, which Luau does not allow.
  • A top-level type named after a Roblox type the module uses is refused, E3005. That covers Player, Instance, RemoteEvent, UnreliableRemoteEvent, CFrame, Vector3, Color3, DateTime, BrickColor, and any class the schema names in Instance(...). type Player = Instance(Player) exported a type that referred to itself and shadowed every Player parameter in the module. A type inside a scope is 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.luau reads 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.
  • f16 lost the sign of zero: -0 arrived as +0. It is now written as 0x8000, 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. Without WriteValidations it 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 --!strict generated module.

Tests

  • Every exported type of the test schema is drawn at random, 100 times, into two builds: one with WriteValidations on 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 WriteValidations must 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_SEED replays 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

  • --check runs 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.
  • --json prints 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 import wrote, 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.blink whatever it was called.
  • A schema without ServerOutput or ClientOutput was 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 --json gives 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 Write lost 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 Sync listener’s error was reported as the sender’s decode failure.
  • OrderedUnreliable accepted 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 Length shadowed 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 WriteValidations off (the default), a 300-byte value in string(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.
  • Predict on a reliable Many event 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 InboundBytesPerSecond and option 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 with Event nil.
  • 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 to MaxEventsPerPacket, and nothing was reported. The channel now fails at the first byte, once, through SetDecodeErrorHandler with the event nil.
  • 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 is release. Choose a profile with --profile on the CLI, or with a Profile attribute 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), so f32(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.blink no 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 < Min passes NaN, so the checks are now written not (x >= Min).

Added

  • SetDecodeErrorHandler tells 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 Casing is caught at parse time (E2003).

  • Component errors now have their own code (E3023), and so do duplicate options (E3024).

  • Poll: true warns (W3017).

  • The Studio plugin’s version had drifted five minor releases behind the compiler’s.

  • --version works, and --quiet silences 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.
  • From on 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

  • SyncValidation says that it discards the rest of the packet, which it always did silently.
  • A second .On on a Single event 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:
    • unknown checks 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: OrderedUnreliable discards 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 Rate and Burst on each event, or option DefaultRate for all of them, and option RequireRates to make a missing rate an error. SetRateLimitHandler receives 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 Instance or unknown cannot be exported.

Fixed

  • Numeric options never lexed.
  • Only the first seventeen option statements were read.
  • Color3(1..2) silently dropped its range, and unknown? 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) and MaxInstancesPerPacket (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.
  • TypesOutput resolves against the output path.

Added

  • option Predict delivers an event to local listeners without a remote.

Full release notes, with the measurements behind each change, are on GitHub Releases.