@sofa-buffers/corelib 0.10.0 → 0.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +213 -0
- package/README.md +725 -125
- package/dist/index.cjs +2689 -1276
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +1672 -477
- package/dist/index.d.ts +1672 -477
- package/dist/index.global.js +2689 -1276
- package/dist/index.global.js.map +1 -1
- package/dist/index.js +2678 -1273
- package/dist/index.js.map +1 -1
- package/package.json +3 -4
package/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,217 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
While the version is below `1.0.0`, breaking changes bump the **minor** version.
|
|
8
8
|
|
|
9
|
+
## [0.11.0] - 2026-09-24
|
|
10
|
+
|
|
11
|
+
> The breaking entries below make this a **minor** bump, per the pre-`1.0.0` rule
|
|
12
|
+
> above — never a patch. The release rebuilds the decode surface against
|
|
13
|
+
> CORELIB_PLAN@`c837108`: the visitor is the only way to decode, it is flat, it
|
|
14
|
+
> allocates nothing after construction, and an array's elements arrive through a
|
|
15
|
+
> hand-off instead of per-element callbacks.
|
|
16
|
+
|
|
17
|
+
### Changed
|
|
18
|
+
|
|
19
|
+
- **BREAKING (decode API) — one visitor surface, flat and heap-free, and no
|
|
20
|
+
views (CORELIB_PLAN §5.3.1, §6.6, §6.7).** `Cursor` is gone, and with it
|
|
21
|
+
`fast.ts` and the `reader.ts` they shared. A second decode surface is a second
|
|
22
|
+
implementation of every rule, which this port had already paid for three times
|
|
23
|
+
(chunk-boundary verdicts, fixlen-array word order, receiver caps). `decode()`
|
|
24
|
+
is now `IStream` fed once, over one resumable machine with a fast lane that
|
|
25
|
+
decodes a field inline whenever its words do not straddle the chunk.
|
|
26
|
+
|
|
27
|
+
| before | after |
|
|
28
|
+
|---|---|
|
|
29
|
+
| `Cursor`, `decode()` as a second implementation | `new IStream(visitor)` and `feed(chunk)`; `decode()` is that machine fed once |
|
|
30
|
+
| a visitor per nested message class | **one flat visitor per message**; nesting arrives as `sequenceBegin(id, depth)` / `sequenceEnd(id, depth)` |
|
|
31
|
+
| `sequenceBegin` returns `null` to decline a subtree | returns **`false`** |
|
|
32
|
+
| `Visitor.longs`, an opt-in `Long` channel | gone — `lo`/`hi` halves are passed beside every integer value |
|
|
33
|
+
| `Visitor.fp32` takes a byte view | takes **`bits`**; re-encode with `OStream.writeFp32Bits` |
|
|
34
|
+
| payloads reported as a view | `(id, total, offset, src, start, end)`, where `src` **is** the caller's fed chunk |
|
|
35
|
+
| `FlushSink(view)` | `FlushSink(buffer, start, end)` — the sink only ever sees the installed buffer |
|
|
36
|
+
|
|
37
|
+
A visitor per nested class made every dispatch site megamorphic on V8 and JSC
|
|
38
|
+
and put a per-scope object on the decode path; flat removes both, and the
|
|
39
|
+
visitor stack with them. No view is built and nothing is retained, and the
|
|
40
|
+
one-shot path has no exemption (§6.7.1). Pass-through of a payload run is
|
|
41
|
+
**forbidden** (§5.1.6), not merely off by default. After construction `write`,
|
|
42
|
+
`feed` and `flush` allocate nothing: fixed-size state is sized from `MAX_DEPTH`
|
|
43
|
+
at construction, and `decode()` pools one decoder so a one-shot caller pays no
|
|
44
|
+
construction per message.
|
|
45
|
+
|
|
46
|
+
One deviation is recorded rather than hidden: a payload split across flushes is
|
|
47
|
+
copied with `set(data.subarray(...))`, one view per piece, because the
|
|
48
|
+
allocation-free alternative measured 358 MB/s against 10,963 MB/s.
|
|
49
|
+
|
|
50
|
+
- **BREAKING (decode API) — an array's elements reach a visitor only through the
|
|
51
|
+
hand-off (§6.7.2).** `Visitor.arrayUnsigned`, `arraySigned`, `arrayFp32` and
|
|
52
|
+
`arrayFp64` are removed. `Visitor.arrayBulk` hands the decoder the destination
|
|
53
|
+
to fill — a `number[]`, a `Long[]`, a `Uint32Array` pair of halves, a
|
|
54
|
+
`Float32Array` / `Float64Array`, or the raw `fp32` words — together with the
|
|
55
|
+
schema's element bound as 32-bit halves. Returning `null`, or declaring no
|
|
56
|
+
hook, walks the elements over without decoding them: the `skip` half of
|
|
57
|
+
§6.7.2's two intents.
|
|
58
|
+
|
|
59
|
+
Two delivery routes for the same elements were two places for the element
|
|
60
|
+
bound to be compared, two resume paths to keep in step, and a standing
|
|
61
|
+
invitation for generated code to take the slower one (§5.3.1: a rule gets one
|
|
62
|
+
implementation).
|
|
63
|
+
|
|
64
|
+
**Migration.** Replace each `array*` callback with one `arrayBulk` that returns
|
|
65
|
+
the destination; fold at `arrayEnd` instead of inside the delivery. Measured
|
|
66
|
+
with `bench/run_callgrind.sh`, 1000-element arrays, Ir/op:
|
|
67
|
+
|
|
68
|
+
| array | destination | before | after | |
|
|
69
|
+
|---|---|---:|---:|---:|
|
|
70
|
+
| `array<u16>` | `number[]` | 235,433 | 184,023 | −21.8% |
|
|
71
|
+
| `array<u64>` | `Long[]` | 576,206 | 429,113 | −25.5% |
|
|
72
|
+
| `array<u64>` | `lo`/`hi` halves | 498,618 | 299,180 | −40.0% |
|
|
73
|
+
| `array<fp64>` | `Float64Array` | 93,520 | 36,893 | −60.5% |
|
|
74
|
+
| `array<fp32>` | `Float32Array` | 94,497 | 35,259 | −62.7% |
|
|
75
|
+
|
|
76
|
+
Declining is cheaper still, since nothing is decoded into existence:
|
|
77
|
+
`array<fp64>` 36,893 → 9,383, `array<u16>` 184,023 → 152,082. A short array is
|
|
78
|
+
not the exception — the ~600 Ir fixed cost is paid once per array, so four
|
|
79
|
+
elements at the end of a 37-byte message cost 578 Ir against the 563 the
|
|
80
|
+
callbacks cost. What did move is a *folding* consumer's side: the same four
|
|
81
|
+
elements cost 1,063 Ir summed at `arrayEnd`.
|
|
82
|
+
|
|
83
|
+
- **BREAKING (decode API) — the codec holds no receiver limit (§6.2.1).** §6.2.1
|
|
84
|
+
fixes the provenance of a `max_dyn_*` number and forbids the codec from
|
|
85
|
+
supplying one: it must not hold a limit of its own, supply a default for one it
|
|
86
|
+
was not given, read an omitted argument as unlimited, or clamp to one. A §6.2
|
|
87
|
+
format ceiling reached because no cap was stated is the **format's** bound, not
|
|
88
|
+
a receiver cap.
|
|
89
|
+
|
|
90
|
+
This port failed both. `src/decode/state.ts` held `maxArrayCount` /
|
|
91
|
+
`maxStringLen` / `maxBlobLen`, each defaulting to `ARRAY_MAX` / `FIXLEN_MAX`,
|
|
92
|
+
so an unconfigured decode reported `LimitExceeded` against a ceiling nobody had
|
|
93
|
+
configured; `src/decode/seq.ts` defaulted `receiverCap` and `receiverElemMax`
|
|
94
|
+
at five collector sites.
|
|
95
|
+
|
|
96
|
+
| before | after |
|
|
97
|
+
|---|---|
|
|
98
|
+
| `src/decode/limits.ts`, `DecodeLimits` | **deleted** |
|
|
99
|
+
| `decode(bytes, visitor, limits?)`, `new IStream(visitor, limits?)` | the `limits` argument is gone |
|
|
100
|
+
| `StringSeq` / `BlobSeq` / `ElementSeq` bounds optional | every bound is a **required** constructor argument |
|
|
101
|
+
| — | `UNBOUNDED` (`-1`) is exported as the explicit "the schema declared none" |
|
|
102
|
+
|
|
103
|
+
The comparison now lives in the layer that has the number: generated code
|
|
104
|
+
compares its cap inside `arrayBegin` / `fixlenBegin`, and the collectors compare
|
|
105
|
+
the two bounds they are handed for the one shape a visitor cannot see — a
|
|
106
|
+
wrapper array's element index and element byte length.
|
|
107
|
+
|
|
108
|
+
- **BREAKING (decode API) — a refused decode is terminal, and `feed` is the only
|
|
109
|
+
answer (§5.2.1, §6.3).** `IStream.status()` is **removed**. `feed` returns
|
|
110
|
+
`FeedStatus`, a type alias admitting only `Complete` and `Incomplete`, so the
|
|
111
|
+
compiler rejects a branch that can never be taken; refusals travel on the thrown
|
|
112
|
+
`SofabError` with their code.
|
|
113
|
+
|
|
114
|
+
Two terminal rejections are decided inside a visitor callback rather than by the
|
|
115
|
+
state machine — §6.4.5 puts the strict UTF-8 check where a string is
|
|
116
|
+
materialized, §6.2.1 puts the receiver caps in the layer holding the numbers —
|
|
117
|
+
and both arrived as a throw out of the parse loop, passing the latch entirely.
|
|
118
|
+
Reproduced before the fix: on wire `0a 12 ff fe`, `feed` threw `INVALID_MSG` and
|
|
119
|
+
`status()` then answered `COMPLETE`; at a receiver cap with chunk size 3, the
|
|
120
|
+
visitor was handed `unsigned(0, 42)` — a field that was never on the wire.
|
|
121
|
+
`push()` now routes both through the existing `fail()` latch, which holds the
|
|
122
|
+
raised `SofabError` so the two codes stay distinct.
|
|
123
|
+
|
|
124
|
+
### Added
|
|
125
|
+
|
|
126
|
+
- **An exact-width typed array is an array destination.**
|
|
127
|
+
`IntegerArrayTarget.typed` takes a `Uint8Array`..`Uint32Array` /
|
|
128
|
+
`Int8Array`..`Int32Array` whose element width **is** the schema's declared
|
|
129
|
+
width, and the encoder's array writers read one without the guards a `number[]`
|
|
130
|
+
element needs: a `Uint16Array` element is an integer in `0..65535` by
|
|
131
|
+
construction. It stores unboxed and never changes element kind the way a
|
|
132
|
+
`number[]` does when a value leaves the small-integer range. The element bound
|
|
133
|
+
is still compared — a typed array masks on store (70000 into a `Uint16Array` is
|
|
134
|
+
4464) and MESSAGE_SPEC §7.1 makes an out-of-width element INVALID, so a fill
|
|
135
|
+
that merely stored would turn a malformed message into an accepted one.
|
|
136
|
+
|
|
137
|
+
- **64-bit and boolean destinations.** `IntegerArrayTarget.typed` also takes a
|
|
138
|
+
`BigUint64Array` / `BigInt64Array`. It cannot be filled the way the 32-bit
|
|
139
|
+
widths are — `b[i] = 5` throws, and a `bigint` store is one allocation per
|
|
140
|
+
element, the cost a 64-bit array most wants to avoid — so the fill writes the
|
|
141
|
+
two 32-bit halves it already holds into a `Uint32Array` over the array's own
|
|
142
|
+
buffer and builds no `bigint` at all. The encoder reads them back the same way,
|
|
143
|
+
so `writeUnsignedArray(id, someBigUint64Array)` is `bigint`-free in both
|
|
144
|
+
directions and byte-identical to the `bigint[]` path. A boolean array gets its
|
|
145
|
+
own destination for the same reason.
|
|
146
|
+
|
|
147
|
+
- **An fp32 array written from a `Float32Array` keeps its bits.** `packFp32Array`
|
|
148
|
+
read `values[i]`, widening each element to a double — and widening a
|
|
149
|
+
**signaling** NaN quiets it (`0x7f800001` came back `0x7fc00001`). A
|
|
150
|
+
`Float32Array` source already holds the 32-bit wire words, so they are copied
|
|
151
|
+
through a `Uint32Array` view instead. Bit-exact for every value, not only NaNs.
|
|
152
|
+
With a `Float32Array` member and the `bits` destination on the way in, the array
|
|
153
|
+
**is** the payload in both directions, and there is nothing left to capture,
|
|
154
|
+
compare or re-attach. The words go out through `DataView.setUint32(..., true)`,
|
|
155
|
+
so the wire stays little-endian whatever the platform's order is.
|
|
156
|
+
|
|
157
|
+
- **The static helpers a generated package still carries.** ARCHITECTURE §8 puts
|
|
158
|
+
a helper here when its code has the same shape for every schema and its schema
|
|
159
|
+
dependence is carried entirely by arguments and type parameters. Three helpers
|
|
160
|
+
in the TypeScript backend's output failed that test only because this library
|
|
161
|
+
had nowhere to put them, so they were re-emitted, textually identical, into
|
|
162
|
+
every generated package. `FramedSeq<T>` is the one that is not a straight move:
|
|
163
|
+
`ElementSeq<T>` holds the index rules of MESSAGE_SPEC §5.1 and the two exclusive
|
|
164
|
+
bounds of §6.2.1 for any element type, but writes a single shared `def` into
|
|
165
|
+
every gap — right for `""` and a zero-length `Uint8Array`, wrong for a `struct`,
|
|
166
|
+
a `union`, a nested row or a `Long[]` row, whose defaults are fresh mutable
|
|
167
|
+
objects.
|
|
168
|
+
|
|
169
|
+
### Fixed
|
|
170
|
+
|
|
171
|
+
- **A source's claimed element width is not a fact the encoder can check.** The
|
|
172
|
+
bulk kernel decided how much room to reserve by reading the source's own
|
|
173
|
+
`constructor` — an ordinary property. An `ArrayLike` claiming to be a
|
|
174
|
+
`Uint16Array` while holding `0xffffffff` got three bytes an element reserved and
|
|
175
|
+
needed five; the kernel then wrote past the buffer, where the writes are no-ops,
|
|
176
|
+
and the message came back short while `bytesUsed` reported a length that was
|
|
177
|
+
never written — CORELIB_PLAN §5.1's "partial output handed back as complete",
|
|
178
|
+
the one failure an encoder must never produce. The estimate is gone and nothing
|
|
179
|
+
replaces it: `out.length` is the kernel's bound.
|
|
180
|
+
|
|
181
|
+
- **A streamed `Float32Array` keeps its signaling NaNs.** `writeFp32Array` copied
|
|
182
|
+
the wire words only on the bulk path; when the array did not fit the buffer it
|
|
183
|
+
fell back to `putFp32(values[i])`, quieting signaling NaNs, so a streamed encode
|
|
184
|
+
differed from the one-shot one (§6.5, §5.1.4). A `Float32Array` now takes its own
|
|
185
|
+
route at the entry, streamed through one `Uint32Array` over the source with the
|
|
186
|
+
words stored by shifts (byte-order independent). Split points are unchanged.
|
|
187
|
+
|
|
188
|
+
- **"Exactly one destination" now means one out of all of them.** `resolveTarget`
|
|
189
|
+
refused a target naming two destinations, but the float branch counted only
|
|
190
|
+
`f32`/`bits`/`f64` and the `bool` branch only the integer fields, so a
|
|
191
|
+
combination straddling the two branches was accepted and the other destination
|
|
192
|
+
left untouched — `{ bool, f64 }` on an fp64 array filled `f64` and left `bool`
|
|
193
|
+
at `[0,0]`, which reads as data. Worse, the extra field *silenced* an error:
|
|
194
|
+
`{ bool }` alone on an fp64 array is an `Argument` refusal, `{ bool, f64 }` was
|
|
195
|
+
not.
|
|
196
|
+
|
|
197
|
+
- **One fp32 NaN word view per array, not per NaN**, and the typed-destination
|
|
198
|
+
check is kept polymorphic, so an `instanceof` slow path no longer forces the
|
|
199
|
+
streaming loop to re-check every element.
|
|
200
|
+
|
|
201
|
+
- **The `node_modules` symlink committed by mistake is untracked.**
|
|
202
|
+
|
|
203
|
+
### Performance
|
|
204
|
+
|
|
205
|
+
Measured with `bench/run_callgrind.sh` (Callgrind Ir/op, Node 24). Wall-clock
|
|
206
|
+
numbers from a shared runner are not usable at this resolution.
|
|
207
|
+
|
|
208
|
+
- **Encode and decode instruction cost cut by up to 4.7×:** `encode: u64 array
|
|
209
|
+
(1000)` 1,229,088 → 259,659 (−78.9%), `decode: u64 array (1000)` 1,458,937 →
|
|
210
|
+
721,953 (−50.5%), `encode: typical message` 13,360 → 10,408 (−22.1%).
|
|
211
|
+
- **The §6.6.2 float handle, taken where it pays and only there:** `encode fp32`
|
|
212
|
+
(1000) 195,320 → 27,970 (−85.7%), while `decode: typical message` moved 5324 →
|
|
213
|
+
6415 Ir/op, and every other decode path paid ~0.4% for code it never runs.
|
|
214
|
+
- **A skipped scope is carried as a sentinel, not a null slot**, recovering the
|
|
215
|
+
`decode: typical message` regression the nullable slot had cost (5366 → 5210
|
|
216
|
+
Ir/op, +0.93% over baseline, was +3.95%).
|
|
217
|
+
- **A collector compares against one bound, picked at construction**, since the
|
|
218
|
+
two-bound compare could not be inlined and a CALL is about 200 Ir.
|
|
219
|
+
|
|
9
220
|
## [0.10.0] - 2026-08-01
|
|
10
221
|
|
|
11
222
|
> The breaking entries below make the next release a **minor** bump (the first
|
|
@@ -204,5 +415,7 @@ decode-side win.
|
|
|
204
415
|
(driving a `Visitor`) to decode, both chunkable, with a swappable acceleration
|
|
205
416
|
`Kernel` seam.
|
|
206
417
|
|
|
418
|
+
[0.11.0]: https://github.com/sofa-buffers/corelib-ts/releases/tag/v0.11.0
|
|
419
|
+
[0.10.0]: https://github.com/sofa-buffers/corelib-ts/releases/tag/v0.10.0
|
|
207
420
|
[0.2.0]: https://github.com/sofa-buffers/corelib-ts/releases/tag/v0.2.0
|
|
208
421
|
[0.1.0]: https://github.com/sofa-buffers/corelib-ts/releases/tag/v0.1.0
|