@sofa-buffers/corelib 0.8.1 → 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 +267 -1
- package/README.md +764 -109
- package/dist/index.cjs +2799 -1171
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +1780 -407
- package/dist/index.d.ts +1780 -407
- package/dist/index.global.js +2799 -1171
- package/dist/index.global.js.map +1 -1
- package/dist/index.js +2788 -1168
- package/dist/index.js.map +1 -1
- package/package.json +3 -3
package/dist/index.d.cts
CHANGED
|
@@ -70,6 +70,28 @@ declare const U64_MAX = 18446744073709551615n;
|
|
|
70
70
|
declare const I64_MIN = -9223372036854775808n;
|
|
71
71
|
/** Largest signed 64-bit value (`2^63 - 1`). */
|
|
72
72
|
declare const I64_MAX = 9223372036854775807n;
|
|
73
|
+
/**
|
|
74
|
+
* The smallest output buffer this port accepts **for streaming** (CORELIB_PLAN
|
|
75
|
+
* §5.1, normative): the number a caller sizes a streaming buffer from.
|
|
76
|
+
*
|
|
77
|
+
* `1`, because the encoder splits every *atomic unit* — a field header varint, a
|
|
78
|
+
* fixlen word, an element count, a scalar or array element varint, an `fp32` /
|
|
79
|
+
* `fp64` element — across a flush, so no write needs contiguous room and the
|
|
80
|
+
* bytes produced are identical at any buffer size. (A port that required atomic
|
|
81
|
+
* units to land contiguously would declare the largest run it reserves instead;
|
|
82
|
+
* §5.1 caps any declaration at `20`.)
|
|
83
|
+
*
|
|
84
|
+
* It binds a buffer installed **with** a flush sink, at construction and at
|
|
85
|
+
* every mid-stream `OStream.setBuffer`: such a buffer must satisfy
|
|
86
|
+
* `buffer.length - offset >= MIN_OUTPUT_BUFFER` and is rejected right there
|
|
87
|
+
* with `SofabErrorCode.Argument`, never partway through a message.
|
|
88
|
+
*
|
|
89
|
+
* A buffer installed **without** a sink is subject to no minimum: no flush can
|
|
90
|
+
* occur, so nothing can be split, and the buffer either holds the whole message
|
|
91
|
+
* or reports `SofabErrorCode.BufferFull`. That is the one-shot `MAX_SIZE` case
|
|
92
|
+
* and it stays exact — a two-byte message encodes into a two-byte buffer.
|
|
93
|
+
*/
|
|
94
|
+
declare const MIN_OUTPUT_BUFFER = 1;
|
|
73
95
|
/**
|
|
74
96
|
* Maximum nested-sequence depth (§4.9 / §6.2). An encoder must not open more
|
|
75
97
|
* than this many nested sequences, and a decoder must reject a message that
|
|
@@ -87,13 +109,22 @@ declare const MAX_DEPTH = 255;
|
|
|
87
109
|
* payload shorter than its declared length, an array that runs off the end, or
|
|
88
110
|
* a nested sequence never closed). **Not an error** — more bytes could
|
|
89
111
|
* complete it, and the caller owns end-of-input.
|
|
90
|
-
* - `Invalid` — the bytes are malformed regardless of what follows.
|
|
112
|
+
* - `Invalid` — the bytes are malformed regardless of what follows. **Terminal**
|
|
113
|
+
* (CORELIB_PLAN §5.2): no later bytes can undo it.
|
|
114
|
+
*
|
|
115
|
+
* **Each of the three leaves by exactly one channel.** {@link IStream.feed}
|
|
116
|
+
* *returns* `Complete` or `Incomplete` — that is the {@link FeedStatus} pair, and
|
|
117
|
+
* the caller needs no end step (CORELIB_PLAN §6). `Invalid` is never returned by
|
|
118
|
+
* anything: it is the name of the outcome the throw carries, a {@link SofabError}
|
|
119
|
+
* whose `code` is {@link SofabErrorCode.InvalidMsg}, and the verdict latches, so
|
|
120
|
+
* every further `feed` re-throws without consuming input. There is no accessor
|
|
121
|
+
* that re-reports any of the three, because a fact with two ways to learn it is a
|
|
122
|
+
* fact that can be learned two different ways.
|
|
91
123
|
*
|
|
92
|
-
* {@link
|
|
93
|
-
*
|
|
94
|
-
* {@link
|
|
95
|
-
*
|
|
96
|
-
* {@link SofabErrorCode.InvalidMsg}, and `Complete` by returning normally.
|
|
124
|
+
* The one-shot {@link decode} — the same stream, fed once — signals `Incomplete`
|
|
125
|
+
* and `Invalid` by throwing a {@link SofabError} whose `code` is
|
|
126
|
+
* {@link SofabErrorCode.Incomplete} or {@link SofabErrorCode.InvalidMsg}, and
|
|
127
|
+
* `Complete` by returning normally.
|
|
97
128
|
*/
|
|
98
129
|
declare const DecodeStatus: {
|
|
99
130
|
/** The bytes ended exactly at a field boundary — a valid message. */
|
|
@@ -105,6 +136,19 @@ declare const DecodeStatus: {
|
|
|
105
136
|
};
|
|
106
137
|
/** A decode's terminal outcome: one of the {@link DecodeStatus} values. */
|
|
107
138
|
type DecodeStatus = (typeof DecodeStatus)[keyof typeof DecodeStatus];
|
|
139
|
+
/**
|
|
140
|
+
* The two outcomes {@link IStream.feed} can *return* — `Complete` and
|
|
141
|
+
* `Incomplete`, the ones that leave a stream usable.
|
|
142
|
+
*
|
|
143
|
+
* `Invalid` is missing on purpose, and the type is the statement: this port puts
|
|
144
|
+
* a refusal on the error channel (CORELIB_PLAN §6.3's second option), so a
|
|
145
|
+
* malformed message throws rather than returning, and a caller switching on the
|
|
146
|
+
* return value has no unreachable arm to write. The refusal is not lost by being
|
|
147
|
+
* absent here — it arrives as a {@link SofabError} whose `code` says which
|
|
148
|
+
* refusal it was, which is more than the outcome triple can express: a
|
|
149
|
+
* `LimitExceeded` rejection has no value among the three at all.
|
|
150
|
+
*/
|
|
151
|
+
type FeedStatus = typeof DecodeStatus.Complete | typeof DecodeStatus.Incomplete;
|
|
108
152
|
|
|
109
153
|
/**
|
|
110
154
|
* Error handling for SofaBuffers.
|
|
@@ -123,16 +167,14 @@ type DecodeStatus = (typeof DecodeStatus)[keyof typeof DecodeStatus];
|
|
|
123
167
|
* bytes — so it is kept distinct from `InvalidMsg`.
|
|
124
168
|
*/
|
|
125
169
|
/**
|
|
126
|
-
* The cause of a {@link SofabError}. `Argument`, `
|
|
127
|
-
*
|
|
170
|
+
* The cause of a {@link SofabError}. `Argument`, `BufferFull` and `InvalidMsg`
|
|
171
|
+
* match the C reference's `sofab_ret_t` codes; `Incomplete` is the
|
|
128
172
|
* finish-less INCOMPLETE decode outcome (MESSAGE_SPEC §7), a distinct,
|
|
129
173
|
* more-bytes-could-complete-it signal split out from `InvalidMsg`.
|
|
130
174
|
*/
|
|
131
175
|
declare const SofabErrorCode: {
|
|
132
176
|
/** A caller argument was invalid (e.g. id out of range, empty array). */
|
|
133
177
|
readonly Argument: "ARGUMENT";
|
|
134
|
-
/** The API was used incorrectly (e.g. unbalanced sequence end). */
|
|
135
|
-
readonly Usage: "USAGE";
|
|
136
178
|
/** The output buffer is full and no flush sink was provided. */
|
|
137
179
|
readonly BufferFull: "BUFFER_FULL";
|
|
138
180
|
/** The input being decoded is malformed regardless of what follows (`INVALID`). */
|
|
@@ -147,7 +189,8 @@ declare const SofabErrorCode: {
|
|
|
147
189
|
/**
|
|
148
190
|
* A receiver-configured decode limit was exceeded — a dynamic array, string or
|
|
149
191
|
* blob on the wire claims more elements / bytes than the caller's
|
|
150
|
-
*
|
|
192
|
+
* receiver cap (`max_dyn_array_count` / `max_dyn_string_len` /
|
|
193
|
+
* `max_dyn_blob_len`, §6.2.1 — stated by generated code, never by this codec)
|
|
151
194
|
* allows. Deliberately distinct from {@link SofabErrorCode.InvalidMsg}:
|
|
152
195
|
* exceeding a limit is *policy*, not wire malformation — the identical bytes
|
|
153
196
|
* decode fine under a looser limit — so differential fuzzing must not read it
|
|
@@ -178,6 +221,12 @@ declare class SofabError extends Error {
|
|
|
178
221
|
*
|
|
179
222
|
* Values are stored as raw two's-complement bits; the same `Long` serves an
|
|
180
223
|
* unsigned or signed field — {@link Long.toBigInt} takes the signedness.
|
|
224
|
+
*
|
|
225
|
+
* The two `bigint` conversions below go through the shared bit-punning scratch
|
|
226
|
+
* ({@link "./varint/bits64"}) that the codec's own hot paths use, rather than
|
|
227
|
+
* through mask-and-shift `bigint` arithmetic: the split allocated four
|
|
228
|
+
* intermediate `bigint`s per call and the join two, on the very boundary this
|
|
229
|
+
* class exists to make cheap.
|
|
181
230
|
*/
|
|
182
231
|
declare class Long {
|
|
183
232
|
/** Low 32 bits (unsigned). */
|
|
@@ -195,7 +244,11 @@ declare class Long {
|
|
|
195
244
|
static readonly ZERO: Long;
|
|
196
245
|
/** Construct from raw 32-bit halves. */
|
|
197
246
|
static fromBits(low: number, high: number): Long;
|
|
198
|
-
/**
|
|
247
|
+
/**
|
|
248
|
+
* Split a `bigint` into its low/high 32-bit halves (two's complement). The
|
|
249
|
+
* `BigInt64Array` store *is* `ToBigInt64` — reduction modulo 2^64 — so an
|
|
250
|
+
* out-of-range value keeps exactly the halves the masks kept (bits64).
|
|
251
|
+
*/
|
|
199
252
|
static fromBigInt(value: bigint): Long;
|
|
200
253
|
/** From an integer `number` (`|n| < 2^53`); sign handled via `bigint` once. */
|
|
201
254
|
static fromNumber(n: number): Long;
|
|
@@ -207,62 +260,245 @@ declare class Long {
|
|
|
207
260
|
toString(signed?: boolean): string;
|
|
208
261
|
}
|
|
209
262
|
|
|
263
|
+
/**
|
|
264
|
+
* Encode-side text helper.
|
|
265
|
+
*
|
|
266
|
+
* Two paths turn strings into UTF-8:
|
|
267
|
+
*
|
|
268
|
+
* - {@link utf8Length} / {@link utf8Write} — an allocation-free, two-pass writer
|
|
269
|
+
* used by the encoder's contiguous-write fast path. `writeString` needs the byte
|
|
270
|
+
* length *before* the payload (it goes into the fixlen length word), so the
|
|
271
|
+
* length is scanned first and the bytes are then written straight into the
|
|
272
|
+
* output buffer. This avoids `TextEncoder.encode`'s per-call WHATWG setup cost
|
|
273
|
+
* plus the throwaway `Uint8Array` it allocates and the second copy into the
|
|
274
|
+
* buffer — which V8 profiling showed to be the encoder's dominant cost (a
|
|
275
|
+
* short string cost ~700 ns almost entirely in `TextEncoder`).
|
|
276
|
+
* - {@link utf8WriteSink} — the same walk, emitting one byte at a time into a
|
|
277
|
+
* {@link ByteSink}, for the streaming path where the output buffer is narrower
|
|
278
|
+
* than the payload and the encoder drains between bytes. It replaces a
|
|
279
|
+
* `TextEncoder.encode` into a throwaway array, which was an allocation on a
|
|
280
|
+
* codec path and so is not available to it (CORELIB_PLAN §6.6).
|
|
281
|
+
*
|
|
282
|
+
* **Strict UTF-8 (MESSAGE_SPEC §8, CORELIB_PLAN §6.4).** A `string` is UTF-8
|
|
283
|
+
* text; the encode side is always strict for this Unicode-string target. The
|
|
284
|
+
* platform encoders are lossy — `TextEncoder`, and the hand-rolled fast path it
|
|
285
|
+
* once mirrored, both replace an **unpaired surrogate** with `U+FFFD` — which is
|
|
286
|
+
* the silent data mutation §8 forbids in every mode and direction. Both paths
|
|
287
|
+
* here instead **reject** an unpaired surrogate with an `InvalidArgument`
|
|
288
|
+
* {@link SofabError} (the encode-side image of the decode `INVALID` outcome), so
|
|
289
|
+
* a producer can never emit bytes a strict decoder would refuse. Every *valid*
|
|
290
|
+
* string — ASCII, multibyte BMP, correctly paired astral code points, embedded
|
|
291
|
+
* `U+0000` — still encodes byte-for-byte as before.
|
|
292
|
+
*/
|
|
293
|
+
/** Anything that takes UTF-8 one byte at a time — in practice, the encoder itself. */
|
|
294
|
+
interface ByteSink {
|
|
295
|
+
/** @internal Append one byte, draining to the flush sink when the buffer is full. */
|
|
296
|
+
putByte(b: number): void;
|
|
297
|
+
}
|
|
298
|
+
|
|
210
299
|
/**
|
|
211
300
|
* The output drain used for streaming encodes.
|
|
212
301
|
*
|
|
213
302
|
* When an {@link OStream} is given a `FlushSink`, it writes into a small caller
|
|
214
|
-
* buffer and hands each filled region to the sink, so a message can be far
|
|
215
|
-
*
|
|
216
|
-
*
|
|
303
|
+
* buffer and hands each filled region to the sink, so a message can be far larger
|
|
304
|
+
* than the buffer — larger than RAM, even.
|
|
305
|
+
*
|
|
306
|
+
* **The sink is only ever handed the installed output buffer** — `buffer` is the
|
|
307
|
+
* very array the caller installed, and the bytes are `buffer[start..end)`. There
|
|
308
|
+
* is no second case: CORELIB_PLAN §5.1.6 forbids an encoder from passing the sink
|
|
309
|
+
* any other memory, so a sink never has to ask whether what it received is its own
|
|
310
|
+
* buffer or a payload the encoder handed through from somewhere else. (An earlier
|
|
311
|
+
* revision of the spec permitted that pass-through, off by default; the permission
|
|
312
|
+
* is withdrawn.)
|
|
313
|
+
*
|
|
314
|
+
* The region is valid for the duration of the call. A sink that **copies** it
|
|
315
|
+
* simply returns, and the encoder keeps writing into the same buffer from offset
|
|
316
|
+
* `0`. A sink that **takes** the buffer — queues it, hands it to a transport —
|
|
317
|
+
* must install a replacement with {@link OStream.setBuffer} before returning
|
|
318
|
+
* (§5.1.5); returning without one means "I copied".
|
|
319
|
+
*
|
|
320
|
+
* Passing the buffer and the coordinates rather than a `subarray` of it is not
|
|
321
|
+
* cosmetic: a view would be an allocation per flush, and the encoder allocates
|
|
322
|
+
* nothing after construction (§6.6).
|
|
323
|
+
*
|
|
324
|
+
* **`needed`** is how many *contiguous* bytes the encoder wants at the cursor
|
|
325
|
+
* once the handover is done, or `0` when this flush is not a request for room —
|
|
326
|
+
* an explicit {@link OStream.flush}, or a drain that only needs *some* space. It
|
|
327
|
+
* is advisory in both directions: a sink is free to ignore it, and the encoder
|
|
328
|
+
* has a route that works without it (an atomic unit is split, a bulk write falls
|
|
329
|
+
* back to writing element by element, producing the identical bytes). What it
|
|
330
|
+
* buys is that a sink which *does* size its replacement — `growingOStream`'s —
|
|
331
|
+
* can open the bulk path instead of guessing. Without it, a caller-supplied
|
|
332
|
+
* growing sink can only double blindly and a large array silently loses its
|
|
333
|
+
* kernel route.
|
|
334
|
+
*
|
|
335
|
+
* A sink written before this argument existed keeps working unchanged: it is the
|
|
336
|
+
* last parameter, optional, and JavaScript simply drops it.
|
|
217
337
|
*/
|
|
218
|
-
type FlushSink = (
|
|
338
|
+
type FlushSink = (buffer: Uint8Array, start: number, end: number, needed?: number) => void;
|
|
219
339
|
|
|
220
340
|
/**
|
|
221
341
|
* The SofaBuffers encoder.
|
|
222
342
|
*
|
|
223
|
-
* `OStream` writes fields into a byte buffer
|
|
343
|
+
* `OStream` writes fields into a **caller-supplied** byte buffer — the corelib
|
|
344
|
+
* allocates no output buffer and never grows or reallocates one it was handed
|
|
345
|
+
* (CORELIB_PLAN §5.1). Two ways to drive it:
|
|
346
|
+
*
|
|
347
|
+
* - **One-shot** (`new OStream(buffer, offset?)`): the buffer must hold the
|
|
348
|
+
* whole message — the size a caller derives from the generated `MAX_SIZE` —
|
|
349
|
+
* and a buffer that fills reports `BUFFER_FULL` rather than growing.
|
|
350
|
+
* - **Streaming** (`new OStream(buffer, offset?, flush?)`): when the buffer
|
|
351
|
+
* fills, the produced bytes go to the `flush` sink and encoding continues —
|
|
352
|
+
* so the buffer can be arbitrarily smaller than the message, **down to a
|
|
353
|
+
* single byte** (§5.1): no single write requires contiguous room, a value
|
|
354
|
+
* larger than the buffer is split across flushes, and the bytes produced are
|
|
355
|
+
* identical either way — which is why this port declares
|
|
356
|
+
* {@link MIN_OUTPUT_BUFFER} = 1, the floor a buffer installed *with* a sink
|
|
357
|
+
* must clear. `offset` reserves room at the front for a lower-layer header,
|
|
358
|
+
* and it is `length - offset` that must clear the floor. Without a `flush`
|
|
359
|
+
* sink there is nowhere to drain to, so no minimum applies.
|
|
224
360
|
*
|
|
225
|
-
*
|
|
226
|
-
*
|
|
227
|
-
*
|
|
228
|
-
*
|
|
229
|
-
*
|
|
230
|
-
*
|
|
361
|
+
* Where the message has no schema-derived bound, the allocating half is the
|
|
362
|
+
* caller's, and CORELIB_PLAN §5.1.2 names the shape it takes: "install a scratch
|
|
363
|
+
* buffer **with a sink** that appends into the growing result". `growingOStream`
|
|
364
|
+
* (`./accumulate.ts`) is that caller ready-made — a **helper** (§6.6.1) built out
|
|
365
|
+
* of the streaming mode below, owning storage the encoder never sees the whole
|
|
366
|
+
* of. `OStream` itself has no growth mechanism at all.
|
|
231
367
|
*
|
|
232
368
|
* Generated code typically writes one field per message field; the methods map
|
|
233
369
|
* one-to-one onto the wire types. Problems throw {@link SofabError}.
|
|
370
|
+
*
|
|
371
|
+
* Nested sequences are opened with {@link OStream.writeSequenceBeginLazy}, which
|
|
372
|
+
* holds the header back until the sequence proves it has content, so an
|
|
373
|
+
* all-default one is omitted rather than framed empty (MESSAGE_SPEC §2). Close a
|
|
374
|
+
* `struct`/`union` field or an array wrapper with {@link OStream.writeSequenceEnd}
|
|
375
|
+
* and a wrapper-array *element* with {@link OStream.writeSequenceEndKeep}.
|
|
234
376
|
*/
|
|
235
377
|
|
|
236
378
|
/**
|
|
237
379
|
* Encoder for the SofaBuffers wire format. Each `write*` method appends one
|
|
238
|
-
* field and maps one-to-one onto a wire type.
|
|
239
|
-
*
|
|
240
|
-
*
|
|
241
|
-
*
|
|
242
|
-
*
|
|
380
|
+
* field and maps one-to-one onto a wire type. It writes into the buffer the
|
|
381
|
+
* caller supplies and into no other: it allocates none of its own, grows
|
|
382
|
+
* none it was given, and has no hook through which anything else could grow one
|
|
383
|
+
* for it (CORELIB_PLAN §5.1.2, §6.6). Hand it a buffer that holds the whole
|
|
384
|
+
* message, or one that drains to a {@link FlushSink} as it fills so the message
|
|
385
|
+
* can outgrow it. Invalid arguments and a full buffer with no sink throw
|
|
386
|
+
* {@link SofabError}. To let the buffer follow the message instead, encode into
|
|
387
|
+
* the accumulator `growingOStream` builds — a helper over the streaming mode,
|
|
388
|
+
* not a mode of this class.
|
|
243
389
|
*/
|
|
244
|
-
declare class OStream {
|
|
390
|
+
declare class OStream implements ByteSink {
|
|
245
391
|
private buf;
|
|
246
392
|
private pos;
|
|
393
|
+
/**
|
|
394
|
+
* Where the **current flush unit** begins in {@link buf}: the first byte the
|
|
395
|
+
* next {@link flush} hands to the sink.
|
|
396
|
+
*/
|
|
247
397
|
private start;
|
|
398
|
+
/**
|
|
399
|
+
* Where the **message so far** begins in {@link buf} — what {@link bytes} and
|
|
400
|
+
* {@link bytesUsed} report from.
|
|
401
|
+
*
|
|
402
|
+
* Equal to {@link start} for every ordinary stream, and the two move together:
|
|
403
|
+
* what has not been flushed is the whole of what is still in the buffer. They
|
|
404
|
+
* part only when a caller installs a replacement that **already holds** the
|
|
405
|
+
* earlier bytes of this message (`setBuffer`'s `carried` argument) — the shape
|
|
406
|
+
* an accumulating caller uses, where each new window opens after the bytes the
|
|
407
|
+
* previous one left behind in the same storage. Keeping the two apart is what
|
|
408
|
+
* lets that caller be an ordinary sink (§5.1.5) instead of a growth hook
|
|
409
|
+
* inside the encoder (§6.6).
|
|
410
|
+
*/
|
|
411
|
+
private origin;
|
|
248
412
|
private readonly flushSink;
|
|
249
|
-
private readonly canGrow;
|
|
250
413
|
private depth;
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
414
|
+
/**
|
|
415
|
+
* Ids of the innermost open sequences whose header has not been written yet
|
|
416
|
+
* (MESSAGE_SPEC §2 lazy framing, {@link OStream.writeSequenceBeginLazy}).
|
|
417
|
+
* Always a contiguous suffix of the open sequences: writing any field commits
|
|
418
|
+
* the whole run at once, so {@link OStream.writeSequenceEnd} can drop the
|
|
419
|
+
* innermost one by dropping the last entry. Held-back ids are encoder state,
|
|
420
|
+
* never buffer content, so a flush can never split a run.
|
|
421
|
+
*
|
|
422
|
+
* Storage plus an explicit count rather than `push`/`pop`, so the slots are
|
|
423
|
+
* reused across messages and so {@link OStream.commitPending} can zero the
|
|
424
|
+
* count *before* it writes.
|
|
425
|
+
*
|
|
426
|
+
* **Fixed size, sized at construction** from `MAX_DEPTH` — the "fixed-size
|
|
427
|
+
* state whose size this document fixes" of CORELIB_PLAN §6.6.2, and the shape
|
|
428
|
+
* §6.0.1 asks for: the run covers the full depth, so it is always a contiguous
|
|
429
|
+
* suffix of the open sequences, every sequence stays canonical however deep it
|
|
430
|
+
* nests, and there is no eager-framing fallback. Nothing is allocated per
|
|
431
|
+
* message, per field or per sequence.
|
|
432
|
+
*
|
|
433
|
+
* A plain array rather than an `Int32Array`: a 255-slot typed array is an
|
|
434
|
+
* external backing store on V8 (~3.1 µs to allocate on Node 24, against ~5 ns
|
|
435
|
+
* for this), and an encoder is constructed per message on the accumulator path.
|
|
436
|
+
* Slots are written before they are read, so it is never read holey.
|
|
437
|
+
*/
|
|
438
|
+
private readonly pending;
|
|
439
|
+
/** Valid entries in {@link OStream.pending}. */
|
|
440
|
+
private nPending;
|
|
441
|
+
/**
|
|
442
|
+
* How many buffer installations {@link OStream.setBuffer} has made. Only ever
|
|
443
|
+
* compared for equality across the flush callback, which is how
|
|
444
|
+
* {@link OStream.flush} tells "the sink copied and returned" from "the sink
|
|
445
|
+
* took the buffer and installed a replacement" — the one distinction §5.1
|
|
446
|
+
* rests the handover contract on, and the one that decides whether the start
|
|
447
|
+
* offset was consumed or re-armed.
|
|
448
|
+
*/
|
|
449
|
+
private installs;
|
|
450
|
+
private readonly kernel;
|
|
451
|
+
/**
|
|
452
|
+
* Encoder over a caller buffer, optionally draining to `flush` as it fills.
|
|
453
|
+
*
|
|
454
|
+
* Those are the only two buffer models there are (CORELIB_PLAN §5.1.1): the
|
|
455
|
+
* buffer the caller hands over, and whatever a sink installs in its place
|
|
456
|
+
* (§5.1.5). There is no third parameter through which the encoder could be
|
|
457
|
+
* given something to enlarge — growing a destination from a write path is what
|
|
458
|
+
* §6.6's second violation row names, whoever owns the allocator.
|
|
459
|
+
*
|
|
460
|
+
* Constructing one is the only allocating step (§6.6): it sizes the hold-back
|
|
461
|
+
* run from `MAX_DEPTH` and reads the active kernel once. No `write*` call after
|
|
462
|
+
* that allocates anything.
|
|
463
|
+
*/
|
|
255
464
|
constructor(buffer: Uint8Array, offset?: number, flush?: FlushSink);
|
|
256
|
-
/** Bytes currently held in the buffer (
|
|
465
|
+
/** Bytes of the message currently held in the buffer (see {@link bytes}). */
|
|
257
466
|
get bytesUsed(): number;
|
|
258
467
|
/**
|
|
259
|
-
* The encoded message so far, as a view into the working buffer
|
|
260
|
-
*
|
|
261
|
-
*
|
|
468
|
+
* The encoded message so far, as a view into the working buffer: everything
|
|
469
|
+
* written since construction, the last {@link reset}, or the last flush the
|
|
470
|
+
* sink returned from without installing a buffer. With a flush sink that is
|
|
471
|
+
* normally only the not-yet-flushed tail; on a stream whose sink hands back a
|
|
472
|
+
* replacement carrying the earlier bytes — the accumulator `growingOStream`
|
|
473
|
+
* builds — it is the whole message. The view is valid until the next write.
|
|
262
474
|
*/
|
|
263
475
|
bytes(): Uint8Array;
|
|
264
|
-
/**
|
|
476
|
+
/**
|
|
477
|
+
* Drain buffered bytes to the flush sink (no-op without one).
|
|
478
|
+
*
|
|
479
|
+
* A sink that returns **without** installing a buffer has copied what it was
|
|
480
|
+
* handed, so the encoder keeps writing into the same buffer — resuming at
|
|
481
|
+
* offset **0**. The start offset belongs to the *installation*, not to the
|
|
482
|
+
* buffer (CORELIB_PLAN §5.1): the buffer-set that armed it — the constructor
|
|
483
|
+
* or {@link OStream.setBuffer} — reserved room in the unit it began, and
|
|
484
|
+
* handing that unit over consumes the reservation. A sink that wants header
|
|
485
|
+
* room in *every* unit re-arms it by calling `setBuffer(buf, offset)` from
|
|
486
|
+
* inside the callback, a new installation like any other; a bare return must
|
|
487
|
+
* not do it implicitly, or the leading bytes would be capacity the rest of the
|
|
488
|
+
* stream could never use and the two shapes would be indistinguishable.
|
|
489
|
+
*/
|
|
265
490
|
flush(): void;
|
|
491
|
+
/**
|
|
492
|
+
* {@link flush}, plus the number of contiguous bytes the caller wants at the
|
|
493
|
+
* cursor afterwards — `0` when it wants none in particular.
|
|
494
|
+
*
|
|
495
|
+
* Split out from the public `flush` so the figure cannot be invented by a
|
|
496
|
+
* caller: it is the encoder's own reserve request, and a sink that sizes a
|
|
497
|
+
* replacement from it (`growingOStream`) has to be able to trust it. Everything
|
|
498
|
+
* else is identical, including that a sink which returns without installing has
|
|
499
|
+
* *copied* and the cursor resumes at `0`.
|
|
500
|
+
*/
|
|
501
|
+
private drain;
|
|
266
502
|
/**
|
|
267
503
|
* Install a fresh output buffer to write into, mid-stream. Intended for the
|
|
268
504
|
* streaming (flush-sink) mode: call it from inside your flush callback to hand
|
|
@@ -270,8 +506,36 @@ declare class OStream {
|
|
|
270
506
|
* without interruption. `offset` reserves space at the front of the new
|
|
271
507
|
* buffer. Any not-yet-flushed bytes in the old buffer are dropped, so
|
|
272
508
|
* {@link flush} first (the flush callback fires before you swap).
|
|
509
|
+
*
|
|
510
|
+
* Every call is a **new installation**, and its `offset` applies to the unit
|
|
511
|
+
* it begins and is consumed when that unit is flushed (CORELIB_PLAN §5.1).
|
|
512
|
+
* Passing the buffer the encoder already has is an installation like any
|
|
513
|
+
* other: that is how a sink gets header room in *every* flushed unit — one
|
|
514
|
+
* framing header per packet — where returning bare would resume at `0`.
|
|
515
|
+
*
|
|
516
|
+
* On a stream that has a flush sink the new buffer must leave at least
|
|
517
|
+
* {@link MIN_OUTPUT_BUFFER} usable bytes (`buffer.length - offset`); a smaller
|
|
518
|
+
* one is rejected here, with {@link SofabErrorCode.Argument}, leaving the
|
|
519
|
+
* encoder on the buffer it already had. A sink-less stream has no minimum.
|
|
520
|
+
*
|
|
521
|
+
* `carried` says how many bytes **immediately before `offset`** the
|
|
522
|
+
* replacement already holds of *this* message — normally `0`, because a fresh
|
|
523
|
+
* buffer holds none. A caller that keeps the whole message in one growing
|
|
524
|
+
* store passes the length it copied across, so {@link bytes} keeps reporting
|
|
525
|
+
* the message rather than only the piece written after the swap. It changes
|
|
526
|
+
* nothing on the wire and nothing about flushing: the next flush still begins
|
|
527
|
+
* at `offset`. `growingOStream` is the caller this exists for; it is the
|
|
528
|
+
* §5.1.5 handover, not a growth hook inside the encoder (§6.6).
|
|
529
|
+
*
|
|
530
|
+
* The accumulating stream `growingOStream` builds is an ordinary streaming
|
|
531
|
+
* stream, so this works on it too, and means exactly what it means anywhere
|
|
532
|
+
* else: the not-yet-flushed bytes in the old buffer are dropped, encoding
|
|
533
|
+
* continues into yours, and its sink takes over growing *that* one from the
|
|
534
|
+
* next flush — reserve offset included, since what it copies out is the
|
|
535
|
+
* message rather than the buffer. Encode into a plain
|
|
536
|
+
* `new OStream(buffer, offset, flush?)` to keep the buffer yours instead.
|
|
273
537
|
*/
|
|
274
|
-
setBuffer(buffer: Uint8Array, offset?: number): void;
|
|
538
|
+
setBuffer(buffer: Uint8Array, offset?: number, carried?: number): void;
|
|
275
539
|
/**
|
|
276
540
|
* Rewind the encoder to empty, reusing the existing buffer. Lets a caller pool
|
|
277
541
|
* one OStream across many messages instead of allocating a fresh buffer per
|
|
@@ -282,21 +546,101 @@ declare class OStream {
|
|
|
282
546
|
writeUnsigned(id: number, value: number | bigint): void;
|
|
283
547
|
/** Write a signed integer field (zig-zag encoded). */
|
|
284
548
|
writeSigned(id: number, value: number | bigint): void;
|
|
549
|
+
/**
|
|
550
|
+
* Write an unsigned 64-bit scalar from a {@link Long} — the `bigint`-free twin
|
|
551
|
+
* of {@link writeUnsigned}, and the scalar counterpart of
|
|
552
|
+
* {@link writeUnsignedArrayLong}. Produces the identical wire.
|
|
553
|
+
*
|
|
554
|
+
* There is no range check and no scratch round-trip: a `Long` *is* two 32-bit
|
|
555
|
+
* halves, so it is in the `uint64` domain by construction — which is the whole
|
|
556
|
+
* of what `splitU64` decides for a `number | bigint`. The halves go straight
|
|
557
|
+
* into the varint writer, so nothing is allocated per value. Nothing needs
|
|
558
|
+
* copying out ahead of `header` either, for the same reason the array writers
|
|
559
|
+
* do not: the halves come off a caller-owned immutable `Long`, not the shared
|
|
560
|
+
* scratch a re-entrant flush sink could overwrite.
|
|
561
|
+
*/
|
|
562
|
+
writeUnsignedLong(id: number, value: Long): void;
|
|
563
|
+
/**
|
|
564
|
+
* Write a signed 64-bit scalar (zig-zag) from a {@link Long} — the
|
|
565
|
+
* `bigint`-free twin of {@link writeSigned}, and the scalar counterpart of
|
|
566
|
+
* {@link writeSignedArrayLong}. Zig-zag `(n << 1) ^ (n >> 63)` is computed on
|
|
567
|
+
* the lo/hi pair, so the varint goes out at its exact size (a fixed caller
|
|
568
|
+
* buffer must not see a 10-byte demand for a 2-byte field) and no `bigint` is
|
|
569
|
+
* created. A `Long` carries exactly 64 bits, so as in {@link writeUnsignedLong}
|
|
570
|
+
* there is nothing left to range-check.
|
|
571
|
+
*/
|
|
572
|
+
writeSignedLong(id: number, value: Long): void;
|
|
285
573
|
/** Write a boolean field (encoded as the unsigned value 0 or 1). */
|
|
286
574
|
writeBoolean(id: number, value: boolean): void;
|
|
287
575
|
/** Write an IEEE-754 32-bit float field. */
|
|
288
576
|
writeFp32(id: number, value: number): void;
|
|
577
|
+
/**
|
|
578
|
+
* Write an fp32 field from its raw wire bits — the 4 little-endian payload
|
|
579
|
+
* bytes as one 32-bit word, which is exactly what {@link Visitor.fp32} delivers
|
|
580
|
+
* as `bits`.
|
|
581
|
+
*
|
|
582
|
+
* This is the re-encode half of the bit-exactness rule (CORELIB_PLAN §6.5). A JS
|
|
583
|
+
* `number` is a 64-bit double, and widening an fp32 **signaling** NaN into one
|
|
584
|
+
* quiets it, so re-encoding through {@link writeFp32} cannot reproduce such a
|
|
585
|
+
* payload; the bits go out verbatim here, so decode → re-encode is byte-for-byte
|
|
586
|
+
* for every fp32 value, sNaN included. §6.5 requires this path of every
|
|
587
|
+
* double-only target, and names it: "a 32-bit bits accessor".
|
|
588
|
+
*/
|
|
589
|
+
writeFp32Bits(id: number, bits: number): void;
|
|
289
590
|
/** Write an IEEE-754 64-bit double field. */
|
|
290
591
|
writeFp64(id: number, value: number): void;
|
|
291
592
|
/** Write a UTF-8 string field. */
|
|
292
593
|
writeString(id: number, text: string): void;
|
|
293
594
|
/** Write a blob (arbitrary bytes) field. */
|
|
294
595
|
writeBlob(id: number, data: Uint8Array): void;
|
|
295
|
-
/**
|
|
596
|
+
/**
|
|
597
|
+
* Write a fixed-length field of the given subtype from raw bytes.
|
|
598
|
+
*
|
|
599
|
+
* This is the byte-level entry point — the one writer that takes the subtype
|
|
600
|
+
* from the caller rather than picking it — so the payload is checked
|
|
601
|
+
* **against that subtype** before a byte is written, and it cannot emit a
|
|
602
|
+
* `fixlen_word` a conformant decoder must reject (`ARGUMENT`, §6.3):
|
|
603
|
+
*
|
|
604
|
+
* * subtypes `0x4`–`0x7` are **reserved** — a decoder must treat a field
|
|
605
|
+
* carrying one as malformed (`INVALID`, §4.6/§5.2);
|
|
606
|
+
* * `Fp32` / `Fp64` payloads are **exactly** 4 / 8 bytes — any other declared
|
|
607
|
+
* length for those subtypes is malformed, rejected the moment the word is
|
|
608
|
+
* read (§4.6);
|
|
609
|
+
* * `String` / `Blob` take any length up to `FIXLEN_MAX`.
|
|
610
|
+
*
|
|
611
|
+
* The typed writers ({@link writeFp32}, {@link writeFp64},
|
|
612
|
+
* {@link writeString}) are correct by construction and go straight to the
|
|
613
|
+
* header; only {@link writeBlob}, whose subtype is unconstrained anyway,
|
|
614
|
+
* shares this path.
|
|
615
|
+
*/
|
|
296
616
|
writeFixlen(id: number, data: Uint8Array, subtype: FixlenSubtype): void;
|
|
297
|
-
/**
|
|
617
|
+
/**
|
|
618
|
+
* Write an array of unsigned integers (each a varint).
|
|
619
|
+
*
|
|
620
|
+
* **The bulk kernel writes a whole array in one pass and cannot flush**, so it
|
|
621
|
+
* runs only where everything is known to fit. Three cases, and none of them
|
|
622
|
+
* asks the source how wide its elements are:
|
|
623
|
+
*
|
|
624
|
+
* * **block mode** (no sink) — the buffer is meant to hold the whole message,
|
|
625
|
+
* so the kernel always runs and the buffer's length is the bound, checked
|
|
626
|
+
* after the fact. A message that does not fit is `BUFFER_FULL`, which is
|
|
627
|
+
* precisely this mode's answer.
|
|
628
|
+
* * **a stream with room** — `VARINT_MAX_BYTES` per element is the true
|
|
629
|
+
* worst case for any 64-bit value, and a *growing* stream always satisfies
|
|
630
|
+
* it because it grows to whatever is asked.
|
|
631
|
+
* * **a chunk too small for that** — fill it to the last byte, hand it over,
|
|
632
|
+
* carry on, splitting an element where it falls. That is not a fallback but
|
|
633
|
+
* the mode's contract: `MIN_OUTPUT_BUFFER` is 1.
|
|
634
|
+
*
|
|
635
|
+
* What is gone is the fourth case, which asked `values.constructor` for a
|
|
636
|
+
* narrower bound so the kernel would run on a tightly-sized chunk. `constructor`
|
|
637
|
+
* is an ordinary property, an `ArrayLike` can claim any width, and a wrong
|
|
638
|
+
* answer silently truncated the message — §5.1's "partial output handed back as
|
|
639
|
+
* complete". The block mode now reaches the kernel without needing the number
|
|
640
|
+
* at all, which is where a tightly-sized buffer actually lives.
|
|
641
|
+
*/
|
|
298
642
|
writeUnsignedArray(id: number, values: ArrayLike<number | bigint>): void;
|
|
299
|
-
/** Write an array of signed integers (each zig-zag + varint). */
|
|
643
|
+
/** Write an array of signed integers (each zig-zag + varint). See {@link writeUnsignedArray}. */
|
|
300
644
|
writeSignedArray(id: number, values: ArrayLike<number | bigint>): void;
|
|
301
645
|
/**
|
|
302
646
|
* Write an unsigned 64-bit array from {@link Long}[] — the `bigint`-free path.
|
|
@@ -311,390 +655,1405 @@ declare class OStream {
|
|
|
311
655
|
writeSignedArrayLong(id: number, values: readonly Long[]): void;
|
|
312
656
|
/** Write an array of IEEE-754 32-bit floats. */
|
|
313
657
|
writeFp32Array(id: number, values: ArrayLike<number>): void;
|
|
658
|
+
/**
|
|
659
|
+
* Write an fp32 array from its raw little-endian element payload. The bytes
|
|
660
|
+
* are emitted verbatim — no per-element `setFloat32` — so a signaling NaN
|
|
661
|
+
* survives bit-for-bit (§4.6), which {@link writeFp32Array} cannot guarantee
|
|
662
|
+
* because it re-quantizes each JS `number`. `payload.length` must be a
|
|
663
|
+
* multiple of 4; the element count is `payload.length / 4`.
|
|
664
|
+
*/
|
|
665
|
+
writeFp32ArrayRaw(id: number, payload: Uint8Array): void;
|
|
314
666
|
/** Write an array of IEEE-754 64-bit doubles. */
|
|
315
667
|
writeFp64Array(id: number, values: ArrayLike<number>): void;
|
|
316
|
-
/**
|
|
317
|
-
|
|
318
|
-
|
|
668
|
+
/**
|
|
669
|
+
* Open a nested sequence (a fresh id scope) whose header is **held back**
|
|
670
|
+
* until the sequence turns out to have content.
|
|
671
|
+
*
|
|
672
|
+
* MESSAGE_SPEC §2 omits a sequence-typed field whose value equals its declared
|
|
673
|
+
* default, and "not one child was written" is exactly that condition —
|
|
674
|
+
* evaluated per child field, recursively, for free, because the message layer
|
|
675
|
+
* already omits every child equal to its default. A sequence closed with
|
|
676
|
+
* nothing in it therefore emits **nothing** instead of a two-byte empty frame,
|
|
677
|
+
* and an all-default message becomes the empty byte string. No byte image is
|
|
678
|
+
* ever compared, so in-memory layout never enters the decision.
|
|
679
|
+
*
|
|
680
|
+
* This is the only way to open a sequence. How it closes decides whether a
|
|
681
|
+
* contentless one survives: {@link OStream.writeSequenceEnd} drops it,
|
|
682
|
+
* {@link OStream.writeSequenceEndKeep} forces the frame out.
|
|
683
|
+
*/
|
|
684
|
+
writeSequenceBeginLazy(id: number): void;
|
|
685
|
+
/**
|
|
686
|
+
* Close the current sequence, letting it **vanish** if it received no content.
|
|
687
|
+
*
|
|
688
|
+
* Use it wherever absence encodes the same value as an empty frame: a
|
|
689
|
+
* `struct`/`union` field, and an array field whose declared `default` is the
|
|
690
|
+
* empty collection (MESSAGE_SPEC §2). Where the frame must be visible, close
|
|
691
|
+
* with {@link OStream.writeSequenceEndKeep} instead.
|
|
692
|
+
*
|
|
693
|
+
* An end with no matching begin is not rejected: the encoder writes what it is
|
|
694
|
+
* told, and the resulting bytes are then malformed, which is the decoder's
|
|
695
|
+
* verdict to make. No other port refuses it. The depth counter stops at zero
|
|
696
|
+
* so the MAX_DEPTH check on begin cannot be fooled by an underflow.
|
|
697
|
+
*/
|
|
319
698
|
writeSequenceEnd(): void;
|
|
320
|
-
/**
|
|
321
|
-
|
|
699
|
+
/**
|
|
700
|
+
* Close the current sequence, **keeping** its frame even when it received no
|
|
701
|
+
* content.
|
|
702
|
+
*
|
|
703
|
+
* Behaves like a write: it first emits any held-back headers — this frame's
|
|
704
|
+
* and every enclosing one's — and then the end marker, so an empty sequence
|
|
705
|
+
* reaches the wire as `begin` + `end`.
|
|
706
|
+
*
|
|
707
|
+
* Required wherever the frame carries information beyond its contents:
|
|
708
|
+
* - a **wrapper-array element** (`struct`/`union`/nested row): element
|
|
709
|
+
* presence is what carries a dynamic array's length — *highest present id +
|
|
710
|
+
* 1* (MESSAGE_SPEC §5.1) — so dropping an all-default element would change
|
|
711
|
+
* the decoded length, not just the bytes;
|
|
712
|
+
* - an array field already known to **differ from a non-empty declared
|
|
713
|
+
* `default`**: absence would reconstruct that default, so the empty frame is
|
|
714
|
+
* the only encoding of "explicitly empty" (§2, §3).
|
|
715
|
+
*
|
|
716
|
+
* The two failure directions are not symmetric, which is why this is the safe
|
|
717
|
+
* choice when in doubt: using it where {@link OStream.writeSequenceEnd} would
|
|
718
|
+
* do costs one non-canonical empty frame that a decoder normalizes away, while
|
|
719
|
+
* the reverse silently changes an array's length.
|
|
720
|
+
*/
|
|
721
|
+
writeSequenceEndKeep(): void;
|
|
322
722
|
/** Ensure exactly `value`'s varint size, then write it (number fast path). */
|
|
323
723
|
private putVarintNum;
|
|
724
|
+
/**
|
|
725
|
+
* The multi-byte / needs-room tail of {@link putVarintNum}, kept in its own
|
|
726
|
+
* method so the single-byte test above stays small enough for the JIT to
|
|
727
|
+
* inline into every `header` / `fixlenHead` / `arrayHead` call site.
|
|
728
|
+
*/
|
|
729
|
+
private putVarintNumSlow;
|
|
730
|
+
/**
|
|
731
|
+
* Write a 64-bit value, held as two 32-bit halves, as a varint.
|
|
732
|
+
*
|
|
733
|
+
* Deliberately just a bounds check and two calls: this is what the array
|
|
734
|
+
* loops call per element, and it only pays its way while it is small enough
|
|
735
|
+
* for the JIT to inline. Spelling the drain case out here instead — three
|
|
736
|
+
* more lines — cost 16-19% on an array streamed through a 32/64-byte buffer,
|
|
737
|
+
* where most elements take the fast path and want it inlined.
|
|
738
|
+
*/
|
|
739
|
+
private putVarintLoHi;
|
|
740
|
+
/**
|
|
741
|
+
* A full buffer that is nonetheless wide enough to hold any varint: drain it
|
|
742
|
+
* and retry the worst case, which is exactly what `ensure(VARINT_MAX_BYTES)`
|
|
743
|
+
* did before §5.1 and is still the common streaming case. Sizing the value
|
|
744
|
+
* first instead — the narrow-buffer path in {@link putVarintLoHiTight} — cost
|
|
745
|
+
* +125 instructions on every element of an array streamed through a
|
|
746
|
+
* one-element-wide buffer.
|
|
747
|
+
*/
|
|
748
|
+
private putVarintLoHiSlow;
|
|
749
|
+
/**
|
|
750
|
+
* Zig-zag {@link putVarintLoHi}: `(n << 1) ^ (n >> 63)` on the halves. Keeps
|
|
751
|
+
* its own worst-case fast path so the signed array loop reaches the combined
|
|
752
|
+
* zig-zag-and-encode writer directly, exactly as it did before; the drain and
|
|
753
|
+
* narrow-buffer cases are the same for both signs, so they are shared.
|
|
754
|
+
*/
|
|
755
|
+
private putZigzagVarintLoHi;
|
|
756
|
+
/**
|
|
757
|
+
* The narrow-buffer tail of {@link putVarintLoHi}: the buffer could not hold a
|
|
758
|
+
* worst-case varint even empty. Sizing the value exactly keeps such a buffer
|
|
759
|
+
* from flushing for room it does not need; only when it cannot hold the varint
|
|
760
|
+
* *at all* does the value get split across flushes, seven bits at a time.
|
|
761
|
+
*/
|
|
762
|
+
private putVarintLoHiTight;
|
|
763
|
+
/**
|
|
764
|
+
* Write the 4 little-endian bytes of an fp32 (§4.6).
|
|
765
|
+
*
|
|
766
|
+
* Through the shared scratch, not a `DataView` over the buffer: building that
|
|
767
|
+
* handle costs ~129 ns against the ~2 ns it saves on one value (§6.6.2 allows the
|
|
768
|
+
* handle, arithmetic forbids it here). The bulk array path amortizes one over the
|
|
769
|
+
* whole run instead — see the kernel.
|
|
770
|
+
*/
|
|
771
|
+
private putFp32;
|
|
772
|
+
private putFp32Slow;
|
|
773
|
+
/**
|
|
774
|
+
* Write the 4 little-endian bytes of an fp32 held as a 32-bit word (§4.6) — the
|
|
775
|
+
* path {@link OStream.writeFp32Bits} takes, and the tail of
|
|
776
|
+
* {@link OStream.putFp32Slow}.
|
|
777
|
+
*/
|
|
778
|
+
private putFp32Bits;
|
|
779
|
+
/**
|
|
780
|
+
* {@link writeFp32Array} for a `Float32Array` source: its own 32-bit words go
|
|
781
|
+
* out, never a `number`, which is what keeps a signaling NaN intact (§6.5) —
|
|
782
|
+
* on the bulk path (the kernel copies words too) and on the streamed one alike,
|
|
783
|
+
* so a small buffer produces the one-shot bytes (§5.1.4).
|
|
784
|
+
*
|
|
785
|
+
* Kept to the header and the bulk call, the streaming loop in its own method:
|
|
786
|
+
* with the loop in this body an 8-element one-shot array cost +416 Ir/op
|
|
787
|
+
* (+8.8%) over the pre-#185 encoder, split like this +1.1% (Callgrind).
|
|
788
|
+
*/
|
|
789
|
+
private writeFp32Words;
|
|
790
|
+
/**
|
|
791
|
+
* The streamed half of {@link writeFp32Words}: one `Uint32Array` over the
|
|
792
|
+
* source per call, the same handle the bulk kernel takes (§6.6.2); reading it gives each word's value whatever the
|
|
793
|
+
* host byte order, and the shifts below store it little-endian. The words go
|
|
794
|
+
* out one run per stretch of free buffer, with `buf`/`pos` in locals, and the
|
|
795
|
+
* split points are {@link putFp32}'s: drain when fewer than 4 bytes are free,
|
|
796
|
+
* split an element byte by byte only when the buffer itself is narrower than 4.
|
|
797
|
+
*/
|
|
798
|
+
private putFp32Words;
|
|
799
|
+
/** Write the 8 little-endian bytes of an fp64 (§4.6) — see {@link putFp32}. */
|
|
800
|
+
private putFp64;
|
|
801
|
+
private putFp64Slow;
|
|
802
|
+
/**
|
|
803
|
+
* Append one byte, draining to the sink first when the buffer is full.
|
|
804
|
+
*
|
|
805
|
+
* @internal Public only because {@link utf8WriteSink} writes through it — the
|
|
806
|
+
* narrow-buffer string path (§5.1.3). Not part of the field-writing API.
|
|
807
|
+
*/
|
|
808
|
+
putByte(b: number): void;
|
|
809
|
+
/**
|
|
810
|
+
* Write a field header, the `(id << 3) | wireType` tag, as a varint.
|
|
811
|
+
*
|
|
812
|
+
* This is the single choke point every field write passes through — the
|
|
813
|
+
* scalar, fixlen, float, string, blob and both array writers all reach the
|
|
814
|
+
* wire through `header` / `fixlenHead` / `arrayHead`, and `fixlenHead` and
|
|
815
|
+
* `arrayHead` are themselves nothing but `header` plus a follow-up varint. So
|
|
816
|
+
* this is also where a held-back sequence run is committed: the field about to
|
|
817
|
+
* be written is content, which means every enclosing sequence is non-default
|
|
818
|
+
* and must be framed after all (MESSAGE_SPEC §2).
|
|
819
|
+
*
|
|
820
|
+
* The only writers that do *not* pass through here are the two sequence
|
|
821
|
+
* closers, which must not commit ({@link OStream.writeSequenceEnd}) or commit
|
|
822
|
+
* explicitly ({@link OStream.writeSequenceEndKeep}), and
|
|
823
|
+
* {@link OStream.writeSequenceBeginLazy}, which writes no byte at all.
|
|
824
|
+
*/
|
|
324
825
|
private header;
|
|
826
|
+
/**
|
|
827
|
+
* Write out the held-back sequence headers, **outermost first**, and clear the
|
|
828
|
+
* run. Runs at most once per non-default sequence, never per field — the cost
|
|
829
|
+
* on the hot path is the single `nPending` test in {@link header}.
|
|
830
|
+
*
|
|
831
|
+
* The count is zeroed before the first byte goes out, so a write re-entered
|
|
832
|
+
* from a flush sink cannot emit the same run twice.
|
|
833
|
+
*/
|
|
834
|
+
private commitPending;
|
|
325
835
|
private fixlenHead;
|
|
326
836
|
private arrayHead;
|
|
327
|
-
/**
|
|
837
|
+
/**
|
|
838
|
+
* Copy `data` out, flushing/growing as needed (large payloads stay chunked).
|
|
839
|
+
*
|
|
840
|
+
* The whole-payload case — the common one, and the only one on a buffer sized
|
|
841
|
+
* from `MAX_SIZE` or on the accumulator — is a single `set` of the caller's
|
|
842
|
+
* array: a `memcpy`, and no view at all.
|
|
843
|
+
*
|
|
844
|
+
* **A payload split across flushes takes a per-piece view: a language-forced
|
|
845
|
+
* handle under CORELIB_PLAN §6.6.2.** `TypedArray.set` is the only `memcpy`
|
|
846
|
+
* this language exposes and it takes a *typed array* as its source, so copying a
|
|
847
|
+
* *range* of one needs a `subarray` — §6.6.2's "the only way to name a region of
|
|
848
|
+
* the caller's buffer is a wrapper over it". It has the two properties that
|
|
849
|
+
* clause requires: it carries no message bytes (the storage is the caller's, on
|
|
850
|
+
* both ends) and no wire number sizes it (a handle over a thousand bytes costs
|
|
851
|
+
* what a handle over ten costs). The allocation-free alternative is a byte loop,
|
|
852
|
+
* measured at 358 MB/s against 10,963 MB/s for `set`.
|
|
853
|
+
*
|
|
854
|
+
* It never leaves this method: `set` consumes it and no caller can reach it, so
|
|
855
|
+
* §6.7's ban on exposing a value that outlives its callback is untouched — and so
|
|
856
|
+
* is §5.1.6, which is why the copy happens at all rather than the payload being
|
|
857
|
+
* handed to the sink. §6.6.2 asks the port to make such handles visible rather
|
|
858
|
+
* than invisible: the README itemises it (§9.6) and `heap-free-codec.test.ts`
|
|
859
|
+
* pins its count and kind.
|
|
860
|
+
*/
|
|
328
861
|
private writeRaw;
|
|
329
|
-
/**
|
|
862
|
+
/**
|
|
863
|
+
* Make room for `n` contiguous bytes at `pos` if the buffer can hold them at
|
|
864
|
+
* all: `true` when it can (flushing or growing as needed), `false` when a
|
|
865
|
+
* fixed caller buffer is simply smaller than `n` and the value must be split
|
|
866
|
+
* across flushes instead — CORELIB_PLAN §5.1 puts the floor on the output
|
|
867
|
+
* buffer at a single byte, so no write may demand a contiguous run.
|
|
868
|
+
*
|
|
869
|
+
* The one case that still fails is a buffer with no sink to drain to: there is
|
|
870
|
+
* nowhere for a split to put the earlier bytes, so it reports BufferFull here,
|
|
871
|
+
* before anything is written, exactly as {@link ensure} did.
|
|
872
|
+
*/
|
|
873
|
+
private tryEnsure;
|
|
874
|
+
/**
|
|
875
|
+
* Reserve `n` contiguous bytes for a bulk write — the whole payload of an
|
|
876
|
+
* array or a string, written in one pass into a buffer that cannot move under
|
|
877
|
+
* it. Every caller has an element-at-a-time route to fall back on when this
|
|
878
|
+
* says no, producing the identical bytes, so a `false` here is never an error.
|
|
879
|
+
*
|
|
880
|
+
* The room already at the cursor counts on **any** buffer, which is what makes
|
|
881
|
+
* the one-shot `new OStream(buf)` case — a caller buffer sized from the
|
|
882
|
+
* schema's `MAX_SIZE`, the shape CORELIB_PLAN §5.1 puts first — the fast one:
|
|
883
|
+
* without it a message encoded into a caller's own buffer took `TextEncoder`
|
|
884
|
+
* for every string and the element loop for every array, measured at 1.9 µs
|
|
885
|
+
* against 0.16 µs for the same five-field message.
|
|
886
|
+
*
|
|
887
|
+
* Beyond that room only a **sink** may be asked, and only by draining. Emptying
|
|
888
|
+
* the buffer is the one thing that can produce room without an allocation
|
|
889
|
+
* anywhere (§6.6), and it is legal wherever this is called — every caller has
|
|
890
|
+
* just written a complete header, so the cursor is on a boundary between atomic
|
|
891
|
+
* units (§5.1.3). A sink that installs a larger replacement (§5.1.5) therefore
|
|
892
|
+
* re-opens the bulk route by itself, which is how the accumulating helper keeps
|
|
893
|
+
* it. Nothing is *demanded*: a `false` sends the caller down its
|
|
894
|
+
* element-at-a-time route, which produces the identical bytes, so a fixed
|
|
895
|
+
* buffer too narrow for the worst case never turns into a spurious
|
|
896
|
+
* `BUFFER_FULL`.
|
|
897
|
+
*/
|
|
898
|
+
private reserveBulk;
|
|
899
|
+
/**
|
|
900
|
+
* Commit the position a bulk kernel returned, in the **block** mode.
|
|
901
|
+
*
|
|
902
|
+
* Nothing reserves room in front of that kernel, and nothing could: the bytes an
|
|
903
|
+
* array takes are only known once it is encoded, and the estimate that used to
|
|
904
|
+
* stand in — the source's own `constructor`, which any object can set — silently
|
|
905
|
+
* truncated the message when it was wrong (§5.1). Here the bound is the buffer's
|
|
906
|
+
* own length, applied afterwards. The varint kernels stop writing at
|
|
907
|
+
* `out.length` and keep counting (see {@link Kernel}), so a `pos` past the end
|
|
908
|
+
* is the exact shortfall and nothing was written outside the buffer.
|
|
909
|
+
*
|
|
910
|
+
* This is the block mode's error and only its: the buffer is meant to hold the
|
|
911
|
+
* whole message, so one that does not fit is exactly what `BUFFER_FULL` means.
|
|
912
|
+
* The streaming mode never gets here, and `BUFFER_FULL` is unreachable there by
|
|
913
|
+
* contract.
|
|
914
|
+
*/
|
|
915
|
+
private bulkEnd;
|
|
916
|
+
/**
|
|
917
|
+
* Ensure `n` contiguous bytes are free at `pos`; returns `pos` for chaining.
|
|
918
|
+
*
|
|
919
|
+
* The only remaining caller is the one-byte sequence-end marker, which is
|
|
920
|
+
* indivisible: there is no smaller piece to split it into, so a buffer that
|
|
921
|
+
* cannot take it has nothing left to report but `BUFFER_FULL`.
|
|
922
|
+
*/
|
|
330
923
|
private ensure;
|
|
331
924
|
/** Ensure *some* room (up to `want`); returns how many bytes are available. */
|
|
332
925
|
private ensureSome;
|
|
333
|
-
private growTo;
|
|
334
926
|
}
|
|
335
927
|
|
|
336
928
|
/**
|
|
337
|
-
*
|
|
929
|
+
* `growingOStream` — the caller that lets the buffer follow the message.
|
|
930
|
+
*
|
|
931
|
+
* **This is the static helper layer, not the codec** (CORELIB_PLAN §6.6.1). The
|
|
932
|
+
* codec is {@link OStream}: it writes into the buffer it was handed and has no
|
|
933
|
+
* way to enlarge one. Where the schema bounds the message, generated code sizes
|
|
934
|
+
* one buffer from `MAX_SIZE` and a plain `new OStream(buf)` is the whole story.
|
|
935
|
+
* Where it does not, §5.1.2 names exactly one shape for the unbounded case:
|
|
936
|
+
*
|
|
937
|
+
* > Install a scratch buffer **with a sink** that appends into the growing
|
|
938
|
+
* > result; the scratch is subject to `MIN_OUTPUT_BUFFER` like any
|
|
939
|
+
* > sink-installed buffer.
|
|
940
|
+
*
|
|
941
|
+
* That is what this file builds, so neither generated code nor a hand-written
|
|
942
|
+
* one-shot encode has to write it again. The scratch and the result are the same
|
|
943
|
+
* storage: the encoder is installed over the free tail of a buffer this module
|
|
944
|
+
* owns, and when it fills, the flush callback fires, this module enlarges its
|
|
945
|
+
* storage and installs the next tail with {@link OStream.setBuffer} — §5.1.5's
|
|
946
|
+
* taking sink, and §6.6.1's second row, a helper reached from inside a callback
|
|
947
|
+
* the codec made. Because the encoder was writing *into the result all along*,
|
|
948
|
+
* absorbing a flush is one assignment: there is no per-flush copy.
|
|
949
|
+
*
|
|
950
|
+
* The predecessor was a `BufferOwner` hook: the encoder called it from
|
|
951
|
+
* `ensureSome`, i.e. from every `write*`, and kept the buffer it returned. That
|
|
952
|
+
* is §6.6's second violation row — "the codec allocates nothing itself but
|
|
953
|
+
* requires a growable destination and grows it … it moved the allocator call one
|
|
954
|
+
* type away, where a source-level audit no longer sees it" — and the hook is gone
|
|
955
|
+
* with it (A2-0159).
|
|
956
|
+
*
|
|
957
|
+
* **No subclass, deliberately.** An `OStream` subclass overriding `bytes()` was
|
|
958
|
+
* the obvious shape and cost 2.3x on `encode: composite`: a program that uses
|
|
959
|
+
* both `growingOStream()` and `new OStream(buf)` would then have two receiver
|
|
960
|
+
* maps at every `os.write*` call site, and V8 stops inlining a polymorphic one.
|
|
961
|
+
* `setBuffer`'s `carried` argument buys the same behaviour with one map and one
|
|
962
|
+
* extra number in the encoder, and a stateless module-level sink keeps the
|
|
963
|
+
* per-stream cost at exactly what it was: one buffer.
|
|
964
|
+
*/
|
|
965
|
+
|
|
966
|
+
/**
|
|
967
|
+
* An {@link OStream} whose **buffer follows the message** — the ready-made form
|
|
968
|
+
* of the caller CORELIB_PLAN §5.1.2 puts the allocation in.
|
|
969
|
+
*
|
|
970
|
+
* It is the one-liner for the 90% case, where the message comfortably fits in
|
|
971
|
+
* memory:
|
|
972
|
+
*
|
|
973
|
+
* ```ts
|
|
974
|
+
* const os = growingOStream();
|
|
975
|
+
* os.writeUnsigned(1, 42);
|
|
976
|
+
* const wire = os.bytes(); // the whole message, as a view valid until the next write
|
|
977
|
+
* ```
|
|
978
|
+
*
|
|
979
|
+
* {@link OStream.bytes} is therefore the **whole** message here rather than a
|
|
980
|
+
* not-yet-flushed tail, and no write reports `BUFFER_FULL`. It is an ordinary
|
|
981
|
+
* streaming stream in every other respect: {@link OStream.setBuffer} works and
|
|
982
|
+
* means what it always means — the not-yet-flushed bytes in the old buffer are
|
|
983
|
+
* dropped and encoding continues into yours, which the accumulator then grows in
|
|
984
|
+
* turn.
|
|
985
|
+
*
|
|
986
|
+
* @param initialCapacity bytes to start from; the accumulator enlarges itself
|
|
987
|
+
* whenever the message outgrows it, so this only trades an initial allocation
|
|
988
|
+
* against the number of enlargements and never limits the message. A caller with
|
|
989
|
+
* a rough size in hand should pass it: 100 KB of *small fields* grows by doubling
|
|
990
|
+
* and costs nine enlargements from the 256-byte default. A single large field
|
|
991
|
+
* does not — a bulk write hands the sink its `needed`, so the buffer reaches that
|
|
992
|
+
* size in one step and the write keeps its bulk route.
|
|
993
|
+
*/
|
|
994
|
+
declare function growingOStream(initialCapacity?: number): OStream;
|
|
995
|
+
|
|
996
|
+
/**
|
|
997
|
+
* The SofaBuffers decoder: `IStream`, and the visitor it drives.
|
|
998
|
+
*
|
|
999
|
+
* The **visitor is the only decode surface** (CORELIB_PLAN §5.3.1). There is no
|
|
1000
|
+
* pull parser, no iterator, no cursor and no convenience wrapper that decodes by
|
|
1001
|
+
* another route: a second surface is a second implementation of every rule in the
|
|
1002
|
+
* spec, and the divergences that produces are invisible to the shared vectors,
|
|
1003
|
+
* which exercise whichever surface the driver happened to pick.
|
|
1004
|
+
*
|
|
1005
|
+
* `IStream` is a push parser: bind a {@link Visitor} at construction, feed bytes
|
|
1006
|
+
* with {@link IStream.feed}, and it calls one method per decoded field. It is a
|
|
1007
|
+
* resumable state machine, so the chunks can be any size — a whole message, a
|
|
1008
|
+
* network packet, or a single byte — and a field that straddles a chunk boundary
|
|
1009
|
+
* is picked up seamlessly on the next call.
|
|
1010
|
+
*
|
|
1011
|
+
* The visitor is **flat**: one object receives the whole message, and nesting is
|
|
1012
|
+
* reported as {@link Visitor.sequenceBegin} / {@link Visitor.sequenceEnd} events
|
|
1013
|
+
* carrying the sequence's id and depth. A visitor per nested scope would make
|
|
1014
|
+
* every dispatch site here megamorphic — one hidden class per generated message
|
|
1015
|
+
* class in the tree — and would put a per-scope object on the decode path; one
|
|
1016
|
+
* flat visitor keeps the call sites (bi)morphic and the decoder allocation-free
|
|
1017
|
+
* (§6.6). Descending and skipping are unchanged by that choice: `sequenceBegin`
|
|
1018
|
+
* answers `false` to decline a subtree whole, and a field whose callback the
|
|
1019
|
+
* visitor does not implement is skipped.
|
|
1020
|
+
*
|
|
1021
|
+
* There is no finish / finalize step (§5.2.4) and no status accessor beside the
|
|
1022
|
+
* call: {@link IStream.feed} *returns* the outcome for the bytes consumed so far,
|
|
1023
|
+
* and that return value is the whole answer. A message that merely ends inside a
|
|
1024
|
+
* field is reported — never thrown — as {@link DecodeStatus.Incomplete}; only a
|
|
1025
|
+
* *malformed* message throws ({@link SofabErrorCode.InvalidMsg}), which is this
|
|
1026
|
+
* port's channel for {@link DecodeStatus.Invalid}. The caller owns end-of-input
|
|
1027
|
+
* and decides whether a trailing `Incomplete` is a truncation error.
|
|
1028
|
+
*
|
|
1029
|
+
* **One fact, one channel.** Each outcome leaves by exactly one route — two of
|
|
1030
|
+
* the three by the return value, the refusals by the throw — because a second way
|
|
1031
|
+
* to ask the same question is a second thing to keep in step, and this family
|
|
1032
|
+
* shipped the drift: a `status()` accessor answered `COMPLETE` for a message
|
|
1033
|
+
* `feed` had already refused. §5.3.1 makes the general form of the argument for
|
|
1034
|
+
* decode surfaces ("every additional surface is a second implementation of every
|
|
1035
|
+
* rule in this document"); the accessor was the same mistake one size down.
|
|
1036
|
+
*/
|
|
1037
|
+
|
|
1038
|
+
/**
|
|
1039
|
+
* Where an array's elements are written when the visitor takes the **bulk
|
|
1040
|
+
* hand-off** ({@link Visitor.arrayBulk}): the destination it already owns, handed
|
|
1041
|
+
* over once, instead of one callback per element.
|
|
1042
|
+
*
|
|
1043
|
+
* **Why this replaced the per-element callbacks.** There used to be an
|
|
1044
|
+
* `arrayUnsigned` / `arraySigned` / `arrayFp32` / `arrayFp64` beside this, one call
|
|
1045
|
+
* per element. Measured with `bench/run_callgrind.sh`'s method over 1000-element
|
|
1046
|
+
* arrays (Ir/op for a message that is one array), against this hand-off filling
|
|
1047
|
+
* the same destination:
|
|
1048
|
+
*
|
|
1049
|
+
* | array | per element (removed) | hand-off | |
|
|
1050
|
+
* |---|---:|---:|---:|
|
|
1051
|
+
* | `array<u16>` into a `number[]` | 235 433 | 184 025 | −21.8% |
|
|
1052
|
+
* | `array<u64>` into a `Long[]` | 576 206 | 429 113 | −25.5% |
|
|
1053
|
+
* | `array<u64>` into {@link IntegerArrayTarget.lo}/`hi` | 498 618 | 299 180 | −40.0% |
|
|
1054
|
+
* | `array<fp64>` into a `Float64Array` | 93 520 | 36 890 | −60.6% |
|
|
1055
|
+
* | `array<fp32>` into a `Float32Array` | 94 497 | 35 259 | −62.7% |
|
|
1056
|
+
*
|
|
1057
|
+
* Floats gain most because their *reading* was already bulk (the §6.6.2 handle in
|
|
1058
|
+
* the element drain), so the callback was very nearly all that was left.
|
|
1059
|
+
*
|
|
1060
|
+
* **A short array is not the exception.** The fixed cost — the offer, the
|
|
1061
|
+
* target's resolution, the bound's validation — is about 600 Ir, but the hand-off
|
|
1062
|
+
* takes a whole array in one call, tail elements included, so it is paid once:
|
|
1063
|
+
* four elements at the end of a 37-byte message cost 578 Ir against the 563 the
|
|
1064
|
+
* removed callbacks cost, and everything longer is the table above.
|
|
1065
|
+
*
|
|
1066
|
+
* **What did move is the consumer's side, for a consumer that folds.** An element
|
|
1067
|
+
* callback let a reader sum, hash or convert *inside* the delivery; reading a
|
|
1068
|
+
* filled destination is a second pass. A reader that wants the values where they
|
|
1069
|
+
* are — which is what generated code wants — has no second pass and gets the
|
|
1070
|
+
* table. A reader that folds pays one: the same four-element array costs 1 063 Ir
|
|
1071
|
+
* if it sums the destination at `arrayEnd`, against 563 folding per element.
|
|
1072
|
+
* It is a cheaper pass than the calls it replaced on any array long enough to
|
|
1073
|
+
* matter, and on a four-element one it is not.
|
|
1074
|
+
*
|
|
1075
|
+
* **Declining costs nothing at all.** An array no visitor takes is walked over
|
|
1076
|
+
* rather than decoded: `array<fp64>` 36 890 → 9 383 Ir/op, `array<u16>` 184 025 →
|
|
1077
|
+
* 152 082.
|
|
1078
|
+
*
|
|
1079
|
+
* **This is how array elements are delivered — the only way.** There is no
|
|
1080
|
+
* callback per element beside it: §5.3.1 allows a rule one implementation, and
|
|
1081
|
+
* two delivery routes for the same elements were two places for the element bound
|
|
1082
|
+
* to be compared, two resume paths to keep in step, and a standing invitation for
|
|
1083
|
+
* generated code to take the slower one. `arrayBegin` and `arrayEnd` still fire
|
|
1084
|
+
* for every array; returning `null` declines delivery, and the elements are then
|
|
1085
|
+
* walked over without being decoded into existence — the `skip` half of §6.7.2's
|
|
1086
|
+
* two intents, which is what a visitor that declares no `arrayBulk` gets for
|
|
1087
|
+
* every array.
|
|
1088
|
+
*
|
|
1089
|
+
* **The destination is filled ascending from index 0**, one write per element,
|
|
1090
|
+
* and it must stay valid — same object, same length — until `arrayEnd`, which for
|
|
1091
|
+
* a chunked decode is several {@link IStream.feed} calls later. A plain-array
|
|
1092
|
+
* destination (`values` / `longs`) is **cut to the elements written when the array
|
|
1093
|
+
* ends** — including to zero for an array that is empty on the wire, and to the
|
|
1094
|
+
* prefix when an element is refused — so reusing one across arrays or messages can
|
|
1095
|
+
* never leave the previous array's tail behind, and after `arrayEnd` its `length`
|
|
1096
|
+
* is exactly this array's element count. (Cutting at the end rather than emptying
|
|
1097
|
+
* up front is measured: emptying first makes every element write a grow, which
|
|
1098
|
+
* cost `array<u16>` 181 583 → 231 815 Ir/op.) A typed destination is neither cut
|
|
1099
|
+
* nor emptied — it cannot be — and must already hold `count` elements.
|
|
1100
|
+
*
|
|
1101
|
+
* A decode that fails *inside* an array for any other reason — malformed bytes, or
|
|
1102
|
+
* input that simply ends — leaves the destination holding what had been written
|
|
1103
|
+
* when it stopped. `length` is a statement about the array only once `arrayEnd`
|
|
1104
|
+
* has been raised or an element has been refused. This is the one
|
|
1105
|
+
* place the codec holds a reference to the caller's storage between calls (§6.6
|
|
1106
|
+
* otherwise holds nothing past a callback), and the reference is dropped at
|
|
1107
|
+
* `arrayEnd` and whenever a pooled machine is released.
|
|
1108
|
+
*
|
|
1109
|
+
* **A refused element leaves the destination holding the prefix**: everything
|
|
1110
|
+
* before it is written, the offending element and everything after it is not, and
|
|
1111
|
+
* a plain-array destination is cut to exactly that length. Like every `INVALID`
|
|
1112
|
+
* verdict it is terminal (§5.2.1).
|
|
1113
|
+
*/
|
|
1114
|
+
type ArrayTarget = IntegerArrayTarget | FloatArrayTarget | BoolArrayTarget;
|
|
1115
|
+
/**
|
|
1116
|
+
* A `boolean` array's destination: one byte per element, `0` or `1`.
|
|
1117
|
+
*
|
|
1118
|
+
* It carries **no bound**, and that is the whole reason it is a shape of its own
|
|
1119
|
+
* rather than a {@link IntegerArrayTarget.typed} `Uint8Array`. §4.4 gives a boolean
|
|
1120
|
+
* no width bound — every non-zero wire value is `true` — so there is no interval to
|
|
1121
|
+
* state and nothing for the width check to compare. A raw store would be wrong
|
|
1122
|
+
* twice over: 256 would mask to `0` and turn `true` into `false`. The decoder
|
|
1123
|
+
* NORMALIZES instead, writing `1` for any non-zero, which is also the only value
|
|
1124
|
+
* §4.4 lets an encoder emit back — so a filled destination re-encodes canonically
|
|
1125
|
+
* with no conversion step in between.
|
|
1126
|
+
*/
|
|
1127
|
+
interface BoolArrayTarget {
|
|
1128
|
+
bool: Uint8Array;
|
|
1129
|
+
}
|
|
1130
|
+
/**
|
|
1131
|
+
* An integer array's destination and the **element bound** to enforce while
|
|
1132
|
+
* filling it — for an `ArrayKind.Unsigned` or `ArrayKind.Signed` array.
|
|
338
1133
|
*
|
|
339
|
-
*
|
|
340
|
-
*
|
|
341
|
-
*
|
|
342
|
-
* to cap memory against a hostile, oversized field. {@link DecodeLimits} is that
|
|
343
|
-
* cap: an optional options object accepted by every decode entry point
|
|
344
|
-
* ({@link decode}, the {@link IStream} constructor, and the {@link Cursor}
|
|
345
|
-
* constructor).
|
|
1134
|
+
* Exactly one destination must be set (`values`, `longs`, or `lo` *and* `hi`);
|
|
1135
|
+
* any other combination is a caller mistake and is refused with
|
|
1136
|
+
* {@link SofabErrorCode.Argument} before a single element is written.
|
|
346
1137
|
*
|
|
347
|
-
* The
|
|
348
|
-
*
|
|
349
|
-
*
|
|
350
|
-
*
|
|
351
|
-
*
|
|
352
|
-
*
|
|
1138
|
+
* **The bound is four halves, and it is never optional.** The interval is the
|
|
1139
|
+
* schema's — `0..65535` for a `u16`, `-2^63..2^63-1` for an `i64` — so its
|
|
1140
|
+
* violation is `INVALID`, a statement about the message (§6.2.1 keeps a
|
|
1141
|
+
* *receiver* cap off a field the schema already bounds; a receiver cap on the
|
|
1142
|
+
* element *count* is compared by the visitor in `arrayBegin`, as before, and this
|
|
1143
|
+
* hand-off happens after it).
|
|
353
1144
|
*
|
|
354
|
-
*
|
|
355
|
-
*
|
|
356
|
-
*
|
|
357
|
-
*
|
|
358
|
-
*
|
|
359
|
-
*
|
|
1145
|
+
* Halves rather than a `number` pair because a `number` cannot express the
|
|
1146
|
+
* 64-bit domain: `u64`'s bound is `2^64-1` and `Number.MAX_SAFE_INTEGER` is
|
|
1147
|
+
* `2^53-1`, so a `number` bound both *rejects valid messages* at the top of the
|
|
1148
|
+
* range and makes the comparison a mixed `bigint`/`number` one, which measured
|
|
1149
|
+
* +35% on `array<u64>` — worse than the per-element path it replaces. Two
|
|
1150
|
+
* unsigned 32-bit comparisons are exact and cost nothing.
|
|
1151
|
+
*
|
|
1152
|
+
* For a **signed** array the halves are the two's-complement ones (an `i16`
|
|
1153
|
+
* minimum of `-32768` is `minLo = 0xffff8000`, `minHi = 0xffffffff`).
|
|
1154
|
+
* {@link Long.fromBigInt} is the ready-made way to compute them from the schema's
|
|
1155
|
+
* bound once.
|
|
1156
|
+
*
|
|
1157
|
+
* The codec **compares** the bound; it never owns one (§6.2.1). There is no
|
|
1158
|
+
* default and no "unbounded" spelling, because an integer element always has a
|
|
1159
|
+
* declared width: the widest `u64` interval is `0 .. 0xffffffff_ffffffff` and is
|
|
1160
|
+
* stated as such.
|
|
360
1161
|
*/
|
|
361
|
-
interface
|
|
1162
|
+
interface IntegerArrayTarget {
|
|
362
1163
|
/**
|
|
363
|
-
*
|
|
364
|
-
*
|
|
1164
|
+
* Number-first destination: each element as the `value` its per-element
|
|
1165
|
+
* callback would have received — a `number` when it fits exactly (`≤ 2^53-1`,
|
|
1166
|
+
* every `u8`..`u32` and small 64-bit values) and a `bigint` beyond that. The
|
|
1167
|
+
* right destination for `u8`..`u32` / `i8`..`i32` arrays, where no `bigint` is
|
|
1168
|
+
* ever built.
|
|
1169
|
+
*
|
|
1170
|
+
* A real `Array` — grown as it fills and cut to length when the array ends, so
|
|
1171
|
+
* it need not be pre-sized and never carries a previous array's tail. A typed
|
|
1172
|
+
* array here is refused with {@link SofabErrorCode.Argument}: it would take the
|
|
1173
|
+
* writes and silently drop everything past its own length.
|
|
365
1174
|
*/
|
|
366
|
-
|
|
1175
|
+
values?: (number | bigint)[];
|
|
367
1176
|
/**
|
|
368
|
-
*
|
|
369
|
-
*
|
|
1177
|
+
* Destination taking each element as a {@link Long} — no `bigint` is
|
|
1178
|
+
* materialised at all, which is most of why this is a quarter cheaper than the
|
|
1179
|
+
* per-element path for a 64-bit array (see {@link ArrayTarget}). A real
|
|
1180
|
+
* `Array`, cut to length at the array's end, exactly like {@link values}.
|
|
370
1181
|
*/
|
|
371
|
-
|
|
1182
|
+
longs?: Long[];
|
|
372
1183
|
/**
|
|
373
|
-
*
|
|
374
|
-
*
|
|
1184
|
+
* Allocation-free destination: the element's low half at `lo[index]` and its
|
|
1185
|
+
* high half at `hi[index]` (for a signed array, the two's-complement halves).
|
|
1186
|
+
* Both must be set and both must hold at least `count` elements. The fastest
|
|
1187
|
+
* shape there is — nothing is allocated per element, not even a `Long`.
|
|
375
1188
|
*/
|
|
376
|
-
|
|
1189
|
+
/**
|
|
1190
|
+
* Exact-width destination: one typed array whose element width IS the schema's
|
|
1191
|
+
* declared width — a `Uint16Array` for a `u16` array, an `Int8Array` for an
|
|
1192
|
+
* `i8` one. Must hold at least `count` elements.
|
|
1193
|
+
*
|
|
1194
|
+
* **The bound is still compared, and that is not negotiable.** A typed array
|
|
1195
|
+
* *masks* on store (`a[0] = 70000` in a `Uint16Array` is 4464), and
|
|
1196
|
+
* MESSAGE_SPEC §7.1 makes an element outside the declared width INVALID —
|
|
1197
|
+
* neither masked to the width nor kept. So this destination buys the storage,
|
|
1198
|
+
* never the verdict: the fill loop compares exactly as {@link values} does and
|
|
1199
|
+
* refuses the same elements.
|
|
1200
|
+
*
|
|
1201
|
+
* What it does buy is the store (unboxed, and no element-kind transition the
|
|
1202
|
+
* way a `number[]` takes when a value leaves the small-integer range), the
|
|
1203
|
+
* memory (2 bytes for a `u16`, against a tagged slot), and the *encoder's*
|
|
1204
|
+
* side, where the width is then statically known.
|
|
1205
|
+
*
|
|
1206
|
+
* The bound must fit the array's own width: a destination narrower than the
|
|
1207
|
+
* interval could not represent every legal element, and is refused with
|
|
1208
|
+
* {@link SofabErrorCode.Argument} rather than silently masking.
|
|
1209
|
+
*/
|
|
1210
|
+
typed?: Uint8Array | Uint16Array | Uint32Array | Int8Array | Int16Array | Int32Array | BigUint64Array | BigInt64Array;
|
|
1211
|
+
lo?: Uint32Array;
|
|
1212
|
+
/** The high halves; see {@link lo}. */
|
|
1213
|
+
hi?: Uint32Array;
|
|
1214
|
+
/** Low half of the smallest element the schema allows. */
|
|
1215
|
+
minLo: number;
|
|
1216
|
+
/** High half of the smallest element the schema allows. */
|
|
1217
|
+
minHi: number;
|
|
1218
|
+
/** Low half of the largest element the schema allows. */
|
|
1219
|
+
maxLo: number;
|
|
1220
|
+
/** High half of the largest element the schema allows. */
|
|
1221
|
+
maxHi: number;
|
|
377
1222
|
}
|
|
378
|
-
|
|
379
1223
|
/**
|
|
380
|
-
*
|
|
1224
|
+
* A float array's destination — for an `ArrayKind.Fp32` or `ArrayKind.Fp64`
|
|
1225
|
+
* array. Exactly one of the two must be set, it must match the element width the
|
|
1226
|
+
* array's own `fixlen_word` declared (§4.8), and it must hold at least `count`
|
|
1227
|
+
* elements; anything else is refused with {@link SofabErrorCode.Argument} before
|
|
1228
|
+
* a single element is written.
|
|
381
1229
|
*
|
|
382
|
-
*
|
|
383
|
-
*
|
|
384
|
-
* resumable state machine, so the chunks you feed can be any size — a whole
|
|
385
|
-
* message, a network packet, or a single byte — and a field that straddles a
|
|
386
|
-
* chunk boundary is picked up seamlessly on the next call.
|
|
1230
|
+
* There is no bound here: §4.6 gives a float no schema interval to violate, so
|
|
1231
|
+
* there is nothing to compare and nothing for the caller to state.
|
|
387
1232
|
*
|
|
388
|
-
*
|
|
389
|
-
*
|
|
390
|
-
*
|
|
391
|
-
* `
|
|
392
|
-
*
|
|
393
|
-
*
|
|
394
|
-
* throws only for a *malformed* message ({@link SofabErrorCode.InvalidMsg}); a
|
|
395
|
-
* message that merely ends inside a field is reported — never thrown — by
|
|
396
|
-
* {@link IStream.end}, which returns {@link DecodeStatus.Incomplete} rather than
|
|
397
|
-
* {@link DecodeStatus.Complete}. The caller owns end-of-input and decides
|
|
398
|
-
* whether a trailing `Incomplete` is a truncation error.
|
|
1233
|
+
* An `fp32` array chooses between the two: {@link f32} takes values, {@link bits}
|
|
1234
|
+
* takes the wire words. The choice is real, not stylistic — reading an `fp32`
|
|
1235
|
+
* through a double quiets a *signaling* NaN (`0x7fa00001` comes back
|
|
1236
|
+
* `0x7fe00001`), so a reader that must reproduce `fp32` payloads bit-for-bit
|
|
1237
|
+
* (§4.6/§6.5) takes {@link bits}. `fp64` needs no such choice: a `Float64Array`
|
|
1238
|
+
* carries all 64 bits, payload NaNs included.
|
|
399
1239
|
*/
|
|
400
|
-
|
|
1240
|
+
interface FloatArrayTarget {
|
|
1241
|
+
/** Value destination for an `ArrayKind.Fp32` array. */
|
|
1242
|
+
f32?: Float32Array;
|
|
1243
|
+
/**
|
|
1244
|
+
* Bit destination for an `ArrayKind.Fp32` array: each element as its 4 wire
|
|
1245
|
+
* bytes in one little-endian 32-bit word — the same number `OStream.writeFp32Bits`
|
|
1246
|
+
* takes, so a payload read through this and written back reproduces the wire
|
|
1247
|
+
* exactly, signaling NaNs included.
|
|
1248
|
+
*/
|
|
1249
|
+
bits?: Uint32Array;
|
|
1250
|
+
/** Destination for an `ArrayKind.Fp64` array. */
|
|
1251
|
+
f64?: Float64Array;
|
|
1252
|
+
}
|
|
401
1253
|
/**
|
|
402
|
-
* Receives decoded fields from an {@link IStream}
|
|
403
|
-
* defaults to a no-op, so a visitor
|
|
404
|
-
* and silently skips the rest
|
|
1254
|
+
* Receives decoded fields from an {@link IStream} — the one decode surface
|
|
1255
|
+
* (§5.3.1). Every method is optional and defaults to a no-op, so a visitor
|
|
1256
|
+
* implements only the fields it cares about and silently skips the rest, which is
|
|
1257
|
+
* the `skip` half of the two per-field intents §6.7.2 allows (the other being
|
|
1258
|
+
* `read`: take the value, in the call).
|
|
405
1259
|
*
|
|
406
|
-
*
|
|
407
|
-
*
|
|
408
|
-
*
|
|
409
|
-
*
|
|
1260
|
+
* **One visitor per message, not per scope.** Nested sequences arrive as
|
|
1261
|
+
* {@link sequenceBegin} / {@link sequenceEnd} events on this same object, each
|
|
1262
|
+
* carrying the sequence's `id` and its `depth` (1 for a sequence opened at the
|
|
1263
|
+
* root). Generated code routes on those two numbers, which it knows statically
|
|
1264
|
+
* from the schema.
|
|
1265
|
+
*
|
|
1266
|
+
* **Nothing handed to a visitor outlives the call.** A `string` / `blob` payload
|
|
1267
|
+
* is reported in pieces as a range of the caller's *own* fed chunk (§6.6.3): the
|
|
1268
|
+
* decoder creates no view over it and holds no storage of its own (§6.6, §6.7),
|
|
1269
|
+
* so a consumer that wants the value copies it out — during the call — into
|
|
1270
|
+
* storage it owns. {@link PayloadAcc} and {@link decodeUtf8} are the ready-made
|
|
1271
|
+
* way to do that.
|
|
410
1272
|
*/
|
|
411
1273
|
interface Visitor {
|
|
412
1274
|
/**
|
|
413
|
-
*
|
|
414
|
-
*
|
|
415
|
-
*
|
|
1275
|
+
* A field **header**: its `id` and `wire` type, announced the moment the header
|
|
1276
|
+
* varint is complete — before the value, and before the value's own header word
|
|
1277
|
+
* (a fixlen length word, an array count word, the fields of a nested sequence).
|
|
1278
|
+
*
|
|
1279
|
+
* An observation point for a reader that wants the field stream as it arrives —
|
|
1280
|
+
* which id, in which scope, in which order — without implementing the value
|
|
1281
|
+
* callbacks it would otherwise take to see the same thing.
|
|
1282
|
+
*
|
|
1283
|
+
* **A schema bound does not belong here.** The header settles `id` and `wire`,
|
|
1284
|
+
* and nothing else. An element id past the schema `count` (MESSAGE_SPEC
|
|
1285
|
+
* §7.1/§5.1) looks decidable from the id alone, and it is not: §7.3 applies that
|
|
1286
|
+
* bound only to a field whose *subtype* has confirmed it is the declared one,
|
|
1287
|
+
* and a contradicting subtype is skipped rather than rejected. The subtype
|
|
1288
|
+
* arrives in the fixlen word, so the verdict is due at {@link fixlenBegin}.
|
|
1289
|
+
* CORELIB_PLAN §4.1.1 makes the timing normative: a message ending inside that
|
|
1290
|
+
* word is `INCOMPLETE` even when the id would violate a schema bound, because
|
|
1291
|
+
* the low 3 bits of an unfinished varint must not influence an outcome even
|
|
1292
|
+
* though they are already arithmetically fixed.
|
|
1293
|
+
*
|
|
1294
|
+
* Called exactly once per field, in every scope, for every wire type — the
|
|
1295
|
+
* sequence *end* marker excepted: it closes a scope rather than opening a field
|
|
1296
|
+
* and its id is discarded (§4.9). For a nested sequence it fires before
|
|
1297
|
+
* {@link sequenceBegin}.
|
|
1298
|
+
*
|
|
1299
|
+
* Throwing from it rejects the field — for a verdict the header really does
|
|
1300
|
+
* settle on its own, such as an id this reader will not accept in any shape.
|
|
1301
|
+
*/
|
|
1302
|
+
fieldBegin?(id: number, wire: WireType): void;
|
|
1303
|
+
/**
|
|
1304
|
+
* An unsigned integer field.
|
|
1305
|
+
*
|
|
1306
|
+
* `value` is number-first: a `number` when the value fits exactly
|
|
1307
|
+
* (`≤ 2^53-1`, covering ids, u8..u32 and small u64s) and a `bigint` only beyond
|
|
1308
|
+
* that. `lo` / `hi` are the exact 64 bits as two unsigned 32-bit halves — the
|
|
1309
|
+
* ones the varint reader already holds — for a consumer that wants the value
|
|
1310
|
+
* bit-exactly without going through `bigint` arithmetic ({@link Long.fromBits}
|
|
1311
|
+
* builds a `Long` from them). Both describe the same value; use whichever fits.
|
|
1312
|
+
*/
|
|
1313
|
+
unsigned?(id: number, value: number | bigint, lo: number, hi: number): void;
|
|
1314
|
+
/**
|
|
1315
|
+
* A signed integer field. `value` is number-first like {@link unsigned}
|
|
1316
|
+
* (`|value| ≤ 2^53-1` ⇒ `number`); `lo` / `hi` are the **decoded**
|
|
1317
|
+
* (zig-zag-undone) two's-complement halves.
|
|
1318
|
+
*/
|
|
1319
|
+
signed?(id: number, value: number | bigint, lo: number, hi: number): void;
|
|
1320
|
+
/**
|
|
1321
|
+
* An IEEE-754 32-bit float field.
|
|
1322
|
+
*
|
|
1323
|
+
* `value` is a JS `number` — a 64-bit double — and widening a *signaling* NaN
|
|
1324
|
+
* into a double quiets it (sets the is-quiet bit), so `value` cannot represent
|
|
1325
|
+
* an fp32 sNaN faithfully. `bits` is the exact 4 wire bytes as one little-endian
|
|
1326
|
+
* 32-bit word, which can: re-encode from it with
|
|
1327
|
+
* {@link OStream.writeFp32Bits} and the payload round-trips bit-for-bit
|
|
1328
|
+
* (§4.6/§6.5). It is the "32-bit bits accessor" §6.5 names, and it is always
|
|
1329
|
+
* present — a number costs nothing to pass and needs no opt-in flag, where the
|
|
1330
|
+
* byte view it replaces was an allocation per value and a borrowed slice §6.7
|
|
1331
|
+
* forbids.
|
|
416
1332
|
*/
|
|
417
|
-
|
|
418
|
-
/**
|
|
419
|
-
signed?(id: number, value: number | bigint): void;
|
|
420
|
-
/** An IEEE-754 32-bit float field. */
|
|
421
|
-
fp32?(id: number, value: number): void;
|
|
422
|
-
/** An IEEE-754 64-bit double field. */
|
|
1333
|
+
fp32?(id: number, value: number, bits: number): void;
|
|
1334
|
+
/** An IEEE-754 64-bit double field. `value` is exact — a double is 64 bits wide. */
|
|
423
1335
|
fp64?(id: number, value: number): void;
|
|
424
|
-
/**
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
1336
|
+
/**
|
|
1337
|
+
* Start of a `string`/`blob` field: `total` payload bytes follow, in one or more
|
|
1338
|
+
* {@link string}/{@link blob} calls.
|
|
1339
|
+
*
|
|
1340
|
+
* The counterpart of {@link arrayBegin}, and it exists for the same reason: a
|
|
1341
|
+
* receiver-side bound on the *declared length* is decided by this word, not by
|
|
1342
|
+
* the payload. Without it a visitor could only see `total` once payload bytes
|
|
1343
|
+
* arrive, so a message that ends right after an over-bound length word would
|
|
1344
|
+
* escape the check and degrade to `INCOMPLETE`, where §5.2.3 requires `INVALID`.
|
|
1345
|
+
*
|
|
1346
|
+
* Called exactly once per field, before any payload call — including for a
|
|
1347
|
+
* zero-length payload, which is still announced here and then delivered as one
|
|
1348
|
+
* empty range.
|
|
1349
|
+
*/
|
|
1350
|
+
fixlenBegin?(id: number, subtype: FixlenSubtype, total: number): void;
|
|
1351
|
+
/**
|
|
1352
|
+
* A piece of a UTF-8 string field: the bytes `src[start..end)`, at `offset` of
|
|
1353
|
+
* a `total`-byte payload.
|
|
1354
|
+
*
|
|
1355
|
+
* `src` is the **caller's own chunk** — the exact array passed to
|
|
1356
|
+
* {@link IStream.feed} (or to {@link decode}) — handed back with the piece's
|
|
1357
|
+
* coordinates (§6.6.3). The decoder builds no view over it, keeps no storage,
|
|
1358
|
+
* and hands out no borrowed slice of its own (§6.6, §6.7). Once `feed` returns,
|
|
1359
|
+
* the caller may reuse that memory, so a consumer that wants the value copies it
|
|
1360
|
+
* out **during the call**: {@link PayloadAcc} joins pieces into a buffer it
|
|
1361
|
+
* owns, and {@link decodeUtf8} turns a range straight into a string.
|
|
1362
|
+
*
|
|
1363
|
+
* The bytes are **not validated**. §6.4.5 puts the UTF-8 check where a string is
|
|
1364
|
+
* *materialized* — a piece may end mid-code-point, and a skipped field is never
|
|
1365
|
+
* validated at all — so on this surface the caller who materializes owns the
|
|
1366
|
+
* check. {@link decodeUtf8} is it, and a hand-rolled one must be built **fatal**
|
|
1367
|
+
* (`new TextDecoder("utf-8", { fatal: true })`): JavaScript's default
|
|
1368
|
+
* `TextDecoder` substitutes `U+FFFD`, which §6.4 forbids in either direction.
|
|
1369
|
+
*/
|
|
1370
|
+
string?(id: number, total: number, offset: number, src: Uint8Array, start: number, end: number): void;
|
|
1371
|
+
/** A piece of a blob field — see {@link string} for the `src`/`start`/`end` contract. */
|
|
1372
|
+
blob?(id: number, total: number, offset: number, src: Uint8Array, start: number, end: number): void;
|
|
428
1373
|
/** Start of an array; `count` elements of `kind` follow. */
|
|
429
1374
|
arrayBegin?(id: number, kind: ArrayKind, count: number): void;
|
|
430
|
-
/**
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
1375
|
+
/**
|
|
1376
|
+
* Offer of the **bulk hand-off**: return the destination this visitor has
|
|
1377
|
+
* already allocated for array `id` and the decoder fills it directly, one write
|
|
1378
|
+
* per element and no callback at all; return `null` (or leave this method
|
|
1379
|
+
* unimplemented) to be served element by element as before.
|
|
1380
|
+
*
|
|
1381
|
+
* Called once per array, after {@link arrayBegin} and before the first element —
|
|
1382
|
+
* so a receiver cap on `count` is still compared where it always was, in
|
|
1383
|
+
* `arrayBegin`, and a rejected array is never offered. **An array that is empty
|
|
1384
|
+
* on the wire is offered too**, with `count` of 0: there is nothing to write,
|
|
1385
|
+
* but a destination held across fields would otherwise still hold the previous
|
|
1386
|
+
* array's elements, and its length is the only place this array's emptiness
|
|
1387
|
+
* could show.
|
|
1388
|
+
*
|
|
1389
|
+
* `kind` is the element kind and it decides which destination is legal
|
|
1390
|
+
* ({@link IntegerArrayTarget} for `Unsigned` / `Signed`,
|
|
1391
|
+
* {@link FloatArrayTarget} for `Fp32` / `Fp64`); a destination that contradicts
|
|
1392
|
+
* it, or that is shorter than `count`, is a caller mistake and is refused with
|
|
1393
|
+
* {@link SofabErrorCode.Argument} before any element is written. Declining on
|
|
1394
|
+
* a kind this visitor did not expect is always available — and is the right
|
|
1395
|
+
* answer, since `null` costs nothing but the call.
|
|
1396
|
+
*
|
|
1397
|
+
* See {@link ArrayTarget} for what the decoder then guarantees: ascending
|
|
1398
|
+
* writes, the element bound enforced here and only here, the destination held
|
|
1399
|
+
* until `arrayEnd` across as many `feed` calls as the chunking takes, and a
|
|
1400
|
+
* partially filled destination if an element is refused.
|
|
1401
|
+
*/
|
|
1402
|
+
arrayBulk?(id: number, kind: ArrayKind, count: number): ArrayTarget | null;
|
|
438
1403
|
/** End of an array. */
|
|
439
1404
|
arrayEnd?(id: number): void;
|
|
440
1405
|
/**
|
|
441
|
-
* Start of a nested sequence
|
|
442
|
-
*
|
|
443
|
-
*
|
|
1406
|
+
* Start of a nested sequence — a fresh id scope (§4.9) — opened by field `id`
|
|
1407
|
+
* at `depth` (1 at the root).
|
|
1408
|
+
*
|
|
1409
|
+
* Return **`false`** to decline the whole subtree: no callback of any kind fires
|
|
1410
|
+
* inside it, nesting included, its own {@link sequenceEnd} included, and a scope
|
|
1411
|
+
* opened within it is never offered either. Return anything else (or nothing) to
|
|
1412
|
+
* descend, and the nested fields arrive on this same visitor with their own ids
|
|
1413
|
+
* and `depth + 1`.
|
|
1414
|
+
*
|
|
1415
|
+
* A declined subtree is still *parsed* — a sequence is framed by markers, not by
|
|
1416
|
+
* a length, so its end has to be found — but nothing in it is decoded into
|
|
1417
|
+
* existence: no piece is reported and no value is built. No receiver cap fires
|
|
1418
|
+
* inside one either (§6.2.1's "a skipped field is never capped"), and that falls
|
|
1419
|
+
* out of the structure rather than needing a rule: a cap is compared by the
|
|
1420
|
+
* handler this stream would have called, and a declined scope calls none. Format
|
|
1421
|
+
* ceilings (`ARRAY_MAX`, `FIXLEN_MAX`, `MAX_DEPTH`, the varint bound) still apply
|
|
1422
|
+
* everywhere: they bound what the wire may express.
|
|
444
1423
|
*/
|
|
445
|
-
sequenceBegin?(id: number):
|
|
446
|
-
/** End of the nested sequence
|
|
447
|
-
sequenceEnd?(): void;
|
|
1424
|
+
sequenceBegin?(id: number, depth: number): boolean | void;
|
|
1425
|
+
/** End of the nested sequence opened by field `id` at `depth`. */
|
|
1426
|
+
sequenceEnd?(id: number, depth: number): void;
|
|
448
1427
|
}
|
|
449
1428
|
/**
|
|
450
|
-
* Push parser for the SofaBuffers wire format
|
|
451
|
-
*
|
|
452
|
-
*
|
|
453
|
-
* {@link
|
|
454
|
-
*
|
|
455
|
-
* the
|
|
1429
|
+
* Push parser for the SofaBuffers wire format, and the library's only decode
|
|
1430
|
+
* surface (§5.3.1).
|
|
1431
|
+
*
|
|
1432
|
+
* Bind a {@link Visitor} at construction, then feed bytes in chunks of any size
|
|
1433
|
+
* with {@link feed}: it calls one visitor method per decoded field and resumes
|
|
1434
|
+
* cleanly across chunk boundaries. Every `feed` returns the decode outcome for the
|
|
1435
|
+
* bytes so far, so no end / finalize call is needed — and there is nothing else to
|
|
1436
|
+
* ask: `feed` is the only way to learn where a stream stands, by what it returns
|
|
1437
|
+
* or by what it throws.
|
|
1438
|
+
*
|
|
1439
|
+
* **No receiver limit is configured here, because this codec holds none**
|
|
1440
|
+
* (§6.2.1). A `max_dyn_*` cap is the receiving *application's* number, stated by
|
|
1441
|
+
* generated code, which knows the schema and the target; it is compared inside the
|
|
1442
|
+
* visitor's own `arrayBegin` / `fixlenBegin` — raised by this stream at the count
|
|
1443
|
+
* or length header, before any payload is delivered and only for a field the
|
|
1444
|
+
* visitor reads — and, for a wrapper array's `string` / `blob` elements, inside
|
|
1445
|
+
* the `StringSeq` / `BlobSeq` collector those bounds were passed to.
|
|
1446
|
+
* This class used to take a `DecodeLimits` and default every absent cap to the
|
|
1447
|
+
* format ceiling, which §6.2.1 forbids twice over: a codec must not supply a
|
|
1448
|
+
* default for a limit it was not given, and a format ceiling reached because no
|
|
1449
|
+
* cap was stated is the format's bound and must not be presented as a receiver
|
|
1450
|
+
* cap.
|
|
1451
|
+
*
|
|
1452
|
+
* Constructing one is the only allocating step (§6.6): `feed` itself allocates
|
|
1453
|
+
* nothing at all. The one-shot {@link decode} is exactly this class fed once.
|
|
456
1454
|
*/
|
|
457
1455
|
declare class IStream {
|
|
458
1456
|
private readonly state;
|
|
459
1457
|
/**
|
|
460
|
-
* @param
|
|
461
|
-
*
|
|
462
|
-
* (`LIMIT_EXCEEDED`) from {@link feed}, at the offending field's header and
|
|
463
|
-
* before any of its payload is streamed to the visitor. Omit for no caps.
|
|
464
|
-
*/
|
|
465
|
-
constructor(limits?: DecodeLimits);
|
|
466
|
-
/**
|
|
467
|
-
* Feed a chunk of bytes, dispatching decoded fields to `visitor`. Throws
|
|
468
|
-
* {@link SofabError} (`INVALID_MSG`) only if the bytes are *malformed*;
|
|
469
|
-
* running out of bytes mid-field is not an error — it simply suspends until
|
|
470
|
-
* the next chunk (see {@link end}).
|
|
1458
|
+
* @param visitor The field handler this stream drives, for its whole life — and
|
|
1459
|
+
* the layer that holds the receiver caps, if any (§6.2.1; see the class doc).
|
|
471
1460
|
*/
|
|
472
|
-
|
|
1461
|
+
constructor(visitor: Visitor);
|
|
473
1462
|
/**
|
|
474
|
-
*
|
|
475
|
-
*
|
|
476
|
-
*
|
|
477
|
-
*
|
|
478
|
-
* nested sequence).
|
|
1463
|
+
* Feed a chunk of bytes, dispatching decoded fields to the bound visitor, and
|
|
1464
|
+
* **return** where the decode stands after them (§5.2.1):
|
|
1465
|
+
* {@link DecodeStatus.Complete} when they end exactly at a field boundary,
|
|
1466
|
+
* {@link DecodeStatus.Incomplete} when they end *inside* a field (a partial
|
|
1467
|
+
* varint, an unfinished payload / array, or a still-open nested sequence).
|
|
1468
|
+
* Running out of bytes mid-field is not an error — the decode merely suspends
|
|
1469
|
+
* until the next chunk, and the caller owns end-of-input.
|
|
1470
|
+
*
|
|
1471
|
+
* **This call is the only place the answer is.** There is no finish / finalize
|
|
1472
|
+
* step (§5.2.4) and no status accessor: what this returns, or throws, is the
|
|
1473
|
+
* whole of what the stream has to say, so a caller is never one question short
|
|
1474
|
+
* after it and never has two answers to reconcile. Feeding an empty chunk
|
|
1475
|
+
* re-reads the same value without consuming anything, for a caller that wants
|
|
1476
|
+
* the outcome again without holding on to it.
|
|
1477
|
+
*
|
|
1478
|
+
* The chunk is borrowed **only for the duration of this call** (§6.0): once it
|
|
1479
|
+
* returns, the caller may reuse, overwrite or free that memory, and the decoded
|
|
1480
|
+
* message is unaffected — the decoder retains nothing that points into it.
|
|
1481
|
+
*
|
|
1482
|
+
* `INVALID` travels on the error channel — this port's idiomatic surfacing of
|
|
1483
|
+
* it: *malformed* bytes throw {@link SofabError} (`INVALID_MSG`) instead of
|
|
1484
|
+
* returning a status, which is why the return type names only the other two.
|
|
1485
|
+
* That verdict is **terminal** (§5.2.1): the stream latches it, so a caller that
|
|
1486
|
+
* catches the throw and feeds on gets the same error again from every later
|
|
1487
|
+
* call — no further byte is consumed and no visitor method is invoked. A caller
|
|
1488
|
+
* that caught it already holds the verdict, in the code on the error it caught.
|
|
479
1489
|
*
|
|
480
|
-
*
|
|
481
|
-
*
|
|
482
|
-
*
|
|
483
|
-
*
|
|
1490
|
+
* A receiver-limit rejection (`LIMIT_EXCEEDED`, §6.2.1) travels the same
|
|
1491
|
+
* channel — thrown out of the visitor callback that compared the cap — but it
|
|
1492
|
+
* is **not** the `INVALID` outcome and never becomes one: the bytes are
|
|
1493
|
+
* well-formed and the same message decodes under a looser cap, so it is a
|
|
1494
|
+
* policy rejection (§6.2.1, §6.3). The two stay distinguishable by their code,
|
|
1495
|
+
* which is what §6.3 requires; §6.3 leaves the surfacing open between "a fourth
|
|
1496
|
+
* decode outcome" and "a terminal failure carrying the `LimitExceeded` code on
|
|
1497
|
+
* the error channel", and this port takes the second. **Terminal** is the other
|
|
1498
|
+
* half of that sentence and holds exactly as it does for `INVALID`: the stream
|
|
1499
|
+
* latches the rejection, so every later call re-throws it under the same code,
|
|
1500
|
+
* consumes no byte and drives no visitor method. It is *only* on the error
|
|
1501
|
+
* channel — the three-valued outcome has no value for "valid, but more than I am
|
|
1502
|
+
* configured to accept", so there is nothing about it to read back as a status,
|
|
1503
|
+
* and nothing that has to be kept in step with the throw.
|
|
484
1504
|
*/
|
|
485
|
-
|
|
1505
|
+
feed(chunk: Uint8Array): FeedStatus;
|
|
486
1506
|
}
|
|
487
1507
|
/**
|
|
488
1508
|
* Decode a complete message held in one contiguous buffer, in a single call.
|
|
489
1509
|
*
|
|
490
|
-
*
|
|
491
|
-
*
|
|
492
|
-
*
|
|
493
|
-
*
|
|
494
|
-
*
|
|
495
|
-
*
|
|
1510
|
+
* The non-streaming convenience, and **not** a second decoder: it is one
|
|
1511
|
+
* {@link IStream.feed} of the whole buffer, so it runs the same code, applies the
|
|
1512
|
+
* same rules and has the same memory behaviour as a chunked decode — §6.7.1
|
|
1513
|
+
* forbids the one-shot path from differing, right down to holding no view into
|
|
1514
|
+
* the buffer it was handed. Feeding a whole message is also the case the decoder's
|
|
1515
|
+
* fast lane is built for, so nothing is given up by having one implementation.
|
|
496
1516
|
*
|
|
497
|
-
* The whole buffer *is* the end of input, so the two failure outcomes both
|
|
498
|
-
*
|
|
499
|
-
*
|
|
500
|
-
*
|
|
501
|
-
* returns normally.
|
|
1517
|
+
* The whole buffer *is* the end of input, so the two failure outcomes both throw a
|
|
1518
|
+
* {@link SofabError} the caller tells apart by `code` (MESSAGE_SPEC §7): malformed
|
|
1519
|
+
* input throws `INVALID_MSG`, while input that ends inside a field — truncation or
|
|
1520
|
+
* an unclosed sequence — throws `INCOMPLETE`. A complete message returns normally.
|
|
502
1521
|
*
|
|
503
|
-
*
|
|
504
|
-
*
|
|
505
|
-
* is materialized. Omit for no caps (the default).
|
|
1522
|
+
* The receiver caps of §6.2.1 are the `visitor`'s, not this function's: it takes
|
|
1523
|
+
* no limits argument because this codec holds none. See {@link IStream}.
|
|
506
1524
|
*/
|
|
507
|
-
declare function decode(bytes: Uint8Array, visitor: Visitor
|
|
1525
|
+
declare function decode(bytes: Uint8Array, visitor: Visitor): void;
|
|
508
1526
|
|
|
509
1527
|
/**
|
|
510
|
-
*
|
|
511
|
-
*
|
|
512
|
-
*
|
|
513
|
-
*
|
|
514
|
-
*
|
|
515
|
-
*
|
|
516
|
-
*
|
|
517
|
-
*
|
|
518
|
-
*
|
|
519
|
-
*
|
|
520
|
-
*
|
|
521
|
-
*
|
|
522
|
-
*
|
|
523
|
-
*
|
|
524
|
-
*
|
|
525
|
-
*
|
|
526
|
-
*
|
|
527
|
-
*
|
|
528
|
-
*
|
|
529
|
-
*
|
|
530
|
-
*
|
|
531
|
-
*
|
|
532
|
-
*
|
|
533
|
-
*
|
|
1528
|
+
* Decode-side text helper — the read-path twin of {@link "../encode/fixlen"}.
|
|
1529
|
+
*
|
|
1530
|
+
* Two things make a short string expensive to decode, and this avoids both.
|
|
1531
|
+
*
|
|
1532
|
+
* `TextDecoder.decode` needs a `Uint8Array` covering exactly the payload, so the
|
|
1533
|
+
* decoder has to build a `subarray` per string; on Node 24 that view alone costs
|
|
1534
|
+
* about a third of the whole read (~500 Ir/op for a 13-byte field). And the
|
|
1535
|
+
* decode itself is a WHATWG entry point whose per-call setup dwarfs the payload
|
|
1536
|
+
* at these sizes.
|
|
1537
|
+
*
|
|
1538
|
+
* The obvious fix — walk the bytes in JS and append with `+=`, the way
|
|
1539
|
+
* protobufjs does — trades one problem for another: the repeated concatenation
|
|
1540
|
+
* builds a **rope**, and the rope is flattened later, by the first consumer that
|
|
1541
|
+
* walks it. In a decode-then-encode round trip that consumer is the encoder's own
|
|
1542
|
+
* `utf8Length` / `utf8Write` pass, so the cost does not disappear, it moves out
|
|
1543
|
+
* of the decode where it is easy to miss. Measured on a 13-byte ASCII field
|
|
1544
|
+
* (decode plus one charCodeAt walk): TextDecoder 2006 Ir/op, rope 2095 — the
|
|
1545
|
+
* rope form looks 40 percent cheaper if only the decode is measured, and is
|
|
1546
|
+
* *worse* once the string is used.
|
|
1547
|
+
*
|
|
1548
|
+
* So an all-ASCII payload short enough to name its length is built with a
|
|
1549
|
+
* SINGLE `String.fromCharCode` call, which returns a flat string with no rope to
|
|
1550
|
+
* pay for afterwards: 858 Ir/op on the same measurement, less than half of
|
|
1551
|
+
* either alternative. Everything else — non-ASCII, or longer than
|
|
1552
|
+
* {@link FLAT_MAX} — goes to the fatal {@link TextDecoder} exactly as before.
|
|
1553
|
+
*
|
|
1554
|
+
* This stays **strict** (MESSAGE_SPEC section 8): the fast path is gated on every
|
|
1555
|
+
* byte being below 0x80, and each such byte is a well-formed single-byte UTF-8
|
|
1556
|
+
* sequence, so an all-ASCII run has nothing to reject. Any payload with a high
|
|
1557
|
+
* bit set is handed to the platform decoder whole and gets exactly the
|
|
1558
|
+
* validation it always got — the fast path can never accept bytes the platform
|
|
1559
|
+
* would refuse.
|
|
1560
|
+
*
|
|
1561
|
+
* {@link decodeUtf8} is **public** because the code that needs it is not in this
|
|
1562
|
+
* library. The decoder reports raw, unvalidated payload pieces (§6.4.5 puts the
|
|
1563
|
+
* UTF-8 check where a string is *materialized*, and a piece may end mid-code-point),
|
|
1564
|
+
* so materialization — and therefore the check — belongs to whoever wants the
|
|
1565
|
+
* value. Generated code is that whoever, and without an exported entry point every
|
|
1566
|
+
* generated package builds a plain fatal `TextDecoder` of its own and pays the full
|
|
1567
|
+
* 2006 Ir/op per short string instead of 858.
|
|
1568
|
+
*
|
|
1569
|
+
* It is **static helper layer**, not codec (§6.6.1): it allocates the string it
|
|
1570
|
+
* returns, and no codec path calls it.
|
|
1571
|
+
*/
|
|
1572
|
+
/**
|
|
1573
|
+
* Decode `buf[start..end)` as strict UTF-8 — the whole of `buf` when the range
|
|
1574
|
+
* is omitted, which is the shape a reassembled payload (see {@link PayloadAcc})
|
|
1575
|
+
* arrives in.
|
|
1576
|
+
*
|
|
1577
|
+
* Malformed bytes throw a {@link SofabError} (`INVALID_MSG`), never the fatal
|
|
1578
|
+
* {@link TextDecoder}'s bare `TypeError`. The mapping belongs here rather than at
|
|
1579
|
+
* each call site: invalid UTF-8 is an invalid *message* (MESSAGE_SPEC §8,
|
|
1580
|
+
* CORELIB_PLAN §6.4), so it has to reach a caller as the same `INVALID` verdict
|
|
1581
|
+
* as every other malformation — through the one `catch (e) { if (e instanceof
|
|
1582
|
+
* SofabError) … }` a consumer writes — and a platform exception escaping that
|
|
1583
|
+
* clause is precisely the bug this closes.
|
|
1584
|
+
*
|
|
1585
|
+
* A zero-length payload is the empty string, decoded without touching the
|
|
1586
|
+
* platform decoder.
|
|
534
1587
|
*/
|
|
1588
|
+
declare function decodeUtf8(buf: Uint8Array, start?: number, end?: number): string;
|
|
535
1589
|
|
|
536
1590
|
/**
|
|
537
|
-
*
|
|
538
|
-
*
|
|
539
|
-
*
|
|
540
|
-
*
|
|
541
|
-
*
|
|
542
|
-
*
|
|
543
|
-
*
|
|
544
|
-
*
|
|
545
|
-
*
|
|
546
|
-
*
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
private
|
|
584
|
-
private
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
*
|
|
590
|
-
*
|
|
591
|
-
*
|
|
592
|
-
*
|
|
593
|
-
*
|
|
594
|
-
*
|
|
595
|
-
*
|
|
596
|
-
*
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
1591
|
+
* Generated-layer support: reassembling a `string` / `blob` payload that the
|
|
1592
|
+
* decoder reported in pieces.
|
|
1593
|
+
*
|
|
1594
|
+
* This is the **static helper layer** of CORELIB_PLAN §6.6.1, not part of the
|
|
1595
|
+
* codec: it allocates, on the generated layer's behalf, and the codec never calls
|
|
1596
|
+
* into it — the call graph runs generated code → helper → codec, never the other
|
|
1597
|
+
* way round. §6.6 binds the codec; this side of the boundary is where a value gets
|
|
1598
|
+
* built.
|
|
1599
|
+
*
|
|
1600
|
+
* {@link IStream} reports a payload to {@link Visitor.string} /
|
|
1601
|
+
* {@link Visitor.blob} as one or more pieces, each a range of the caller's own fed
|
|
1602
|
+
* chunk tagged with the field's `total` byte length and the `offset` of the piece
|
|
1603
|
+
* within it. That memory is borrowed only for the duration of the call (§6.0), so
|
|
1604
|
+
* a consumer that wants the *value* has to copy out — and generated code always
|
|
1605
|
+
* wants the value: a schema field is a `string`, not a sequence of pieces.
|
|
1606
|
+
*
|
|
1607
|
+
* That join has the same shape for every schema — it is decided entirely by
|
|
1608
|
+
* `total` and `offset`, both runtime values — so it lives here rather than in
|
|
1609
|
+
* every generated package (ARCHITECTURE §8). It is also the piece that is easiest
|
|
1610
|
+
* to get subtly wrong and hardest to catch: two implementations can disagree about
|
|
1611
|
+
* where a payload was split and still produce byte-identical output on every
|
|
1612
|
+
* shared vector, because the split is a property of how the caller fed the bytes,
|
|
1613
|
+
* not of the bytes. {@link PayloadAcc} is pinned instead by a unit test that
|
|
1614
|
+
* splits one payload at *every* offset and requires one answer.
|
|
1615
|
+
*/
|
|
1616
|
+
/**
|
|
1617
|
+
* Joins a `string` / `blob` payload that arrived across several fed pieces.
|
|
1618
|
+
*
|
|
1619
|
+
* **One per decoder, shared by every field.** Exactly one payload is in flight at
|
|
1620
|
+
* a time across a whole decode, however deep the nesting — a fixlen payload is
|
|
1621
|
+
* atomic on the wire, so nothing can begin inside it — so one accumulator is
|
|
1622
|
+
* enough.
|
|
1623
|
+
*
|
|
1624
|
+
* **What it returns is owned by the caller** and aliases nothing: the bytes are
|
|
1625
|
+
* copied into storage this accumulator allocated, on the whole-payload path
|
|
1626
|
+
* exactly as on the split one (§6.7 — "there is no mode in which the destination
|
|
1627
|
+
* aliases the input"). A consumer may keep the result, and a payload that arrived
|
|
1628
|
+
* whole is not a special case with a different lifetime.
|
|
1629
|
+
*
|
|
1630
|
+
* Sizing follows the declared `total`, so a hostile length word is bounded by
|
|
1631
|
+
* whatever bound the caller already applied to it — the schema `maxlen` its
|
|
1632
|
+
* generated guard checks, or the receiver caps of §6.2.1, both of which
|
|
1633
|
+
* are enforced at the length word before a piece is ever delivered. This class
|
|
1634
|
+
* enforces neither: it is handed a payload the caller has already accepted.
|
|
1635
|
+
*/
|
|
1636
|
+
declare class PayloadAcc {
|
|
1637
|
+
private buf;
|
|
1638
|
+
private len;
|
|
1639
|
+
/**
|
|
1640
|
+
* Contribute one piece — the bytes `src[start..end)` at `offset` of a
|
|
1641
|
+
* `total`-byte payload — and return the **whole** payload once it is complete,
|
|
1642
|
+
* or `null` while bytes are still outstanding.
|
|
1643
|
+
*
|
|
1644
|
+
* `offset === 0` starts a payload — and *resets* the accumulator, so a decode
|
|
1645
|
+
* that was abandoned mid-payload (an `INVALID` field, a declined subtree) leaves
|
|
1646
|
+
* nothing behind to corrupt the next one.
|
|
1647
|
+
*
|
|
1648
|
+
* A late piece with no payload in progress returns `null` rather than writing
|
|
1649
|
+
* anywhere: the accumulator has no buffer to append to, and inventing one would
|
|
1650
|
+
* fabricate a payload out of a fragment.
|
|
1651
|
+
*/
|
|
1652
|
+
take(total: number, offset: number, src: Uint8Array, start: number, end: number): Uint8Array | null;
|
|
1653
|
+
}
|
|
1654
|
+
|
|
1655
|
+
/**
|
|
1656
|
+
* Generated-layer support: the element collectors for a **wrapper-sequence array**
|
|
1657
|
+
* (MESSAGE_SPEC §5.1) — {@link ElementSeq} and {@link FramedSeq} for the index
|
|
1658
|
+
* rules of any element kind, {@link StringSeq} / {@link BlobSeq} for the two leaf
|
|
1659
|
+
* kinds whose payload arrives here as well.
|
|
1660
|
+
*
|
|
1661
|
+
* Static helper layer, not codec (CORELIB_PLAN §6.6.1): these allocate — that is
|
|
1662
|
+
* their job — and no codec path reaches them. Generated code drives them from
|
|
1663
|
+
* inside its own flat {@link Visitor}, which is what knows, from the schema, that
|
|
1664
|
+
* the scope it is currently in *is* the wrapper array.
|
|
1665
|
+
*
|
|
1666
|
+
* A wrapper array is an ordinary sequence whose child fields *are* the elements,
|
|
1667
|
+
* and whose field id *is* the array index. So a collector is fed the two events a
|
|
1668
|
+
* `string` / `blob` element produces — {@link StringSeq.begin} at the length word
|
|
1669
|
+
* and {@link StringSeq.element} per payload piece — and places each value at
|
|
1670
|
+
* `out[id]`.
|
|
1671
|
+
*
|
|
1672
|
+
* Every part of that is schema-independent. Both bounds on the index (the schema
|
|
1673
|
+
* `count`, the receiver cap) and both on the element's byte length (the element
|
|
1674
|
+
* `maxlen`, the receiver cap) arrive as constructor arguments — runtime values —
|
|
1675
|
+
* and the field name is carried along only so a rejection can say which field it
|
|
1676
|
+
* was about. So the collectors belong here rather than being re-emitted, textually
|
|
1677
|
+
* identical, into every generated package (ARCHITECTURE §8).
|
|
1678
|
+
*
|
|
1679
|
+
* Four rules are implemented here and are worth stating once, since nothing in the
|
|
1680
|
+
* shared vectors can distinguish an implementation that gets them wrong from one
|
|
1681
|
+
* that does not — they are properties of the *decoded value*, not of the bytes
|
|
1682
|
+
* (which is exactly why CORELIB_PLAN §7.2 item 8 asks for them separately):
|
|
1683
|
+
*
|
|
1684
|
+
* - **Gaps are legal.** An interior element equal to its default is not written at
|
|
1685
|
+
* all, so ids `0, 2, 3` are well-formed. The missing index is filled with the
|
|
1686
|
+
* element default, never skipped over: everything after a gap keeps its index.
|
|
1687
|
+
* - **Length is highest present id + 1.** The wrapper carries no length, and the
|
|
1688
|
+
* last element is never elided, so growing to `id + 1` on each element is
|
|
1689
|
+
* exactly right and no trailing fill is ever needed.
|
|
1690
|
+
* - **A repeated element id replaces.** Last occurrence wins per id (§7.4);
|
|
1691
|
+
* placing at `out[id]` does that by construction, with no bookkeeping.
|
|
1692
|
+
* - **A rejected id extends nothing.** Every bound is checked before the
|
|
1693
|
+
* destination grows, so a rejection leaves the array exactly as it was and a
|
|
1694
|
+
* lower id delivered afterwards still lands correctly.
|
|
1695
|
+
*
|
|
1696
|
+
* **Two bounds, never both** (§6.2.1). Index and element length each carry a
|
|
1697
|
+
* schema bound and a receiver cap, and exactly one of the pair applies: where the
|
|
1698
|
+
* schema declared a bound its violation is `INVALID` — a statement about validity
|
|
1699
|
+
* — and where it declared none the receiver's cap governs and its violation is
|
|
1700
|
+
* `LIMIT_EXCEEDED`, a policy rejection on well-formed bytes. "They **MUST NOT** be
|
|
1701
|
+
* applied to a field the schema already bounds", so the caps are exclusive with
|
|
1702
|
+
* the bounds and never additive.
|
|
1703
|
+
*
|
|
1704
|
+
* **Every bound is a required argument, and none of them has a default** (§6.2.1).
|
|
1705
|
+
* The comparison runs here; the *number* never originates here. A corelib "**MUST
|
|
1706
|
+
* NOT** hold a limit of its own, **MUST NOT** supply a default for one it was not
|
|
1707
|
+
* given, **MUST NOT** read an omitted argument as *unlimited*, and **MUST NOT**
|
|
1708
|
+
* clamp to one", and a format ceiling (§6.2) reached because no cap was stated "is
|
|
1709
|
+
* the **format's** bound, not a receiver cap, and a port **MUST NOT** present it as
|
|
1710
|
+
* one". These constructors used to default `receiverCap` to `ARRAY_MAX` and
|
|
1711
|
+
* `receiverElemMax` to `FIXLEN_MAX`, which did exactly that: they reported
|
|
1712
|
+
* `LIMIT_EXCEEDED` against a ceiling nobody configured. The defaults are gone and
|
|
1713
|
+
* the arguments are required — §6.2.1's "strictest form, and the recommended one".
|
|
1714
|
+
* The *schema* bounds beside them (`cap`, `elemMax`) are required too, with
|
|
1715
|
+
* {@link UNBOUNDED} as the explicit "the schema declared none" spelling: an absent
|
|
1716
|
+
* schema bound is a fact about the schema, which only the caller knows.
|
|
1717
|
+
*
|
|
1718
|
+
* **And an argument that states no cap is a caller mistake, not a policy
|
|
1719
|
+
* rejection.** A required argument can still arrive as `-1`, `undefined` or
|
|
1720
|
+
* `Infinity` from an untyped caller, and the same reasoning applies one level in:
|
|
1721
|
+
* no receiver policy was set, so there is no `LIMIT_EXCEEDED` to report and no
|
|
1722
|
+
* unlimited mode to fall back to. Such a bound is refused at construction with
|
|
1723
|
+
* `Argument` (§6.3's third row: "every remaining caller mistake is
|
|
1724
|
+
* `InvalidArgument`") — see {@link requireReceiverBound}.
|
|
1725
|
+
*/
|
|
1726
|
+
|
|
1727
|
+
/**
|
|
1728
|
+
* No *schema* bound — what `cap` / `elemMax` take for an array or element the
|
|
1729
|
+
* schema left open (§7.2), so the receiver cap beside them governs instead.
|
|
1730
|
+
*
|
|
1731
|
+
* Exported because the schema bounds are required arguments with no default: an
|
|
1732
|
+
* absent one is a fact about the **schema**, which only generated code knows, and
|
|
1733
|
+
* §6.2.1's ban on a codec defaulting a bound is easiest to keep honest when every
|
|
1734
|
+
* bound has to be spelled. This is the spelling for "there is none".
|
|
1735
|
+
*/
|
|
1736
|
+
declare const UNBOUNDED = -1;
|
|
1737
|
+
/**
|
|
1738
|
+
* The slots of a wrapper-sequence array: the index rules of MESSAGE_SPEC §5.1 and
|
|
1739
|
+
* the two bounds of CORELIB_PLAN §6.2.1, once, for any element type.
|
|
1740
|
+
*
|
|
1741
|
+
* {@link StringSeq} and {@link BlobSeq} place leaf elements through it, and
|
|
1742
|
+
* generated code places a **framed** element — a `struct` / `union` / nested row —
|
|
1743
|
+
* through it directly: {@link reserve} at the element's `sequenceBegin`, then build
|
|
1744
|
+
* the child into `out[id]`. The element kind changes which path arrives here and
|
|
1745
|
+
* nothing else, which is the point — the bound is the index (§7.2 item 8).
|
|
1746
|
+
*
|
|
1747
|
+
* @param out The destination array; grown to `id + 1` as elements arrive.
|
|
1748
|
+
* @param def The element default, written into a gap and into a reserved slot.
|
|
1749
|
+
* @param cap The schema `count` as an index capacity: `id >= cap` is `INVALID`
|
|
1750
|
+
* (§7.1) — a statement about validity. Pass {@link UNBOUNDED} (`-1`) for an array
|
|
1751
|
+
* the schema left open, where `receiverCap` governs instead.
|
|
1752
|
+
* @param name The schema field name, used only in a rejection message.
|
|
1753
|
+
* @param receiverCap The receiver-side index cap for a schema-unbounded array
|
|
1754
|
+
* (§6.2.1): `id >= receiverCap` is `LIMIT_EXCEEDED`, a policy rejection. There is
|
|
1755
|
+
* no unlimited setting — a wrapper array announces no count, so the index is the
|
|
1756
|
+
* only place a receiver can bound it. **Required, with no default**: the number is
|
|
1757
|
+
* generated code's, and falling back to `ARRAY_MAX` would report a policy
|
|
1758
|
+
* rejection against a format ceiling nobody configured (§6.2.1). A value that
|
|
1759
|
+
* states no cap at all — negative, `NaN`, `Infinity` — is `Argument` at
|
|
1760
|
+
* construction, never `LIMIT_EXCEEDED` ({@link requireReceiverBound}).
|
|
1761
|
+
*/
|
|
1762
|
+
declare class ElementSeq<T> {
|
|
1763
|
+
readonly out: T[];
|
|
1764
|
+
readonly def: T;
|
|
1765
|
+
readonly cap: number;
|
|
1766
|
+
readonly name: string;
|
|
1767
|
+
readonly receiverCap: number;
|
|
1768
|
+
/**
|
|
1769
|
+
* The one index bound in force, picked once by `indexBound`: the schema
|
|
1770
|
+
* `count` where the schema stated one, the receiver cap where it did not. Which
|
|
1771
|
+
* of the two it is decides the verdict, which is `rejectIndex`'s job.
|
|
1772
|
+
*/
|
|
1773
|
+
private readonly bound;
|
|
1774
|
+
constructor(out: T[], def: T, cap: number, name: string, receiverCap: number);
|
|
1775
|
+
/**
|
|
1776
|
+
* Bound-check `id` and grow `out` to `id + 1`, filling any gap — and the slot
|
|
1777
|
+
* itself — with the element default.
|
|
1778
|
+
*
|
|
1779
|
+
* The check runs **before** the growth, which is the whole of §7.2 item 8's
|
|
1780
|
+
* "after a rejected id the container is not left partially extended": a
|
|
1781
|
+
* rejection leaves `out` exactly as it was, so a lower id delivered afterwards
|
|
1782
|
+
* still lands at its own index.
|
|
1783
|
+
*/
|
|
1784
|
+
reserve(id: number): void;
|
|
1785
|
+
/**
|
|
1786
|
+
* The two index bounds, without growing: the schema `count` as validity
|
|
1787
|
+
* (`INVALID`) or, where the schema left the array open, the receiver cap as
|
|
1788
|
+
* capacity (`LIMIT_EXCEEDED`). Never both — §6.2.1 keeps a cap off a field the
|
|
1789
|
+
* schema already bounds, which is why one `bound` can stand for both.
|
|
1790
|
+
*
|
|
1791
|
+
* Split out from {@link reserve} because a leaf element is bound-checked at its
|
|
1792
|
+
* length word, before its payload has arrived and so before there is anything to
|
|
1793
|
+
* place ({@link StringSeq.begin}).
|
|
1794
|
+
*/
|
|
1795
|
+
checkIndex(id: number): void;
|
|
1796
|
+
/**
|
|
1797
|
+
* What {@link reserve} does, then `value` written into the slot. A repeat
|
|
1798
|
+
* replaces (§7.4).
|
|
1799
|
+
*
|
|
1800
|
+
* Written out rather than delegating to {@link reserve}: on the baseline tier a
|
|
1801
|
+
* call is not free, and this is the per-element path. The check still precedes
|
|
1802
|
+
* the growth, which is the property §7.2 item 8 asks for.
|
|
1803
|
+
*/
|
|
1804
|
+
place(id: number, value: T): void;
|
|
1805
|
+
}
|
|
1806
|
+
/**
|
|
1807
|
+
* The slots of a wrapper-sequence array whose element default is a **fresh
|
|
1808
|
+
* object**: a framed element (`struct` / `union`) or a nested row.
|
|
1809
|
+
*
|
|
1810
|
+
* {@link ElementSeq}'s twin, and it differs in exactly one thing — the gap value
|
|
1811
|
+
* comes from a **factory** instead of being shared. That single axis is why it is
|
|
1812
|
+
* a second class rather than a second argument: a framed element's default is
|
|
1813
|
+
* `new Elem()` and a row's is `[]`, both mutable and both reachable by the
|
|
1814
|
+
* caller, so one shared instance would alias every gap of the array onto it and —
|
|
1815
|
+
* since an arriving element decodes into the slot the reservation placed — would
|
|
1816
|
+
* alias every *element* onto it too, which is not a near miss but a flatly wrong
|
|
1817
|
+
* decode. {@link ElementSeq}'s `""` and zero-length `Uint8Array` have no state to
|
|
1818
|
+
* share, which is what makes sharing right there and wrong here.
|
|
1819
|
+
*
|
|
1820
|
+
* Everything else is {@link ElementSeq}, deliberately: the same argument order
|
|
1821
|
+
* with `make` in `def`'s slot, the same two exclusive index bounds through the
|
|
1822
|
+
* same `indexBound` / `rejectIndex` pair, the same growth geometry, the same
|
|
1823
|
+
* construction-time refusal of a receiver cap that states no cap.
|
|
1824
|
+
*
|
|
1825
|
+
* Generated code calls {@link reserve} at the element's own `sequenceBegin` and
|
|
1826
|
+
* then keeps routing the child's fields into `out[id]` itself. This class owns
|
|
1827
|
+
* the bound and the growth; it never owns the routing, which is the part that has
|
|
1828
|
+
* a different shape for every schema (ARCHITECTURE §8).
|
|
1829
|
+
*
|
|
1830
|
+
* @param out The destination array; grown to `id + 1` as elements arrive.
|
|
1831
|
+
* @param make Builds one element default. Called once per gap slot, and once for
|
|
1832
|
+
* the reserved slot itself in {@link reserve} — never for a slot already present,
|
|
1833
|
+
* so a re-opened element keeps the object earlier fields decoded into.
|
|
1834
|
+
* @param cap The schema `count` as an index capacity: `id >= cap` is `INVALID`
|
|
1835
|
+
* (§7.1) — a statement about validity. Pass {@link UNBOUNDED} (`-1`) for an array
|
|
1836
|
+
* the schema left open, where `receiverCap` governs instead.
|
|
1837
|
+
* @param name The schema field name, used only in a rejection message.
|
|
1838
|
+
* @param receiverCap The receiver-side index cap for a schema-unbounded array
|
|
1839
|
+
* (§6.2.1): `id >= receiverCap` is `LIMIT_EXCEEDED`, a policy rejection, never
|
|
1840
|
+
* `INVALID`, and never applied beside a `cap` the schema stated. **Required, with
|
|
1841
|
+
* no default**; a value that states no cap at all is `Argument` at construction
|
|
1842
|
+
* ({@link requireReceiverBound}).
|
|
1843
|
+
*/
|
|
1844
|
+
declare class FramedSeq<T> {
|
|
1845
|
+
readonly out: T[];
|
|
1846
|
+
readonly make: () => T;
|
|
1847
|
+
readonly cap: number;
|
|
1848
|
+
readonly name: string;
|
|
1849
|
+
readonly receiverCap: number;
|
|
1850
|
+
/** The one index bound in force, picked once by `indexBound` — see {@link ElementSeq}. */
|
|
1851
|
+
private readonly bound;
|
|
1852
|
+
constructor(out: T[], make: () => T, cap: number, name: string, receiverCap: number);
|
|
1853
|
+
/**
|
|
1854
|
+
* The two index bounds, without growing — see {@link ElementSeq.checkIndex}.
|
|
1855
|
+
*
|
|
1856
|
+
* Split out for the same reason it is there: a caller may have a second bound
|
|
1857
|
+
* to take before anything is allocated. A native matrix row is that case in this
|
|
1858
|
+
* port — its element `count` is rejected at the array header, and §7.2 item 8
|
|
1859
|
+
* wants that rejection to leave the row container exactly as it was.
|
|
1860
|
+
*/
|
|
1861
|
+
checkIndex(id: number): void;
|
|
1862
|
+
/**
|
|
1863
|
+
* Bound-check `id`, **then** grow `out` to `id + 1`, each new slot its own
|
|
1864
|
+
* `make()`.
|
|
1865
|
+
*
|
|
1866
|
+
* The order is §7.2 item 8's "after a rejected id the container is not left
|
|
1867
|
+
* partially extended": a rejection leaves `out` exactly as it was, so a lower id
|
|
1868
|
+
* delivered afterwards still lands at its own index. A slot already present is
|
|
1869
|
+
* left alone — a re-opened `struct` / `union` element merges into the object it
|
|
1870
|
+
* already built, which is what §7.4's last-occurrence-wins means for a scope
|
|
1871
|
+
* whose value *is* the scope.
|
|
1872
|
+
*/
|
|
1873
|
+
reserve(id: number): void;
|
|
1874
|
+
/**
|
|
1875
|
+
* Bound-check `id`, fill the gap below it, then write `value` into the slot —
|
|
1876
|
+
* what a nested **row** needs, an array wrapper *replacing* whatever an earlier
|
|
1877
|
+
* opening built at that index (§7.4) rather than merging into it.
|
|
1878
|
+
*
|
|
1879
|
+
* The gap fill stops one short of `id` on purpose: the slot is about to be
|
|
1880
|
+
* overwritten, so calling `make()` for it would allocate an element default
|
|
1881
|
+
* nobody ever reads. Assigning at `out.length` extends the array by exactly one,
|
|
1882
|
+
* which is the same array a {@link reserve} would have left.
|
|
1883
|
+
*/
|
|
1884
|
+
place(id: number, value: T): void;
|
|
697
1885
|
}
|
|
1886
|
+
/**
|
|
1887
|
+
* Collects the elements of a `string` wrapper-sequence array into `out`.
|
|
1888
|
+
*
|
|
1889
|
+
* @param out The destination, placed at `out[id]`; grown as elements arrive, with
|
|
1890
|
+
* gaps filled by the element default `""`.
|
|
1891
|
+
* @param acc The decoder's shared {@link PayloadAcc} — one per decode, since only
|
|
1892
|
+
* one payload is ever in flight.
|
|
1893
|
+
* @param cap The schema `count`, an index **capacity**: an element id at or above
|
|
1894
|
+
* it is `INVALID` (§7.1/§5.1) — a statement about validity. Pass
|
|
1895
|
+
* {@link UNBOUNDED} (`-1`) for an array the schema left unbounded, where
|
|
1896
|
+
* `receiverCap` governs instead.
|
|
1897
|
+
* @param elemMax The element `maxlen` in bytes, or {@link UNBOUNDED} (`-1`) for an
|
|
1898
|
+
* element the schema left open, where `receiverElemMax` governs instead.
|
|
1899
|
+
* @param name The schema field name, used only in the rejection message.
|
|
1900
|
+
* @param receiverCap The receiver-side index cap that applies **only** when the
|
|
1901
|
+
* schema left the array unbounded (§6.2.1): exceeding it is `LIMIT_EXCEEDED`, a
|
|
1902
|
+
* policy rejection, not `INVALID`. There is no unlimited setting — a wrapper array
|
|
1903
|
+
* announces no count, so the index is the only place a receiver can bound it.
|
|
1904
|
+
* **Required, with no default** (§6.2.1).
|
|
1905
|
+
* @param receiverElemMax The receiver-side `max_dyn_string_len` for an element the
|
|
1906
|
+
* schema left unbounded (§6.2.1), checked at the length word and answered with
|
|
1907
|
+
* `LIMIT_EXCEEDED`. A wrapper array's `string` elements never reach the generated
|
|
1908
|
+
* visitor — their length words come here — so this is where that cap belongs, and
|
|
1909
|
+
* it is why the decoder needs no module-wide limit object at all. **Required, with
|
|
1910
|
+
* no default**: `FIXLEN_MAX` is the format's bound, not a receiver cap, and §6.2.1
|
|
1911
|
+
* forbids presenting it as one.
|
|
1912
|
+
*/
|
|
1913
|
+
declare class StringSeq {
|
|
1914
|
+
readonly out: string[];
|
|
1915
|
+
readonly acc: PayloadAcc;
|
|
1916
|
+
readonly cap: number;
|
|
1917
|
+
readonly elemMax: number;
|
|
1918
|
+
readonly name: string;
|
|
1919
|
+
readonly receiverCap: number;
|
|
1920
|
+
readonly receiverElemMax: number;
|
|
1921
|
+
/** The index rules and both index bounds, shared with every other element kind. */
|
|
1922
|
+
private readonly slots;
|
|
1923
|
+
constructor(out: string[], acc: PayloadAcc, cap: number, elemMax: number, name: string, receiverCap: number, receiverElemMax: number);
|
|
1924
|
+
/**
|
|
1925
|
+
* The element's fixlen length word ({@link Visitor.fixlenBegin}).
|
|
1926
|
+
*
|
|
1927
|
+
* The bounds are decided by this word, so they are checked here — before any
|
|
1928
|
+
* payload byte — and again in {@link element} below.
|
|
1929
|
+
*
|
|
1930
|
+
* That is not redundancy for its own sake: a message that ends *inside* an
|
|
1931
|
+
* over-long element must still be `INVALID`, and only this event runs early
|
|
1932
|
+
* enough to say so. Without it the verdict would degrade to `INCOMPLETE`, which
|
|
1933
|
+
* §5.2.3 forbids for input already known to be malformed and which §6.4 forbids
|
|
1934
|
+
* a chunk boundary from changing.
|
|
1935
|
+
*
|
|
1936
|
+
* An element of the wrong fixlen subtype is left alone: §7.3 requires it to be
|
|
1937
|
+
* *skipped*, not rejected, and it is skipped by this class simply ignoring it.
|
|
1938
|
+
*/
|
|
1939
|
+
begin(id: number, subtype: FixlenSubtype, total: number): void;
|
|
1940
|
+
/** One payload piece of element `id` ({@link Visitor.string}). */
|
|
1941
|
+
element(id: number, total: number, offset: number, src: Uint8Array, start: number, end: number): void;
|
|
1942
|
+
/** Every bound for one element. Rejects **before** the destination grows. */
|
|
1943
|
+
private check;
|
|
1944
|
+
}
|
|
1945
|
+
/**
|
|
1946
|
+
* Collects the elements of a `blob` wrapper-sequence array into `out`. The
|
|
1947
|
+
* `string` twin above, with two differences: the payload is stored as bytes rather
|
|
1948
|
+
* than decoded, and gaps are filled with an empty {@link Uint8Array}.
|
|
1949
|
+
*
|
|
1950
|
+
* Each element is storage of its own — what {@link PayloadAcc} returns is a buffer
|
|
1951
|
+
* it allocated and handed over, never a view into the fed chunk (§6.7) — so a
|
|
1952
|
+
* stored element cannot rot when the caller reuses its input.
|
|
1953
|
+
*
|
|
1954
|
+
* @param out The destination, placed at `out[id]`.
|
|
1955
|
+
* @param acc The decoder's shared {@link PayloadAcc}.
|
|
1956
|
+
* @param cap The schema `count` as an index capacity, or {@link UNBOUNDED} (`-1`)
|
|
1957
|
+
* for unbounded.
|
|
1958
|
+
* @param elemMax The element `maxlen` in bytes, or {@link UNBOUNDED} (`-1`) for an
|
|
1959
|
+
* element the schema left open, where `receiverElemMax` governs instead.
|
|
1960
|
+
* @param name The schema field name, used only in the rejection message.
|
|
1961
|
+
* @param receiverCap The receiver-side index cap for a schema-unbounded array
|
|
1962
|
+
* (§6.2.1) — see {@link StringSeq}. Required, with no default.
|
|
1963
|
+
* @param receiverElemMax The receiver-side `max_dyn_blob_len` for a schema-unbounded
|
|
1964
|
+
* element (§6.2.1) — see {@link StringSeq}. Required, with no default.
|
|
1965
|
+
*/
|
|
1966
|
+
declare class BlobSeq {
|
|
1967
|
+
readonly out: Uint8Array[];
|
|
1968
|
+
readonly acc: PayloadAcc;
|
|
1969
|
+
readonly cap: number;
|
|
1970
|
+
readonly elemMax: number;
|
|
1971
|
+
readonly name: string;
|
|
1972
|
+
readonly receiverCap: number;
|
|
1973
|
+
readonly receiverElemMax: number;
|
|
1974
|
+
/** The index rules and both index bounds, shared with every other element kind. */
|
|
1975
|
+
private readonly slots;
|
|
1976
|
+
constructor(out: Uint8Array[], acc: PayloadAcc, cap: number, elemMax: number, name: string, receiverCap: number, receiverElemMax: number);
|
|
1977
|
+
/** See {@link StringSeq.begin} — the early bound check, for subtype `blob`. */
|
|
1978
|
+
begin(id: number, subtype: FixlenSubtype, total: number): void;
|
|
1979
|
+
/** One payload piece of element `id` ({@link Visitor.blob}). */
|
|
1980
|
+
element(id: number, total: number, offset: number, src: Uint8Array, start: number, end: number): void;
|
|
1981
|
+
/** Every bound for one element. Rejects **before** the destination grows. */
|
|
1982
|
+
private check;
|
|
1983
|
+
}
|
|
1984
|
+
|
|
1985
|
+
/**
|
|
1986
|
+
* Generated-layer support: the array half of the ≠-default test that decides
|
|
1987
|
+
* whether a field is written at all (MESSAGE_SPEC §2).
|
|
1988
|
+
*
|
|
1989
|
+
* SofaBuffers omits a field whose value equals its schema default, so every
|
|
1990
|
+
* generated `serialize` compares before it writes. For a scalar that is `!==`;
|
|
1991
|
+
* for an array it is element-wise, because two distinct `Uint8Array` /
|
|
1992
|
+
* `number[]` instances holding the same values are the same *value* and must
|
|
1993
|
+
* encode the same way — `a !== b` would emit the field whenever the destination
|
|
1994
|
+
* happened to be a fresh object, and the wire is supposed to be canonical.
|
|
1995
|
+
*
|
|
1996
|
+
* The comparison has no schema in it: the declared type only decides which
|
|
1997
|
+
* arrays are handed over. So it is written once here instead of being emitted
|
|
1998
|
+
* into every generated package (ARCHITECTURE §8).
|
|
1999
|
+
*/
|
|
2000
|
+
|
|
2001
|
+
/**
|
|
2002
|
+
* Element-wise equality for two array-likes: same length, and `===` at every
|
|
2003
|
+
* index. Covers what a leaf array field can hold — `Uint8Array` (blob), and
|
|
2004
|
+
* `number[]` / `bigint[]` / `boolean[]` / `string[]` — including a mixed pair
|
|
2005
|
+
* such as a `Uint8Array` value against a plain-array default from the schema.
|
|
2006
|
+
*
|
|
2007
|
+
* Deliberately **not** deep: a nested (wrapper) array of messages is compared by
|
|
2008
|
+
* the generated `isDefault()` of its element type, which is the only code that
|
|
2009
|
+
* knows what a default element is. Equally deliberately `===`, which makes `NaN`
|
|
2010
|
+
* unequal to itself — matching the scalar `!==` test, the IEEE-754 rule the rest
|
|
2011
|
+
* of the encoder follows, and the only reading under which an `fp32`/`fp64`
|
|
2012
|
+
* `NaN` element survives the round trip rather than being omitted as "default".
|
|
2013
|
+
* There is no identity short-circuit for the same reason: `elementsEqual(x, x)`
|
|
2014
|
+
* has to give the same answer as `elementsEqual(x, x.slice())`.
|
|
2015
|
+
*/
|
|
2016
|
+
declare function elementsEqual(a: ArrayLike<unknown>, b: ArrayLike<unknown>): boolean;
|
|
2017
|
+
/**
|
|
2018
|
+
* The {@link Long} flavour of {@link elementsEqual}: same length, and the same
|
|
2019
|
+
* 64-bit value at every index.
|
|
2020
|
+
*
|
|
2021
|
+
* A separate function rather than an argument to the one above, because the
|
|
2022
|
+
* comparison itself is different. A `Long` is an *object identity* — two
|
|
2023
|
+
* instances holding the same 64 bits are `!==` — so `elementsEqual`'s `===` would
|
|
2024
|
+
* report every `Long`-backed array as unequal to its schema default and the
|
|
2025
|
+
* ≠-default test (MESSAGE_SPEC §2) would emit a field that should have been
|
|
2026
|
+
* omitted. Comparing the `(low, high)` halves is what "same value" means for this
|
|
2027
|
+
* representation, and it is the only 64-bit representation that needs it:
|
|
2028
|
+
* `bigint` and `number` elements are values already and go through
|
|
2029
|
+
* {@link elementsEqual}.
|
|
2030
|
+
*
|
|
2031
|
+
* Which arrays are handed over is the declared type's business, so there is no
|
|
2032
|
+
* schema in here either (ARCHITECTURE §8).
|
|
2033
|
+
*/
|
|
2034
|
+
declare function longElementsEqual(a: ArrayLike<Long>, b: ArrayLike<Long>): boolean;
|
|
2035
|
+
|
|
2036
|
+
/**
|
|
2037
|
+
* A fresh 4-byte companion holding one fp32 word's wire image: the four
|
|
2038
|
+
* little-endian bytes of its 32-bit word, byte `k` in bits `8*k`.
|
|
2039
|
+
*
|
|
2040
|
+
* Generated-layer support, not a codec path: an `fp32` field whose value is a
|
|
2041
|
+
* `NaN` cannot be re-encoded from the `number` a JS host stored it in — the host
|
|
2042
|
+
* normalizes the payload bits — so the generated message keeps the four raw wire
|
|
2043
|
+
* bytes beside the value and re-emits those (MESSAGE_SPEC §6.5). What the decoder
|
|
2044
|
+
* hands over is the 32-bit *word*, because a number costs nothing to pass where
|
|
2045
|
+
* the byte view it replaced was an allocation per value and a borrowed slice
|
|
2046
|
+
* §6.7 forbids; turning that word back into bytes is this function, and it is the
|
|
2047
|
+
* same four shifts for every schema (ARCHITECTURE §8).
|
|
2048
|
+
*
|
|
2049
|
+
* One function and not two. An `into(out, off, bits)` flavour beside it would be
|
|
2050
|
+
* the natural companion — it is what the fp32 *array* path would want — but that
|
|
2051
|
+
* path was retired before this one moved here (§6.7 killed the view), so the
|
|
2052
|
+
* whole family builds this companion per `NaN` scalar and nothing else calls it.
|
|
2053
|
+
* Public API with no consumer drifts, so there is exactly one entry point and it
|
|
2054
|
+
* allocates: the allocation is per `NaN`, not per field.
|
|
2055
|
+
*/
|
|
2056
|
+
declare function fp32RawBytes(bits: number): Uint8Array;
|
|
698
2057
|
|
|
699
2058
|
/**
|
|
700
2059
|
* The acceleration seam.
|
|
@@ -702,19 +2061,47 @@ declare class Cursor {
|
|
|
702
2061
|
* The encoder's bulk array paths run through a {@link Kernel} — a small set of
|
|
703
2062
|
* self-contained, buffer-oriented transforms over a region the caller has
|
|
704
2063
|
* already sized. The default {@link "./js"} kernel is pure TypeScript and works
|
|
705
|
-
* everywhere
|
|
706
|
-
*
|
|
707
|
-
*
|
|
2064
|
+
* everywhere, and is the active kernel unless the caller replaces it: no
|
|
2065
|
+
* accelerated build is published, and this interface is the entire seam for one
|
|
2066
|
+
* (#115). A C++ (N-API) or WebAssembly build implements the same interface and
|
|
2067
|
+
* is swapped in with {@link setKernel} — the caller loads its own module and
|
|
2068
|
+
* hands over the kernel it builds — for a speed-up with **no change to the
|
|
2069
|
+
* public API**. The boundary is deliberately *bulk* (a whole
|
|
708
2070
|
* array per call, into guaranteed capacity) so the cost of crossing into native
|
|
709
2071
|
* code is amortised — never one call per element.
|
|
710
2072
|
*/
|
|
711
2073
|
/**
|
|
712
|
-
* Bulk
|
|
2074
|
+
* Bulk transforms used on the encoder's fast path.
|
|
713
2075
|
*
|
|
714
|
-
* Every method writes into `out` starting at `pos
|
|
715
|
-
*
|
|
716
|
-
*
|
|
717
|
-
*
|
|
2076
|
+
* Every method writes into `out` starting at `pos` and returns the position just
|
|
2077
|
+
* past the last byte written.
|
|
2078
|
+
*
|
|
2079
|
+
* **`out.length` is the bound, and the only one.** The two packers are handed a
|
|
2080
|
+
* region the caller has already sized exactly (4 or 8 bytes an element, a number
|
|
2081
|
+
* the caller can compute), so for them "enough room" is a guarantee. The two
|
|
2082
|
+
* *varint* kernels are not: a varint's length depends on the value, so the caller
|
|
2083
|
+
* cannot know the size without encoding it, and asking the source how wide its
|
|
2084
|
+
* elements are is exactly the guess that silently truncated messages (§5.1). So a
|
|
2085
|
+
* varint kernel must
|
|
2086
|
+
*
|
|
2087
|
+
* * **never write at or past `out.length`**, and
|
|
2088
|
+
* * **keep counting anyway**, returning the position it *would* have reached.
|
|
2089
|
+
*
|
|
2090
|
+
* The caller compares that against `out.length`: past the end means the message
|
|
2091
|
+
* does not fit, which in the block mode is `BUFFER_FULL` — the answer that mode
|
|
2092
|
+
* exists to give. The JS kernel satisfies this for free (a typed-array store past
|
|
2093
|
+
* the end is dropped while `pos` keeps advancing); a native or WebAssembly kernel
|
|
2094
|
+
* must implement it deliberately, counting the remaining elements without storing. Headers, counts and flushing stay in the stream classes; a kernel
|
|
2095
|
+
* only moves bytes — with one obligation it cannot delegate, because it is the
|
|
2096
|
+
* only code that ever looks at the elements: **the integer kernels must reject
|
|
2097
|
+
* an element outside the 64-bit value domain** (CORELIB_PLAN §6.2 — `0 .. 2^64
|
|
2098
|
+
* - 1` unsigned, `-2^63 .. 2^63 - 1` signed) by throwing `argumentError`,
|
|
2099
|
+
* rather than reducing it modulo 2^64. The stream classes range-check the same
|
|
2100
|
+
* values on their element-at-a-time streaming path, so a kernel that skipped
|
|
2101
|
+
* the check would make the wire depend on which constructor the caller used
|
|
2102
|
+
* (#106). The check is one store, one load and one compare on the `bigint`
|
|
2103
|
+
* branch (`splitU64` / `splitI64`); the `number` fast paths are already gated
|
|
2104
|
+
* on the domain and pay nothing.
|
|
718
2105
|
*/
|
|
719
2106
|
interface Kernel {
|
|
720
2107
|
/** A short identifier, surfaced in diagnostics and the parity tests. */
|
|
@@ -749,36 +2136,6 @@ declare function getKernel(): Kernel;
|
|
|
749
2136
|
*/
|
|
750
2137
|
declare const jsKernel: Kernel;
|
|
751
2138
|
|
|
752
|
-
/**
|
|
753
|
-
* Attempt to load and install the native kernel.
|
|
754
|
-
*
|
|
755
|
-
* @returns `true` if a valid native kernel was installed, `false` if it could
|
|
756
|
-
* not be loaded (not on Node, addon not installed, or wrong shape). Never
|
|
757
|
-
* throws for a missing addon — the JS kernel remains the fallback.
|
|
758
|
-
*/
|
|
759
|
-
declare function loadNativeKernel(): Promise<boolean>;
|
|
760
|
-
|
|
761
|
-
/**
|
|
762
|
-
* Optional WebAssembly acceleration loader.
|
|
763
|
-
*
|
|
764
|
-
* Unlike the native addon, a WASM kernel runs in the browser too. This module
|
|
765
|
-
* does nothing at import time; call {@link loadWasmKernel} with the compiled
|
|
766
|
-
* module's bytes (or a streaming source) to instantiate it and install it as
|
|
767
|
-
* the active {@link Kernel}. A real WASM build is shipped separately; this
|
|
768
|
-
* loader is the stable entry point the rest of the library is wired through.
|
|
769
|
-
*/
|
|
770
|
-
|
|
771
|
-
/** A factory the WASM glue exposes: given the instance exports, build a Kernel. */
|
|
772
|
-
type WasmKernelFactory = (exports: WebAssembly.Exports) => Kernel;
|
|
773
|
-
/**
|
|
774
|
-
* Instantiate a WASM module and install the kernel it produces.
|
|
775
|
-
*
|
|
776
|
-
* @param source compiled-module bytes, a `Response`/stream, or a ready module.
|
|
777
|
-
* @param factory wraps the instance exports into a {@link Kernel}.
|
|
778
|
-
* @returns `true` once installed; throws only if instantiation itself fails.
|
|
779
|
-
*/
|
|
780
|
-
declare function loadWasmKernel(source: BufferSource | Response | PromiseLike<Response> | WebAssembly.Module, factory: WasmKernelFactory, imports?: WebAssembly.Imports): Promise<boolean>;
|
|
781
|
-
|
|
782
2139
|
/**
|
|
783
2140
|
* The complete public surface of the library, re-exported by {@link "./index"}
|
|
784
2141
|
* both as flat named exports and, aggregated, under the `sofab` namespace.
|
|
@@ -787,39 +2144,55 @@ declare function loadWasmKernel(source: BufferSource | Response | PromiseLike<Re
|
|
|
787
2144
|
declare const _public_API_VERSION: typeof API_VERSION;
|
|
788
2145
|
declare const _public_ARRAY_MAX: typeof ARRAY_MAX;
|
|
789
2146
|
type _public_ArrayKind = ArrayKind;
|
|
790
|
-
type
|
|
791
|
-
|
|
792
|
-
|
|
2147
|
+
type _public_ArrayTarget = ArrayTarget;
|
|
2148
|
+
type _public_BlobSeq = BlobSeq;
|
|
2149
|
+
declare const _public_BlobSeq: typeof BlobSeq;
|
|
2150
|
+
type _public_BoolArrayTarget = BoolArrayTarget;
|
|
793
2151
|
type _public_DecodeStatus = DecodeStatus;
|
|
2152
|
+
type _public_ElementSeq<T> = ElementSeq<T>;
|
|
2153
|
+
declare const _public_ElementSeq: typeof ElementSeq;
|
|
794
2154
|
declare const _public_FIXLEN_MAX: typeof FIXLEN_MAX;
|
|
2155
|
+
type _public_FeedStatus = FeedStatus;
|
|
795
2156
|
type _public_FixlenSubtype = FixlenSubtype;
|
|
2157
|
+
type _public_FloatArrayTarget = FloatArrayTarget;
|
|
796
2158
|
type _public_FlushSink = FlushSink;
|
|
2159
|
+
type _public_FramedSeq<T> = FramedSeq<T>;
|
|
2160
|
+
declare const _public_FramedSeq: typeof FramedSeq;
|
|
797
2161
|
declare const _public_I64_MAX: typeof I64_MAX;
|
|
798
2162
|
declare const _public_I64_MIN: typeof I64_MIN;
|
|
799
2163
|
declare const _public_ID_MAX: typeof ID_MAX;
|
|
800
2164
|
type _public_IStream = IStream;
|
|
801
2165
|
declare const _public_IStream: typeof IStream;
|
|
2166
|
+
type _public_IntegerArrayTarget = IntegerArrayTarget;
|
|
802
2167
|
type _public_Kernel = Kernel;
|
|
803
2168
|
type _public_Long = Long;
|
|
804
2169
|
declare const _public_Long: typeof Long;
|
|
805
2170
|
declare const _public_MAX_DEPTH: typeof MAX_DEPTH;
|
|
2171
|
+
declare const _public_MIN_OUTPUT_BUFFER: typeof MIN_OUTPUT_BUFFER;
|
|
806
2172
|
type _public_OStream = OStream;
|
|
807
2173
|
declare const _public_OStream: typeof OStream;
|
|
2174
|
+
type _public_PayloadAcc = PayloadAcc;
|
|
2175
|
+
declare const _public_PayloadAcc: typeof PayloadAcc;
|
|
808
2176
|
type _public_SofabError = SofabError;
|
|
809
2177
|
declare const _public_SofabError: typeof SofabError;
|
|
810
2178
|
type _public_SofabErrorCode = SofabErrorCode;
|
|
2179
|
+
type _public_StringSeq = StringSeq;
|
|
2180
|
+
declare const _public_StringSeq: typeof StringSeq;
|
|
811
2181
|
declare const _public_U64_MAX: typeof U64_MAX;
|
|
2182
|
+
declare const _public_UNBOUNDED: typeof UNBOUNDED;
|
|
812
2183
|
type _public_Visitor = Visitor;
|
|
813
|
-
type _public_WasmKernelFactory = WasmKernelFactory;
|
|
814
2184
|
type _public_WireType = WireType;
|
|
815
2185
|
declare const _public_decode: typeof decode;
|
|
2186
|
+
declare const _public_decodeUtf8: typeof decodeUtf8;
|
|
2187
|
+
declare const _public_elementsEqual: typeof elementsEqual;
|
|
2188
|
+
declare const _public_fp32RawBytes: typeof fp32RawBytes;
|
|
816
2189
|
declare const _public_getKernel: typeof getKernel;
|
|
2190
|
+
declare const _public_growingOStream: typeof growingOStream;
|
|
817
2191
|
declare const _public_jsKernel: typeof jsKernel;
|
|
818
|
-
declare const
|
|
819
|
-
declare const _public_loadWasmKernel: typeof loadWasmKernel;
|
|
2192
|
+
declare const _public_longElementsEqual: typeof longElementsEqual;
|
|
820
2193
|
declare const _public_setKernel: typeof setKernel;
|
|
821
2194
|
declare namespace _public {
|
|
822
|
-
export { _public_API_VERSION as API_VERSION, _public_ARRAY_MAX as ARRAY_MAX, type _public_ArrayKind as ArrayKind,
|
|
2195
|
+
export { _public_API_VERSION as API_VERSION, _public_ARRAY_MAX as ARRAY_MAX, type _public_ArrayKind as ArrayKind, type _public_ArrayTarget as ArrayTarget, _public_BlobSeq as BlobSeq, type _public_BoolArrayTarget as BoolArrayTarget, type _public_DecodeStatus as DecodeStatus, _public_ElementSeq as ElementSeq, _public_FIXLEN_MAX as FIXLEN_MAX, type _public_FeedStatus as FeedStatus, type _public_FixlenSubtype as FixlenSubtype, type _public_FloatArrayTarget as FloatArrayTarget, type _public_FlushSink as FlushSink, _public_FramedSeq as FramedSeq, _public_I64_MAX as I64_MAX, _public_I64_MIN as I64_MIN, _public_ID_MAX as ID_MAX, _public_IStream as IStream, type _public_IntegerArrayTarget as IntegerArrayTarget, type _public_Kernel as Kernel, _public_Long as Long, _public_MAX_DEPTH as MAX_DEPTH, _public_MIN_OUTPUT_BUFFER as MIN_OUTPUT_BUFFER, _public_OStream as OStream, _public_PayloadAcc as PayloadAcc, _public_SofabError as SofabError, type _public_SofabErrorCode as SofabErrorCode, _public_StringSeq as StringSeq, _public_U64_MAX as U64_MAX, _public_UNBOUNDED as UNBOUNDED, type _public_Visitor as Visitor, type _public_WireType as WireType, _public_decode as decode, _public_decodeUtf8 as decodeUtf8, _public_elementsEqual as elementsEqual, _public_fp32RawBytes as fp32RawBytes, _public_getKernel as getKernel, _public_growingOStream as growingOStream, _public_jsKernel as jsKernel, _public_longElementsEqual as longElementsEqual, _public_setKernel as setKernel };
|
|
823
2196
|
}
|
|
824
2197
|
|
|
825
|
-
export { API_VERSION, ARRAY_MAX, ArrayKind,
|
|
2198
|
+
export { API_VERSION, ARRAY_MAX, ArrayKind, type ArrayTarget, BlobSeq, type BoolArrayTarget, DecodeStatus, ElementSeq, FIXLEN_MAX, type FeedStatus, FixlenSubtype, type FloatArrayTarget, type FlushSink, FramedSeq, I64_MAX, I64_MIN, ID_MAX, IStream, type IntegerArrayTarget, type Kernel, Long, MAX_DEPTH, MIN_OUTPUT_BUFFER, OStream, PayloadAcc, SofabError, SofabErrorCode, StringSeq, U64_MAX, UNBOUNDED, type Visitor, WireType, decode, decodeUtf8, elementsEqual, fp32RawBytes, getKernel, growingOStream, jsKernel, longElementsEqual, setKernel, _public as sofab };
|