Skip to content

Commit

Permalink
feat: fixed-length arrays
Browse files Browse the repository at this point in the history
  • Loading branch information
cha0s committed Nov 26, 2024
1 parent 9c2ee69 commit a10e37d
Show file tree
Hide file tree
Showing 3 changed files with 232 additions and 67 deletions.
39 changes: 28 additions & 11 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

# crunches :muscle:

The (as of the time of writing this) smallest **and** fastest JavaScript value serialization library in the wild. **2.64 kB** gzipped; **0 dependencies**. Efficiently encode and decode your values to and from `ArrayBuffer`s. Integrates very well with WebSockets.
The smallest **and** fastest JavaScript web standards value serialization library in the wild. **3.06 kB** gzipped; **0 dependencies**. Efficiently encode and decode your values to and from `ArrayBuffer`s. Integrates very well with WebSockets.

## Example

Expand Down Expand Up @@ -205,7 +205,7 @@ Inside your codec, you must increment `target.byteOffset` as you decode bytes.

Just set a key on the `Codecs` object and go. Too easy!

## Schema types
## Primitive types

| Type Name | Bytes | Range of Values |
|-----------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|--------------------------------------------------------------------------------------------------------|
Expand All @@ -224,9 +224,9 @@ Just set a key on the `Codecs` object and go. Too easy!
| varint | <table><tr><th>size</th><th>min</th><th>max</th></tr><tr><td>1</td><td>-64</td><td>63</td></tr><tr><td>2</td><td>-8,192</td><td>8,191</td></tr><tr><td>3</td><td>-1,048,576</td><td>1,048,575</td></tr><tr><td>4</td><td>-134,217,728</td><td>134,217,727</td></tr><tr><td>5</td><td>-17,179,869,184</td><td>17,179,869,183</td></tr><tr><td>6</td><td>-2,199,023,255,552</td><td>2,199,023,255,551</td></tr><tr><td>7</td><td>-281,474,976,710,656</td><td>281,474,976,710,655</td></tr></table> | -281,474,976,710,656 to 281,474,976,710,655 |
| date | Same as `string` above after calling [`toIsoString`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/toISOString) | Value is coerced to `Date` e.g. `new Date(value).toIsoString()` |

### Aggregate types
## Aggregate types

#### `object`
### `object`

Requires a `properties` key to define the properties on the object. Supports [`optional` fields](#optional-fields). Booleans are [coalesced](#boolean-coalescence).

Expand All @@ -247,7 +247,7 @@ console.log(schema.size({foo: 32, bar: 'hello'}));
console.log(schema.size({foo: 32}));
```

#### `array`
### `array`

Requires an `element` key to define the structure of the array elements. Encodes a 32-bit prefix followed by the contents of the array.

Expand All @@ -261,9 +261,26 @@ const schema = new Schema({
console.log(schema.size([1, 2, 3]));
```

[Arrays of number types decode to the corresponding `TypedArray`](#buffers-and-arrays).
Arrays of number types decode to [the corresponding `TypedArray`](#buffers-and-arrays).

#### `map`
#### Fixed-length arrays

Arrays may be specified as fixed-length through the `length` key.

```js
const schema = new Schema({
type: 'array',
element: {type: 'uint32'},
length: 3,
});

// 12 = uint32 (4) + uint32 (4) + uint32 (4)
console.log(schema.size([1, 2, 3]));
```

No prefix is written, saving 4 bytes!

### `map`

Requires a `key` and `value` key to define the structure of the map. Any [iterable](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Iteration_protocols) will be coerced as [entries](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map/Map#iterable). Encoded as an array of entries. Decodes to a native `Map` object.

Expand All @@ -284,7 +301,7 @@ console.log(schema.size(value));
console.log(schema.size([[32, 'sup'], [64, 'hi']]));
```

#### `set`
### `set`

Requires an `element` key to define the structure of the map. Any [iterable](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Iteration_protocols) will be coerced. Encoded as an array. Decodes to a native `Set` object.

Expand Down Expand Up @@ -315,7 +332,7 @@ No validation is done on the values you encode. If you'd like to validate your v

### Blueprint verbosity

Defining schema blueprints are slightly more verbose than SchemaPack. The tradeoff is that we're able to define more aggregate types like `Set`, `Map`, and maybe others in the future. While technically possible to allow e.g. a `Map` object in a blueprint, it would be even more cumbersome in my opinion.
Defining schema blueprints are slightly more verbose than SchemaPack. The tradeoff is that we're able to define more aggregate types like `Set`, `Map`, fixed-length arrays, and have made space for even more in the future.

### Varint expansion

Expand All @@ -342,12 +359,12 @@ Instead of copying the data from the buffer, a [`TypedArray`](https://developer.

# TODO

- Fixed-length arrays
- Coalescence for boolean arrays?
- Sparse arrays/optional elements?
- Type aliases?
- BigInts?
- Endianness?
- Optional varuint for array/buffer/string prefixes

# Q/A

Expand All @@ -358,4 +375,4 @@ Instead of copying the data from the buffer, a [`TypedArray`](https://developer.
**A**: Feel free to contribute typing!

**Q**: How fast is it, overall?
**A**: Benchmarks are generally dubious in my opinion, but the `benchmark.js` script included in the repository runs 50,000 iterations of both SchemaPack and `crunches` encoding and decoding a schema. SchemaPack validation is disabled, to be as fair as possible. On the machine used to benchmark, `crunches` runs consistently **3-4x faster** than SchemaPack.
**A**: Benchmarks are generally dubious in my opinion, but the `benchmark.js` script included in the repository runs 50,000 iterations of both SchemaPack and `crunches` encoding and decoding a schema. SchemaPack validation is disabled, to be as fair as possible. On the machine used to benchmark, `crunches` runs consistently **2-4x faster** than SchemaPack.
198 changes: 145 additions & 53 deletions src/codecs/array.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import {Codecs} from '../codecs.js';

function typeToElementClass(type) {
export function typeToElementClass(type) {
switch (type) {
case 'int8': return Int8Array;
case 'uint8': return Uint8Array;
Expand All @@ -14,7 +14,7 @@ function typeToElementClass(type) {
return undefined;
}

function paddingForType(type) {
export function paddingForType(type) {
let padding = 0;
switch (type) {
case 'float64': padding = 4; break;
Expand All @@ -33,66 +33,158 @@ class ArrayCodec {
// todo: throw on optional or honor and encode sparse arrays
// todo: boolean coalescence
this.$$elementCodec = new Codecs[blueprint.element.type](blueprint.element);
this.$$type = blueprint.element.type;
}

decode(view, target) {
const length = view.getUint32(target.byteOffset);
target.byteOffset += 4;
let ElementClass = typeToElementClass(this.$$type);
const {length = 0} = blueprint;
const {type} = blueprint.element;
const ElementClass = typeToElementClass(type);
let decoderCode = '', encoderCode = '';
// varlen
if (0 === length) {
decoderCode += `
const length = view.getUint32(target.byteOffset);
target.byteOffset += 4 + ${paddingForType(type)};
`;
encoderCode += `
let length = 0;
let written = 4 + ${paddingForType(type)};
`;
if (ElementClass) {
encoderCode += `
if (Array.isArray(value)) {
length = value.length;
new ElementClass(
view.buffer,
view.byteOffset + byteOffset + written
).set(new ElementClass(value));
written += ElementClass.BYTES_PER_ELEMENT * length;
}
else {
`;
}
encoderCode += `
for (const element of value) {
length += 1;
written += this.$$elementCodec.encode(element, view, byteOffset + written);
}
`;
if (ElementClass) {
encoderCode += '}';
}
encoderCode += `
view.setUint32(byteOffset, length);
return written;
`;
if (ElementClass) {
this.$$size = (value) => {
let size = 4 + paddingForType(type);
if (Array.isArray(value)) {
return size + value.length * ElementClass.BYTES_PER_ELEMENT;
}
if (value instanceof Set) {
return size + value.size * ElementClass.BYTES_PER_ELEMENT;
}
for (const element of value) {
size += this.$$elementCodec.size(element);
}
return size;
};
}
else {
this.$$size = (value) => {
let size = 4 + paddingForType(type);
for (const element of value) {
size += this.$$elementCodec.size(element);
}
return size;
};
}
}
// fixed
else {
decoderCode += `const length = ${length};`;
encoderCode += 'let written = 0;';
if (ElementClass) {
encoderCode += `
if (Array.isArray(value)) {
new ElementClass(
view.buffer,
view.byteOffset + byteOffset + written
).set(new ElementClass(value));
written += ElementClass.BYTES_PER_ELEMENT * ${length};
}
else {
`;
}
encoderCode += `
// let the environment report
if (!value[Symbol.iterator]) {
for (const _ of {}) {/* ... */} // eslint-disable-line no-unused-vars
}
let protocol = value[Symbol.iterator]();
let result = protocol.next();
for (let i = 0; !result.done && i < ${length}; ++i) {
written += this.$$elementCodec.encode(result.value, view, byteOffset + written);
result = protocol.next();
}
`;
if (ElementClass) {
encoderCode += '}';
}
encoderCode += 'return written;';
if (ElementClass) {
this.$$size = () => {
return length * ElementClass.BYTES_PER_ELEMENT;
};
}
else {
this.$$size = (value) => {
let size = 0;
// let the environment report
if (!value[Symbol.iterator]) {
for (const _ of {}) {/* ... */} // eslint-disable-line no-unused-vars
}
let protocol = value[Symbol.iterator]();
let result = protocol.next();
for (let i = 0; !result.done && i < length; ++i) {
size += this.$$elementCodec.size(result.value);
result = protocol.next();
}
return size;
};
}
}
// static shape
if (ElementClass) {
const value = new ElementClass(
view.buffer,
view.byteOffset + target.byteOffset + paddingForType(this.$$type),
length,
);
target.byteOffset += length;
return value;
decoderCode += `
const value = new ElementClass(view.buffer, view.byteOffset + target.byteOffset, length);
target.byteOffset += ${ElementClass.BYTES_PER_ELEMENT} * length;
`;
}
const value = Array(length);
for (let i = 0; i < length; ++i) {
value[i] = this.$$elementCodec.decode(view, target);
// dynamic shape
else {
decoderCode += `
const value = Array(length);
for (let i = 0; i < length; ++i) {
value[i] = this.$$elementCodec.decode(view, target);
}
`;
}
return value;
decoderCode += 'return value;';
const decoder = new Function('ElementClass, view, target', decoderCode);
this.$$decode = decoder.bind(this, ElementClass);
const encoder = new Function('ElementClass, value, view, byteOffset', encoderCode);
this.$$encode = encoder.bind(this, ElementClass);
}

decode(view, target) {
return this.$$decode(view, target);
}

encode(value, view, byteOffset) {
let length = 0;
let written = 4;
let ElementClass = typeToElementClass(this.$$type);
if (ElementClass && Array.isArray(value)) {
length = value.length;
new ElementClass(
view.buffer,
view.byteOffset + byteOffset + written + paddingForType(this.$$type)
).set(new ElementClass(value));
written += paddingForType(this.$$type) + ElementClass.BYTES_PER_ELEMENT * length;
}
else {
for (const element of value) {
length += 1;
written += this.$$elementCodec.encode(element, view, byteOffset + written);
}
}
view.setUint32(byteOffset, length);
return written;
return this.$$encode(value, view, byteOffset);
}

size(value) {
let size = 4;
let ElementClass = typeToElementClass(this.$$type);
if (ElementClass) {
if (Array.isArray(value)) {
return size + paddingForType(this.$$type) + value.length * ElementClass.BYTES_PER_ELEMENT;
}
if (value instanceof Set) {
return size + paddingForType(this.$$type) + value.size * ElementClass.BYTES_PER_ELEMENT;
}
}
for (const element of value) {
size += this.$$elementCodec.size(element);
}
return size;
return this.$$size(value);
}

}
Expand Down
Loading

0 comments on commit a10e37d

Please sign in to comment.