diff --git a/packages/protobuf/src/to-binary-fast.ts b/packages/protobuf/src/to-binary-fast.ts index 53e47919c..546a3577f 100644 --- a/packages/protobuf/src/to-binary-fast.ts +++ b/packages/protobuf/src/to-binary-fast.ts @@ -37,6 +37,16 @@ // single tight loop with no intermediate `Uint8Array`/`number[]` objects // per field. // +// Hot-path specialization (H3A): rather than re-dispatching on +// `field.fieldKind` and `field.scalar` for every field of every message +// instance, we precompute an array of closures per `DescMessage` the +// first time we touch that schema. Each closure pre-captures +// (tagBytes, localName, scalar-specific writer) so the inner loop is a +// flat `for (const step of steps) off = step(msg, ...)` — no switch, +// no property lookups per field. Step arrays are cached in a WeakMap +// keyed by `DescMessage` and live for the lifetime of the schema. +// CSP-safe: no `eval`, no `new Function()`, no dynamic source generation. +// // Scope: // - supported: scalar fields (all 15 types), enums, nested messages, // repeated scalar (packed + unpacked), repeated message, @@ -72,6 +82,11 @@ import { getTextEncoding } from "./wire/text-encoding.js"; const supportCache = new WeakMap(); +// `0n` requires target >= ES2020, but this package is compiled for ES2017. +// Materialize the bigint zero once at module load so closures can compare +// against it without the BigInt() call on the hot path. +const BIGINT_ZERO = BigInt(0); + /** * Walk the descriptor (including transitive message fields) and return * true iff every field in the subtree uses an MVP-supported shape. The @@ -191,6 +206,25 @@ function tagSize(fieldNo: number, wireType: number): number { return varintSize32(((fieldNo << 3) | wireType) >>> 0); } +/** + * Encode a tag as a pre-built Uint8Array once per field. At write time + * we `buf.set(tagBytes, pos)` to blit the bytes — this removes both the + * varint loop and the repeated `(fieldNo << 3) | wireType` work from the + * hot path. Tag size is 1 byte for fields 1-15, 2 bytes up to 2047, etc. + */ +function encodeTag(fieldNo: number, wireType: number): Uint8Array { + let v = ((fieldNo << 3) | wireType) >>> 0; + const size = varintSize32(v); + const out = new Uint8Array(size); + let i = 0; + while (v > 0x7f) { + out[i++] = (v & 0x7f) | 0x80; + v = v >>> 7; + } + out[i] = v; + return out; +} + /** * UTF-8 byte length of a JS string without encoding. Mirrors the helper * used in opentelemetry-js#6390 — correct for valid UTF-16 input (which @@ -228,7 +262,7 @@ function utf8ByteLength(str: string): number { type SizeMap = Map; // ----------------------------------------------------------------------------- -// Pass 1 — size estimation +// Scalar size / write — shared helpers // ----------------------------------------------------------------------------- function scalarSize(type: ScalarType, value: unknown): number { @@ -299,59 +333,6 @@ function scalarWireType(type: ScalarType): number { } } -/** - * Should this non-oneof field be emitted for the given message? - * Oneof members are dispatched separately and never flow through this - * predicate. - */ -function isFieldSet(field: DescField, value: unknown): boolean { - // Explicit presence (proto2 / proto3 optional): the generated setters - // only assign when the property was set. Missing ⇒ undefined. - if (value === undefined || value === null) return false; - - // Implicit presence (proto3 singular scalar/enum): zero value means - // "not set" and must not be emitted. Lists/maps handled separately - // (empty list/map means "not set" too). - switch (field.fieldKind) { - case "scalar": { - const t = field.scalar; - if (field.presence !== 2 /* IMPLICIT */) { - // Explicit / legacy required: any defined value counts as set. - return true; - } - if (t === ScalarType.STRING) return (value as string).length > 0; - if (t === ScalarType.BYTES) return (value as Uint8Array).length > 0; - if (t === ScalarType.BOOL) return value === true; - if ( - t === ScalarType.INT64 || - t === ScalarType.UINT64 || - t === ScalarType.SINT64 || - t === ScalarType.FIXED64 || - t === ScalarType.SFIXED64 - ) { - // bigint zero, numeric zero, "0" string all represent unset. - // Compare via coercion so 0n / 0 / "0" all return false. - return value !== 0 && value !== 0n && value !== "0"; - } - return (value as number) !== 0; - } - case "enum": - if (field.presence !== 2 /* IMPLICIT */) return true; - return (value as number) !== 0; - case "message": - return true; // already filtered by undefined check above - case "list": - return (value as unknown[]).length > 0; - case "map": - // Map fields carry their own "any entry" gate here — empty object - // ⇒ not set ⇒ omit. Same semantics as reflect.unsafeIsSet. - return Object.keys(value as object).length > 0; - } - // Exhaustive switch; unreachable. Return true so unexpected shapes - // surface as a size/write mismatch error rather than silent data loss. - return true; -} - // ----------------------------------------------------------------------------- // Map key helpers // ----------------------------------------------------------------------------- @@ -388,222 +369,10 @@ function coerceMapKey(stringKey: string, keyType: MapKeyScalar): unknown { } } -/** - * Body-size of a single map entry message `{ key, value }`, excluding - * the outer field tag and length prefix. Returns both the body size and, - * for message-typed values, the submessage body size (so the writer - * doesn't recompute it). - */ -function estimateMapEntryBody( - field: DescField & { fieldKind: "map" }, - keyTyped: unknown, - value: unknown, - sizes: SizeMap, -): { body: number; valueSubSize: number } { - // Entry key is always field number 1. - const keySize = - tagSize(1, scalarWireType(field.mapKey)) + scalarSize(field.mapKey, keyTyped); - let valSize: number; - let valueSubSize = 0; - switch (field.mapKind) { - case "scalar": - valSize = - tagSize(2, scalarWireType(field.scalar)) + - scalarSize(field.scalar, value); - break; - case "enum": - valSize = tagSize(2, WIRE_VARINT) + int32Size(value as number); - break; - case "message": { - const sub = value as Record; - valueSubSize = estimateMessageSize(field.message, sub, sizes); - sizes.set(sub, valueSubSize); - valSize = - tagSize(2, WIRE_LENGTH_DELIMITED) + - varintSize32(valueSubSize) + - valueSubSize; - break; - } - } - return { body: keySize + valSize, valueSubSize }; -} - -/** - * Size contribution of a map field: for every entry, an outer tag + length - * prefix + entry body. Map entries are always length-delimited — map fields - * cannot use delimited (group) encoding. - */ -function estimateMapFieldSize( - field: DescField & { fieldKind: "map" }, - obj: Record, - sizes: SizeMap, -): number { - const tagBytes = tagSize(field.number, WIRE_LENGTH_DELIMITED); - let size = 0; - for (const strKey of Object.keys(obj)) { - const keyTyped = coerceMapKey(strKey, field.mapKey); - const { body } = estimateMapEntryBody(field, keyTyped, obj[strKey], sizes); - size += tagBytes + varintSize32(body) + body; - } - return size; -} - -/** - * Size contribution of a single non-oneof non-map "regular" field. Broken - * out so that the oneof dispatch can reuse the same switch. - */ -function estimateRegularFieldSize( - field: DescField, - value: unknown, - sizes: SizeMap, -): number { - switch (field.fieldKind) { - case "scalar": - return ( - tagSize(field.number, scalarWireType(field.scalar)) + - scalarSize(field.scalar, value) - ); - case "enum": - return tagSize(field.number, WIRE_VARINT) + int32Size(value as number); - case "message": { - const sub = value as Record; - const subSize = estimateMessageSize(field.message, sub, sizes); - sizes.set(sub, subSize); - return ( - tagSize(field.number, WIRE_LENGTH_DELIMITED) + - varintSize32(subSize) + - subSize - ); - } - case "list": { - const list = value as unknown[]; - let size = 0; - if (field.listKind === "message") { - const tagBytes = tagSize(field.number, WIRE_LENGTH_DELIMITED); - for (let k = 0; k < list.length; k++) { - const sub = list[k] as Record; - const subSize = estimateMessageSize(field.message, sub, sizes); - sizes.set(sub, subSize); - size += tagBytes + varintSize32(subSize) + subSize; - } - return size; - } - if (field.listKind === "enum") { - if (field.packed) { - let body = 0; - for (let k = 0; k < list.length; k++) { - body += int32Size(list[k] as number); - } - return ( - tagSize(field.number, WIRE_LENGTH_DELIMITED) + - varintSize32(body) + - body - ); - } - const tagBytes = tagSize(field.number, WIRE_VARINT); - for (let k = 0; k < list.length; k++) { - size += tagBytes + int32Size(list[k] as number); - } - return size; - } - // listKind === "scalar" - const t = field.scalar; - const wt = scalarWireType(t); - if (field.packed && wt !== WIRE_LENGTH_DELIMITED) { - let body = 0; - for (let k = 0; k < list.length; k++) { - body += scalarSize(t, list[k]); - } - return ( - tagSize(field.number, WIRE_LENGTH_DELIMITED) + - varintSize32(body) + - body - ); - } - const tagBytes = tagSize(field.number, wt); - for (let k = 0; k < list.length; k++) { - size += tagBytes + scalarSize(t, list[k]); - } - return size; - } - case "map": - // Map fields flow through estimateMapFieldSize; this branch is - // defensive and never taken on the estimation hot path. - return estimateMapFieldSize( - field as DescField & { fieldKind: "map" }, - value as Record, - sizes, - ); - } - return 0; -} - -function estimateMessageSize( - desc: DescMessage, - message: Record, - sizes: SizeMap, -): number { - let size = 0; - const fields = desc.fields; - for (let i = 0; i < fields.length; i++) { - const field = fields[i]; - // Oneof members are dispatched via the `desc.oneofs` loop below. - if (field.oneof !== undefined) continue; - - if (field.fieldKind === "map") { - const obj = message[field.localName] as - | Record - | undefined; - if (!obj || Object.keys(obj).length === 0) continue; - size += estimateMapFieldSize( - field as DescField & { fieldKind: "map" }, - obj, - sizes, - ); - continue; - } - - const value = message[field.localName]; - if (!isFieldSet(field, value)) continue; - size += estimateRegularFieldSize(field, value, sizes); - } - // Oneof dispatch: at most one field per oneof contributes, identified by - // the `case` discriminator on the oneof ADT object. Zero values are - // emitted when a oneof case is explicitly set — that's the whole point - // of the oneof: presence is carried by the discriminator, not by value. - const oneofs = desc.oneofs; - for (let i = 0; i < oneofs.length; i++) { - const oneof = oneofs[i]; - const adt = message[oneof.localName] as - | { case: string | undefined; value?: unknown } - | undefined; - if (!adt || adt.case === undefined) continue; - const selected = findOneofField(oneof, adt.case); - if (!selected) continue; - size += estimateRegularFieldSize(selected, adt.value, sizes); - } - return size; -} - -function findOneofField( - oneof: DescOneof, - caseName: string, -): DescField | undefined { - const fs = oneof.fields; - for (let i = 0; i < fs.length; i++) { - if (fs[i].localName === caseName) return fs[i]; - } - return undefined; -} - // ----------------------------------------------------------------------------- -// Pass 2 — write into pre-allocated buffer +// Cursor — mutable writer state bundled for closure access // ----------------------------------------------------------------------------- -/** - * Writer state bundled into a plain object so that helper functions can - * mutate `pos` without paying for method-call indirection on a class. - */ interface Cursor { buf: Uint8Array; view: DataView; @@ -620,10 +389,6 @@ function writeVarint32(c: Cursor, v: number): void { c.buf[c.pos++] = v; } -function writeTag(c: Cursor, fieldNo: number, wireType: number): void { - writeVarint32(c, ((fieldNo << 3) | wireType) >>> 0); -} - function writeVarint64(c: Cursor, lo: number, hi: number): void { let l = lo >>> 0; let h = hi >>> 0; @@ -744,193 +509,1080 @@ function writeScalar(c: Cursor, type: ScalarType, value: unknown): void { } } -function writeMapEntry( +/** Blit a pre-encoded tag into the buffer. Faster than re-running the + * varint loop for every field on every message. */ +function writeTagBytes(c: Cursor, tagBytes: Uint8Array): void { + // Hand-inline the common tag sizes (1 byte for fields 1-15, 2 bytes + // up to 2047). The majority of OTel-shaped fields hit the 1-byte case. + const len = tagBytes.length; + if (len === 1) { + c.buf[c.pos++] = tagBytes[0]; + return; + } + if (len === 2) { + c.buf[c.pos++] = tagBytes[0]; + c.buf[c.pos++] = tagBytes[1]; + return; + } + c.buf.set(tagBytes, c.pos); + c.pos += len; +} + +// ----------------------------------------------------------------------------- +// Per-schema step compilation (H3A template specialization) +// ----------------------------------------------------------------------------- +// +// Each field of a message compiles to a pair of closures: +// - SizeStep: given a message, returns the number of bytes this field +// will contribute in pass 2 (or 0 if the field is unset). +// - EncodeStep: given a cursor + message, writes the field bytes. +// +// Closures pre-capture (tagBytes, localName, scalar-specific helpers). +// The inner encode loop becomes `for (const step of steps) step(c, msg, sizes)` +// with no switch dispatch — the branch tables live in the step factory +// and run once per schema at compilation time. + +type SizeStep = (msg: Record, sizes: SizeMap) => number; +type EncodeStep = ( c: Cursor, - field: DescField & { fieldKind: "map" }, - keyTyped: unknown, - value: unknown, + msg: Record, sizes: SizeMap, -): void { - // Entry key: field number 1. - writeTag(c, 1, scalarWireType(field.mapKey)); - writeScalar(c, field.mapKey, keyTyped); - // Entry value: field number 2. - switch (field.mapKind) { - case "scalar": - writeTag(c, 2, scalarWireType(field.scalar)); - writeScalar(c, field.scalar, value); - return; - case "enum": - writeTag(c, 2, WIRE_VARINT); - writeInt32(c, value as number); - return; - case "message": { - const sub = value as Record; - const subSize = sizes.get(sub) ?? 0; - writeTag(c, 2, WIRE_LENGTH_DELIMITED); - writeVarint32(c, subSize); - writeMessageInto(c, field.message, sub, sizes); - return; +) => void; + +const sizeStepsCache = new WeakMap(); +const encodeStepsCache = new WeakMap(); + +function getSizeSteps(desc: DescMessage): SizeStep[] { + let cached = sizeStepsCache.get(desc); + if (cached !== undefined) return cached; + cached = buildSizeSteps(desc); + sizeStepsCache.set(desc, cached); + return cached; +} + +function getEncodeSteps(desc: DescMessage): EncodeStep[] { + let cached = encodeStepsCache.get(desc); + if (cached !== undefined) return cached; + cached = buildEncodeSteps(desc); + encodeStepsCache.set(desc, cached); + return cached; +} + +function findOneofField( + oneof: DescOneof, + caseName: string, +): DescField | undefined { + const fs = oneof.fields; + for (let i = 0; i < fs.length; i++) { + if (fs[i].localName === caseName) return fs[i]; + } + return undefined; +} + +// ----------------------------------------------------------------------------- +// Size steps +// ----------------------------------------------------------------------------- + +function buildScalarSizeStep( + field: DescField & { fieldKind: "scalar" }, +): SizeStep { + const localName = field.localName; + const tagLen = tagSize(field.number, scalarWireType(field.scalar)); + const t = field.scalar; + const explicit = field.presence !== 2; /* 2 = IMPLICIT */ + + // Specialize on scalar type. Each branch returns a closure that + // reads exactly one slot and computes its own implicit-zero guard + // inline — keeping the hot path free of further switches. + if (t === ScalarType.STRING) { + if (explicit) { + return (msg) => { + const v = msg[localName] as string | undefined | null; + if (v === undefined || v === null) return 0; + const byteLen = utf8ByteLength(v); + return tagLen + varintSize32(byteLen) + byteLen; + }; + } + return (msg) => { + const v = msg[localName] as string | undefined | null; + if (!v) return 0; + const byteLen = utf8ByteLength(v); + return tagLen + varintSize32(byteLen) + byteLen; + }; + } + if (t === ScalarType.BOOL) { + if (explicit) { + return (msg) => { + const v = msg[localName]; + if (v === undefined || v === null) return 0; + return tagLen + 1; + }; } + return (msg) => { + const v = msg[localName]; + if (v !== true) return 0; + return tagLen + 1; + }; } + if (t === ScalarType.INT32) { + if (explicit) { + return (msg) => { + const v = msg[localName] as number | undefined | null; + if (v === undefined || v === null) return 0; + return tagLen + int32Size(v); + }; + } + return (msg) => { + const v = msg[localName] as number | undefined | null; + if (!v) return 0; + return tagLen + int32Size(v); + }; + } + if (t === ScalarType.UINT32) { + if (explicit) { + return (msg) => { + const v = msg[localName] as number | undefined | null; + if (v === undefined || v === null) return 0; + return tagLen + varintSize32(v >>> 0); + }; + } + return (msg) => { + const v = msg[localName] as number | undefined | null; + if (!v) return 0; + return tagLen + varintSize32(v >>> 0); + }; + } + if (t === ScalarType.SINT32) { + if (explicit) { + return (msg) => { + const v = msg[localName] as number | undefined | null; + if (v === undefined || v === null) return 0; + return tagLen + sint32Size(v); + }; + } + return (msg) => { + const v = msg[localName] as number | undefined | null; + if (!v) return 0; + return tagLen + sint32Size(v); + }; + } + if (t === ScalarType.DOUBLE || t === ScalarType.FIXED64 || t === ScalarType.SFIXED64) { + if (explicit) { + return (msg) => { + const v = msg[localName]; + if (v === undefined || v === null) return 0; + return tagLen + 8; + }; + } + return (msg) => { + const v = msg[localName]; + if ( + v === undefined || + v === null || + v === 0 || + v === BIGINT_ZERO || + v === "0" + ) + return 0; + return tagLen + 8; + }; + } + if (t === ScalarType.FLOAT || t === ScalarType.FIXED32 || t === ScalarType.SFIXED32) { + if (explicit) { + return (msg) => { + const v = msg[localName]; + if (v === undefined || v === null) return 0; + return tagLen + 4; + }; + } + return (msg) => { + const v = msg[localName]; + if (v === undefined || v === null || v === 0) return 0; + return tagLen + 4; + }; + } + if (t === ScalarType.BYTES) { + if (explicit) { + return (msg) => { + const v = msg[localName] as Uint8Array | undefined | null; + if (v === undefined || v === null) return 0; + return tagLen + varintSize32(v.length) + v.length; + }; + } + return (msg) => { + const v = msg[localName] as Uint8Array | undefined | null; + if (!v || v.length === 0) return 0; + return tagLen + varintSize32(v.length) + v.length; + }; + } + // 64-bit varint scalars: INT64, UINT64, SINT64. + return (msg) => { + const v = msg[localName]; + if (v === undefined || v === null) return 0; + if (!explicit && (v === 0 || v === BIGINT_ZERO || v === "0")) return 0; + return tagLen + scalarSize(t, v); + }; } -function writeMapField( - c: Cursor, +function buildEnumSizeStep( + field: DescField & { fieldKind: "enum" }, +): SizeStep { + const localName = field.localName; + const tagLen = tagSize(field.number, WIRE_VARINT); + const explicit = field.presence !== 2; + if (explicit) { + return (msg) => { + const v = msg[localName] as number | undefined | null; + if (v === undefined || v === null) return 0; + return tagLen + int32Size(v); + }; + } + return (msg) => { + const v = msg[localName] as number | undefined | null; + if (!v) return 0; + return tagLen + int32Size(v); + }; +} + +function buildMessageSizeStep( + field: DescField & { fieldKind: "message" }, +): SizeStep { + const localName = field.localName; + const tagLen = tagSize(field.number, WIRE_LENGTH_DELIMITED); + const subDesc = field.message; + return (msg, sizes) => { + const sub = msg[localName] as Record | undefined | null; + if (sub === undefined || sub === null) return 0; + const subSize = computeMessageSize(subDesc, sub, sizes); + sizes.set(sub, subSize); + return tagLen + varintSize32(subSize) + subSize; + }; +} + +function buildListSizeStep( + field: DescField & { fieldKind: "list" }, +): SizeStep { + const localName = field.localName; + if (field.listKind === "message") { + const tagLen = tagSize(field.number, WIRE_LENGTH_DELIMITED); + const subDesc = field.message; + return (msg, sizes) => { + const list = msg[localName] as unknown[] | undefined | null; + if (!list || list.length === 0) return 0; + let size = 0; + for (let k = 0; k < list.length; k++) { + const sub = list[k] as Record; + const subSize = computeMessageSize(subDesc, sub, sizes); + sizes.set(sub, subSize); + size += tagLen + varintSize32(subSize) + subSize; + } + return size; + }; + } + if (field.listKind === "enum") { + if (field.packed) { + const tagLen = tagSize(field.number, WIRE_LENGTH_DELIMITED); + return (msg) => { + const list = msg[localName] as number[] | undefined | null; + if (!list || list.length === 0) return 0; + let body = 0; + for (let k = 0; k < list.length; k++) { + body += int32Size(list[k]); + } + return tagLen + varintSize32(body) + body; + }; + } + const tagLen = tagSize(field.number, WIRE_VARINT); + return (msg) => { + const list = msg[localName] as number[] | undefined | null; + if (!list || list.length === 0) return 0; + let size = 0; + for (let k = 0; k < list.length; k++) { + size += tagLen + int32Size(list[k]); + } + return size; + }; + } + // listKind === "scalar" + const t = field.scalar; + const wt = scalarWireType(t); + if (field.packed && wt !== WIRE_LENGTH_DELIMITED) { + const tagLen = tagSize(field.number, WIRE_LENGTH_DELIMITED); + return (msg) => { + const list = msg[localName] as unknown[] | undefined | null; + if (!list || list.length === 0) return 0; + let body = 0; + for (let k = 0; k < list.length; k++) { + body += scalarSize(t, list[k]); + } + return tagLen + varintSize32(body) + body; + }; + } + const tagLen = tagSize(field.number, wt); + return (msg) => { + const list = msg[localName] as unknown[] | undefined | null; + if (!list || list.length === 0) return 0; + let size = 0; + for (let k = 0; k < list.length; k++) { + size += tagLen + scalarSize(t, list[k]); + } + return size; + }; +} + +function buildMapSizeStep( field: DescField & { fieldKind: "map" }, - obj: Record, - sizes: SizeMap, -): void { - for (const strKey of Object.keys(obj)) { - const keyTyped = coerceMapKey(strKey, field.mapKey); - const value = obj[strKey]; - // Body size is recomputed here rather than cached because caching it - // per-entry would require either (1) a second identity-keyed cache - // separate from `sizes` or (2) wrapping each entry in a synthetic - // object. Recompute is cheap — scalar types only, except for the - // `value` submessage which reads from `sizes` anyway. - const { body } = estimateMapEntryBody(field, keyTyped, value, sizes); - writeTag(c, field.number, WIRE_LENGTH_DELIMITED); - writeVarint32(c, body); - writeMapEntry(c, field, keyTyped, value, sizes); +): SizeStep { + const localName = field.localName; + const outerTagLen = tagSize(field.number, WIRE_LENGTH_DELIMITED); + const keyType = field.mapKey; + const keyTagLen = tagSize(1, scalarWireType(keyType)); + const mapKind = field.mapKind; + + if (mapKind === "scalar") { + const valType = field.scalar; + const valTagLen = tagSize(2, scalarWireType(valType)); + return (msg) => { + const obj = msg[localName] as Record | undefined | null; + if (!obj) return 0; + const keys = Object.keys(obj); + if (keys.length === 0) return 0; + let size = 0; + for (let k = 0; k < keys.length; k++) { + const strKey = keys[k]; + const keyTyped = coerceMapKey(strKey, keyType); + const keyBytes = keyTagLen + scalarSize(keyType, keyTyped); + const valBytes = valTagLen + scalarSize(valType, obj[strKey]); + const body = keyBytes + valBytes; + size += outerTagLen + varintSize32(body) + body; + } + return size; + }; } + if (mapKind === "enum") { + const valTagLen = tagSize(2, WIRE_VARINT); + return (msg) => { + const obj = msg[localName] as Record | undefined | null; + if (!obj) return 0; + const keys = Object.keys(obj); + if (keys.length === 0) return 0; + let size = 0; + for (let k = 0; k < keys.length; k++) { + const strKey = keys[k]; + const keyTyped = coerceMapKey(strKey, keyType); + const keyBytes = keyTagLen + scalarSize(keyType, keyTyped); + const valBytes = valTagLen + int32Size(obj[strKey]); + const body = keyBytes + valBytes; + size += outerTagLen + varintSize32(body) + body; + } + return size; + }; + } + // mapKind === "message" + const valDesc = field.message; + const valTagLen = tagSize(2, WIRE_LENGTH_DELIMITED); + return (msg, sizes) => { + const obj = msg[localName] as Record | undefined | null; + if (!obj) return 0; + const keys = Object.keys(obj); + if (keys.length === 0) return 0; + let size = 0; + for (let k = 0; k < keys.length; k++) { + const strKey = keys[k]; + const keyTyped = coerceMapKey(strKey, keyType); + const keyBytes = keyTagLen + scalarSize(keyType, keyTyped); + const sub = obj[strKey] as Record; + const subSize = computeMessageSize(valDesc, sub, sizes); + sizes.set(sub, subSize); + const valBytes = valTagLen + varintSize32(subSize) + subSize; + const body = keyBytes + valBytes; + size += outerTagLen + varintSize32(body) + body; + } + return size; + }; } /** - * Write one non-oneof non-map field. Matches estimateRegularFieldSize - * exactly so that pass 1 and pass 2 stay in sync. + * Build a size step for a single field (used both for regular fields + * and as the per-case handler inside an oneof dispatch). The resulting + * step reads `msg[localName]` directly; oneof callers must adapt by + * routing through the oneof ADT's `value` slot (handled in the oneof + * step builder by wrapping the per-case step). */ -function writeRegularField( - c: Cursor, - field: DescField, - value: unknown, - sizes: SizeMap, -): void { +function buildFieldSizeStep(field: DescField): SizeStep { switch (field.fieldKind) { case "scalar": - writeTag(c, field.number, scalarWireType(field.scalar)); - writeScalar(c, field.scalar, value); - return; + return buildScalarSizeStep(field); case "enum": - writeTag(c, field.number, WIRE_VARINT); - writeInt32(c, value as number); - return; + return buildEnumSizeStep(field); + case "message": + return buildMessageSizeStep(field); + case "list": + return buildListSizeStep(field); + case "map": + return buildMapSizeStep(field); + } +} + +function buildOneofSizeStep(oneof: DescOneof): SizeStep { + const oneofLocalName = oneof.localName; + // Per-case handlers: indexed by `case` discriminator. Each handler + // reads from an ADT-shaped object `{ case, value }`, so we build a + // thin step that rewrites `localName` lookups to `"value"`. + const perCase = new Map(); + for (const field of oneof.fields) { + // Build a size step as if the field's storage slot was `value`. + // This is the trick: clone the field descriptor with localName set + // to "value" by wrapping the underlying step. We avoid mutating the + // descriptor — instead, we build a step closure that reads adt.value. + const step = buildFieldSizeStepForOneof(field); + perCase.set(field.localName, step); + } + return (msg, sizes) => { + const adt = msg[oneofLocalName] as + | { case: string | undefined; value?: unknown } + | undefined + | null; + if (!adt || adt.case === undefined) return 0; + const step = perCase.get(adt.case); + if (!step) return 0; + // Route the ADT through as if it were a plain message with `value` + // as the single readable slot. The per-case step treats `.value` + // as its own field slot. + return step(adt as Record, sizes); + }; +} + +/** + * Variant of buildFieldSizeStep for fields that live inside a oneof. + * The field always reads `msg.value` (the ADT payload) and always emits + * (oneof presence is carried by the discriminator, not by the value). + */ +function buildFieldSizeStepForOneof(field: DescField): SizeStep { + // For oneofs we don't check implicit-zero: any set case must emit. + switch (field.fieldKind) { + case "scalar": { + const tagLen = tagSize(field.number, scalarWireType(field.scalar)); + const t = field.scalar; + if (t === ScalarType.STRING) { + return (msg) => { + const v = msg.value as string; + const byteLen = utf8ByteLength(v); + return tagLen + varintSize32(byteLen) + byteLen; + }; + } + if (t === ScalarType.BOOL) { + return () => tagLen + 1; + } + if (t === ScalarType.INT32) { + return (msg) => tagLen + int32Size(msg.value as number); + } + if (t === ScalarType.UINT32) { + return (msg) => tagLen + varintSize32((msg.value as number) >>> 0); + } + if (t === ScalarType.SINT32) { + return (msg) => tagLen + sint32Size(msg.value as number); + } + if ( + t === ScalarType.DOUBLE || + t === ScalarType.FIXED64 || + t === ScalarType.SFIXED64 + ) { + return () => tagLen + 8; + } + if ( + t === ScalarType.FLOAT || + t === ScalarType.FIXED32 || + t === ScalarType.SFIXED32 + ) { + return () => tagLen + 4; + } + if (t === ScalarType.BYTES) { + return (msg) => { + const v = msg.value as Uint8Array; + return tagLen + varintSize32(v.length) + v.length; + }; + } + // 64-bit varints + return (msg) => tagLen + scalarSize(t, msg.value); + } + case "enum": { + const tagLen = tagSize(field.number, WIRE_VARINT); + return (msg) => tagLen + int32Size(msg.value as number); + } case "message": { - const sub = value as Record; - const subSize = sizes.get(sub) ?? 0; - writeTag(c, field.number, WIRE_LENGTH_DELIMITED); - writeVarint32(c, subSize); - writeMessageInto(c, field.message, sub, sizes); - return; + const tagLen = tagSize(field.number, WIRE_LENGTH_DELIMITED); + const subDesc = field.message; + return (msg, sizes) => { + const sub = msg.value as Record; + const subSize = computeMessageSize(subDesc, sub, sizes); + sizes.set(sub, subSize); + return tagLen + varintSize32(subSize) + subSize; + }; } - case "list": { - const list = value as unknown[]; - if (field.listKind === "message") { - for (let k = 0; k < list.length; k++) { - const sub = list[k] as Record; - const subSize = sizes.get(sub) ?? 0; - writeTag(c, field.number, WIRE_LENGTH_DELIMITED); - writeVarint32(c, subSize); - writeMessageInto(c, field.message, sub, sizes); - } - return; - } - if (field.listKind === "enum") { - if (field.packed) { - let body = 0; - for (let k = 0; k < list.length; k++) { - body += int32Size(list[k] as number); - } - writeTag(c, field.number, WIRE_LENGTH_DELIMITED); - writeVarint32(c, body); - for (let k = 0; k < list.length; k++) { - writeInt32(c, list[k] as number); - } - return; - } - for (let k = 0; k < list.length; k++) { - writeTag(c, field.number, WIRE_VARINT); - writeInt32(c, list[k] as number); - } - return; + // Lists and maps can't appear inside oneofs (protobuf spec). + default: + return () => 0; + } +} + +function buildSizeSteps(desc: DescMessage): SizeStep[] { + const steps: SizeStep[] = []; + const fields = desc.fields; + for (let i = 0; i < fields.length; i++) { + const field = fields[i]; + if (field.oneof !== undefined) continue; + steps.push(buildFieldSizeStep(field)); + } + const oneofs = desc.oneofs; + for (let i = 0; i < oneofs.length; i++) { + steps.push(buildOneofSizeStep(oneofs[i])); + } + return steps; +} + +function computeMessageSize( + desc: DescMessage, + message: Record, + sizes: SizeMap, +): number { + const steps = getSizeSteps(desc); + let size = 0; + for (let i = 0; i < steps.length; i++) { + size += steps[i](message, sizes); + } + return size; +} + +// ----------------------------------------------------------------------------- +// Encode steps +// ----------------------------------------------------------------------------- + +function buildScalarEncodeStep( + field: DescField & { fieldKind: "scalar" }, +): EncodeStep { + const localName = field.localName; + const tagBytes = encodeTag(field.number, scalarWireType(field.scalar)); + const t = field.scalar; + const explicit = field.presence !== 2; + + if (t === ScalarType.STRING) { + if (explicit) { + return (c, msg) => { + const v = msg[localName] as string | undefined | null; + if (v === undefined || v === null) return; + writeTagBytes(c, tagBytes); + writeScalar(c, ScalarType.STRING, v); + }; + } + return (c, msg) => { + const v = msg[localName] as string | undefined | null; + if (!v) return; + writeTagBytes(c, tagBytes); + writeScalar(c, ScalarType.STRING, v); + }; + } + if (t === ScalarType.BOOL) { + if (explicit) { + return (c, msg) => { + const v = msg[localName]; + if (v === undefined || v === null) return; + writeTagBytes(c, tagBytes); + c.buf[c.pos++] = (v as boolean) ? 1 : 0; + }; + } + return (c, msg) => { + const v = msg[localName]; + if (v !== true) return; + writeTagBytes(c, tagBytes); + c.buf[c.pos++] = 1; + }; + } + if (t === ScalarType.INT32) { + if (explicit) { + return (c, msg) => { + const v = msg[localName] as number | undefined | null; + if (v === undefined || v === null) return; + writeTagBytes(c, tagBytes); + writeInt32(c, v); + }; + } + return (c, msg) => { + const v = msg[localName] as number | undefined | null; + if (!v) return; + writeTagBytes(c, tagBytes); + writeInt32(c, v); + }; + } + if (t === ScalarType.UINT32) { + if (explicit) { + return (c, msg) => { + const v = msg[localName] as number | undefined | null; + if (v === undefined || v === null) return; + writeTagBytes(c, tagBytes); + writeVarint32(c, v >>> 0); + }; + } + return (c, msg) => { + const v = msg[localName] as number | undefined | null; + if (!v) return; + writeTagBytes(c, tagBytes); + writeVarint32(c, v >>> 0); + }; + } + if (t === ScalarType.SINT32) { + if (explicit) { + return (c, msg) => { + const v = msg[localName] as number | undefined | null; + if (v === undefined || v === null) return; + writeTagBytes(c, tagBytes); + writeSInt32(c, v); + }; + } + return (c, msg) => { + const v = msg[localName] as number | undefined | null; + if (!v) return; + writeTagBytes(c, tagBytes); + writeSInt32(c, v); + }; + } + if (t === ScalarType.DOUBLE) { + if (explicit) { + return (c, msg) => { + const v = msg[localName]; + if (v === undefined || v === null) return; + writeTagBytes(c, tagBytes); + c.view.setFloat64(c.pos, v as number, true); + c.pos += 8; + }; + } + return (c, msg) => { + const v = msg[localName]; + if (v === undefined || v === null || v === 0) return; + writeTagBytes(c, tagBytes); + c.view.setFloat64(c.pos, v as number, true); + c.pos += 8; + }; + } + if (t === ScalarType.FLOAT) { + if (explicit) { + return (c, msg) => { + const v = msg[localName]; + if (v === undefined || v === null) return; + writeTagBytes(c, tagBytes); + c.view.setFloat32(c.pos, v as number, true); + c.pos += 4; + }; + } + return (c, msg) => { + const v = msg[localName]; + if (v === undefined || v === null || v === 0) return; + writeTagBytes(c, tagBytes); + c.view.setFloat32(c.pos, v as number, true); + c.pos += 4; + }; + } + if (t === ScalarType.BYTES) { + if (explicit) { + return (c, msg) => { + const v = msg[localName] as Uint8Array | undefined | null; + if (v === undefined || v === null) return; + writeTagBytes(c, tagBytes); + writeVarint32(c, v.length); + c.buf.set(v, c.pos); + c.pos += v.length; + }; + } + return (c, msg) => { + const v = msg[localName] as Uint8Array | undefined | null; + if (!v || v.length === 0) return; + writeTagBytes(c, tagBytes); + writeVarint32(c, v.length); + c.buf.set(v, c.pos); + c.pos += v.length; + }; + } + // Fallback for remaining scalars (FIXED32/SFIXED32/FIXED64/SFIXED64/ + // INT64/UINT64/SINT64). The shared writeScalar helper handles all of + // them; the per-step closure avoids dispatching the outer field kind + // again, which is the main win. + if (explicit) { + return (c, msg) => { + const v = msg[localName]; + if (v === undefined || v === null) return; + writeTagBytes(c, tagBytes); + writeScalar(c, t, v); + }; + } + return (c, msg) => { + const v = msg[localName]; + if ( + v === undefined || + v === null || + v === 0 || + v === BIGINT_ZERO || + v === "0" + ) + return; + writeTagBytes(c, tagBytes); + writeScalar(c, t, v); + }; +} + +function buildEnumEncodeStep( + field: DescField & { fieldKind: "enum" }, +): EncodeStep { + const localName = field.localName; + const tagBytes = encodeTag(field.number, WIRE_VARINT); + const explicit = field.presence !== 2; + if (explicit) { + return (c, msg) => { + const v = msg[localName] as number | undefined | null; + if (v === undefined || v === null) return; + writeTagBytes(c, tagBytes); + writeInt32(c, v); + }; + } + return (c, msg) => { + const v = msg[localName] as number | undefined | null; + if (!v) return; + writeTagBytes(c, tagBytes); + writeInt32(c, v); + }; +} + +function buildMessageEncodeStep( + field: DescField & { fieldKind: "message" }, +): EncodeStep { + const localName = field.localName; + const tagBytes = encodeTag(field.number, WIRE_LENGTH_DELIMITED); + const subDesc = field.message; + return (c, msg, sizes) => { + const sub = msg[localName] as Record | undefined | null; + if (sub === undefined || sub === null) return; + const subSize = sizes.get(sub) ?? 0; + writeTagBytes(c, tagBytes); + writeVarint32(c, subSize); + writeMessageInto(c, subDesc, sub, sizes); + }; +} + +function buildListEncodeStep( + field: DescField & { fieldKind: "list" }, +): EncodeStep { + const localName = field.localName; + if (field.listKind === "message") { + const tagBytes = encodeTag(field.number, WIRE_LENGTH_DELIMITED); + const subDesc = field.message; + return (c, msg, sizes) => { + const list = msg[localName] as unknown[] | undefined | null; + if (!list || list.length === 0) return; + for (let k = 0; k < list.length; k++) { + const sub = list[k] as Record; + const subSize = sizes.get(sub) ?? 0; + writeTagBytes(c, tagBytes); + writeVarint32(c, subSize); + writeMessageInto(c, subDesc, sub, sizes); } - // scalar list - const t = field.scalar; - const wt = scalarWireType(t); - if (field.packed && wt !== WIRE_LENGTH_DELIMITED) { + }; + } + if (field.listKind === "enum") { + if (field.packed) { + const tagBytes = encodeTag(field.number, WIRE_LENGTH_DELIMITED); + return (c, msg) => { + const list = msg[localName] as number[] | undefined | null; + if (!list || list.length === 0) return; let body = 0; for (let k = 0; k < list.length; k++) { - body += scalarSize(t, list[k]); + body += int32Size(list[k]); } - writeTag(c, field.number, WIRE_LENGTH_DELIMITED); + writeTagBytes(c, tagBytes); writeVarint32(c, body); for (let k = 0; k < list.length; k++) { - writeScalar(c, t, list[k]); + writeInt32(c, list[k]); } - return; + }; + } + const tagBytes = encodeTag(field.number, WIRE_VARINT); + return (c, msg) => { + const list = msg[localName] as number[] | undefined | null; + if (!list || list.length === 0) return; + for (let k = 0; k < list.length; k++) { + writeTagBytes(c, tagBytes); + writeInt32(c, list[k]); } + }; + } + // listKind === "scalar" + const t = field.scalar; + const wt = scalarWireType(t); + if (field.packed && wt !== WIRE_LENGTH_DELIMITED) { + const tagBytes = encodeTag(field.number, WIRE_LENGTH_DELIMITED); + return (c, msg) => { + const list = msg[localName] as unknown[] | undefined | null; + if (!list || list.length === 0) return; + let body = 0; + for (let k = 0; k < list.length; k++) { + body += scalarSize(t, list[k]); + } + writeTagBytes(c, tagBytes); + writeVarint32(c, body); for (let k = 0; k < list.length; k++) { - writeTag(c, field.number, wt); writeScalar(c, t, list[k]); } - return; + }; + } + const tagBytes = encodeTag(field.number, wt); + return (c, msg) => { + const list = msg[localName] as unknown[] | undefined | null; + if (!list || list.length === 0) return; + for (let k = 0; k < list.length; k++) { + writeTagBytes(c, tagBytes); + writeScalar(c, t, list[k]); } + }; +} + +function buildMapEncodeStep( + field: DescField & { fieldKind: "map" }, +): EncodeStep { + const localName = field.localName; + const outerTagBytes = encodeTag(field.number, WIRE_LENGTH_DELIMITED); + const keyType = field.mapKey; + const keyWire = scalarWireType(keyType); + const keyTagBytes = encodeTag(1, keyWire); + const keyTagLen = keyTagBytes.length; + const mapKind = field.mapKind; + + if (mapKind === "scalar") { + const valType = field.scalar; + const valWire = scalarWireType(valType); + const valTagBytes = encodeTag(2, valWire); + const valTagLen = valTagBytes.length; + return (c, msg) => { + const obj = msg[localName] as Record | undefined | null; + if (!obj) return; + const keys = Object.keys(obj); + if (keys.length === 0) return; + for (let k = 0; k < keys.length; k++) { + const strKey = keys[k]; + const keyTyped = coerceMapKey(strKey, keyType); + const value = obj[strKey]; + const keyBytes = keyTagLen + scalarSize(keyType, keyTyped); + const valBytes = valTagLen + scalarSize(valType, value); + const body = keyBytes + valBytes; + writeTagBytes(c, outerTagBytes); + writeVarint32(c, body); + writeTagBytes(c, keyTagBytes); + writeScalar(c, keyType, keyTyped); + writeTagBytes(c, valTagBytes); + writeScalar(c, valType, value); + } + }; + } + if (mapKind === "enum") { + const valTagBytes = encodeTag(2, WIRE_VARINT); + const valTagLen = valTagBytes.length; + return (c, msg) => { + const obj = msg[localName] as Record | undefined | null; + if (!obj) return; + const keys = Object.keys(obj); + if (keys.length === 0) return; + for (let k = 0; k < keys.length; k++) { + const strKey = keys[k]; + const keyTyped = coerceMapKey(strKey, keyType); + const value = obj[strKey]; + const keyBytes = keyTagLen + scalarSize(keyType, keyTyped); + const valBytes = valTagLen + int32Size(value); + const body = keyBytes + valBytes; + writeTagBytes(c, outerTagBytes); + writeVarint32(c, body); + writeTagBytes(c, keyTagBytes); + writeScalar(c, keyType, keyTyped); + writeTagBytes(c, valTagBytes); + writeInt32(c, value); + } + }; + } + // mapKind === "message" + const valDesc = field.message; + const valTagBytes = encodeTag(2, WIRE_LENGTH_DELIMITED); + const valTagLen = valTagBytes.length; + return (c, msg, sizes) => { + const obj = msg[localName] as Record | undefined | null; + if (!obj) return; + const keys = Object.keys(obj); + if (keys.length === 0) return; + for (let k = 0; k < keys.length; k++) { + const strKey = keys[k]; + const keyTyped = coerceMapKey(strKey, keyType); + const sub = obj[strKey] as Record; + const subSize = sizes.get(sub) ?? 0; + const keyBytes = keyTagLen + scalarSize(keyType, keyTyped); + const valBytes = valTagLen + varintSize32(subSize) + subSize; + const body = keyBytes + valBytes; + writeTagBytes(c, outerTagBytes); + writeVarint32(c, body); + writeTagBytes(c, keyTagBytes); + writeScalar(c, keyType, keyTyped); + writeTagBytes(c, valTagBytes); + writeVarint32(c, subSize); + writeMessageInto(c, valDesc, sub, sizes); + } + }; +} + +function buildFieldEncodeStep(field: DescField): EncodeStep { + switch (field.fieldKind) { + case "scalar": + return buildScalarEncodeStep(field); + case "enum": + return buildEnumEncodeStep(field); + case "message": + return buildMessageEncodeStep(field); + case "list": + return buildListEncodeStep(field); case "map": - // Map fields are dispatched through writeMapField from the caller; - // this branch is unreachable on the hot path but defensive. - writeMapField( - c, - field as DescField & { fieldKind: "map" }, - value as Record, - sizes, - ); - return; + return buildMapEncodeStep(field); } } -function writeMessageInto( - c: Cursor, - desc: DescMessage, - message: Record, - sizes: SizeMap, -): void { +function buildFieldEncodeStepForOneof(field: DescField): EncodeStep { + switch (field.fieldKind) { + case "scalar": { + const tagBytes = encodeTag(field.number, scalarWireType(field.scalar)); + const t = field.scalar; + if (t === ScalarType.STRING) { + return (c, msg) => { + writeTagBytes(c, tagBytes); + writeScalar(c, ScalarType.STRING, msg.value as string); + }; + } + if (t === ScalarType.BOOL) { + return (c, msg) => { + writeTagBytes(c, tagBytes); + c.buf[c.pos++] = (msg.value as boolean) ? 1 : 0; + }; + } + if (t === ScalarType.INT32) { + return (c, msg) => { + writeTagBytes(c, tagBytes); + writeInt32(c, msg.value as number); + }; + } + if (t === ScalarType.UINT32) { + return (c, msg) => { + writeTagBytes(c, tagBytes); + writeVarint32(c, (msg.value as number) >>> 0); + }; + } + if (t === ScalarType.SINT32) { + return (c, msg) => { + writeTagBytes(c, tagBytes); + writeSInt32(c, msg.value as number); + }; + } + if (t === ScalarType.DOUBLE) { + return (c, msg) => { + writeTagBytes(c, tagBytes); + c.view.setFloat64(c.pos, msg.value as number, true); + c.pos += 8; + }; + } + if (t === ScalarType.FLOAT) { + return (c, msg) => { + writeTagBytes(c, tagBytes); + c.view.setFloat32(c.pos, msg.value as number, true); + c.pos += 4; + }; + } + if (t === ScalarType.BYTES) { + return (c, msg) => { + const v = msg.value as Uint8Array; + writeTagBytes(c, tagBytes); + writeVarint32(c, v.length); + c.buf.set(v, c.pos); + c.pos += v.length; + }; + } + return (c, msg) => { + writeTagBytes(c, tagBytes); + writeScalar(c, t, msg.value); + }; + } + case "enum": { + const tagBytes = encodeTag(field.number, WIRE_VARINT); + return (c, msg) => { + writeTagBytes(c, tagBytes); + writeInt32(c, msg.value as number); + }; + } + case "message": { + const tagBytes = encodeTag(field.number, WIRE_LENGTH_DELIMITED); + const subDesc = field.message; + return (c, msg, sizes) => { + const sub = msg.value as Record; + const subSize = sizes.get(sub) ?? 0; + writeTagBytes(c, tagBytes); + writeVarint32(c, subSize); + writeMessageInto(c, subDesc, sub, sizes); + }; + } + default: + return () => {}; + } +} + +function buildOneofEncodeStep(oneof: DescOneof): EncodeStep { + const oneofLocalName = oneof.localName; + const perCase = new Map(); + for (const field of oneof.fields) { + perCase.set(field.localName, buildFieldEncodeStepForOneof(field)); + } + return (c, msg, sizes) => { + const adt = msg[oneofLocalName] as + | { case: string | undefined; value?: unknown } + | undefined + | null; + if (!adt || adt.case === undefined) return; + const step = perCase.get(adt.case); + if (!step) return; + step(c, adt as Record, sizes); + }; +} + +function buildEncodeSteps(desc: DescMessage): EncodeStep[] { + const steps: EncodeStep[] = []; const fields = desc.fields; for (let i = 0; i < fields.length; i++) { const field = fields[i]; - // Oneof members: dispatched via the oneof loop below. if (field.oneof !== undefined) continue; - - if (field.fieldKind === "map") { - const obj = message[field.localName] as - | Record - | undefined; - if (!obj || Object.keys(obj).length === 0) continue; - writeMapField( - c, - field as DescField & { fieldKind: "map" }, - obj, - sizes, - ); - continue; - } - - const value = message[field.localName]; - if (!isFieldSet(field, value)) continue; - writeRegularField(c, field, value, sizes); + steps.push(buildFieldEncodeStep(field)); } const oneofs = desc.oneofs; for (let i = 0; i < oneofs.length; i++) { - const oneof = oneofs[i]; - const adt = message[oneof.localName] as - | { case: string | undefined; value?: unknown } - | undefined; - if (!adt || adt.case === undefined) continue; - const selected = findOneofField(oneof, adt.case); - if (!selected) continue; - writeRegularField(c, selected, adt.value, sizes); + steps.push(buildOneofEncodeStep(oneofs[i])); + } + return steps; +} + +function writeMessageInto( + c: Cursor, + desc: DescMessage, + message: Record, + sizes: SizeMap, +): void { + const steps = getEncodeSteps(desc); + for (let i = 0; i < steps.length; i++) { + steps[i](c, message, sizes); } } +// findOneofField kept for potential future callers — currently unused on the +// hot path because oneof dispatch is table-driven via the per-case Map in +// buildOneofSizeStep / buildOneofEncodeStep. +void findOneofField; + // ----------------------------------------------------------------------------- // Entry point // ----------------------------------------------------------------------------- @@ -958,7 +1610,7 @@ export function toBinaryFast( } const sizes: SizeMap = new Map(); const msg = message as unknown as Record; - const total = estimateMessageSize(schema, msg, sizes); + const total = computeMessageSize(schema, msg, sizes); const buf = new Uint8Array(total); const cursor: Cursor = { buf,