@zakkster/lite-bake-stream 1.0.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 +457 -0
- package/LICENSE +21 -0
- package/README.md +150 -0
- package/SPEC.md +364 -0
- package/llms.txt +81 -0
- package/package.json +117 -0
- package/src/FileIngest.js +104 -0
- package/src/MultiReader.js +160 -0
- package/src/PreserveReader.js +180 -0
- package/src/PreserveTokenizer.js +172 -0
- package/src/PreserveWriter.js +218 -0
- package/src/RangeReader.js +470 -0
- package/src/Reader.js +349 -0
- package/src/Split.js +359 -0
- package/src/StringTable.js +225 -0
- package/src/Tokenizer.js +691 -0
- package/src/Writer.js +713 -0
- package/src/index.js +193 -0
- package/types/FileIngest.d.ts +44 -0
- package/types/MultiReader.d.ts +36 -0
- package/types/PreserveReader.d.ts +44 -0
- package/types/PreserveTokenizer.d.ts +27 -0
- package/types/PreserveWriter.d.ts +35 -0
- package/types/RangeReader.d.ts +93 -0
- package/types/Reader.d.ts +85 -0
- package/types/Split.d.ts +66 -0
- package/types/StringTable.d.ts +23 -0
- package/types/Tokenizer.d.ts +42 -0
- package/types/Writer.d.ts +92 -0
- package/types/index.d.ts +58 -0
package/README.md
ADDED
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
# @zakkster/lite-bake-stream
|
|
2
|
+
|
|
3
|
+
[](https://www.npmjs.com/package/@zakkster/lite-bake-stream)
|
|
4
|
+
[](https://github.com/sponsors/PeshoVurtoleta)
|
|
5
|
+

|
|
6
|
+
[](https://bundlephobia.com/result?p=@zakkster/lite-bake-stream)
|
|
7
|
+
[](https://www.npmjs.com/package/@zakkster/lite-bake-stream)
|
|
8
|
+
[](https://www.npmjs.com/package/@zakkster/lite-bake-stream)
|
|
9
|
+

|
|
10
|
+

|
|
11
|
+

|
|
12
|
+
[](./LICENSE)
|
|
13
|
+
|
|
14
|
+
> Streaming byte-level JSON compiler for [`@zakkster/lite-bake`](https://github.com/PeshoVurtoleta/lite-bake). Zero-GC, tree-shakeable, gigabyte-scale.
|
|
15
|
+
|
|
16
|
+
**Status:** v1.0.0. LBK1 format frozen at `format_version: 1`. Qualified on an 8 GB soak with zero GC and byte-exact preservation across 590 million cells.
|
|
17
|
+
|
|
18
|
+
## Two modes, one API
|
|
19
|
+
|
|
20
|
+
- **Schema mode** (default): F64/U32 lane packing, zone-maps query pruning, byte-exact preservation for flat records. Millions of rows/sec random access, zero-GC hot path.
|
|
21
|
+
- **Preserve mode**: `serialize(input, { preserve: true })`. Opaque byte blobs — the module doesn't crack open records, it just moves them intact. Deeply nested API JSON, arrays, mixed types — any valid JSON round-trips byte-for-byte identical. Reader exposes `getBytes(i)` (zero-alloc), `getString(i)`, `getJSON(i)`.
|
|
22
|
+
|
|
23
|
+
`deserialize()` auto-dispatches on the container's flag bit. If you're compiling gigabytes of API responses with 2-3 levels of nesting, preserve mode is what you want. If the data is flat and you want columnar-style range queries, schema mode is what you want.
|
|
24
|
+
|
|
25
|
+
## Why this exists
|
|
26
|
+
|
|
27
|
+
`lite-bake` compiles a JS array into a flat binary buffer. That's the destination format you want. But if your input is a gigabyte of JSON, you can't call `JSON.parse` on it — V8's string limit is ~512 MB, and even under that, the intermediate object graph will trash the heap and destroy your process before `bake()` ever runs.
|
|
28
|
+
|
|
29
|
+
`lite-bake-stream` is the front door: a chunk-safe UTF-8 JSON SAX scanner that reads bytes off a stream and emits `lite-bake`-shaped output shards, without ever materializing the object graph.
|
|
30
|
+
|
|
31
|
+
## Qualified at 8 GB
|
|
32
|
+
|
|
33
|
+
Overnight soak, M1 MacBook Pro, both release gates armed:
|
|
34
|
+
|
|
35
|
+
| | |
|
|
36
|
+
| :-- | --: |
|
|
37
|
+
| Source | 8.00 GB NDJSON |
|
|
38
|
+
| Container | 4.89 GB (61.1% of source) |
|
|
39
|
+
| Rows | 98,367,702 |
|
|
40
|
+
| Ingest throughput | 110.0 MB/s |
|
|
41
|
+
| **Major GC** | **0** |
|
|
42
|
+
| **Minor GC** | **0** |
|
|
43
|
+
| **Total heap allocation** | **499.2 KB** |
|
|
44
|
+
| Cells verified | 590,206,212 |
|
|
45
|
+
| **Preservation mismatches** | **0** |
|
|
46
|
+
|
|
47
|
+
**499 KB of heap allocation to compile 8 GB of JSON.** Total, for the whole run — not per shard, not per second. Zero garbage collections of any kind. Every one of 590 million declared cells round-tripped byte-exact.
|
|
48
|
+
|
|
49
|
+
Tokenizer throughput is ~55% of `JSON.parse` (237 MB/s vs 416 MB/s on a 50 MB fixture). That's the trade: you give up some raw speed and you get an input size ceiling that's bounded by your disk instead of your heap.
|
|
50
|
+
|
|
51
|
+
## Quickstart
|
|
52
|
+
|
|
53
|
+
**Schema mode** (flat records, columnar queries):
|
|
54
|
+
|
|
55
|
+
```js
|
|
56
|
+
import { serialize, deserialize } from '@zakkster/lite-bake-stream';
|
|
57
|
+
|
|
58
|
+
const container = serialize(ndjsonBytes);
|
|
59
|
+
const reader = deserialize(container);
|
|
60
|
+
reader.get(0, 'id'); // 1
|
|
61
|
+
reader.findShards('id', { min: 500 }); // shard pruning via zone maps
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
**Preserve mode** (any JSON shape, byte-exact passthrough):
|
|
65
|
+
|
|
66
|
+
```js
|
|
67
|
+
import { serialize, deserialize } from '@zakkster/lite-bake-stream';
|
|
68
|
+
|
|
69
|
+
// Handles arbitrarily nested API JSON
|
|
70
|
+
const container = serialize(apiResponseBytes, { preserve: true });
|
|
71
|
+
const reader = deserialize(container);
|
|
72
|
+
reader.getBytes(0); // Uint8Array view, zero allocation
|
|
73
|
+
reader.getString(0); // TextDecoder allocation
|
|
74
|
+
reader.getJSON(0); // full JSON.parse, allocates the parsed value graph
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
For streaming ingest with progress, use `/file-ingest`. For HTTP Range-fetched containers (schema mode), `/range-reader`. For joining multiple containers into one logical view (schema mode), `/multi-reader`. For parallel or checkpointed compilation (schema mode), `/split`.
|
|
78
|
+
|
|
79
|
+
## What shipped in 1.0
|
|
80
|
+
|
|
81
|
+
| Milestone | Deliverable |
|
|
82
|
+
| :-------- | :-------------------------------------------------------------------------- |
|
|
83
|
+
| ✅ **M0** | LBK1 container format spec, forward-compat seams for the I64 lane. |
|
|
84
|
+
| ✅ **M1** | Chunk-safe tokenizer; zero-GC on the hot path after warmup. |
|
|
85
|
+
| ✅ **M2** | Schema-driven writer; sample-and-infer path; typed-lane shard buffers. |
|
|
86
|
+
| ✅ **M3** | Per-shard UTF-8 string table with pre-alloc typed-array hash-map interning. |
|
|
87
|
+
| ✅ **M4** | `serialize()`/`deserialize()`, MultiReader, full type declarations. |
|
|
88
|
+
| ✅ **M5** | Browser reader, HTTP Range fetch, `File.stream()` client-side ingest. |
|
|
89
|
+
| ✅ **M6** | NDJSON splitter + per-part compilation + container merge. |
|
|
90
|
+
| ✅ **M7** | Per-shard zone maps (min/max per F64 column) + query-pruning APIs. |
|
|
91
|
+
| ✅ **M8** | RFC 8259 conformance corpus + robustness fuzz. |
|
|
92
|
+
|
|
93
|
+
## Roadmap beyond 1.0
|
|
94
|
+
|
|
95
|
+
All of these fit the format's existing forward-compat seams — `format_version` stays at `1`.
|
|
96
|
+
|
|
97
|
+
| | |
|
|
98
|
+
| :-- | :-- |
|
|
99
|
+
| **I64 lane** | Exact 64-bit integer preservation. FieldDescriptor flags are already reserved for it; shards using it set `min_reader_version: 2`. |
|
|
100
|
+
| **Columnar payload mode** | Column-contiguous shard payloads instead of interleaved rows. Better compression, SIMD-friendly scans. Opt-in via a shard flag. |
|
|
101
|
+
| **Container-level string table** | Cross-shard interning for high-repetition corpora. Lands in the `metadata_off` block alongside zone maps. |
|
|
102
|
+
| **Oscilloscope-rack demo** | Reel-to-reel tape transport at actual MB/s, waterfall spectrum per shard, VU needles, seven-segment counters. Locked 60fps on an iPhone 7 with 100 MB NDJSON. |
|
|
103
|
+
|
|
104
|
+
## What's here today
|
|
105
|
+
|
|
106
|
+
- `SPEC.md` — LBK1 container spec draft 0.1, per-shard string table (§3.3), preservation contract (§7).
|
|
107
|
+
- `src/index.js` — top-level convenience API: `serialize(input)` and `deserialize(bytes)` plus class re-exports.
|
|
108
|
+
- `src/Tokenizer.js` — chunk-safe SAX tokenizer with Clinger's fast-path decimal→F64 conversion.
|
|
109
|
+
- `src/Writer.js` — LBK1 shard emitter with F64 and U32 lanes, sample-and-infer schema, per-shard string tables, per-shard zone maps.
|
|
110
|
+
- `src/Reader.js` — LBK1 container parser with typed-array views, transparent string-field resolution, and zone-map query APIs.
|
|
111
|
+
- `src/StringTable.js` — open-addressed byte-level interning with zero-alloc hit path.
|
|
112
|
+
- `src/RangeReader.js` — LBK1 reader with lazy shard loading via an IO adapter (HTTP Range or in-memory mock); zone maps loaded once at open enable synchronous query pruning.
|
|
113
|
+
- `src/MultiReader.js` — union view over N Reader instances with matching schemas; merged shard indices for query pruning across containers.
|
|
114
|
+
- `src/Split.js` — NDJSON splitter, per-part compilation, and container merge for worker-parallel or checkpointed ingest.
|
|
115
|
+
- `src/FileIngest.js` — browser helper: `File.stream()` → Reader. Any `ReadableStream<Uint8Array>` works.
|
|
116
|
+
- `types/*.d.ts` — 8 hand-written TypeScript declaration files, one per subpath.
|
|
117
|
+
- `demo/` — single-file HTML + module JS. Ingest a JSON file → LBK1 → browse in `@zakkster/lite-table`. Zone-maps query-pruning panel visualizes the fetch-set reduction. `npm run demo`.
|
|
118
|
+
- `test/*.test.js` — 233 tests across 12 files, including a 66-fixture RFC 8259 conformance corpus and a 7-scenario robustness fuzz (~2,300 tokenizer invocations per run).
|
|
119
|
+
- `test/_verify.js` — universal round-trip verifier used by tests, torture, and soak.
|
|
120
|
+
- `bench/bench-tokenizer.js` — interleaved-rep bench vs `JSON.parse`.
|
|
121
|
+
- `bench/torture.js` — 37-scenario CI harness. Two release gates: `maxMajor: 0` (zero-GC) AND `verify: true` (value-exact round-trip on 77,500+ rows per run).
|
|
122
|
+
- `bench/soak.js` — CLI-configurable soak with the preservation gate wired in.
|
|
123
|
+
|
|
124
|
+
Two release gates. Zero major GC AND every declared cell round-trips exactly. If either fails, no publish.
|
|
125
|
+
|
|
126
|
+
## Release testing tiers
|
|
127
|
+
|
|
128
|
+
The gates run at four scales, chosen to fit different hardware and time budgets:
|
|
129
|
+
|
|
130
|
+
| Command | Scale | Use |
|
|
131
|
+
| :-- | :-- | :-- |
|
|
132
|
+
| `npm test` | 96 tests | Dev loop, every save |
|
|
133
|
+
| `npm run torture` | 37 scenarios (~1 MB each) | Before every commit |
|
|
134
|
+
| `npm run torture:full` | + 500 MB full-tier soak scenarios | Before every publish |
|
|
135
|
+
| `npm run soak` | 100 MB with preservation gate | Sanity check |
|
|
136
|
+
| `npm run soak:500` | 500 MB | Pre-publish scale check |
|
|
137
|
+
| `npm run soak:1gb` | 1 GB | Real-hardware qualification (M4 Pro etc.) |
|
|
138
|
+
| `npm run soak:overnight` | 8 GB | Overnight `caffeinate` run |
|
|
139
|
+
|
|
140
|
+
Custom sizes:
|
|
141
|
+
```
|
|
142
|
+
node --expose-gc bench/soak.js --gb=4 --verify
|
|
143
|
+
caffeinate node --expose-gc bench/soak.js --gb=16 --verify # macOS overnight
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
Every soak run reports ingest throughput (MB/s), verification throughput (rows/s), the zero-GC gate result, container compression ratio, and progress lines so long runs are observable. The preservation gate proves — cell by cell — that every declared field of every row round-trips exactly.
|
|
147
|
+
|
|
148
|
+
## License
|
|
149
|
+
|
|
150
|
+
MIT © 2026 Zahary Shinikchiev
|
package/SPEC.md
ADDED
|
@@ -0,0 +1,364 @@
|
|
|
1
|
+
# LBK1 Format Specification
|
|
2
|
+
|
|
3
|
+
> Version 1 — frozen as of `@zakkster/lite-bake-stream` v1.0.0.
|
|
4
|
+
> Future lane kinds and payload modes land via the forward-compat seams
|
|
5
|
+
> (`min_reader_version`, reserved FieldDescriptor flags, the `metadata_off`
|
|
6
|
+
> block wrapper) without a `format_version` bump.
|
|
7
|
+
> Copyright (c) 2026 Zahary Shinikchiev. MIT.
|
|
8
|
+
|
|
9
|
+
## 0. Scope
|
|
10
|
+
|
|
11
|
+
LBK1 is the on-disk / on-wire container for streaming-compiled `lite-bake` datasets. It carries one or more shards of interleaved binary rows plus the metadata needed to reconstruct a Reader over the whole dataset without re-parsing JSON.
|
|
12
|
+
|
|
13
|
+
The tokenizer specified in section 5 is the reference producer path from UTF-8 JSON (top-level array or NDJSON) into an LBK1 container. Non-JSON producers are allowed as long as they emit a well-formed container.
|
|
14
|
+
|
|
15
|
+
Numbers on the hot path are F64. Section 4.3 reserves the schema-level bit that will admit a future `I64_BYTES_PRESERVED` lane without a container format-version bump.
|
|
16
|
+
|
|
17
|
+
## 1. Design invariants
|
|
18
|
+
|
|
19
|
+
These are load-bearing; every other decision follows from them.
|
|
20
|
+
|
|
21
|
+
1. **Streaming-first.** A producer must be able to emit a valid, seekable container without buffering the entire input. Readers must be able to consume shard N without having seen shards 0..N-1.
|
|
22
|
+
2. **Forward-compatible via strict decoders.** Unknown flags or lane kinds are rejected, not ignored. Silent corruption is the failure mode we refuse.
|
|
23
|
+
3. **Shard-scoped versioning.** A shard header carries the minimum reader version required to decode that shard. Old readers refuse new shards cleanly; new readers read old shards forever.
|
|
24
|
+
4. **Zero-copy where the CPU allows it.** Row payloads are aligned so that typed-array views over the shard buffer read fields with native alignment.
|
|
25
|
+
5. **Zero runtime dependencies.** Reference producer and reader share the `@zakkster` no-dep constraint.
|
|
26
|
+
|
|
27
|
+
## 2. Notation
|
|
28
|
+
|
|
29
|
+
- `u8`, `u16`, `u32`, `u64`, `f32`, `f64` — little-endian unless stated.
|
|
30
|
+
- `bytes(n)` — a raw byte run of length n.
|
|
31
|
+
- `varlen_utf8` — u32 length prefix followed by that many UTF-8 bytes.
|
|
32
|
+
- Offsets are relative to the start of the container unless stated.
|
|
33
|
+
- All multi-byte integers in headers are little-endian. Row payloads use native endianness with a byte-order marker in the container header (section 3.1); non-native containers require a byteswap on load or must be rejected.
|
|
34
|
+
|
|
35
|
+
## 3. Container layout
|
|
36
|
+
|
|
37
|
+
```
|
|
38
|
+
+---------------------+ offset 0
|
|
39
|
+
| Container Header |
|
|
40
|
+
+---------------------+
|
|
41
|
+
| Schema Block |
|
|
42
|
+
+---------------------+
|
|
43
|
+
| String Table Segment| (optional; length 0 = absent)
|
|
44
|
+
+---------------------+
|
|
45
|
+
| Shard Directory |
|
|
46
|
+
+---------------------+
|
|
47
|
+
| Shard 0 payload |
|
|
48
|
+
| Shard 1 payload |
|
|
49
|
+
| ... |
|
|
50
|
+
+---------------------+
|
|
51
|
+
| Container Footer |
|
|
52
|
+
+---------------------+
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
### 3.1 Container Header (48 bytes, fixed)
|
|
56
|
+
|
|
57
|
+
| Offset | Size | Field | Notes |
|
|
58
|
+
| -----: | ---: | -------------------- | --------------------------------------------------------- |
|
|
59
|
+
| 0 | 4 | `magic` | ASCII `L B K 1` = `0x4C 0x42 0x4B 0x31` |
|
|
60
|
+
| 4 | 2 | `format_version` | u16. This spec = `1`. |
|
|
61
|
+
| 6 | 1 | `endian` | `0x01` = little, `0x02` = big. Determines row payload byte order. |
|
|
62
|
+
| 7 | 1 | `flags` | bit 0 = **preserve mode** (opaque byte blobs, any JSON shape). bits 1-7 reserved, must be 0. |
|
|
63
|
+
| 8 | 8 | `schema_block_off` | u64 byte offset to schema block. |
|
|
64
|
+
| 16 | 8 | `metadata_off` | u64 byte offset to an optional metadata block (M7+). `0` = none. Currently carries zone maps only; wrapper block reserved for M4+ container-level string table extensions. |
|
|
65
|
+
| 24 | 8 | `shard_directory_off`| u64. |
|
|
66
|
+
| 32 | 4 | `shard_count` | u32. |
|
|
67
|
+
| 36 | 4 | `reserved1` | Must be `0x00000000`. |
|
|
68
|
+
| 40 | 8 | `total_rows` | u64. Sum of `row_count` across all shards. |
|
|
69
|
+
|
|
70
|
+
Header uses u64 offsets throughout so containers can safely exceed 4 GiB. A container that stores 8 GB of data across ~120 shards would have late-shard offsets past the u32 boundary; truncating to u32 would point the reader at arbitrary earlier bytes (regression covered by `test/OffsetOverflow.test.js`). Header stays 8-byte aligned; readers reject `reserved0`/`reserved1` non-zero.
|
|
71
|
+
|
|
72
|
+
### 3.2 Schema Block
|
|
73
|
+
|
|
74
|
+
```
|
|
75
|
+
u32 field_count
|
|
76
|
+
u32 row_stride_bytes // padded to 8
|
|
77
|
+
FieldDescriptor[field_count]
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
Padded with zero bytes so the block ends on an 8-byte boundary.
|
|
81
|
+
|
|
82
|
+
### 3.3 String Table Segment
|
|
83
|
+
|
|
84
|
+
There are two string-table locations in an LBK1 container:
|
|
85
|
+
|
|
86
|
+
- **Container-level** at `string_table_off` (v1 reserved; not written by the current Writer). Reserved for future container-wide interning across shards.
|
|
87
|
+
- **Per-shard** at each ShardEntry's `local_string_off` (M3, implemented). Every shard with U32-lane fields has its own local string table so shards remain independently decodable — critical for HTTP Range fetches (M5+) and worker-parallel readers (M6+).
|
|
88
|
+
|
|
89
|
+
Both use the same on-disk layout:
|
|
90
|
+
|
|
91
|
+
```
|
|
92
|
+
u32 entry_count
|
|
93
|
+
u32 blob_length_bytes
|
|
94
|
+
u32 offsets[entry_count + 1] // byte offsets into blob, INCLUDING a trailing sentinel = blob_length_bytes
|
|
95
|
+
u8 blob[blob_length_bytes]
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
Padded to 8. Strings are UTF-8, not null-terminated. Length of string `i` is `offsets[i+1] - offsets[i]`; the trailing sentinel makes this valid for `i = entry_count - 1` without a branch. Entry index 0 is not reserved; the empty string is a legitimate entry.
|
|
99
|
+
|
|
100
|
+
Per-shard tables are placed immediately after their shard payload in the container, so a single HTTP Range fetch covering `[payload_off, payload_off + payload_len + local_string_len)` retrieves both.
|
|
101
|
+
|
|
102
|
+
### 3.4 Shard Directory
|
|
103
|
+
|
|
104
|
+
```
|
|
105
|
+
ShardEntry[shard_count]
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
Each entry (40 bytes):
|
|
109
|
+
|
|
110
|
+
| Offset | Size | Field | Notes |
|
|
111
|
+
| -----: | ---: | -------------------- | --------------------------------------------------------- |
|
|
112
|
+
| 0 | 8 | `payload_off` | u64 offset to shard payload. |
|
|
113
|
+
| 8 | 4 | `payload_len_bytes` | u32. |
|
|
114
|
+
| 12 | 4 | `row_count` | u32. |
|
|
115
|
+
| 16 | 2 | `min_reader_version` | u16. Reader with lower version must refuse this shard. |
|
|
116
|
+
| 18 | 2 | `flags` | u16. Must be `0` in v1. Reserved. |
|
|
117
|
+
| 20 | 4 | `reserved` | Must be `0`. Pads to next u64 alignment. |
|
|
118
|
+
| 24 | 8 | `local_string_off` | u64. Absolute byte offset to this shard's local string table (M3+). `0` if the shard has no U32-lane fields. |
|
|
119
|
+
| 32 | 8 | `local_string_len` | u64. Byte length of the local string table. `0` if none. |
|
|
120
|
+
|
|
121
|
+
`min_reader_version` is the load-bearing forward-compat lever. A shard that uses a lane kind introduced in reader v2 sets `min_reader_version = 2`; a v1 reader detects the mismatch at directory load and refuses cleanly (not on first bad read).
|
|
122
|
+
|
|
123
|
+
### 3.5 Shard Payload
|
|
124
|
+
|
|
125
|
+
Interleaved rows, `row_count` many, each `row_stride_bytes` wide. Field byte offsets within a row come from the schema block. No per-row header.
|
|
126
|
+
|
|
127
|
+
Payloads are padded to a multiple of 8 bytes so the last row's F64 fields are safely readable.
|
|
128
|
+
|
|
129
|
+
### 3.6 Zone Maps Segment (M7, optional)
|
|
130
|
+
|
|
131
|
+
Present iff header's `metadata_off != 0`. Carries per-shard min/max for every F64-lane field, enabling query pruning: a Reader can skip shards whose bounds cannot contain a filter's target range, without fetching the shard payload at all. String (U32) fields are not tracked in v1 zone maps; dictionary-level bounds are M8+ material.
|
|
132
|
+
|
|
133
|
+
Layout at `metadata_off`:
|
|
134
|
+
|
|
135
|
+
```
|
|
136
|
+
u32 magic 'ZM01' (0x314D5A30 in little-endian byte order)
|
|
137
|
+
u32 shard_count Must equal the container's shard_count.
|
|
138
|
+
u32 tracked_field_count Number of F64 fields with zone maps.
|
|
139
|
+
u32 reserved0 Must be 0.
|
|
140
|
+
u16 field_indices[tracked_field_count] Indices into the schema's fields array.
|
|
141
|
+
u8 pad to next 8-byte boundary.
|
|
142
|
+
f64 mins[shard_count * tracked_field_count] Row-major: mins[s*T + t].
|
|
143
|
+
f64 maxes[shard_count * tracked_field_count] Same layout.
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
- `tracked_field_count` MAY be less than the schema's F64 field count (the writer chooses which fields to track). For fields not present in `field_indices`, `shardBounds` returns `null`.
|
|
147
|
+
- `mins[s*T + t]` and `maxes[s*T + t]` are the exact min/max of the tracked field's values across the rows in shard `s`. For shards where the field is missing (defaults to `0`), the min/max collapse to `[0, 0]` — still valid input to range predicates.
|
|
148
|
+
- Zone maps segment is placed between the shard directory and the first shard payload. This keeps it inside a single Range fetch alongside the header/schema/directory during Reader initialization.
|
|
149
|
+
|
|
150
|
+
Reader query APIs (see `Reader.js`):
|
|
151
|
+
|
|
152
|
+
```
|
|
153
|
+
reader.shardBounds(shardIdx, fieldName) → {min, max} | null
|
|
154
|
+
reader.findShards(fieldName, {min, max}) → shardIdx[] // shards that MAY contain the range
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
`findShards` returns exactly the shards whose bounds overlap `[min, max]`; a shard whose bounds are disjoint is skipped, saving one range fetch per skipped shard in the RangeReader path.
|
|
158
|
+
|
|
159
|
+
### 3.7 Container Footer (16 bytes)
|
|
160
|
+
|
|
161
|
+
| Offset | Size | Field | Notes |
|
|
162
|
+
| -----: | ---: | ----------- | --------------------------------------------------------- |
|
|
163
|
+
| 0 | 4 | `crc32` | CRC-32C over bytes [0, footer_off). `0xFFFFFFFF` = absent.|
|
|
164
|
+
| 4 | 4 | `reserved1` | Zero. |
|
|
165
|
+
| 8 | 4 | `magic_end` | ASCII `1 K B L` = `0x31 0x4B 0x42 0x4C`. |
|
|
166
|
+
| 12 | 4 | `footer_len`| u32. Currently `16`. Lets future versions grow the footer without breaking magic detection. |
|
|
167
|
+
|
|
168
|
+
CRC is optional in M4; producers that omit it write `0xFFFFFFFF`.
|
|
169
|
+
|
|
170
|
+
## 4. Schema and field descriptors
|
|
171
|
+
|
|
172
|
+
### 4.1 FieldDescriptor (24 bytes, fixed)
|
|
173
|
+
|
|
174
|
+
| Offset | Size | Field | Notes |
|
|
175
|
+
| -----: | ---: | -------------------- | --------------------------------------------------------- |
|
|
176
|
+
| 0 | 2 | `name_len_bytes` | u16. Field name UTF-8 length. Max 255 in v1. |
|
|
177
|
+
| 2 | 2 | `offset_in_row` | u16. Byte offset within a row. |
|
|
178
|
+
| 4 | 1 | `lane_kind` | u8. See 4.2. |
|
|
179
|
+
| 5 | 1 | `flags` | u8. See 4.3. Reserved in v1; readers reject non-zero. |
|
|
180
|
+
| 6 | 2 | `reserved2` | u16. Zero. |
|
|
181
|
+
| 8 | 8 | `name_str_off` | u64. Offset into per-schema name blob (see below). |
|
|
182
|
+
| 16 | 8 | `reserved3` | u64. Zero. |
|
|
183
|
+
|
|
184
|
+
After the descriptor array, a `u32 name_blob_len` is followed by concatenated UTF-8 field names, padded to 8.
|
|
185
|
+
|
|
186
|
+
Fixed-width descriptors let a Reader load the schema with a single `Uint32Array` cast plus a name-blob view.
|
|
187
|
+
|
|
188
|
+
### 4.2 `lane_kind` values (v1)
|
|
189
|
+
|
|
190
|
+
| Value | Name | Bytes | Notes |
|
|
191
|
+
| ----: | ------- | ----: | ------------------------------------------------------------------------ |
|
|
192
|
+
| `1` | `F64` | 8 | The only numeric lane in M4. Matches lite-bake's `Types.F64`. |
|
|
193
|
+
| `2` | `F32` | 4 | Reserved for future producer opt-in via schema override. |
|
|
194
|
+
| `3` | `U32` | 4 | String-table index (M3+). Row cell holds a u32 index into the shard's `local_string_off` table. |
|
|
195
|
+
| `4` | `U8` | 1 | Reserved. |
|
|
196
|
+
|
|
197
|
+
Values 5..127 reserved for future lite-bake type lanes (matching `Types.*`).
|
|
198
|
+
|
|
199
|
+
Values 128..255 reserved for stream-specific lanes; see 4.4.
|
|
200
|
+
|
|
201
|
+
### 4.3 `flags` (per-field, v1)
|
|
202
|
+
|
|
203
|
+
Currently all bits are reserved and must be zero. Reserved bit assignments (v2+) will use:
|
|
204
|
+
|
|
205
|
+
- Bit 0: `LANE_KIND_EXT` — if set, the field uses an extended lane; consult 4.4.
|
|
206
|
+
- Bits 1..7: unassigned.
|
|
207
|
+
|
|
208
|
+
This is the seam Option (c) — `I64_BYTES_PRESERVED` — slides into without a container format-version bump. A shard using it sets `min_reader_version = 2` at the directory level; v1 readers refuse the shard, not the whole container.
|
|
209
|
+
|
|
210
|
+
### 4.4 Extended lane kinds (reserved for v2)
|
|
211
|
+
|
|
212
|
+
When `flags` bit 0 is set on a FieldDescriptor, `lane_kind` is interpreted from the extended-lane table:
|
|
213
|
+
|
|
214
|
+
| Value | Name | Bytes | Notes |
|
|
215
|
+
| ----: | ----------------------- | ----: | --------------------------------------------------------- |
|
|
216
|
+
| `128` | `I64_BYTES_PRESERVED` | 8 | Two-`u32` little-endian pair. Bytes preserved verbatim from source. |
|
|
217
|
+
| `129` | `U64_BYTES_PRESERVED` | 8 | As above, unsigned. |
|
|
218
|
+
|
|
219
|
+
All other values in the extended range are reserved and must be rejected.
|
|
220
|
+
|
|
221
|
+
## 5. Reference producer: tokenizer contract
|
|
222
|
+
|
|
223
|
+
The M1 tokenizer is a chunk-safe UTF-8 JSON SAX scanner. It does not itself write LBK1 shards — that is the M2 writer's job. This section specifies the contract between them so M1 can be built and benched in isolation.
|
|
224
|
+
|
|
225
|
+
### 5.1 Input framing
|
|
226
|
+
|
|
227
|
+
Two top-level modes, auto-detected by the first non-whitespace byte:
|
|
228
|
+
|
|
229
|
+
- `[` — Top-level JSON array. Records are the array's elements.
|
|
230
|
+
- Any other JSON value byte (`{`, `"`, `-`, `0`..`9`, `t`, `f`, `n`) — NDJSON. Records are separated by JSON whitespace (`0x20`, `0x09`, `0x0A`, `0x0D`).
|
|
231
|
+
|
|
232
|
+
Records themselves must be JSON objects for the reference writer path. Non-object records are accepted by the tokenizer (it is generic) but rejected by the writer.
|
|
233
|
+
|
|
234
|
+
### 5.2 Chunk safety
|
|
235
|
+
|
|
236
|
+
`feed(chunk)` accepts a `Uint8Array` of any length, including chunks that split a token, an escape sequence, a UTF-8 code point, or a number across the boundary. `end()` signals input completion. The tokenizer never assumes chunk-aligned tokens.
|
|
237
|
+
|
|
238
|
+
### 5.3 Sink interface
|
|
239
|
+
|
|
240
|
+
The tokenizer emits events to a caller-provided sink object:
|
|
241
|
+
|
|
242
|
+
```
|
|
243
|
+
sink.onStartObject()
|
|
244
|
+
sink.onEndObject()
|
|
245
|
+
sink.onStartArray()
|
|
246
|
+
sink.onEndArray()
|
|
247
|
+
sink.onKey(bytes, start, end) // UTF-8 range; ephemeral, copy if retained
|
|
248
|
+
sink.onString(bytes, start, end) // ephemeral
|
|
249
|
+
sink.onNumber(value) // F64
|
|
250
|
+
sink.onTrue()
|
|
251
|
+
sink.onFalse()
|
|
252
|
+
sink.onNull()
|
|
253
|
+
sink.onEnd()
|
|
254
|
+
```
|
|
255
|
+
|
|
256
|
+
Ranges reference the tokenizer's internal accumulation buffer. The bytes at `[start, end)` are valid only until control returns from the sink call. Sinks that need the bytes past that point must copy them.
|
|
257
|
+
|
|
258
|
+
For a string that spans multiple chunks, the tokenizer accumulates into an internal buffer and emits a single `onString` at the closing quote. Numbers are parsed to F64 incrementally and emitted as a single `onNumber`.
|
|
259
|
+
|
|
260
|
+
### 5.4 Number semantics (v1)
|
|
261
|
+
|
|
262
|
+
- `NaN`, `Infinity`, `-Infinity` are not valid JSON per RFC 8259 and are rejected.
|
|
263
|
+
- Numbers are parsed to IEEE 754 double. Values exceeding representable magnitude round to `+/-Infinity` inside the parser and are rejected as a "number overflow" error before reaching the sink.
|
|
264
|
+
- Integers above 2^53 lose precision silently. Producers concerned with 64-bit ID fidelity should target v2 with the `I64_BYTES_PRESERVED` lane (4.4). The M0/M1 spec commits to F64 semantics on the hot path.
|
|
265
|
+
|
|
266
|
+
#### 5.4.1 F64 preservation contract (M3)
|
|
267
|
+
|
|
268
|
+
The tokenizer implements Clinger's fast-path decimal→F64 conversion with an exact `POW10[0..22]` table. This gives a two-tier preservation guarantee that consumers can rely on:
|
|
269
|
+
|
|
270
|
+
- **Fast path (guaranteed correctly-rounded):** any numeric literal whose decimal representation has ≤ 15 significant digits AND absolute net exponent ≤ 22 produces the SAME IEEE 754 double as JavaScript's `Number(str)`. This covers all integer IDs within `Number.MAX_SAFE_INTEGER`, all "typical" decimals (prices, ratios, timestamps, coordinates), and scientific notation within `1e±22`. This is the domain the round-trip verifier (`test/_verify.js`) asserts bit-exact across every torture scenario.
|
|
271
|
+
- **Slow path (may drift ≤ 1 ULP):** literals outside the fast path (16+ significant digits, or `|exponent| > 22`) fall through to a naive `intPart + fracPart/fracDiv * Math.pow(10, exp)` computation. The result is within 1 ULP of the correctly-rounded value. Correctly-rounded parsing across the full F64 domain (David Gay's `strtod` or equivalent) is planned for M4+.
|
|
272
|
+
|
|
273
|
+
The boundary is exercised explicitly by `test/NumericBoundary.test.js`, which pins expected drift so any regression is caught.
|
|
274
|
+
|
|
275
|
+
### 5.5 String semantics (v1)
|
|
276
|
+
|
|
277
|
+
- Input MUST be valid UTF-8. Invalid sequences are a parse error.
|
|
278
|
+
- All standard JSON escapes are supported: `\"`, `\\`, `\/`, `\b`, `\f`, `\n`, `\r`, `\t`, `\uXXXX`.
|
|
279
|
+
- Surrogate pairs (`\uD800`..`\uDBFF` followed by `\uDC00`..`\uDFFF`) are combined and emitted as UTF-8.
|
|
280
|
+
- Unpaired surrogates are a parse error.
|
|
281
|
+
|
|
282
|
+
### 5.6 Errors
|
|
283
|
+
|
|
284
|
+
Errors are surfaced synchronously as thrown `TokenizerError` instances with a `code`, `byteOffset` (absolute across chunks), and human-readable message. Codes are stable and enumerated in `Tokenizer.js` header comment.
|
|
285
|
+
|
|
286
|
+
## 6. Package exports map (M4 shipped, publish-ready)
|
|
287
|
+
|
|
288
|
+
Subpath entries carved so consumers pay only for what they import. Each entry is a self-contained single file to preserve the one-file-per-unit convention per entry. `sideEffects: false` in `package.json` so tree-shakers drop everything not explicitly imported.
|
|
289
|
+
|
|
290
|
+
| Subpath | Purpose |
|
|
291
|
+
| :-- | :-- |
|
|
292
|
+
| `@zakkster/lite-bake-stream` | Root convenience API: `serialize(input, opts)`, `deserialize(bytes)`, plus re-exports of `Tokenizer`, `Writer`, `Reader`, `StringTable`. |
|
|
293
|
+
| `@zakkster/lite-bake-stream/tokenizer` | Chunk-safe UTF-8 JSON SAX tokenizer. Zero-alloc post-warmup. |
|
|
294
|
+
| `@zakkster/lite-bake-stream/writer` | LBK1 shard emitter. Explicit or sample-and-infer schema. Emits per-shard zone maps. |
|
|
295
|
+
| `@zakkster/lite-bake-stream/reader` | LBK1 parser over an in-memory `ArrayBuffer`. Synchronous. Zone-maps query APIs. |
|
|
296
|
+
| `@zakkster/lite-bake-stream/string-table` | Byte-level UTF-8 interning primitive. |
|
|
297
|
+
| `@zakkster/lite-bake-stream/file-ingest` | Browser helper: `File.stream()` → Reader. Any `ReadableStream<Uint8Array>` works. |
|
|
298
|
+
| `@zakkster/lite-bake-stream/range-reader` | LBK1 parser with lazy shard loading via an IO adapter. HTTP Range fetch or in-memory mock. Synchronous zone-maps pruning. |
|
|
299
|
+
| `@zakkster/lite-bake-stream/multi-reader` | Logical union over N Reader instances. Rows and shards cumulatively addressed; findShards merges across sub-readers. |
|
|
300
|
+
| `@zakkster/lite-bake-stream/split` | NDJSON splitter + per-part compile + container merge. Executor-agnostic — wire to any worker pool or run serially. |
|
|
301
|
+
|
|
302
|
+
Every entry works in browsers (evergreen Chromium / Firefox / Safari) and Node 18+; no Node-specific APIs are used in the source. Every subpath has a matching `.d.ts` file in `types/` for TypeScript consumers.
|
|
303
|
+
|
|
304
|
+
### 3.8 Preserve-Mode Containers (`flags` bit 0 = 1)
|
|
305
|
+
|
|
306
|
+
A preserve-mode container carries opaque byte blobs -- one per record -- with no schema, no lane packing, no interning, no zone maps. Bytes in, same bytes out. Reader-side query surface is `getBytes(i) / getString(i) / getJSON(i)`.
|
|
307
|
+
|
|
308
|
+
Motivation: real-world API JSON is often 2-3 levels deep with mixed nested objects and arrays. The schema-mode F64/U32 lane model rejects nested records by design. Preserve mode is the escape hatch: a fast mediator for arbitrary JSON payloads that the frontend/backend already knows how to parse.
|
|
309
|
+
|
|
310
|
+
The container reuses the same 48-byte header and 40-byte ShardEntry, with these differences:
|
|
311
|
+
|
|
312
|
+
- `flags` byte at offset 7 has bit 0 set to `1`.
|
|
313
|
+
- `schema_block_off` (header offset 8) must be `0`.
|
|
314
|
+
- `metadata_off` (header offset 16) must be `0` -- preserve mode carries no zone maps.
|
|
315
|
+
- Each ShardEntry's `local_string_off` and `local_string_len` are `0`.
|
|
316
|
+
- Each shard payload has this layout:
|
|
317
|
+
|
|
318
|
+
```
|
|
319
|
+
[ blob_0 | blob_1 | ... | blob_{N-1} | u32 offsets[N] ]
|
|
320
|
+
^payload_off ^offset_table_off = payload_off + payload_len - N*4
|
|
321
|
+
```
|
|
322
|
+
|
|
323
|
+
The trailing `u32 offsets[N]` are little-endian, one per record. `offsets[i]` is the byte position of record `i` relative to `payload_off`. Record `i`'s byte range is `[offsets[i], offsets[i+1])`, or `[offsets[N-1], payload_len - N*4)` for the last record.
|
|
324
|
+
|
|
325
|
+
Dispatch: a schema-mode `Reader` opening a preserve container throws `R_WRONG_MODE`. A `PreserveReader` opening a schema-mode container also throws `R_WRONG_MODE`. `deserialize()` reads the flag byte and returns the right one.
|
|
326
|
+
|
|
327
|
+
Not currently supported in preserve mode: `RangeReader` (HTTP-Range lazy loading), `MultiReader` (unifying multiple containers), and `Split.mergeContainers`. These are additive future work; preserve containers work with `serialize`, `deserialize`, `ingestStream`, and `ingestFile` today.
|
|
328
|
+
|
|
329
|
+
## 7. Round-trip preservation contract
|
|
330
|
+
|
|
331
|
+
Given source NDJSON `S` and a schema `Σ` (either explicit or inferred), the LBK1 container `C` produced by the reference writer satisfies the following for every row `i ∈ [0, rowCount(S))` and every field `f ∈ Σ`:
|
|
332
|
+
|
|
333
|
+
| Source value at (i, f) | Lane | Round-trip result |
|
|
334
|
+
| :-- | :-- | :-- |
|
|
335
|
+
| JSON number in fast-path domain (≤15 sig digits, `|exp| ≤ 22`) | F64 | **bit-exact IEEE 754 double** |
|
|
336
|
+
| JSON number in slow-path domain | F64 | within 1 ULP of the correctly-rounded value |
|
|
337
|
+
| JSON string (any valid UTF-8) | U32 | **byte-exact UTF-8 sequence** |
|
|
338
|
+
| JSON `true` / `false` / `null` | F64 | `1` / `0` / `0` (documented coercion) |
|
|
339
|
+
| absent field (missing in source) | F64 | `0` (default) |
|
|
340
|
+
| absent field | U32 | string at index 0 of the shard's local table |
|
|
341
|
+
| field in source but NOT in `Σ` | — | silently dropped (matches lite-bake core) |
|
|
342
|
+
| field in `Σ` with wrong value type (post-freeze) | — | `W_LANE_MISMATCH` error, container not produced |
|
|
343
|
+
|
|
344
|
+
The preservation contract is asserted mechanically by three complementary layers:
|
|
345
|
+
|
|
346
|
+
1. **Unit tests** — `test/RoundTrip.test.js`, `test/StringRoundTrip.test.js` verify individual known-value scenarios.
|
|
347
|
+
2. **Property-based fuzz** — `test/DataPreservation.test.js` generates arbitrary random shapes across 12 seeds and asserts exact preservation on every cell of every row.
|
|
348
|
+
3. **Torture-scale verification** — `bench/torture.js` runs the `verifyRoundTrip` helper (`test/_verify.js`) OUTSIDE the GC-measured band, asserting that every scenario opting into `verify: true` produces a container whose Reader output matches its source NDJSON byte-for-byte. The verification budget currently covers ~73,500 rows across 12 scenarios per fast-tier run; the `verified` column in the torture report shows the row count actually checked.
|
|
349
|
+
|
|
350
|
+
The boundary between "bit-exact" and "1 ULP" for F64 is pinned by `test/NumericBoundary.test.js`. Any drift in the tokenizer's number parser trips these tests before it can reach production.
|
|
351
|
+
|
|
352
|
+
Correctness of the preservation contract is a release gate on par with the zero-GC gate. If either fails, no publish.
|
|
353
|
+
|
|
354
|
+
## 8. Release gates
|
|
355
|
+
|
|
356
|
+
- **M4 GA gate.** JSON conformance corpus (RFC 8259 + JSONTestSuite), fuzzed chunk-boundary splits at every byte position, F64 round-trip parity, container round-trip parity, zero-alloc-per-row proven by lite-gc-profiler after warmup.
|
|
357
|
+
- **M8 demo gate.** Locked 60 fps on iPhone 7 (A10, iOS 15 Safari) while ingesting 100 MB of synthetic NDJSON from a fetch stream. Under-featured demo beats a laggy full-featured one.
|
|
358
|
+
|
|
359
|
+
## 9. Non-goals (v1)
|
|
360
|
+
|
|
361
|
+
- JSON5, JSONC, trailing commas, comments. Not accepted.
|
|
362
|
+
- Streaming output of individual field updates (LBK1 is a write-once container).
|
|
363
|
+
- Compression. Compose gzip/zstd around the container at the transport layer.
|
|
364
|
+
- Multi-language producer libraries. JS-first; formats are portable, ports welcome.
|
package/llms.txt
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
# @zakkster/lite-bake-stream
|
|
2
|
+
|
|
3
|
+
Streaming byte-level JSON to lite-bake binary compiler for JS runtimes.
|
|
4
|
+
|
|
5
|
+
## Purpose
|
|
6
|
+
|
|
7
|
+
Ingest gigabyte-scale JSON (top-level array or NDJSON) into the `lite-bake` LBK1 binary format without materializing the intermediate object graph. Reference producer path for downstream consumers of the flat, interleaved, zero-GC `lite-bake` reader API.
|
|
8
|
+
|
|
9
|
+
## Status
|
|
10
|
+
|
|
11
|
+
v1.0.0 — stable. LBK1 format frozen at `format_version: 1`. Qualified on an 8 GB soak (M1 MacBook Pro): 98.37M rows, 4.89 GB container, zero major GC, zero minor GC, 499 KB total heap allocation, 590.21M cells verified byte-exact, zero mismatches. Tokenizer benches at 222-237 MB/s (~55% of JSON.parse, with no object graph allocated).
|
|
12
|
+
|
|
13
|
+
Public API follows semver from 1.0.0. Future additions (I64 lane, columnar payload mode, container-level string table) land via the format's forward-compat seams -- `min_reader_version` on ShardEntry, reserved FieldDescriptor flags, the `metadata_off` block wrapper -- without a format_version bump.
|
|
14
|
+
|
|
15
|
+
See SPEC.md for the LBK1 container format, section 3.6 for zone maps, section 4.3 for the reserved field flags.
|
|
16
|
+
|
|
17
|
+
## Public API (v1.0.0)
|
|
18
|
+
|
|
19
|
+
Two ingest modes share one top-level API:
|
|
20
|
+
|
|
21
|
+
- **Schema mode** (default): F64/U32 lane packing, zone-maps query pruning. Flat records only.
|
|
22
|
+
- **Preserve mode** (`{ preserve: true }`): opaque byte blobs, any JSON shape. Deeply nested API responses, arrays, mixed types round-trip byte-for-byte identical.
|
|
23
|
+
|
|
24
|
+
Root import (`@zakkster/lite-bake-stream`): `serialize(input, opts)` and `deserialize(bytes)`. `deserialize` auto-dispatches on the container's flag bit. Plus class re-exports.
|
|
25
|
+
|
|
26
|
+
Schema-mode classes:
|
|
27
|
+
- `Tokenizer` (`/tokenizer`): chunk-safe UTF-8 JSON SAX scanner.
|
|
28
|
+
- `Writer` (`/writer`): LBK1 shard emitter, plugs into Tokenizer. F64/U32 lanes, per-shard string tables and zone maps.
|
|
29
|
+
- `Reader` (`/reader`): container parser, sync, zero-alloc `get(row, field)` accessors + zone-maps query APIs (`shardBounds`, `findShards`).
|
|
30
|
+
- `RangeReader` (`/range-reader`): HTTP Range lazy shard loading, synchronous query pruning after open.
|
|
31
|
+
- `MultiReader` (`/multi-reader`): logical union over N Readers sharing a schema.
|
|
32
|
+
- `splitNDJSON`, `compilePart`, `compileInParts`, `mergeContainers` (`/split`): worker-agnostic split/compile/merge.
|
|
33
|
+
|
|
34
|
+
Preserve-mode classes:
|
|
35
|
+
- `PreserveTokenizer` (`/preserve-tokenizer`): NDJSON record-boundary scanner, JSON-aware depth tracking, chunk-safe.
|
|
36
|
+
- `PreserveWriter` (`/preserve-writer`): opaque byte-blob sink, pre-allocated shard buffer, zero-GC record path.
|
|
37
|
+
- `PreserveReader` (`/preserve-reader`): tri-API — `getBytes(i)` (zero-alloc view), `getString(i)`, `getJSON(i)`.
|
|
38
|
+
|
|
39
|
+
Shared:
|
|
40
|
+
- `StringTable` (`/string-table`): byte-level UTF-8 interning primitive.
|
|
41
|
+
- `ingestStream`, `ingestFile` (`/file-ingest`): browser helpers piping a `ReadableStream<Uint8Array>` through the pipeline. `preserve` option dispatches to the right writer.
|
|
42
|
+
|
|
43
|
+
Error classes with stable `code`: `TokenizerError`, `WriterError`, `ReaderError`, `RangeReaderError`, `MultiReaderError`, `SplitError`, `PreserveTokenizerError`, `PreserveWriterError`, `PreserveReaderError`.
|
|
44
|
+
- `ingestStream`, `ingestFile` (`/file-ingest`): browser helpers piping a `ReadableStream<Uint8Array>` (e.g. `File.stream()`) through the Tokenizer + Writer, returning a Reader. Per-chunk `onProgress` callback.
|
|
45
|
+
- `TokenizerError`, `WriterError`, `ReaderError`, `RangeReaderError`: thrown on parse/write/read errors with stable `code`.
|
|
46
|
+
- `VERSION` const per subpath.
|
|
47
|
+
|
|
48
|
+
## Schema forms
|
|
49
|
+
|
|
50
|
+
- `{ fields: ['id', 'x', 'y'] }` — all-F64 shorthand, back-compat with M2.
|
|
51
|
+
- `{ fields: [{name:'id', laneKind:'f64'}, {name:'tag', laneKind:'u32'}] }` — mixed lanes (M3).
|
|
52
|
+
- Sample-and-infer (default when no schema is passed): first `sampleBytes` of input observed; fields with only numbers become F64, only strings become U32, mixed types raise `W_MIXED_LANE_TYPES` at freeze.
|
|
53
|
+
|
|
54
|
+
## Contract
|
|
55
|
+
|
|
56
|
+
- `feed(chunk: Uint8Array)`: consumes bytes; sink events fire synchronously.
|
|
57
|
+
- `end()`: signals input completion. Errors if input ends mid-token.
|
|
58
|
+
- Sink methods: `onStartObject`, `onEndObject`, `onStartArray`, `onEndArray`, `onKey`, `onString`, `onNumber`, `onTrue`, `onFalse`, `onNull`, `onEnd`.
|
|
59
|
+
- Key/String byte ranges are ephemeral views into the tokenizer's internal buffer; copy if retained past the sink call.
|
|
60
|
+
|
|
61
|
+
## Preservation contract
|
|
62
|
+
|
|
63
|
+
Every declared field of every row round-trips. F64 lanes: bit-exact for numeric literals with ≤15 significant digits and |exponent| ≤ 22 (Clinger's fast path); ≤1 ULP drift outside that domain (documented, pinned by NumericBoundary.test.js). U32 lanes: byte-exact UTF-8, unconditional. Missing fields → documented defaults. Unknown keys → silently dropped. Wrong-type value on post-freeze schema → W_LANE_MISMATCH error, no corrupt container produced. See SPEC section 7 for the full table. Asserted by tests, property-based fuzz, AND torture-scale verification (~73,500 rows per fast-tier run).
|
|
64
|
+
|
|
65
|
+
## Numbers
|
|
66
|
+
|
|
67
|
+
F64 only. Values exceeding IEEE 754 double range are rejected as `E_NUMBER_OVERFLOW`. 64-bit integer IDs above 2^53 lose precision silently (documented in SPEC 5.4); v2 will introduce an opt-in bytes-preserved lane.
|
|
68
|
+
|
|
69
|
+
## Framing
|
|
70
|
+
|
|
71
|
+
- `framing: 'auto'` (default): first non-ws byte decides. `[` = top-level array, else NDJSON.
|
|
72
|
+
- `framing: 'array'`: enforces `[ ... ]` wrapper.
|
|
73
|
+
- `framing: 'ndjson'`: whitespace-separated top-level values.
|
|
74
|
+
|
|
75
|
+
## Tree-shaking
|
|
76
|
+
|
|
77
|
+
Subpath entries per SPEC section 6. `sideEffects: false`. Consumers import only the path they need; the browser reader never pulls the writer.
|
|
78
|
+
|
|
79
|
+
## Non-goals
|
|
80
|
+
|
|
81
|
+
JSON5, JSONC, comments, trailing commas, streaming field updates, compression, native (non-JS) producers.
|