microcbor 0.3.0 → 0.4.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/README.md CHANGED
@@ -8,7 +8,7 @@ microcbor is a minimal JavaScript [CBOR](https://cbor.io/) implementation featur
8
8
 
9
9
  - a small footprint,
10
10
  - fast performance, and
11
- - an async iterable streaming API
11
+ - `Iterable` and `AsyncIterable` streaming interfaces
12
12
 
13
13
  microcbor follows the [deterministic CBOR encoding requirements](https://www.rfc-editor.org/rfc/rfc8949.html#core-det) - all floating-point numbers are serialized in the smallest possible size without losing precision, and object entries are always sorted by key in byte-wise lexicographic order. `NaN` is always serialized as `0xf97e00`. **microcbor doesn't support tags, bigints, typed arrays, non-string keys, or indefinite-length collections.**
14
14
 
@@ -58,40 +58,27 @@ console.log(decode(data))
58
58
  ## API
59
59
 
60
60
  ```ts
61
- declare type CBORValue =
62
- | undefined
63
- | null
64
- | boolean
65
- | number
66
- | string
67
- | Uint8Array
68
- | CBORArray
69
- | CBORMap
61
+ declare type CBORValue = undefined | null | boolean | number | string | Uint8Array | CBORArray | CBORMap
70
62
 
71
63
  interface CBORArray extends Array<CBORValue> {}
72
64
  interface CBORMap {
73
- [key: string]: CBORValue
65
+ [key: string]: CBORValue
74
66
  }
75
67
 
76
68
  // If not provided, chunkSize defaults to 512 bytes.
77
69
  // It's only a guideline; `encodeStream` won't break up
78
70
  // individual CBOR values like strings or byte arrays
79
71
  // that are larger than the provided chunk size.
80
- declare function encode(
81
- value: CBORValue,
82
- options?: { chunkSize?: number }
83
- ): Uint8Array
72
+ declare function encode(value: CBORValue, options?: { chunkSize?: number }): Uint8Array
84
73
 
85
74
  declare function encodeStream(
86
- source: AsyncIterable<CBORValue>,
87
- options?: { chunkSize?: number }
75
+ source: AsyncIterable<CBORValue>,
76
+ options?: { chunkSize?: number },
88
77
  ): AsyncIterable<Uint8Array>
89
78
 
90
79
  declare function decode(data: Uint8Array): CBORValue
91
80
 
92
- declare function decodeStream(
93
- source: AsyncIterable<Uint8Array>
94
- ): AsyncIterable<CBORValue>
81
+ declare function decodeStream(source: AsyncIterable<Uint8Array>): AsyncIterable<CBORValue>
95
82
 
96
83
  // You can measure the byte length that a given value will
97
84
  // serialize to without actually allocating anything.
@@ -105,7 +92,7 @@ declare function encodingLength(value: CBORValue): number
105
92
 
106
93
  ```typescript
107
94
  declare class UnsafeIntegerError extends RangeError {
108
- readonly value: bigint
95
+ readonly value: bigint
109
96
  }
110
97
  ```
111
98
 
@@ -138,26 +125,40 @@ npm run test
138
125
  - microcbor runs isomorphically on the web, in Node, and in Deno. node-cbor ships a separate cbor-web package.
139
126
  - microcbor encodes `Uint8Array` values as CBOR byte strings (major type 2). node-cbor encodes `Uint8Array` values as tagged type arrays (major type 6 / RFC 8746), and encodes NodeJS `Buffer` values as CBOR byte strings (major type 2).
140
127
  - microcbor uses async iterables for its streaming API. node-cbor uses NodeJS streams.
141
- - microcbor is about **2x faster** than node-cbor at encoding and about **1.5x faster** than node-cbor at decoding.
128
+ - microcbor is about **5x faster** than node-cbor at encoding, and comparable at decoding.
142
129
 
143
130
  ```
144
- microcbor % npm run test -- test/benchmarks.test.js
145
-
146
- > microcbor@0.2.0 test
147
- > ava
148
-
149
-
150
- time encode() (390ms)
151
- ℹ microcbor: 66.47262525558472 (ms)
152
- ℹ node-cbor: 155.0249171257019 (ms)
153
- JSON.stringify: 5.56374979019165 (ms)
154
- time decode() (161ms)
155
- ℹ microcbor: 64.23729228973389 (ms)
156
- ℹ node-cbor: 91.34658432006836 (ms)
157
- JSON.parse: 2.7592921257019043 (ms)
158
-
159
-
160
- 2 tests passed
131
+ microcbor % npm run test -- test/benchmarks.test.ts
132
+
133
+ > microcbor@0.3.0 test
134
+ > ava test/benchmarks.test.ts
135
+
136
+ ✔ time encode() (196ms)
137
+ microcbor: {
138
+ avg: 0.20750288999999897,
139
+ std: 0.1599497238014431,
140
+ } (ms)
141
+ node-cbor: {
142
+ avg: 1.012210439999998,
143
+ std: 1.1648550529513988,
144
+ } (ms)
145
+ ℹ JSON.stringify: {
146
+ avg: 0.011432450000000358,
147
+ std: 0.0014736483187953618,
148
+ } (ms)
149
+ ✔ time decode()
150
+ ℹ microcbor: {
151
+ avg: 0.3199704100000008,
152
+ std: 0.6562532760030573,
153
+ } (ms)
154
+ ℹ node-cbor: {
155
+ avg: 0.35316917000000214,
156
+ std: 0.32859580328312393,
157
+ } (ms)
158
+ ℹ JSON.parse: {
159
+ avg: 0.016885789999999474,
160
+ std: 0.0041605677456241505,
161
+ } (ms)
161
162
  ```
162
163
 
163
164
  ## Contributing
package/lib/encode.d.ts CHANGED
@@ -1,16 +1,18 @@
1
1
  import type { CBORValue } from "./types.js";
2
2
  export declare class Encoder {
3
+ #private;
3
4
  static defaultChunkSize: number;
4
- closed: boolean;
5
- private buffer;
6
- private view;
7
- private offset;
5
+ readonly noCopy: boolean;
6
+ readonly chunkSize: number;
8
7
  private readonly encoder;
9
- private readonly chunkSize;
8
+ private readonly buffer;
9
+ private readonly view;
10
+ private offset;
10
11
  constructor(options?: {
11
- chunkSize?: number;
12
12
  noCopy?: boolean;
13
+ chunkSize?: number;
13
14
  });
15
+ get closed(): boolean;
14
16
  private allocate;
15
17
  private float16;
16
18
  private float32;
package/lib/encode.js CHANGED
@@ -1,20 +1,38 @@
1
- import { getFloat16Precision, getFloat32Precision, setFloat16, Precision, } from "fp16";
1
+ var __classPrivateFieldSet = (this && this.__classPrivateFieldSet) || function (receiver, state, value, kind, f) {
2
+ if (kind === "m") throw new TypeError("Private method is not writable");
3
+ if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a setter");
4
+ if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot write private member to an object whose class did not declare it");
5
+ return (kind === "a" ? f.call(receiver, value) : f ? f.value = value : state.set(receiver, value)), value;
6
+ };
7
+ var __classPrivateFieldGet = (this && this.__classPrivateFieldGet) || function (receiver, state, kind, f) {
8
+ if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a getter");
9
+ if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot read private member from an object whose class did not declare it");
10
+ return kind === "m" ? f : kind === "a" ? f.call(receiver) : f ? f.value : state.get(receiver);
11
+ };
12
+ var _Encoder_instances, _Encoder_closed, _Encoder_flush;
13
+ import { getFloat16Precision, getFloat32Precision, setFloat16, Precision } from "fp16";
14
+ import { getByteLength } from "./encodingLength.js";
15
+ import { assert } from "./utils.js";
2
16
  export class Encoder {
3
17
  constructor(options = {}) {
18
+ _Encoder_instances.add(this);
19
+ _Encoder_closed.set(this, void 0);
4
20
  this.encoder = new TextEncoder();
5
- this.chunkSize = options.chunkSize || Encoder.defaultChunkSize;
21
+ this.noCopy = options.noCopy ?? false;
22
+ this.chunkSize = options.chunkSize ?? Encoder.defaultChunkSize;
23
+ assert(this.chunkSize >= 8, "expected chunkSize >= 8");
6
24
  this.buffer = new ArrayBuffer(this.chunkSize);
7
25
  this.view = new DataView(this.buffer);
8
26
  this.offset = 0;
9
- this.closed = false;
27
+ __classPrivateFieldSet(this, _Encoder_closed, false, "f");
28
+ }
29
+ get closed() {
30
+ return __classPrivateFieldGet(this, _Encoder_closed, "f");
10
31
  }
11
32
  *allocate(size) {
33
+ assert(size <= 8, "expected size <= 8");
12
34
  if (this.buffer.byteLength < this.offset + size) {
13
- yield new Uint8Array(this.buffer, 0, this.offset);
14
- const byteLength = Math.max(size, this.chunkSize);
15
- this.buffer = new ArrayBuffer(byteLength);
16
- this.view = new DataView(this.buffer);
17
- this.offset = 0;
35
+ yield __classPrivateFieldGet(this, _Encoder_instances, "m", _Encoder_flush).call(this);
18
36
  }
19
37
  }
20
38
  *float16(value) {
@@ -73,9 +91,7 @@ export class Encoder {
73
91
  else if (Object.is(value, -0)) {
74
92
  yield* this.encodeFloat(value);
75
93
  }
76
- else if (Math.floor(value) === value &&
77
- Number.MIN_SAFE_INTEGER <= value &&
78
- value <= Number.MAX_SAFE_INTEGER) {
94
+ else if (Math.floor(value) === value && Number.MIN_SAFE_INTEGER <= value && value <= Number.MAX_SAFE_INTEGER) {
79
95
  yield* this.encodeInteger(value);
80
96
  }
81
97
  else {
@@ -105,20 +121,38 @@ export class Encoder {
105
121
  }
106
122
  }
107
123
  *encodeString(value) {
108
- const data = this.encoder.encode(value);
109
- yield* this.encodeTypeAndArgument(3, data.byteLength);
110
- yield* this.allocate(data.byteLength);
111
- new Uint8Array(this.buffer, this.offset).set(data);
112
- this.offset += data.byteLength;
124
+ const byteLength = getByteLength(value);
125
+ yield* this.encodeTypeAndArgument(3, byteLength);
126
+ let start = 0;
127
+ while (start < value.length) {
128
+ if (this.offset + 4 > this.buffer.byteLength) {
129
+ yield __classPrivateFieldGet(this, _Encoder_instances, "m", _Encoder_flush).call(this);
130
+ }
131
+ const target = new Uint8Array(this.buffer, this.offset);
132
+ const result = this.encoder.encodeInto(value.slice(start), target);
133
+ start += result.read;
134
+ this.offset += result.written;
135
+ assert(this.offset <= this.buffer.byteLength, "expected this.offset <= this.buffer.byteLength");
136
+ }
113
137
  }
114
138
  *encodeBytes(value) {
115
139
  yield* this.encodeTypeAndArgument(2, value.byteLength);
116
- yield* this.allocate(value.byteLength);
117
- new Uint8Array(this.buffer, this.offset, value.byteLength).set(value);
118
- this.offset += value.byteLength;
140
+ const target = new Uint8Array(this.buffer, 0, this.buffer.byteLength);
141
+ let start = 0;
142
+ while (start < value.byteLength) {
143
+ if (this.offset >= this.buffer.byteLength) {
144
+ yield __classPrivateFieldGet(this, _Encoder_instances, "m", _Encoder_flush).call(this);
145
+ }
146
+ const capacity = this.buffer.byteLength - this.offset;
147
+ const remaining = value.byteLength - start;
148
+ const chunkLength = Math.min(capacity, remaining);
149
+ target.set(value.subarray(start, start + chunkLength), this.offset);
150
+ start += chunkLength;
151
+ this.offset += chunkLength;
152
+ }
119
153
  }
120
154
  *encodeValue(value) {
121
- if (this.closed) {
155
+ if (__classPrivateFieldGet(this, _Encoder_closed, "f")) {
122
156
  return;
123
157
  }
124
158
  if (value === false) {
@@ -158,10 +192,10 @@ export class Encoder {
158
192
  }
159
193
  }
160
194
  *flush() {
161
- if (this.closed) {
195
+ if (__classPrivateFieldGet(this, _Encoder_closed, "f")) {
162
196
  return;
163
197
  }
164
- this.closed = true;
198
+ __classPrivateFieldSet(this, _Encoder_closed, true, "f");
165
199
  if (this.offset > 0) {
166
200
  yield new Uint8Array(this.buffer, 0, this.offset);
167
201
  }
@@ -205,6 +239,19 @@ export class Encoder {
205
239
  }
206
240
  }
207
241
  }
242
+ _Encoder_closed = new WeakMap(), _Encoder_instances = new WeakSet(), _Encoder_flush = function _Encoder_flush() {
243
+ if (this.noCopy) {
244
+ const chunk = new Uint8Array(this.buffer, 0, this.offset);
245
+ this.offset = 0;
246
+ return chunk;
247
+ }
248
+ else {
249
+ const chunk = new Uint8Array(this.offset);
250
+ chunk.set(new Uint8Array(this.buffer, 0, this.offset));
251
+ this.offset = 0;
252
+ return chunk;
253
+ }
254
+ };
208
255
  Encoder.defaultChunkSize = 512;
209
256
  export function encode(value, options = {}) {
210
257
  const encoder = new Encoder(options);
@@ -1,4 +1,5 @@
1
1
  import type { CBORValue } from "./types.js";
2
2
  export declare function encodeStream(source: AsyncIterable<CBORValue>, options?: {
3
3
  chunkSize?: number;
4
+ noCopy?: boolean;
4
5
  }): AsyncIterable<Uint8Array>;
@@ -1,2 +1,3 @@
1
1
  import type { CBORValue } from "./types.js";
2
2
  export declare function encodingLength(value: CBORValue): number;
3
+ export declare function getByteLength(string: string): number;
@@ -70,9 +70,7 @@ function numberEncodingLength(value) {
70
70
  else if (Object.is(value, -0)) {
71
71
  return floatEncodingLength(value);
72
72
  }
73
- else if (Math.floor(value) === value &&
74
- Number.MIN_SAFE_INTEGER <= value &&
75
- value <= Number.MAX_SAFE_INTEGER) {
73
+ else if (Math.floor(value) === value && Number.MIN_SAFE_INTEGER <= value && value <= Number.MAX_SAFE_INTEGER) {
76
74
  return integerEncodingLength(value);
77
75
  }
78
76
  else {
@@ -99,7 +97,7 @@ function floatEncodingLength(value) {
99
97
  }
100
98
  }
101
99
  function stringEncodingLength(value) {
102
- const length = byteLength(value);
100
+ const length = getByteLength(value);
103
101
  return argumentEncodingLength(length) + length;
104
102
  }
105
103
  function bytesEncodingLength(value) {
@@ -107,7 +105,7 @@ function bytesEncodingLength(value) {
107
105
  return argumentEncodingLength(length) + length;
108
106
  }
109
107
  // https://github.com/feross/buffer/blob/57caad4450d241207066ca3832fb8e9095ad402f/index.js#L434
110
- function byteLength(string) {
108
+ export function getByteLength(string) {
111
109
  let codePoint;
112
110
  const length = string.length;
113
111
  let leadSurrogate = null;
@@ -140,8 +138,7 @@ function byteLength(string) {
140
138
  continue;
141
139
  }
142
140
  // valid surrogate pair
143
- codePoint =
144
- (((leadSurrogate - 0xd800) << 10) | (codePoint - 0xdc00)) + 0x10000;
141
+ codePoint = (((leadSurrogate - 0xd800) << 10) | (codePoint - 0xdc00)) + 0x10000;
145
142
  }
146
143
  else if (leadSurrogate) {
147
144
  // valid bmp char, but last char was a lead
package/lib/utils.d.ts CHANGED
@@ -4,3 +4,9 @@ export declare class UnsafeIntegerError extends RangeError {
4
4
  readonly value: bigint;
5
5
  constructor(message: string, value: bigint);
6
6
  }
7
+ export declare class AssertError extends Error {
8
+ readonly message: string;
9
+ readonly props?: any | undefined;
10
+ constructor(message: string, props?: any | undefined);
11
+ }
12
+ export declare function assert(condition: unknown, message?: string, props?: any): asserts condition;
package/lib/utils.js CHANGED
@@ -6,3 +6,15 @@ export class UnsafeIntegerError extends RangeError {
6
6
  this.value = value;
7
7
  }
8
8
  }
9
+ export class AssertError extends Error {
10
+ constructor(message, props) {
11
+ super(message);
12
+ this.message = message;
13
+ this.props = props;
14
+ }
15
+ }
16
+ export function assert(condition, message = "assertion failed", props) {
17
+ if (!condition) {
18
+ throw new AssertError(message, props);
19
+ }
20
+ }
package/package.json CHANGED
@@ -1,15 +1,20 @@
1
1
  {
2
2
  "name": "microcbor",
3
- "version": "0.3.0",
4
- "description": "Encode JavaScript values as canonical CBOR",
3
+ "version": "0.4.0",
5
4
  "type": "module",
6
- "main": "lib/index.js",
7
- "types": "lib/index.d.ts",
8
5
  "files": [
9
6
  "lib"
10
7
  ],
8
+ "description": "Encode JavaScript values as canonical CBOR",
9
+ "main": "lib/index.js",
10
+ "types": "./lib/index.d.ts",
11
+ "exports": {
12
+ ".": "./lib/index.js"
13
+ },
11
14
  "scripts": {
12
- "build": "tsc",
15
+ "dev": "tsc --build tsconfig.json test/tsconfig.json --watch",
16
+ "build": "tsc --build tsconfig.json test/tsconfig.json",
17
+ "clean": "tsc --build tsconfig.json test/tsconfig.json --clean",
13
18
  "test": "ava"
14
19
  },
15
20
  "repository": {
@@ -27,11 +32,13 @@
27
32
  },
28
33
  "homepage": "https://github.com/joeltg/microcbor#readme",
29
34
  "devDependencies": {
30
- "ava": "^5.3.1",
35
+ "@ava/typescript": "^5.0.0",
36
+ "@types/node": "^22.13.9",
37
+ "ava": "^6.2.0",
31
38
  "cbor": "^8.1.0",
32
- "typescript": "^5.1.6"
39
+ "typescript": "^5.6.0"
33
40
  },
34
41
  "dependencies": {
35
- "fp16": "^0.2.0"
42
+ "fp16": "^1.0.0"
36
43
  }
37
44
  }