cborg 4.4.1 → 4.5.1

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.
Files changed (59) hide show
  1. package/CHANGELOG.md +9 -0
  2. package/README.md +21 -0
  3. package/bench/README.md +115 -0
  4. package/bench/bench-comparative.js +133 -0
  5. package/bench/bench.js +414 -101
  6. package/bench/fixtures.js +558 -0
  7. package/bench/index.html +405 -0
  8. package/cborg.js +2 -1
  9. package/interface.ts +8 -2
  10. package/lib/0uint.js +11 -11
  11. package/lib/1negint.js +4 -4
  12. package/lib/2bytes.js +5 -5
  13. package/lib/3string.js +1 -1
  14. package/lib/4array.js +4 -4
  15. package/lib/5map.js +4 -4
  16. package/lib/6tag.js +4 -4
  17. package/lib/7float.js +10 -10
  18. package/lib/bl.js +46 -0
  19. package/lib/encode.js +40 -15
  20. package/lib/is.js +12 -31
  21. package/lib/json/encode.js +10 -10
  22. package/package.json +1 -1
  23. package/test/test-0uint.js +12 -1
  24. package/test/test-1negint.js +12 -1
  25. package/test/test-2bytes.js +11 -1
  26. package/test/test-3string.js +11 -1
  27. package/test/test-4array.js +11 -1
  28. package/test/test-5map.js +11 -3
  29. package/test/test-6tag.js +19 -1
  30. package/test/test-7float.js +11 -1
  31. package/test/test-cbor-vectors.js +13 -2
  32. package/test/test-encodeInto.js +246 -0
  33. package/types/cborg.d.ts +2 -1
  34. package/types/cborg.d.ts.map +1 -1
  35. package/types/interface.d.ts +7 -2
  36. package/types/interface.d.ts.map +1 -1
  37. package/types/lib/0uint.d.ts +6 -6
  38. package/types/lib/0uint.d.ts.map +1 -1
  39. package/types/lib/1negint.d.ts +4 -4
  40. package/types/lib/1negint.d.ts.map +1 -1
  41. package/types/lib/2bytes.d.ts +3 -3
  42. package/types/lib/2bytes.d.ts.map +1 -1
  43. package/types/lib/3string.d.ts +1 -1
  44. package/types/lib/3string.d.ts.map +1 -1
  45. package/types/lib/4array.d.ts +3 -3
  46. package/types/lib/4array.d.ts.map +1 -1
  47. package/types/lib/5map.d.ts +3 -3
  48. package/types/lib/5map.d.ts.map +1 -1
  49. package/types/lib/6tag.d.ts +4 -4
  50. package/types/lib/6tag.d.ts.map +1 -1
  51. package/types/lib/7float.d.ts +3 -3
  52. package/types/lib/7float.d.ts.map +1 -1
  53. package/types/lib/bl.d.ts +25 -0
  54. package/types/lib/bl.d.ts.map +1 -1
  55. package/types/lib/encode.d.ts +12 -1
  56. package/types/lib/encode.d.ts.map +1 -1
  57. package/types/lib/is.d.ts.map +1 -1
  58. package/types/lib/json/encode.d.ts +1 -1
  59. package/types/lib/json/encode.d.ts.map +1 -1
package/CHANGELOG.md CHANGED
@@ -1,3 +1,12 @@
1
+ ## [4.5.1](https://github.com/rvagg/cborg/compare/v4.5.0...v4.5.1) (2026-01-20)
2
+
3
+ ## [4.5.0](https://github.com/rvagg/cborg/compare/v4.4.1...v4.5.0) (2026-01-20)
4
+
5
+ ### Features
6
+
7
+ * add encodeInto() for BYO buffer encoding ([a577843](https://github.com/rvagg/cborg/commit/a5778432faa44be491ce2fdd814eceddd1979d77))
8
+ * **bench:** add realistic IPLD workload benchmarks ([07d9618](https://github.com/rvagg/cborg/commit/07d9618f1a86262d59690ff00e268db0ad0c6d4a))
9
+
1
10
  ## [4.4.1](https://github.com/rvagg/cborg/compare/v4.4.0...v4.4.1) (2026-01-19)
2
11
 
3
12
  ### Trivial Changes
package/README.md CHANGED
@@ -25,6 +25,7 @@
25
25
  * [API](#api)
26
26
  * [`encode(object[, options])`](#encodeobject-options)
27
27
  * [Options](#options)
28
+ * [`encodeInto(data, destination[, options])`](#encodeintodata-destination-options)
28
29
  * [`decode(data[, options])`](#decodedata-options)
29
30
  * [Options](#options-1)
30
31
  * [`decodeFirst(data[, options])`](#decodefirstdata-options)
@@ -221,6 +222,26 @@ Encode a JavaScript object and return a `Uint8Array` with the CBOR byte represen
221
222
  * `mapSorter` (function): a function taking two arguments, where each argument is a `Token`, or an array of `Token`s representing the keys of a map being encoded. Similar to other JavaScript compare functions, a `-1`, `1` or `0` (which shouldn't be possible) should be returned depending on the sorting order of the keys. See the source code for the default sorting order which uses the length-first rule recommendation from [RFC 7049](https://tools.ietf.org/html/rfc7049).
222
223
  * `ignoreUndefinedProperties` (boolean, default `false`): when encoding a plain object, properties with `undefined` values will be omitted. Does not apply to `Map`s or arrays.
223
224
 
225
+ ### `encodeInto(data, destination[, options])`
226
+
227
+ ```js
228
+ import { encodeInto } from 'cborg'
229
+ ```
230
+
231
+ Encode a JavaScript object directly into a provided `Uint8Array` destination buffer, returning an object with a `written` property indicating the number of bytes written.
232
+
233
+ This API mirrors [`TextEncoder.encodeInto()`](https://developer.mozilla.org/en-US/docs/Web/API/TextEncoder/encodeInto) and is useful for performance-critical scenarios where you want to avoid allocations by reusing a buffer.
234
+
235
+ ```js
236
+ const destination = new Uint8Array(1024)
237
+ const { written } = encodeInto({ hello: 'world' }, destination)
238
+ const encoded = destination.subarray(0, written)
239
+ ```
240
+
241
+ If the destination buffer is too small to hold the encoded data, an error will be thrown. Use `encodedLength()` to pre-calculate the required size if needed.
242
+
243
+ The same encoding rules and options as [`encode()`](#encodeobject-options) apply.
244
+
224
245
  ### `decode(data[, options])`
225
246
 
226
247
  ```js
@@ -0,0 +1,115 @@
1
+ # cborg Benchmarks
2
+
3
+ Benchmarks for measuring cborg encode/decode performance with realistic IPLD workloads.
4
+
5
+ ## Quick Start
6
+
7
+ ```bash
8
+ # Run all benchmarks (dag-cbor mode with tag 42 + strict options)
9
+ node bench/bench.js
10
+
11
+ # Run specific suite
12
+ node bench/bench.js --suite=bsky # Bluesky (string-heavy)
13
+ node bench/bench.js --suite=filecoin # Filecoin (bytes-heavy)
14
+ node bench/bench.js --suite=micro # Micro-benchmarks (maps, strings, integers, etc.)
15
+
16
+ # Run in raw mode (no tags, minimal options) for baseline comparison
17
+ node bench/bench.js --mode=raw
18
+
19
+ # Test encodeInto() performance
20
+ node bench/bench.js --encode-into
21
+
22
+ # Adjust duration per test (default 1000ms)
23
+ node bench/bench.js --duration=2000
24
+ ```
25
+
26
+ ## Browser Benchmarks
27
+
28
+ Open `bench/index.html` in a browser. Select mode, suite, and duration, then click "Run Benchmarks".
29
+
30
+ ## Capturing and Comparing Results
31
+
32
+ ### Save a baseline
33
+
34
+ ```bash
35
+ # Save results to a file
36
+ node bench/bench.js --json > bench/baselines/before-change.json
37
+
38
+ # Or with a version/date identifier
39
+ node bench/bench.js --json > bench/baselines/v4.4.0.json
40
+ node bench/bench.js --json > bench/baselines/$(date +%Y%m%d).json
41
+ ```
42
+
43
+ ### Compare against baseline
44
+
45
+ ```bash
46
+ node bench/bench.js --compare=bench/baselines/before-change.json
47
+ ```
48
+
49
+ This shows percentage differences for each benchmark.
50
+
51
+ ### Typical workflow
52
+
53
+ 1. Capture baseline before making changes:
54
+ ```bash
55
+ node bench/bench.js --json > bench/baselines/before.json
56
+ ```
57
+
58
+ 2. Make your changes to cborg
59
+
60
+ 3. Compare:
61
+ ```bash
62
+ node bench/bench.js --compare=bench/baselines/before.json
63
+ ```
64
+
65
+ 4. Optionally save the new results:
66
+ ```bash
67
+ node bench/bench.js --json > bench/baselines/after.json
68
+ ```
69
+
70
+ ## Benchmark Modes
71
+
72
+ ### dag-cbor mode (default)
73
+
74
+ Simulates `@ipld/dag-cbor` usage:
75
+ - CID encoding/decoding via CBOR tag 42
76
+ - `float64: true` (always 64-bit floats)
77
+ - `strict: true` on decode
78
+ - `rejectDuplicateMapKeys: true`
79
+ - Bluesky suite uses `ignoreUndefinedProperties: true`
80
+ - Filecoin/micro suites throw on undefined (strict IPLD)
81
+
82
+ ### raw mode (`--mode=raw`)
83
+
84
+ Minimal cborg options for baseline comparison:
85
+ - No tag encoding/decoding
86
+ - Default encode/decode options
87
+ - Shows overhead of dag-cbor-specific features
88
+
89
+ ## Files
90
+
91
+ - `bench.js` - Main benchmark runner (Node.js)
92
+ - `index.html` - Browser benchmark UI
93
+ - `fixtures.js` - Deterministic fixture generators (seeded PRNG)
94
+ - `bench-comparative.js` - Historical cborg vs borc comparison
95
+ - `json.js` - JSON encode/decode benchmarks
96
+
97
+ ## Fixture Suites
98
+
99
+ ### Bluesky (string-heavy)
100
+ - Posts, follows, likes, reposts, profiles
101
+ - MST (Merkle Search Tree) nodes
102
+ - Heavy on strings, DIDs, AT-URIs, timestamps
103
+
104
+ ### Filecoin (bytes-heavy)
105
+ - Messages, block headers
106
+ - HAMT and AMT nodes
107
+ - CID arrays
108
+ - Heavy on byte arrays and CIDs
109
+
110
+ ### Micro-benchmarks
111
+ - Maps (small/medium/large key counts)
112
+ - Nesting depth (shallow/deep)
113
+ - Strings (short/medium/long)
114
+ - Integers (small/medium/large)
115
+ - Bytes (small/medium/large)
@@ -0,0 +1,133 @@
1
+ // can be run in a browser with `polendina --runner=bare-sync --timeout 6000 --cleanup bench.js`
2
+ // with additional dependencies for cborg installed here
3
+
4
+ import assert from 'assert'
5
+ import { garbage } from 'ipld-garbage'
6
+ import { decode, encode, encodeInto } from '../cborg.js'
7
+ import borc from 'borc'
8
+
9
+ const WITH_CBORG_FIXED_DESTINATION = true
10
+
11
+ let writebuf = ''
12
+ const write = process.stdout
13
+ ? process.stdout.write.bind(process.stdout)
14
+ : (str) => {
15
+ writebuf += str
16
+ if (str.endsWith('\n')) {
17
+ console.log(writebuf.replace(/\n$/, ''))
18
+ writebuf = ''
19
+ }
20
+ }
21
+
22
+ function runWith (description, count, targetTime, size, options) {
23
+ let borcDecoder = null
24
+ const borcDecode = (bytes) => {
25
+ if (!borcDecoder) {
26
+ // account for initial allocation & setup time in benchmark
27
+ borcDecoder = new borc.Decoder({ size: 10 * 1024 * 1024 })
28
+ }
29
+ return borcDecoder.decodeAll(bytes)[0]
30
+ }
31
+
32
+ let cborgEncoder = WITH_CBORG_FIXED_DESTINATION ? null : encode
33
+
34
+ const cborgEncode = (bytes) => {
35
+ if (!cborgEncoder) {
36
+ // account for initial allocation & setup time in benchmark
37
+ const fixedDestination = new Uint8Array(10 * 1024 * 1024)
38
+ cborgEncoder = (bytes) => {
39
+ const { written } = encodeInto(bytes, fixedDestination)
40
+ return fixedDestination.subarray(0, written)
41
+ }
42
+ }
43
+ return cborgEncoder(bytes)
44
+ }
45
+
46
+ const fixtures = []
47
+
48
+ console.log(`${description} @ ${count.toLocaleString()}`)
49
+ for (let i = 0; i < count; i++) {
50
+ const obj = garbage(size, options)
51
+ const cbyts = encode(obj)
52
+ /*
53
+ const bbyts = borc.encode(obj)
54
+ if (Buffer.compare(Buffer.from(cbyts), bbyts) !== 0) {
55
+ console.log(`mismatch for obj: ${JSON.stringify(obj)}`)
56
+ console.log('\t', Buffer.from(cbyts).toString('hex'))
57
+ console.log('\t', Buffer.from(bbyts).toString('hex'))
58
+ }
59
+ */
60
+ if (cbyts.length <= size * 2) {
61
+ fixtures.push([obj, cbyts])
62
+ }
63
+ }
64
+ const avgSize = Math.round(fixtures.reduce((p, c) => p + c[1].length, 0) / fixtures.length)
65
+
66
+ const enc = (encoder) => {
67
+ for (const [obj, byts] of fixtures) {
68
+ const ebyts = encoder(obj)
69
+ if (byts.length !== ebyts.length) {
70
+ throw new Error('bork')
71
+ }
72
+ }
73
+ return fixtures.length
74
+ }
75
+
76
+ const bench = (bfn) => {
77
+ const start = Date.now()
78
+ let opcount = 0
79
+ do {
80
+ opcount += bfn()
81
+ } while (Date.now() - start < targetTime)
82
+ const ops = Math.round(opcount / ((Date.now() - start) / 1000))
83
+ return ops
84
+ }
85
+
86
+ const dec = (decoder) => {
87
+ for (const [obj, byts] of fixtures) {
88
+ const cobj = decoder(byts)
89
+ if (obj != null && typeof obj === 'object') {
90
+ assert.deepStrictEqual(Object.keys(cobj).length, Object.keys(obj).length)
91
+ } else {
92
+ assert.deepStrictEqual(obj, cobj)
93
+ }
94
+ }
95
+ return fixtures.length
96
+ }
97
+
98
+ const cmp = (desc, cbfn, bofn) => {
99
+ write(`\t${desc} (avg ${avgSize.toLocaleString()} b):`)
100
+ const cborgOps = bench(cbfn)
101
+ write(` cborg @ ${cborgOps.toLocaleString()} op/s`)
102
+ const borcOps = bench(bofn)
103
+ write(` / borc @ ${borcOps.toLocaleString()} op/s`)
104
+ const percent = Math.round((cborgOps / borcOps) * 1000) / 10
105
+ write(` = ${(percent).toLocaleString()} %\n`)
106
+ return percent
107
+ }
108
+
109
+ return [
110
+ cmp('encode', () => enc(cborgEncode), () => enc(borc.encode)),
111
+ cmp('decode', () => dec(decode), () => dec(borcDecode))
112
+ ]
113
+ }
114
+
115
+ const targetTime = 1000
116
+ const accum = []
117
+ accum.push(runWith('rnd-100', 1000, targetTime, 100, { weights: { CID: 0 } }))
118
+ accum.push(runWith('rnd-300', 1000, targetTime, 300, { weights: { CID: 0 } }))
119
+ accum.push(runWith('rnd-nomap-300', 1000, targetTime, 300, { weights: { CID: 0, map: 0 } }))
120
+ accum.push(runWith('rnd-nolist-300', 1000, targetTime, 300, { weights: { CID: 0, list: 0 } }))
121
+ accum.push(runWith('rnd-nofloat-300', 1000, targetTime, 300, { weights: { CID: 0, float: 0 } }))
122
+ accum.push(runWith('rnd-nomaj7-300', 1000, targetTime, 300, { weights: { CID: 0, float: 0, null: 0, boolean: 0 } }))
123
+ accum.push(runWith('rnd-nostr-300', 1000, targetTime, 300, { weights: { CID: 0, string: 0, bytes: 0 } }))
124
+ accum.push(runWith('rnd-nostrbyts-300', 1000, targetTime, 300, { weights: { CID: 0, string: 0 } }))
125
+ accum.push(runWith('rnd-1000', 1000, targetTime, 1000, { weights: { CID: 0 } }))
126
+ accum.push(runWith('rnd-2000', 1000, targetTime, 2000, { weights: { CID: 0 } }))
127
+ accum.push(runWith('rnd-fil-100', 1000, targetTime, 100, { weights: { float: 0, map: 0, CID: 0 } }))
128
+ accum.push(runWith('rnd-fil-300', 1000, targetTime, 300, { weights: { float: 0, map: 0, CID: 0 } }))
129
+ accum.push(runWith('rnd-fil-500', 1000, targetTime, 500, { weights: { float: 0, map: 0, CID: 0 } }))
130
+ accum.push(runWith('rnd-fil-1000', 1000, targetTime, 1000, { weights: { float: 0, map: 0, CID: 0 } }))
131
+ accum.push(runWith('rnd-fil-2000', 1000, targetTime, 2000, { weights: { float: 0, map: 0, CID: 0 } }))
132
+ console.log(`Avg encode: ${Math.round(accum.reduce((p, c) => p + c[0], 0) / accum.length).toLocaleString()} %`)
133
+ console.log(`Avg decode: ${Math.round(accum.reduce((p, c) => p + c[1], 0) / accum.length).toLocaleString()} %`)