@btoon/js 0.0.0-stage → 0.1.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/LICENSE +21 -0
- package/README.md +328 -2
- package/dist/cjs/constants.d.ts +34 -0
- package/dist/cjs/constants.js +32 -0
- package/dist/cjs/core.d.ts +15 -0
- package/dist/cjs/core.js +275 -0
- package/dist/cjs/decode.d.ts +10 -0
- package/dist/cjs/decode.js +554 -0
- package/dist/cjs/encode.d.ts +2 -0
- package/dist/cjs/encode.js +946 -0
- package/dist/cjs/errors.d.ts +53 -0
- package/dist/cjs/errors.js +45 -0
- package/dist/cjs/index.d.ts +68 -0
- package/dist/cjs/index.js +86 -0
- package/dist/cjs/inflate.d.ts +9 -0
- package/dist/cjs/inflate.js +353 -0
- package/dist/cjs/node.d.ts +64 -0
- package/dist/cjs/node.js +82 -0
- package/dist/cjs/options.d.ts +95 -0
- package/dist/cjs/options.js +128 -0
- package/dist/cjs/package.json +3 -0
- package/dist/cjs/platform-node.d.ts +3 -0
- package/dist/cjs/platform-node.js +132 -0
- package/dist/cjs/platform-portable.d.ts +7 -0
- package/dist/cjs/platform-portable.js +16 -0
- package/dist/cjs/sha256.d.ts +5 -0
- package/dist/cjs/sha256.js +141 -0
- package/dist/cjs/utf8.d.ts +19 -0
- package/dist/cjs/utf8.js +107 -0
- package/dist/cjs/values.d.ts +58 -0
- package/dist/cjs/values.js +129 -0
- package/dist/cjs/web.d.ts +25 -0
- package/dist/cjs/web.js +151 -0
- package/dist/esm/constants.d.ts +34 -0
- package/dist/esm/constants.js +29 -0
- package/dist/esm/core.d.ts +15 -0
- package/dist/esm/core.js +269 -0
- package/dist/esm/decode.d.ts +10 -0
- package/dist/esm/decode.js +549 -0
- package/dist/esm/encode.d.ts +2 -0
- package/dist/esm/encode.js +943 -0
- package/dist/esm/errors.d.ts +53 -0
- package/dist/esm/errors.js +39 -0
- package/dist/esm/index.d.ts +68 -0
- package/dist/esm/index.js +74 -0
- package/dist/esm/inflate.d.ts +9 -0
- package/dist/esm/inflate.js +349 -0
- package/dist/esm/node.d.ts +64 -0
- package/dist/esm/node.js +70 -0
- package/dist/esm/options.d.ts +95 -0
- package/dist/esm/options.js +124 -0
- package/dist/esm/platform-node.d.ts +3 -0
- package/dist/esm/platform-node.js +96 -0
- package/dist/esm/platform-portable.d.ts +7 -0
- package/dist/esm/platform-portable.js +13 -0
- package/dist/esm/sha256.d.ts +5 -0
- package/dist/esm/sha256.js +136 -0
- package/dist/esm/utf8.d.ts +19 -0
- package/dist/esm/utf8.js +99 -0
- package/dist/esm/values.d.ts +58 -0
- package/dist/esm/values.js +120 -0
- package/dist/esm/web.d.ts +25 -0
- package/dist/esm/web.js +143 -0
- package/package.json +85 -4
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 BTOON Contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,329 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @btoon/js
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
BTOON ("Binary TOON") for JavaScript and TypeScript. BTOON is
|
|
4
|
+
[MessagePack](https://github.com/msgpack/msgpack/blob/master/spec.md) with a canonical
|
|
5
|
+
encoding, a columnar ("tabular") encoding for arrays of uniform objects, a few extension
|
|
6
|
+
types (timestamp, date, datetime, bigint, float32/float64 vectors), an optional
|
|
7
|
+
compression envelope and an optional HMAC-signed envelope.
|
|
8
|
+
|
|
9
|
+
- **Pure TypeScript, zero runtime dependencies.** Runs in browsers, Node.js ≥ 18, Deno,
|
|
10
|
+
Bun and edge runtimes (Cloudflare Workers, Vercel Edge). ESM and CommonJS builds with
|
|
11
|
+
type definitions.
|
|
12
|
+
- **Conforming.** Implements [BTOON spec 0.1](https://github.com/BTOON-project/btoon-core/blob/main/docs/SPEC.md)
|
|
13
|
+
and passes all 260 conformance vectors of btoon-core (see [Conformance](#conformance)).
|
|
14
|
+
- **Canonical.** The same value encodes to the same bytes as in every other conforming
|
|
15
|
+
implementation (C++, Python, Go, PHP), so encodings can be hashed, cached and signed.
|
|
16
|
+
- **Hardened decoder.** Bounds-checked reads, a nesting limit, no allocation larger than
|
|
17
|
+
the input justifies, strict UTF-8, duplicate-key rejection, capped decompression, and
|
|
18
|
+
signatures verified before anything is parsed.
|
|
19
|
+
|
|
20
|
+
Version 0.1.0, implementing spec 0.1. In Node.js it is also the fastest BTOON package: on
|
|
21
|
+
10,000 records it encodes in 1.1 ms and decodes in 1.6 ms, against 7.8 ms and 7.1 ms for
|
|
22
|
+
the native [`btoon`](https://github.com/BTOON-project/btoon-nodejs) addon, with
|
|
23
|
+
byte-identical output.
|
|
24
|
+
|
|
25
|
+
## Install
|
|
26
|
+
|
|
27
|
+
```sh
|
|
28
|
+
npm install @btoon/js
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
## Usage
|
|
32
|
+
|
|
33
|
+
```js
|
|
34
|
+
import { encode, decode } from '@btoon/js';
|
|
35
|
+
|
|
36
|
+
const bytes = encode({ name: 'Alice', age: 30, active: true });
|
|
37
|
+
// Uint8Array: 83 a6 616374697665 c3 a3 616765 1e a4 6e616d65 a5 416c696365
|
|
38
|
+
|
|
39
|
+
decode(bytes); // { active: true, age: 30, name: 'Alice' }
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
CommonJS works the same way: `const { encode, decode } = require('@btoon/js')`.
|
|
43
|
+
|
|
44
|
+
In a browser without a bundler, import the ESM build directly (it has no `node:` imports):
|
|
45
|
+
|
|
46
|
+
```html
|
|
47
|
+
<script type="module">
|
|
48
|
+
import { encode, decode } from './node_modules/@btoon/js/dist/esm/index.js';
|
|
49
|
+
</script>
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
[`examples/index.html`](examples/index.html) is a working page: run `npm run build`, serve
|
|
53
|
+
the repository (`make serve`, or `python3 -m http.server`), and open `/examples/`.
|
|
54
|
+
|
|
55
|
+
### Arrays of records become tables
|
|
56
|
+
|
|
57
|
+
Arrays of objects with the same keys are written column by column, with each key once,
|
|
58
|
+
whenever that is smaller (SPEC §5.6.2). They decode back to ordinary arrays of objects.
|
|
59
|
+
|
|
60
|
+
```js
|
|
61
|
+
const rows = Array.from({ length: 100 }, (_, i) => ({ id: i, name: `user${i}`, active: i % 2 === 0 }));
|
|
62
|
+
encode(rows).length; // 945 bytes
|
|
63
|
+
encode(rows, { autoTabular: false }).length; // 2493 bytes (plain MessagePack)
|
|
64
|
+
JSON.stringify(rows).length; // 4031 bytes
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
### Compression and signing
|
|
68
|
+
|
|
69
|
+
```js
|
|
70
|
+
import { encode, decode, encodeAsync, decodeAsync } from '@btoon/js';
|
|
71
|
+
|
|
72
|
+
const key = crypto.getRandomValues(new Uint8Array(32));
|
|
73
|
+
|
|
74
|
+
const sealed = encode(rows, { compress: true, hmacKey: key }); // Node.js: synchronous
|
|
75
|
+
decode(sealed, { hmacKey: key });
|
|
76
|
+
|
|
77
|
+
const sealed2 = await encodeAsync(rows, { compress: true, hmacKey: key }); // anywhere
|
|
78
|
+
await decodeAsync(sealed2, { hmacKey: key });
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
Envelopes are layered as sign(compress(document)). With `hmacKey`, `decode` requires a
|
|
82
|
+
signed envelope and verifies it in constant time before parsing; without a key, a signed
|
|
83
|
+
input is rejected rather than silently accepted. Compression is skipped for documents
|
|
84
|
+
under 256 bytes and whenever it would save less than 5% (SPEC §6).
|
|
85
|
+
|
|
86
|
+
## Environments
|
|
87
|
+
|
|
88
|
+
The package's `exports` map picks an entry point per environment. Both have the same
|
|
89
|
+
API; they differ only in which synchronous codecs are built in.
|
|
90
|
+
|
|
91
|
+
| | Node.js, Bun (`node` condition) | Browsers, Deno, workers, edge (`browser`, `deno`, `worker`, `workerd`, `edge-light`, default) |
|
|
92
|
+
|---|---|---|
|
|
93
|
+
| `decode` of zlib envelopes | `node:zlib` | built-in pure-JS inflate |
|
|
94
|
+
| `decode` of Brotli / Zstandard | `node:zlib` (Zstandard from Node.js 22.15) | `options.codecs` |
|
|
95
|
+
| `encode` with `compress` | zlib, Brotli, Zstandard via `node:zlib` | `options.codecs`, otherwise `unsupported_compression` (use `encodeAsync`) |
|
|
96
|
+
| HMAC in `encode`/`decode` | `node:crypto` | built-in pure-JS SHA-256 |
|
|
97
|
+
| `encodeAsync` / `decodeAsync` compression | `CompressionStream` / `DecompressionStream` where the runtime supports the format, else the synchronous codecs | the same |
|
|
98
|
+
| `encodeAsync` / `decodeAsync` HMAC | WebCrypto (`crypto.subtle`), else `node:crypto` | WebCrypto, else pure JS (for example on `http://` pages) |
|
|
99
|
+
| LZ4, Snappy | `options.codecs` | `options.codecs` |
|
|
100
|
+
|
|
101
|
+
`DecompressionStream('deflate')` is the RFC 1950 zlib format the spec requires. Other
|
|
102
|
+
formats depend on the runtime: at the time of writing, Brotli streams exist in Node.js 22+
|
|
103
|
+
and Bun, and Zstandard streams in Bun.
|
|
104
|
+
|
|
105
|
+
Why a pure-JS inflate and SHA-256 in the portable entry: zlib decoding is mandatory in
|
|
106
|
+
the spec and signatures are a security feature, so synchronous `decode` handles both in
|
|
107
|
+
every environment. Together they add about 8.4 KB minified (3.6 KB gzipped). Inflate
|
|
108
|
+
enforces the declared output size as it goes, and its accept/reject decisions match zlib's
|
|
109
|
+
(tested differentially against `node:zlib`). Synchronous compression is not built in:
|
|
110
|
+
a good deflate encoder is large, and `encodeAsync` or a codec covers it.
|
|
111
|
+
|
|
112
|
+
### Custom codecs
|
|
113
|
+
|
|
114
|
+
`options.codecs` supplies or overrides compression algorithms. For example, synchronous
|
|
115
|
+
zlib compression in a browser with [fflate](https://github.com/101arrowz/fflate):
|
|
116
|
+
|
|
117
|
+
```js
|
|
118
|
+
import { zlibSync } from 'fflate';
|
|
119
|
+
|
|
120
|
+
const codecs = { zlib: { compress: (data) => zlibSync(data, { level: 6 }) } };
|
|
121
|
+
const bytes = encode(value, { compress: true, codecs });
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
A codec is `{ compress?(data), decompress?(data, uncompressedSize) }`. Either function may
|
|
125
|
+
return a `Promise`, but only the async API accepts one. `decompress` receives the size the
|
|
126
|
+
envelope declares (already checked against `maxDecompressedSize`) and must not produce
|
|
127
|
+
more than that. LZ4 block decoders need it as their output size.
|
|
128
|
+
|
|
129
|
+
## Type mapping
|
|
130
|
+
|
|
131
|
+
Encoding (JavaScript → BTOON):
|
|
132
|
+
|
|
133
|
+
| JavaScript | BTOON |
|
|
134
|
+
|---|---|
|
|
135
|
+
| `null`, `undefined` | nil (object properties and array holes holding `undefined` too) |
|
|
136
|
+
| `boolean` | boolean |
|
|
137
|
+
| `number` that is a safe integer | integer, smallest format |
|
|
138
|
+
| any other `number`, including `-0`, `NaN`, `±Infinity` and integral values beyond ±2^53 | float64 (NaN written as `7ff8000000000000`) |
|
|
139
|
+
| `bigint` | integer if within [-2^63, 2^64-1], else bigint extension |
|
|
140
|
+
| `string` | string (UTF-8). A lone surrogate throws `invalid_utf8` |
|
|
141
|
+
| `Uint8Array` (and `Buffer`), `ArrayBuffer`, `DataView`, other typed arrays | binary (the view's bytes) |
|
|
142
|
+
| `Float32Array`, `Float64Array` | float32 / float64 vector (little-endian, NaN bits preserved) |
|
|
143
|
+
| `Array` | array, or tabular when the size rule says so |
|
|
144
|
+
| plain object, `Map` with string keys | map, keys sorted by UTF-8 bytes |
|
|
145
|
+
| `Date` | timestamp (millisecond precision, UTC) |
|
|
146
|
+
| `Timestamp` | timestamp (nanoseconds, optional UTC offset) |
|
|
147
|
+
| `Ext` | extension, written as is |
|
|
148
|
+
| other objects | like `JSON.stringify`: the result of `toJSON()` if present (called once), else the object's own enumerable string-keyed properties. `Number`/`String`/`Boolean`/`BigInt` wrappers are unwrapped |
|
|
149
|
+
| functions, symbols | `invalid_argument` |
|
|
150
|
+
|
|
151
|
+
Decoding (BTOON → JavaScript):
|
|
152
|
+
|
|
153
|
+
| BTOON | JavaScript |
|
|
154
|
+
|---|---|
|
|
155
|
+
| nil | `null` |
|
|
156
|
+
| integer | `number` if a safe integer, else `bigint`. With `bigint: 'always'`, uint64/int64 markers and bigint extensions always give `bigint` |
|
|
157
|
+
| float | `number` |
|
|
158
|
+
| string | `string` |
|
|
159
|
+
| binary | `Uint8Array` (a copy, never a view of the input) |
|
|
160
|
+
| array, tabular | `Array` |
|
|
161
|
+
| map | plain object |
|
|
162
|
+
| timestamp, date, datetime | `Date` (milliseconds, offset dropped), or `Timestamp` with `timestamps: 'object'`. Instants outside the range of `Date` always give `Timestamp` |
|
|
163
|
+
| float32 / float64 vector | `Float32Array` / `Float64Array` (an aligned copy) |
|
|
164
|
+
| any other extension | `Ext` |
|
|
165
|
+
|
|
166
|
+
Notes:
|
|
167
|
+
|
|
168
|
+
- **Numbers.** JavaScript can't tell `2.0` from `2`, so whole numbers encode as integers.
|
|
169
|
+
`-0` is the exception: it encodes as float64 `-0.0` so its sign survives.
|
|
170
|
+
- **`__proto__`.** A map key `"__proto__"` becomes an own property of the decoded object,
|
|
171
|
+
as with `JSON.parse`. It never changes the object's prototype.
|
|
172
|
+
- **Key order.** Keys are sorted by their UTF-8 bytes, which is not JavaScript's default
|
|
173
|
+
`sort()` order for characters outside the Basic Multilingual Plane.
|
|
174
|
+
- **`date` and `datetime`.** These extension types have no JavaScript type of their own and
|
|
175
|
+
decode to `Date` (or `Timestamp`). To write one, use `new Ext(-2, bytes)` (int64
|
|
176
|
+
milliseconds) or `new Ext(-3, bytes)` (int64 nanoseconds). An `Ext` with an id BTOON
|
|
177
|
+
defines must carry a valid payload for that type, or `encode` throws `invalid_argument`.
|
|
178
|
+
- **Cycles** throw `max_depth`, like any value nested deeper than `maxDepth`.
|
|
179
|
+
- `Ext` and `Timestamp` are recognised through `Symbol.for('btoon.Ext')` and
|
|
180
|
+
`Symbol.for('btoon.Timestamp')` brands, so instances from the ESM and CommonJS builds
|
|
181
|
+
(or two copies of the package) are interchangeable.
|
|
182
|
+
|
|
183
|
+
## API
|
|
184
|
+
|
|
185
|
+
```ts
|
|
186
|
+
const VERSION: '0.1.0';
|
|
187
|
+
const SPEC_VERSION: '0.1';
|
|
188
|
+
|
|
189
|
+
function encode(value: unknown, options?: EncodeOptions): Uint8Array;
|
|
190
|
+
function decode(data: Uint8Array | ArrayBuffer | ArrayBufferView, options?: DecodeOptions): unknown;
|
|
191
|
+
function encodeAsync(value: unknown, options?: EncodeOptions): Promise<Uint8Array>;
|
|
192
|
+
function decodeAsync(data: Uint8Array | ArrayBuffer | ArrayBufferView, options?: DecodeOptions): Promise<unknown>;
|
|
193
|
+
|
|
194
|
+
class BtoonError extends Error { readonly code: BtoonErrorCode }
|
|
195
|
+
class Ext { constructor(readonly type: number, readonly data: Uint8Array) }
|
|
196
|
+
class Timestamp {
|
|
197
|
+
constructor(seconds: bigint | number, nanoseconds: number, offsetMinutes?: number);
|
|
198
|
+
readonly seconds: bigint; readonly nanoseconds: number; readonly offsetMinutes?: number;
|
|
199
|
+
toDate(): Date;
|
|
200
|
+
static fromDate(date: Date): Timestamp;
|
|
201
|
+
}
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
`encode` returns `Uint8Array<ArrayBuffer>`; the type definitions need TypeScript 5.7 or
|
|
205
|
+
later (or `skipLibCheck`). `decodeAsync` copies its input first, so the caller may reuse
|
|
206
|
+
the buffer right away. The async functions report every failure as a rejected promise.
|
|
207
|
+
|
|
208
|
+
### Encode options
|
|
209
|
+
|
|
210
|
+
| Option | Default | Meaning |
|
|
211
|
+
|---|---|---|
|
|
212
|
+
| `compress` | `false` | Wrap in a compression envelope. |
|
|
213
|
+
| `compression` | `'zlib'` | `'zlib'`, `'lz4'`, `'zstd'`, `'brotli'` or `'snappy'`. Only used when `compress` is true. |
|
|
214
|
+
| `autoTabular` | `true` | Use the tabular encoding where the size rule says so. |
|
|
215
|
+
| `maxDepth` | `128` | Nesting limit. Deeper or cyclic values throw `max_depth`. |
|
|
216
|
+
| `hmacKey` | none | `Uint8Array` or string (its UTF-8 bytes). Signs the output. Must not be empty. |
|
|
217
|
+
| `codecs` | none | Caller-supplied compression codecs. |
|
|
218
|
+
|
|
219
|
+
### Decode options
|
|
220
|
+
|
|
221
|
+
| Option | Default | Meaning |
|
|
222
|
+
|---|---|---|
|
|
223
|
+
| `strict` | `true` | SPEC §7 strict mode. `false` allows trailing bytes and duplicate map keys (last wins) and replaces invalid UTF-8 with U+FFFD. Bounds, depth, allocation and other structural checks are never relaxed. |
|
|
224
|
+
| `maxDepth` | `128` | Nesting limit. A tabular array counts two levels. |
|
|
225
|
+
| `maxDecompressedSize` | 256 MiB | Largest accepted `uncompressed_size`, checked before decompressing. |
|
|
226
|
+
| `autoDecompress` | `true` | Accept compression envelopes; `false` rejects them with `invalid_envelope`. |
|
|
227
|
+
| `hmacKey` | none | Require a signed envelope and verify it before parsing. Must not be empty. |
|
|
228
|
+
| `timestamps` | `'date'` | `'date'` or `'object'` (always `Timestamp`, full precision). |
|
|
229
|
+
| `bigint` | `'auto'` | `'auto'` or `'always'`. |
|
|
230
|
+
| `codecs` | none | Caller-supplied compression codecs. |
|
|
231
|
+
|
|
232
|
+
Invalid options throw `invalid_argument`. Unknown option names are ignored.
|
|
233
|
+
|
|
234
|
+
## Errors
|
|
235
|
+
|
|
236
|
+
Every failure is a `BtoonError` whose `code` is stable across BTOON implementations:
|
|
237
|
+
|
|
238
|
+
| Code | Meaning |
|
|
239
|
+
|---|---|
|
|
240
|
+
| `truncated` | Input ends before a value is complete, or a length runs past the input. |
|
|
241
|
+
| `invalid_marker` | The reserved byte `0xc1`. |
|
|
242
|
+
| `trailing_data` | Bytes after the top-level value (strict mode). |
|
|
243
|
+
| `invalid_utf8` | A string, key or column name isn't valid UTF-8, or a string to encode has a lone surrogate. |
|
|
244
|
+
| `non_string_key` | A map key that isn't a string. |
|
|
245
|
+
| `duplicate_key` | The same map key twice (strict mode). |
|
|
246
|
+
| `max_depth` | Nesting deeper than `maxDepth`, or a cyclic value. |
|
|
247
|
+
| `invalid_extension` | A malformed timestamp, date, datetime, bigint or vector payload. |
|
|
248
|
+
| `invalid_tabular` | A malformed tabular payload. |
|
|
249
|
+
| `invalid_envelope` | A malformed envelope, or a compressed payload that is corrupt, has trailing bytes, or decompresses to a size other than the declared one. |
|
|
250
|
+
| `size_limit` | A declared size above `maxDecompressedSize`, or a value too large for the engine. |
|
|
251
|
+
| `unsupported_compression` | An unknown algorithm id, or one not available in this environment. |
|
|
252
|
+
| `authentication_failed` | A missing or invalid signature, or a signed input decoded without a key. |
|
|
253
|
+
| `invalid_argument` | A value that can't be encoded, or an invalid option. |
|
|
254
|
+
|
|
255
|
+
```js
|
|
256
|
+
try {
|
|
257
|
+
decode(bytes);
|
|
258
|
+
} catch (e) {
|
|
259
|
+
if (e instanceof BtoonError && e.code === 'truncated') { /* … */ }
|
|
260
|
+
}
|
|
261
|
+
```
|
|
262
|
+
|
|
263
|
+
Exceptions thrown by your own `toJSON()` methods or getters propagate unchanged.
|
|
264
|
+
|
|
265
|
+
## Security
|
|
266
|
+
|
|
267
|
+
Decoding is designed for hostile input (SPEC §7, §9): every read is bounds-checked; arrays,
|
|
268
|
+
maps and tables are checked against the remaining input before anything is allocated; the
|
|
269
|
+
nesting limit applies to input and output; strings are validated with a fatal
|
|
270
|
+
`TextDecoder`; compressed payloads are checked against `maxDecompressedSize` before
|
|
271
|
+
decompressing and stopped at their declared size while decompressing; Zstandard frames
|
|
272
|
+
whose window exceeds max(8 MiB, min(declared size, 128 MiB)) are rejected; HMACs are
|
|
273
|
+
compared in constant time and checked before the payload is parsed. Raising `maxDepth`
|
|
274
|
+
far above the default is safe: exhausting the JavaScript stack is reported as
|
|
275
|
+
`max_depth`.
|
|
276
|
+
|
|
277
|
+
## Conformance
|
|
278
|
+
|
|
279
|
+
The test suite runs btoon-core's conformance vectors (vendored in `test/vectors/`) against
|
|
280
|
+
the ESM and CommonJS builds of both entry points, synchronously and asynchronously: every
|
|
281
|
+
non-error vector must decode to its value, every canonical vector must re-encode
|
|
282
|
+
byte-for-byte, and every error vector must fail with its error code. All 260 pass, with
|
|
283
|
+
these exceptions:
|
|
284
|
+
|
|
285
|
+
- `float/0` is not re-encoded: the float `0.0` is indistinguishable from the integer `0`
|
|
286
|
+
in JavaScript (it is still decoded and checked).
|
|
287
|
+
- Vectors that need an optional codec are skipped where it is missing: LZ4 everywhere;
|
|
288
|
+
Brotli and Zstandard in the portable entry without `codecs` (and Zstandard on
|
|
289
|
+
Node.js < 22.15).
|
|
290
|
+
|
|
291
|
+
## Performance
|
|
292
|
+
|
|
293
|
+
Measured with `npm run bench` (Node.js 22.22.2, Apple M4 Max, best of 7 rounds; Node
|
|
294
|
+
entry). Throughput is relative to the BTOON size. JSON figures are for comparison only.
|
|
295
|
+
|
|
296
|
+
| Case | BTOON size | encode | decode | JSON size | `JSON.stringify` | `JSON.parse` |
|
|
297
|
+
|---|---|---|---|---|---|---|
|
|
298
|
+
| 10k records, tabular | 571,851 B | 1.54 ms | 2.92 ms | 1,242,300 B | 2.01 ms | 2.80 ms |
|
|
299
|
+
| 10k records, `autoTabular: false` | 941,744 B | 3.76 ms | 3.97 ms | 1,242,300 B | 2.01 ms | 2.81 ms |
|
|
300
|
+
| 2k nested objects | 177,644 B | 0.74 ms | 0.66 ms | 298,714 B | 0.63 ms | 0.96 ms |
|
|
301
|
+
|
|
302
|
+
The portable entry measured up to 6% slower than these numbers on the same machine.
|
|
303
|
+
|
|
304
|
+
Bundle size of the portable ESM entry with everything imported, bundled with esbuild
|
|
305
|
+
0.28.2 (`--minify`): 44.1 KB, 14.7 KB gzipped. An application that imports only `decode`
|
|
306
|
+
bundles 25.9 KB (9.4 KB gzipped).
|
|
307
|
+
|
|
308
|
+
## Development
|
|
309
|
+
|
|
310
|
+
```sh
|
|
311
|
+
npm install
|
|
312
|
+
npm run build # dist/esm and dist/cjs, each with .d.ts files (tsc)
|
|
313
|
+
npm run typecheck # strict type check of src/
|
|
314
|
+
npm test # node:test suite against dist/ (vectors, unit, fuzz, packaging)
|
|
315
|
+
npm run bench
|
|
316
|
+
npm run vectors:sync # re-copy the vectors from ../btoon-core (or a path argument)
|
|
317
|
+
```
|
|
318
|
+
|
|
319
|
+
`npm test -- fuzz vectors` runs only the matching test files. `FUZZ_ITERATIONS`,
|
|
320
|
+
`FUZZ_SEED`, `INFLATE_MUTATIONS` and `CANONICAL_ITERATIONS` scale the randomized tests.
|
|
321
|
+
The `Makefile` wraps the same commands. CI runs type checking, the build and the tests on
|
|
322
|
+
Node.js 20, 22, 24 and 26.
|
|
323
|
+
|
|
324
|
+
Never edit `test/vectors/btoon-vectors.json` by hand: it is generated by btoon-core's
|
|
325
|
+
`tests/vectors/generate.py`.
|
|
326
|
+
|
|
327
|
+
## License
|
|
328
|
+
|
|
329
|
+
MIT. See [LICENSE](LICENSE).
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/** Version of this library. */
|
|
2
|
+
export declare const VERSION: "0.1.0";
|
|
3
|
+
/** Version of the BTOON specification this library implements. */
|
|
4
|
+
export declare const SPEC_VERSION: "0.1";
|
|
5
|
+
export declare const EXT_TIMESTAMP = -1;
|
|
6
|
+
export declare const EXT_DATE = -2;
|
|
7
|
+
export declare const EXT_DATETIME = -3;
|
|
8
|
+
export declare const EXT_BIGINT = -4;
|
|
9
|
+
export declare const EXT_F32_VECTOR = -5;
|
|
10
|
+
export declare const EXT_F64_VECTOR = -6;
|
|
11
|
+
export declare const EXT_TABULAR = -10;
|
|
12
|
+
export declare const TABULAR_VERSION = 1;
|
|
13
|
+
export declare const MAX_OFFSET_MINUTES = 1080;
|
|
14
|
+
export declare const NANOS_PER_SECOND = 1000000000;
|
|
15
|
+
export declare const DEFAULT_MAX_DEPTH = 128;
|
|
16
|
+
export declare const DEFAULT_MAX_DECOMPRESSED_SIZE: number;
|
|
17
|
+
/** Encoders SHOULD skip compression below this size (SPEC §6). */
|
|
18
|
+
export declare const MIN_COMPRESSION_SIZE = 256;
|
|
19
|
+
export declare const COMPRESSION_HEADER_SIZE = 16;
|
|
20
|
+
export declare const SIGNED_HEADER_SIZE = 40;
|
|
21
|
+
/** Compression algorithm ids of the envelope header (SPEC §6). */
|
|
22
|
+
export declare const ALGORITHM_IDS: {
|
|
23
|
+
readonly zlib: 0;
|
|
24
|
+
readonly lz4: 1;
|
|
25
|
+
readonly zstd: 2;
|
|
26
|
+
readonly brotli: 3;
|
|
27
|
+
readonly snappy: 4;
|
|
28
|
+
};
|
|
29
|
+
export declare const ALGORITHM_NAMES: readonly ["zlib", "lz4", "zstd", "brotli", "snappy"];
|
|
30
|
+
/** Brands that identify {@link Ext} and {@link Timestamp} instances across module copies. */
|
|
31
|
+
export declare const EXT_BRAND: unique symbol;
|
|
32
|
+
export declare const TIMESTAMP_BRAND: unique symbol;
|
|
33
|
+
/** Date's representable range is ±8.64e15 ms around the epoch. */
|
|
34
|
+
export declare const MAX_DATE_MS = 8640000000000000;
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.MAX_DATE_MS = exports.TIMESTAMP_BRAND = exports.EXT_BRAND = exports.ALGORITHM_NAMES = exports.ALGORITHM_IDS = exports.SIGNED_HEADER_SIZE = exports.COMPRESSION_HEADER_SIZE = exports.MIN_COMPRESSION_SIZE = exports.DEFAULT_MAX_DECOMPRESSED_SIZE = exports.DEFAULT_MAX_DEPTH = exports.NANOS_PER_SECOND = exports.MAX_OFFSET_MINUTES = exports.TABULAR_VERSION = exports.EXT_TABULAR = exports.EXT_F64_VECTOR = exports.EXT_F32_VECTOR = exports.EXT_BIGINT = exports.EXT_DATETIME = exports.EXT_DATE = exports.EXT_TIMESTAMP = exports.SPEC_VERSION = exports.VERSION = void 0;
|
|
4
|
+
/** Version of this library. */
|
|
5
|
+
exports.VERSION = '0.1.0';
|
|
6
|
+
/** Version of the BTOON specification this library implements. */
|
|
7
|
+
exports.SPEC_VERSION = '0.1';
|
|
8
|
+
// Extension type ids (SPEC §5).
|
|
9
|
+
exports.EXT_TIMESTAMP = -1;
|
|
10
|
+
exports.EXT_DATE = -2;
|
|
11
|
+
exports.EXT_DATETIME = -3;
|
|
12
|
+
exports.EXT_BIGINT = -4;
|
|
13
|
+
exports.EXT_F32_VECTOR = -5;
|
|
14
|
+
exports.EXT_F64_VECTOR = -6;
|
|
15
|
+
exports.EXT_TABULAR = -10;
|
|
16
|
+
exports.TABULAR_VERSION = 1;
|
|
17
|
+
exports.MAX_OFFSET_MINUTES = 1080;
|
|
18
|
+
exports.NANOS_PER_SECOND = 1000000000;
|
|
19
|
+
exports.DEFAULT_MAX_DEPTH = 128;
|
|
20
|
+
exports.DEFAULT_MAX_DECOMPRESSED_SIZE = 256 * 1024 * 1024;
|
|
21
|
+
/** Encoders SHOULD skip compression below this size (SPEC §6). */
|
|
22
|
+
exports.MIN_COMPRESSION_SIZE = 256;
|
|
23
|
+
exports.COMPRESSION_HEADER_SIZE = 16;
|
|
24
|
+
exports.SIGNED_HEADER_SIZE = 40;
|
|
25
|
+
/** Compression algorithm ids of the envelope header (SPEC §6). */
|
|
26
|
+
exports.ALGORITHM_IDS = { zlib: 0, lz4: 1, zstd: 2, brotli: 3, snappy: 4 };
|
|
27
|
+
exports.ALGORITHM_NAMES = ['zlib', 'lz4', 'zstd', 'brotli', 'snappy'];
|
|
28
|
+
/** Brands that identify {@link Ext} and {@link Timestamp} instances across module copies. */
|
|
29
|
+
exports.EXT_BRAND = Symbol.for('btoon.Ext');
|
|
30
|
+
exports.TIMESTAMP_BRAND = Symbol.for('btoon.Timestamp');
|
|
31
|
+
/** Date's representable range is ±8.64e15 ms around the epoch. */
|
|
32
|
+
exports.MAX_DATE_MS = 8640000000000000;
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { type CompressionAlgorithm, type DecodeOptions, type EncodeOptions } from './options.js';
|
|
2
|
+
/** Synchronous building blocks of one environment. */
|
|
3
|
+
export interface Platform {
|
|
4
|
+
/** Decompressors; `size` is the declared uncompressed size (already checked against the limit). */
|
|
5
|
+
readonly decompress: Readonly<Partial<Record<CompressionAlgorithm, (data: Uint8Array, size: number) => Uint8Array>>>;
|
|
6
|
+
readonly compress: Readonly<Partial<Record<CompressionAlgorithm, (data: Uint8Array) => Uint8Array>>>;
|
|
7
|
+
hmac(key: Uint8Array, parts: readonly Uint8Array[]): Uint8Array;
|
|
8
|
+
equal(a: Uint8Array, b: Uint8Array): boolean;
|
|
9
|
+
}
|
|
10
|
+
/** Input accepted by `decode`. */
|
|
11
|
+
export type BinaryInput = Uint8Array | ArrayBuffer | ArrayBufferView;
|
|
12
|
+
export declare function encodeWith(value: unknown, options: EncodeOptions | undefined, platform: Platform): Uint8Array<ArrayBuffer>;
|
|
13
|
+
export declare function decodeWith(data: BinaryInput, options: DecodeOptions | undefined, platform: Platform): unknown;
|
|
14
|
+
export declare function encodeAsyncWith(value: unknown, options: EncodeOptions | undefined, platform: Platform): Promise<Uint8Array<ArrayBuffer>>;
|
|
15
|
+
export declare function decodeAsyncWith(data: BinaryInput, options: DecodeOptions | undefined, platform: Platform): Promise<unknown>;
|