diff --git a/README.md b/README.md index 63b6eb0..8eadae8 100644 --- a/README.md +++ b/README.md @@ -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 kB** gzipped. Efficiently encode and decode your values to and from `ArrayBuffer`s. Integrates very well with WebSockets. +The (as of the time of writing this) smallest **and** fastest JavaScript value serialization library in the wild. **2.3 kB** gzipped. Efficiently encode and decode your values to and from `ArrayBuffer`s. Integrates very well with WebSockets. ## Example @@ -75,7 +75,7 @@ In this example, the size of payload is only **22 bytes**. `JSON.stringify` woul [SchemaPack](https://github.com/phretaddin/schemapack/tree/master) (huge respect from and inspiration for this library! :heart:) is great for packing objects into Node buffers. Over time, this approach has become outdated in favor of modern standards like `ArrayBuffer`. -It is also frequently desirable to preallocate and reuse buffers for performance reasons. SchemaPack always allocates new buffers when encoding. The performance hit is generally less than the naive case since Node is good about buffer pooling, but performance degrades in the browser (and doesn't exist on any other platform). Buffer reuse is the Correct Way™. Even with Node's pooling, we are still roughly **twice as fast or faster than SchemaPack** in many cases. (We could probably get even faster if we did crazy stuff like compiled unrolled codecs like SchemaPack does. PRs along those lines would be interesting if there's big gains! :muscle:) +It is also frequently desirable to preallocate and reuse buffers for performance reasons. SchemaPack always allocates new buffers when encoding. The performance hit is generally less than the naive case since Node is good about buffer pooling, but performance degrades in the browser (and doesn't exist on any other platform). Buffer reuse is the Correct Way™. I also wanted an implementation that does amazing things like [boolean coalescence](#boolean-coalescence) and [optional fields](#optional-fields) (also with [coalescence](#optional-field-coalescence)) as well as supporting more even more types like `Map`s, `Set`s, `Date`s, etc. @@ -160,7 +160,7 @@ class YourCodec { decode(view: DataView, target: {byteOffset: number}): any // return the number of bytes written - encode(value: any, view: DataView, byteOffset = 0): number + encode(value: any, view: DataView, byteOffset): number size(value: any): number @@ -182,7 +182,7 @@ class MyDateCodec extends Codecs.string { return new Date(decoded); } - encode(value, view, byteOffset = 0) { + encode(value, view, byteOffset) { // convert it to a string const converted = new Date(value).toISOString(); // pass it along to the `string` codec's encode method diff --git a/src/codecs/object.js b/src/codecs/object.js index 3981796..ee5450b 100644 --- a/src/codecs/object.js +++ b/src/codecs/object.js @@ -7,107 +7,137 @@ class ObjectCodec { $$optionals = 0; constructor(blueprint) { + let encoderCode = ''; + let decoderCode = ` + const value = {}; + `; + let i = 0; for (const key in blueprint.properties) { const property = blueprint.properties[key]; if (!(property.type in Codecs)) { throw new TypeError(`No such codec '${property.type}'`); } - if ('bool' === property.type) { - this.$$booleans += 1; - } const codec = new Codecs[property.type](property); if (property.optional) { this.$$optionals += 1; - } - this.$$codecs.push({codec, key, property}); - } - } - - decode(view, target) { - const booleanFlags = []; - const optionalFlags = []; - let currentBoolean = 0; - let currentOptional = 0; - let {$$booleans} = this; - const booleanBackpatches = []; - const optionalCount = Math.ceil(this.$$optionals / 8); - for (let i = 0; i < optionalCount; ++i) { - optionalFlags.push(view.getUint8(target.byteOffset)); - target.byteOffset += 1; - } - const value = {}; - for (const {codec, key, property} of this.$$codecs) { - if (property.optional) { - const index = currentOptional >> 3; - const bit = currentOptional & 7; - currentOptional += 1; - const isPresent = optionalFlags[index] & (1 << bit); - if (!isPresent) { - if ('bool' === property.type) { - $$booleans -= 1; + decoderCode += ` + index = currentOptional >> 3; + bit = currentOptional & 7; + currentOptional += 1; + if (!(optionalFlags[index] & (1 << bit))) { + ${ + 'bool' === property.type + ? '$$booleans -= 1' + : '' + } } - continue; - } + else { + `; + encoderCode += ` + index = currentOptional >> 3; + bit = currentOptional & 7; + isPresent = 'undefined' !== typeof value['${key}']; + optionalFlags[index] |= (isPresent ? 1 : 0) << bit; + currentOptional += 1; + if (isPresent) { + `; } if ('bool' === property.type) { - const index = currentBoolean >> 3; - const bit = currentBoolean & 7; - currentBoolean += 1; - booleanBackpatches.push({bit, index, key}); + this.$$booleans += 1; + decoderCode += ` + index = currentBoolean >> 3; + bit = currentBoolean & 7; + currentBoolean += 1; + booleanBackpatches.push({bit, index, key: '${key}'}); + `; + encoderCode += ` + index = currentBoolean >> 3; + bit = currentBoolean & 7; + booleanFlags[index] |= (value['${key}'] ? 1 : 0) << bit; + currentBoolean += 1; + `; } else { - value[key] = codec.decode(view, target); - } - } - const booleanCount = Math.ceil($$booleans / 8); - if (booleanCount > 0) { - for (let i = 0; i < booleanCount; ++i) { - booleanFlags.push(view.getUint8(target.byteOffset)); - target.byteOffset += 1; + decoderCode += `value['${key}'] = this.$$codecs[${i}].codec.decode(view, target);`; + encoderCode += `written += this.$$codecs[${i}].codec.encode(value['${key}'], view, byteOffset + written);`; } - for (const {bit, index, key} of booleanBackpatches) { - value[key] = !!(booleanFlags[index] & (1 << bit)); + if (property.optional) { + decoderCode += '}'; + encoderCode += '}'; } + this.$$codecs.push({codec, key, property}); + this.$$flatCodecs + i += 1; } - return value; - } - - encode(value, view, byteOffset) { - const booleanFlags = []; - const optionalFlags = []; - let currentBoolean = 0; - let currentOptional = 0; - let written = 0; - written += Math.ceil(this.$$optionals / 8); - for (const {codec, key, property} of this.$$codecs) { - if (property.optional) { - const index = currentOptional >> 3; - const bit = currentOptional & 7; - const isPresent = 'undefined' !== typeof value[key]; - optionalFlags[index] |= (isPresent ? 1 : 0) << bit; - currentOptional += 1; - if (!isPresent) { - continue; + if (this.$$booleans > 0) { + decoderCode = ` + let currentBoolean = 0; + let {$$booleans} = this; + const booleanBackpatches = []; + ` + decoderCode; + decoderCode += ` + const booleanFlags = []; + const booleanCount = Math.ceil($$booleans / 8); + if (booleanCount > 0) { + for (let i = 0; i < booleanCount; ++i) { + booleanFlags.push(view.getUint8(target.byteOffset)); + target.byteOffset += 1; + } + for (const {bit, index, key} of booleanBackpatches) { + value[key] = !!(booleanFlags[index] & (1 << bit)); + } } - } - if ('bool' === property.type) { - const index = currentBoolean >> 3; - const bit = currentBoolean & 7; - booleanFlags[index] |= (value[key] ? 1 : 0) << bit; - currentBoolean += 1; - } - else { - written += codec.encode(value[key], view, byteOffset + written); - } + `; + encoderCode += ` + for (let i = 0; i < booleanFlags.length; ++i) { + view.setUint8(byteOffset + written + i, booleanFlags[i]); + } + written += booleanFlags.length; + `; + encoderCode = ` + const booleanFlags = []; + let currentBoolean = 0; + ` + encoderCode; } - for (let i = 0; i < booleanFlags.length; ++i) { - view.setUint8(byteOffset + written + i, booleanFlags[i]); + if (this.$$optionals > 0) { + decoderCode = ` + const optionalFlags = []; + let currentOptional = 0; + const optionalCount = Math.ceil(this.$$optionals / 8); + for (let i = 0; i < optionalCount; ++i) { + optionalFlags.push(view.getUint8(target.byteOffset)); + target.byteOffset += 1; + } + ` + decoderCode; + encoderCode += ` + for (let i = 0; i < optionalFlags.length; ++i) { + view.setUint8(byteOffset + i, optionalFlags[i]); + } + `; + encoderCode = ` + const optionalFlags = []; + let currentOptional = 0; + let isPresent; + written += Math.ceil(this.$$optionals / 8); + ` + encoderCode } - written += booleanFlags.length; - for (let i = 0; i < optionalFlags.length; ++i) { - view.setUint8(byteOffset + i, optionalFlags[i]); + if (this.$$optionals > 0 || this.$$booleans > 0) { + decoderCode = ` + let bit, index; + ` + decoderCode; + encoderCode = ` + let bit, index; + ` + encoderCode; } - return written; + encoderCode = ` + let written = 0; + ` + encoderCode; + encoderCode += ` + return written; + `; + decoderCode += 'return value'; + this.decode = new Function('view, target', decoderCode); + this.encode = new Function('value, view, byteOffset', encoderCode); } size(value) {