Documentation
¶
Overview ¶
Package clientcodec generates the browser runtime's protobuf codec, its predicate manifest, and the cross-runtime golden vectors, from the same FileDescriptorSet that drives the Go refinement generator.
Why generated ¶
The bugs this code attracts are a wrong field number, a wrong wire type, or a field somebody forgot — and none of them is visible in review. Reading the descriptors makes the client and the server incapable of disagreeing about the wire, and makes "add a field" a regeneration rather than a transcription (docs/protocol.md §10.2).
What it emits ¶
client/codec.gen.js the codec: schema table, encoder, decoder client/predicates.manifest.txt every predicate, and who enforces it client/test/golden.json Go-encoded vectors for the JS round-trip
All three are committed. Consumers of this module never run this package, and the browser never sees a code generator.
A schema table, not straight-line code ¶
The codec is a compact schema string plus one generic encoder and one generic decoder that interpret it. Straight-line per-field code would be bigger and would grow linearly with the schema; a table grows by one line per field and compresses, which is what a 12,288-byte gzip ceiling (PRD NFR-2) actually rewards. It is still generated code: every field number, wire kind and length bound in the table comes from the descriptors, which is the property docs/protocol.md §10.2 asks for.
Predicate enforcement is directional ¶
docs/protocol.md §10.3 fixes the line: the client enforces length bounds on decode, because the decoder has already read a length prefix and the check costs two comparisons; it enforces nothing else, because an RE2 engine and a predicate evaluator are not in the byte budget. That asymmetry is emitted as a manifest a reviewer can read rather than left as an unwritten assumption.
Determinism ¶
Regenerating twice must be byte-identical: descriptors are walked in declaration order, nothing is ranged over a map, and no clock, hostname or path enters the output. A spec asserts it.
Index ¶
Constants ¶
This section is empty.
Variables ¶
This section is empty.
Functions ¶
func EmitCodec ¶
EmitCodec renders client/codec.gen.js.
The file is three parts: a generated schema table, a generated enum block, and a fixed encoder/decoder pair that interprets the table. Only the first two vary with the schema, which is the point — the wire knowledge is data read out of the descriptors, and the code that consumes it never has to be re-reviewed when a field is added.
func EmitGolden ¶
EmitGolden renders client/test/golden.json.
The frames below are built with the Go runtime, marshalled, and then read back through protoreflect to produce the expected JavaScript-shaped value. Reading it back rather than writing it by hand is deliberate: the expected object then omits exactly the fields proto3 omits on the wire, so a disagreement about default-value handling shows up as a test failure instead of being encoded into the fixture by whoever wrote it.
func EmitManifest ¶
EmitManifest renders client/predicates.manifest.txt.
docs/protocol.md §10.3 requires this file and requires CI to fail when it drifts from the descriptors. Its job is to make the enforcement asymmetry a generated artifact a reviewer can read, rather than a paragraph somebody has to remember.
Types ¶
type Artifact ¶
type Artifact struct {
// Path is relative to the output directory and is part of the generated
// output: gen.sh compares it, so a renamed artifact is a diff rather than
// a silently orphaned file.
Path string
// Data is the file's exact bytes. Generating into memory is what makes the
// determinism spec cheap: it generates twice and compares these.
Data []byte
}
Artifact is one generated file: a path relative to the output directory and its exact bytes.
type Conjunct ¶
type Conjunct struct {
// Text is the term's source, verbatim.
Text string
// Class is length, numeric or matches — the three rows of
// docs/protocol.md §10.3's table.
Class string
// Enforced records whether the generated client decoder checks it. Only
// length conjuncts on string and bytes fields are.
Enforced bool
}
Conjunct is one `&&`-separated term of a predicate.
type Enum ¶
type Enum struct {
// Name is the enum's proto name, unqualified.
Name string
// Values are its non-zero members in declaration order.
Values []EnumValue
}
Enum is an enumeration the client reads or writes. The zero member is dropped: docs/protocol.md §3.5 says it is never valid on the wire, so emitting it would only invite somebody to send it.
type EnumValue ¶
type EnumValue struct {
// Name is the member's proto name, emitted verbatim.
Name string
// Number is the value on the wire.
Number int32
}
EnumValue is one non-zero member.
type Field ¶
type Field struct {
// Number is the proto field number, which is what actually appears on the
// wire and therefore what the decoder dispatches on.
Number int
// Kind is the single-letter wire kind the emitted table uses:
//
// v varint scalar or enum b bool
// s string y bytes
// m singular message r repeated message
// p repeated packed varint
Kind string
// Name is the field's proto name. The emitted JavaScript uses it verbatim,
// so the client's object keys are the schema's names rather than a
// camel-cased translation of them.
Name string
// Arg is "min:max" for s and y when a length predicate bounds them, the
// message name for m and r, and empty otherwise.
Arg string
}
Field is one field, reduced to what a codec needs: where it sits, how it is framed, what to call it, and — for length-delimited scalars — the bound the decoder enforces.
type Message ¶
type Message struct {
// Name is the message's proto name, unqualified, and the identifier the
// emitted codec's table is keyed by.
Name string
// Fields are its fields in declaration order, not field-number order: the
// emitted table's order is part of the generated bytes.
Fields []Field
}
Message is one wire message and its fields.
type Predicate ¶
type Predicate struct {
// Message is the unqualified name of the message the field belongs to.
Message string
// Field is the field's proto name. The pair identifies the row in the
// manifest.
Field string
// Expr is the predicate source text from the .proto option, verbatim. It
// is reproduced rather than rewritten so the manifest quotes what a
// reviewer will find in the schema.
Expr string
// Conjuncts is Expr split on `&&`, because enforcement is decided term by
// term: a field can carry a length bound the client checks and a regular
// expression it does not.
Conjuncts []Conjunct
}
Predicate is one field's refinement expression, split into the conjuncts the manifest reports individually. Splitting matters: a field can carry a length bound the client enforces and a regular expression it does not, and a manifest that reported one verdict per field could not say so.
type Schema ¶
type Schema struct {
// Messages are the wire messages, in declaration order.
Messages []Message
// Enums are the enumerations the client reads or writes, in declaration
// order and without their zero members.
Enums []Enum
// Predicates are the refinement expressions, one per refined field, in the
// order the fields were declared. They drive the manifest rather than the
// codec: most of them are not enforced client-side, and the manifest is
// where that is said out loud.
Predicates []Predicate
}
Schema is the whole input to code generation, in the order it was declared. Order is the determinism guarantee: nothing below is sorted, keyed by map, or otherwise given a chance to vary between runs.
type Vector ¶
type Vector struct {
// Name identifies the case in the JS test's failure output.
Name string `json:"name"`
// Note says what the case is for, so a failure names the property rather
// than only the vector.
Note string `json:"note"`
// Hex is the canonical Go encoding of Frame, hex-encoded because the
// fixture is JSON.
Hex string `json:"hex"`
// Frame is the expected decoded value in JavaScript shape, read back
// through protoreflect rather than written by hand so that it omits
// exactly the fields proto3 omits on the wire.
Frame map[string]any `json:"frame"`
// Reencode asks the JS test for the harder half: encode Frame and require
// the bytes to equal Hex. A vector without it checks only that the client
// can read what Go writes.
Reencode bool `json:"reencode"`
}
Vector is one cross-runtime round-trip case.
Hex is what the Go protobuf runtime produced for Frame. The JS test decodes it and asserts the result equals Frame, which checks Go→JS; when Reencode is set it also encodes Frame and asserts the bytes come back byte-identical to Hex, which checks JS→Go — byte equality with the canonical Go encoding is a stronger statement than "Go can parse it", because it also rules out a client that is merely tolerantly parseable.