Repository navigation
Conversation
Tom32i
force-pushed
the
feat/js-codec-cursor
branch
3 times, most recently
from
October 14, 2026 15:01
70acb2e to
b794f30
Compare
Codecs now receive a Cursor that advances on every read/write: encode(cursor, data) / decode(cursor). Composite codecs just call their sub-codecs in order, like the Go side with *bytes.Buffer. - Cursor: typed write*/read* methods, writeBytes/readBytes, writeUtf8/readUtf8, and reserve() to fill bytes in later. - BinaryEncoder creates a cursor per call (nested calls stay safe). - String codecs encode once, straight into the buffer, with a UTF-8 byte count that does not encode (utf8ByteLength). - String codecs (JS and Go) refuse strings longer than their length prefix allows, instead of wrapping it: a wrapped prefix let a crafted string forge the fields that follow it. JS throws RangeError, Go's BinaryEncoder.Encode returns ErrEncodeFailed. - UIntLongCodec uses arithmetic instead of binary strings. - Client catches decode errors like the server does. - Demo: composite PositionCodec event, in JS and Go. - Docs: cursor API, v3 -> v4 migration, FloatPrecisionCodec example, string limits, Go encode/decode errors. - Tests import from src and cover the cursor, UTF-8 length, nested calls and string limits. BREAKING CHANGE: JS codec signatures change from (buffer, offset) to (cursor). Strings over the codec limit now throw (JS) or return ErrEncodeFailed (Go) instead of being sent corrupted. The wire format is unchanged. Closes #25
Tom32i
force-pushed
the
feat/js-codec-cursor
branch
from
October 14, 2026 15:07
b794f30 to
a4aa905
Compare
4 tasks done
data * factor is not exact in floating point: 0.29 * 100 is 28.999999999999996, which the int codec truncated to 28, so 0.29 decoded as 0.28. Both JS and Go now round to the nearest integer, halves away from zero, so a JS and a Go sender produce the same bytes.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #25
JS codecs no longer keep the offset by hand. They receive a
Cursorthat moves forward on every read and write, the same way Go codecs use*bytes.Buffer. A composite codec is now just its fields in order:Changes
Cursor(src/encoder/Cursor.js): aDataViewplus a position, withwrite*/read*methods forUint8,Int8,Uint16,Int16,Uint32,Int32,Float32andFloat64, pluswriteBytes/readBytes, andreserve(byteLength)to skip bytes and fill them in later (it returns their offset). It's exported fromnetcode,netcode/client,netcode/serverandnetcode/encoder.BinaryEncodercreates one cursor perencode/decodecall. That's oneDataViewper message instead of one per field. A shared, reused cursor was tried and benchmarked: it was no faster (each new buffer needs a newDataViewanyway), and nested calls on the same encoder corrupted the outer message.encode(cursor, data)anddecode(cursor).getByteLength(data)is unchanged.TextEncoder.encodeInto), and the length prefix is filled in from the bytes written.getByteLengthcounts UTF-8 bytes without encoding (utf8ByteLength, with a native ASCII check for longer strings).CursorgainswriteUtf8/readUtf8.StringCodec, 65,535 forStringLongCodec) are now refused instead of wrapping the prefix. Before, a wrapped prefix let a crafted string forge the fields that follow it in a composite codec. For example, a player's 259-byte name relayed by the server decoded as"Bob"plus a forgedroleandx. JS throwsRangeError(ingetByteLength, so before allocating), and Go'sBinaryEncoder.Encodenow recovers codec panics and returns a newErrEncodeFailederror. No measurable performance cost.UIntLongCodecuses arithmetic instead of binary strings. Values too large for the byte length still throwRangeError.doc/codecs.md: rewrote the custom codec example, added a reference for theCursormethods, and added a v3 → v4 migration guide.getByteLength(value). Added a composite-codec test and aBinaryEncoderround-trip test.positionevent{ id, x, y }using a custom composite codec:demo/PositionCodec.js(cursor API, shared by the JS client and server), plus the matching GoPositionCodecindemo/server.go.test/encoder.test.jsnow imports fromsrcinstead of'netcode', which resolved to an unbuiltdist/and failed.FloatPrecisionCodecexample indoc/codecs.mdpassed an instance; the constructor expects the class.Performance
How many times faster this PR is than v3 (
main), in messages per second throughBinaryEncoder(2× = twice as many messages per second). Node 22, Apple Silicon, median of 3 runs, after warmup:UInt8{ id, x, y }StringLongCodec)UIntLongCodec(6)timestampBreaking change
The JS codec signatures change from
(buffer, offset)to(cursor), so this needs a major version (v4.0.0). Strings over a codec's limit now throw (JS) or returnErrEncodeFailed(Go) instead of being sent corrupted. The version bump is left for the release commit. Custom codecs (e.g. the 41 client codecs in curvytron2) need migrating; see "Upgrading from v3" indoc/codecs.md.The wire format is unchanged. The same 7 messages (uint8, 6-byte long int, bool, short and long strings, int16, float64) encode to identical bytes in JS and Go, so v3 and v4 clients can talk to each other and to the Go server. The Go side is not touched.
Test plan
npm test: 85 passed (includes UTF-8 length vsTextEncoder, nested encoder calls,UIntLongCodecbyte order and range)npm run lint,npm run buildgo test ./go/...(includes string limits: 255/65,535 bytes accepted, one more refused, multi-byte counted in bytes)demo/server.jsanddemo/server.go. All events, includingposition, go through in both directions.