structurae
Advanced tools
Comparing version 3.1.1 to 3.2.0
@@ -7,9 +7,13 @@ # Changelog | ||
## [3.2.0] - 2020-07-09 | ||
- Support required fields and default values in MapView. | ||
- Support nested MapViews. | ||
## [3.1.1] - 2020-06-25 | ||
### Changed | ||
- Optimize StringView encoding and decoding | ||
- Optimize StringView encoding and decoding. | ||
## [3.1.0] - 2020-05-28 | ||
### Added | ||
- Support setting maximum size for strings and arrays in MapView | ||
- Support setting maximum size for strings and arrays in MapView. | ||
@@ -19,3 +23,3 @@ ## [3.0.6] - 2020-04-28 | ||
- Use custom UTF8 encoding for StringView | ||
as a workaround to solve performance issues in V8 | ||
as a workaround to solve performance issues in V8. | ||
@@ -22,0 +26,0 @@ ## [3.0.5] - 2020-04-07 |
@@ -419,3 +419,7 @@ // Type definitions for structurae | ||
static layout: ViewLayout; | ||
static fields: string[]; | ||
static optionalFields: string[]; | ||
static requiredFields: string[]; | ||
static optionalOffset: number; | ||
static lengthOffset: number; | ||
static defaultBuffer: Uint8Array; | ||
static ObjectViewClass: typeof ObjectView; | ||
@@ -432,6 +436,12 @@ static Views: ViewTypes; | ||
toJSON(): object; | ||
static from(value: object): MapView; | ||
static from(value: object, view?: View, start?: number): View; | ||
static toJSON(view: View, start?: number): object; | ||
static getLength(value: any): number; | ||
static initialize(): void; | ||
private static getFieldLayout( | ||
field: ViewLayoutField, | ||
start: number, | ||
required: boolean, | ||
): ViewLayoutField; | ||
private static setDefaultBuffer(): void; | ||
} | ||
@@ -438,0 +448,0 @@ |
class BitPair { | ||
/** | ||
/* | ||
* @param {number|BitPair|Array<number>} [data=0] a single number value of the field | ||
@@ -4,0 +4,0 @@ * or a map of field names with their respective values |
const { ObjectView, ObjectViewMixin } = require('./object-view'); | ||
const StringView = require('./string-view'); | ||
const ArrayViewMixin = require('./array-view-mixin'); | ||
const { writeUTF8 } = require('./utilities'); | ||
@@ -16,5 +17,6 @@ /** | ||
get(field) { | ||
const [View, start, end] = this.getLayout(field); | ||
if (start === end) return undefined; | ||
return View.toJSON(this, this.byteOffset + start, end - start); | ||
const layout = this.getLayout(field); | ||
if (!layout) return undefined; | ||
const [View, start, length] = layout; | ||
return View.toJSON(this, start, length); | ||
} | ||
@@ -29,5 +31,6 @@ | ||
getView(field) { | ||
const [View, start, end] = this.getLayout(field); | ||
if (start === end) return undefined; | ||
return new View(this.buffer, this.byteOffset + start, end - start); | ||
const layout = this.getLayout(field); | ||
if (!layout) return undefined; | ||
const [View, start, length] = layout; | ||
return new View(this.buffer, start, length); | ||
} | ||
@@ -41,10 +44,12 @@ | ||
getLayout(field) { | ||
const { layout } = this.constructor; | ||
const definition = layout[field]; | ||
if (!definition) throw TypeError(`Field "${field}" is not found.`); | ||
const { View, start } = definition; | ||
const startOffset = start << 2; | ||
const fieldStart = this.getUint32(startOffset, true); | ||
const end = this.getUint32(startOffset + 4, true); | ||
return [View, fieldStart, end]; | ||
const definition = this.constructor.layout[field]; | ||
if (!definition) return undefined; | ||
const { View, start, required, length } = definition; | ||
if (required) { | ||
return [View, start, length]; | ||
} | ||
const startOffset = this.getUint32(start, true); | ||
const end = this.getUint32(start + 4, true); | ||
if (startOffset === end) return undefined; | ||
return [View, startOffset, end - startOffset]; | ||
} | ||
@@ -60,4 +65,6 @@ | ||
set(field, value) { | ||
const [View, start, end] = this.getLayout(field); | ||
if (start !== end) View.from(value, this, this.byteOffset + start, end - start); | ||
const layout = this.getLayout(field); | ||
if (!layout) return undefined; | ||
const [View, start, length] = layout; | ||
View.from(value, this, this.byteOffset + start, length); | ||
return this; | ||
@@ -74,9 +81,8 @@ } | ||
setView(field, value) { | ||
const [, start, end] = this.getLayout(field); | ||
if (start !== end) { | ||
new Uint8Array(this.buffer, this.byteOffset, this.byteLength).set( | ||
new Uint8Array(value.buffer, value.byteOffset, value.byteLength), | ||
start, | ||
); | ||
} | ||
const layout = this.getLayout(field); | ||
if (!layout) return undefined; | ||
new Uint8Array(this.buffer, this.byteOffset, this.byteLength).set( | ||
new Uint8Array(value.buffer, value.byteOffset, value.byteLength), | ||
layout[1], | ||
); | ||
return this; | ||
@@ -98,26 +104,46 @@ } | ||
* @param {Object} value the object to take data from | ||
* @returns {MapView} | ||
* @param {View} [view] the view to assign fields to | ||
* @param {number} [start=0] | ||
* @returns {View} | ||
*/ | ||
static from(value) { | ||
const { layout, fields } = this; | ||
const fieldCount = fields.length; | ||
let offset = (fieldCount + 1) << 2; | ||
const view = this.bufferView; | ||
view.setUint32(0, offset, true); | ||
for (let i = 0; i < fieldCount; i++) { | ||
const field = fields[i]; | ||
static from(value, view, start = 0) { | ||
const mapView = view || this.bufferView; | ||
const mapArray = new Uint8Array(mapView.buffer, mapView.byteOffset); | ||
if (this.defaultBuffer) { | ||
mapArray.set(this.defaultBuffer, start); | ||
} | ||
const { layout, requiredFields, optionalFields, lengthOffset } = this; | ||
for (let i = 0; i < requiredFields.length; i++) { | ||
const field = requiredFields[i]; | ||
const fieldValue = value[field]; | ||
if (fieldValue != null) { | ||
const { View, length } = layout[field]; | ||
const start = offset; | ||
const valueLength = | ||
typeof fieldValue === 'string' | ||
? StringView.getByteSize(fieldValue) | ||
: View.getLength(fieldValue.length || 1); | ||
offset += Math.min(valueLength, length); | ||
View.from(fieldValue, view, start, offset - start); | ||
const { View, length: maxLength, start: fieldStart } = layout[field]; | ||
View.from(fieldValue, mapView, start + fieldStart, maxLength); | ||
} | ||
view.setUint32((i + 1) << 2, offset, true); | ||
} | ||
return new this(view.buffer.slice(0, offset)); | ||
let end = lengthOffset + 4; | ||
for (let i = 0; i < optionalFields.length; i++) { | ||
const field = optionalFields[i]; | ||
const fieldValue = value[field]; | ||
const { View, length: maxLength, start: fieldStart } = layout[field]; | ||
let fieldLength = 0; | ||
if (fieldValue != null) { | ||
const caret = start + end; | ||
if (View === StringView) { | ||
fieldLength = writeUTF8(fieldValue, mapArray, caret); | ||
} else if (View.prototype instanceof MapView) { | ||
View.from(fieldValue, mapView, caret); | ||
const fieldEnd = caret + View.lengthOffset; | ||
fieldLength = mapView.getUint32(fieldEnd, true); | ||
} else { | ||
fieldLength = View.getLength(fieldValue.length || 1); | ||
View.from(fieldValue, mapView, caret, fieldLength); | ||
} | ||
fieldLength = Math.min(fieldLength, maxLength); | ||
} | ||
mapView.setUint32(start + fieldStart, end, true); | ||
end += fieldLength; | ||
} | ||
mapView.setUint32(start + lengthOffset, end, true); | ||
return view || new this(mapView.buffer.slice(0, end)); | ||
} | ||
@@ -132,14 +158,18 @@ | ||
static getLength(value) { | ||
const { layout, fields } = this; | ||
const fieldCount = fields.length; | ||
let length = (fieldCount + 1) << 2; | ||
for (let i = 0; i < fieldCount; i++) { | ||
const field = fields[i]; | ||
if (value[field] == null) continue; | ||
const { View } = layout[field]; | ||
const { layout, optionalFields, lengthOffset } = this; | ||
let length = lengthOffset + 4; | ||
for (let i = 0; i < optionalFields.length; i++) { | ||
const field = optionalFields[i]; | ||
const fieldValue = value[field]; | ||
length += | ||
typeof fieldValue === 'string' | ||
? StringView.getByteSize(fieldValue) | ||
: View.getLength(fieldValue.length || 1); | ||
if (fieldValue == null) continue; | ||
let fieldLength = 0; | ||
const { View, length: maxLength } = layout[field]; | ||
if (View.prototype instanceof MapView) { | ||
fieldLength = View.getLength(fieldValue); | ||
} else if (View.getByteSize) { | ||
fieldLength = View.getByteSize(fieldValue); | ||
} else { | ||
fieldLength = View.getLength(fieldValue.length || 1); | ||
} | ||
length += Math.min(fieldLength, maxLength); | ||
} | ||
@@ -157,10 +187,14 @@ return length; | ||
static toJSON(view, start = 0) { | ||
const { layout, fields } = this; | ||
const { layout, requiredFields, optionalFields } = this; | ||
const object = {}; | ||
for (let i = 0; i < fields.length; i++) { | ||
const field = fields[i]; | ||
const { View } = layout[field]; | ||
const startOffset = start + (i << 2); | ||
const fieldStart = view.getUint32(startOffset, true); | ||
const end = view.getUint32(startOffset + 4, true); | ||
for (let i = 0; i < requiredFields.length; i++) { | ||
const field = requiredFields[i]; | ||
const { View, start: startOffset, length } = layout[field]; | ||
object[field] = View.toJSON(view, start + startOffset, length); | ||
} | ||
for (let i = 0; i < optionalFields.length; i++) { | ||
const field = optionalFields[i]; | ||
const { View, start: startOffset } = layout[field]; | ||
const fieldStart = view.getUint32(start + startOffset, true); | ||
const end = view.getUint32(start + startOffset + 4, true); | ||
if (fieldStart === end) continue; | ||
@@ -182,35 +216,98 @@ object[field] = View.toJSON(view, start + fieldStart, end - fieldStart); | ||
for (let i = objects.length - 1; i > 0; i--) { | ||
ObjectViewMixin(objects[i], ObjectViewClass); | ||
if (objects[i].btype === 'map') { | ||
MapViewMixin(objects[i], this, ObjectViewClass); | ||
} else { | ||
ObjectViewMixin(objects[i], ObjectViewClass); | ||
} | ||
} | ||
} | ||
const properties = Object.keys(schema.properties); | ||
const required = schema.required || []; | ||
const optional = Object.keys(schema.properties).filter((i) => !required.includes(i)); | ||
const layout = {}; | ||
for (let i = 0; i < properties.length; i++) { | ||
const property = properties[i]; | ||
let field = schema.properties[property]; | ||
let View; | ||
let length = Infinity; | ||
if (field.type !== 'array') { | ||
View = ObjectViewClass.getViewFromSchema(field); | ||
length = field.type === 'string' ? field.maxLength : View.getLength(); | ||
} else { | ||
const sizes = []; | ||
while (field && field.type === 'array') { | ||
sizes.push(field.maxItems); | ||
field = field.items; | ||
} | ||
View = ArrayViewMixin(ObjectViewClass.getViewFromSchema(field), field.maxLength); | ||
let itemLength = View.getLength(sizes.pop()); | ||
for (let j = sizes.length - 1; j >= 0; j--) { | ||
View = ArrayViewMixin(View, itemLength); | ||
itemLength = View.getLength(sizes[j]); | ||
} | ||
length = itemLength; | ||
} | ||
layout[property] = { View, start: i, length: length || Infinity }; | ||
let offset = 0; | ||
for (let i = 0; i < required.length; i++) { | ||
const property = required[i]; | ||
const field = schema.properties[property]; | ||
const fieldLayout = this.getFieldLayout(field, offset, true); | ||
layout[property] = fieldLayout; | ||
offset += fieldLayout.length; | ||
} | ||
this.optionalOffset = offset; | ||
for (let i = 0; i < optional.length; i++) { | ||
const property = optional[i]; | ||
const field = schema.properties[property]; | ||
layout[property] = this.getFieldLayout(field, offset + (i << 2), false); | ||
} | ||
this.lengthOffset = offset + (optional.length << 2); | ||
this.layout = layout; | ||
this.fields = properties; | ||
this.requiredFields = required; | ||
this.optionalFields = optional; | ||
if (offset) this.setDefaultBuffer(); | ||
} | ||
/** | ||
* @private | ||
* @param {Object} field | ||
* @param {number} start | ||
* @param {boolean} required | ||
* @returns {Object} | ||
*/ | ||
static getFieldLayout(field, start, required) { | ||
let currentField = field; | ||
let View; | ||
let length; | ||
if (currentField.btype === 'map') { | ||
View = this.Views[currentField.$id]; | ||
} else if (currentField.type !== 'array') { | ||
View = this.ObjectViewClass.getViewFromSchema(currentField); | ||
length = currentField.type === 'string' ? currentField.maxLength : View.getLength(); | ||
} else { | ||
const sizes = []; | ||
while (currentField && currentField.type === 'array') { | ||
sizes.push(currentField.maxItems); | ||
currentField = currentField.items; | ||
} | ||
View = ArrayViewMixin( | ||
this.ObjectViewClass.getViewFromSchema(currentField), | ||
currentField.maxLength, | ||
); | ||
let itemLength = View.getLength(sizes.pop()); | ||
for (let j = sizes.length - 1; j >= 0; j--) { | ||
View = ArrayViewMixin(View, itemLength); | ||
itemLength = View.getLength(sizes[j]); | ||
} | ||
length = itemLength; | ||
} | ||
if (!length) length = Infinity; | ||
if (required && length === Infinity) | ||
throw new TypeError('The length of a required field is undefined.'); | ||
const layout = { View, start, length, required }; | ||
if (Reflect.has(field, 'default')) layout.default = field.default; | ||
return layout; | ||
} | ||
/** | ||
* @private | ||
* @returns {void} | ||
*/ | ||
static setDefaultBuffer() { | ||
const { requiredFields, layout, optionalOffset } = this; | ||
const buffer = new ArrayBuffer(optionalOffset); | ||
const array = new Uint8Array(buffer); | ||
const view = new this(buffer); | ||
for (let i = 0; i < requiredFields.length; i++) { | ||
const name = requiredFields[i]; | ||
const field = layout[name]; | ||
if (Reflect.has(field, 'default')) { | ||
view.set(name, field.default); | ||
} else if (field.View.defaultBuffer) { | ||
array.set(new Uint8Array(field.View.defaultBuffer), field.start); | ||
} | ||
} | ||
this.defaultBuffer = array; | ||
} | ||
/** | ||
* @type {DataView} | ||
*/ | ||
static get bufferView() { | ||
@@ -235,5 +332,25 @@ if (!this.maxView) this.maxView = new DataView(new ArrayBuffer(this.maxLength)); | ||
*/ | ||
MapView.fields = undefined; | ||
MapView.optionalFields = undefined; | ||
/** | ||
* @type {Array<string>} | ||
*/ | ||
MapView.requiredFields = undefined; | ||
/** | ||
* @type {number} | ||
*/ | ||
MapView.optionalOffset = 0; | ||
/** | ||
* @type {number} | ||
*/ | ||
MapView.lengthOffset = 0; | ||
/** | ||
* @type {Uint8Array} | ||
*/ | ||
MapView.defaultBuffer = undefined; | ||
/** | ||
* @type {Class<ObjectView>} | ||
@@ -240,0 +357,0 @@ */ |
@@ -135,15 +135,15 @@ const BigInt = globalThis.BigInt || Number; | ||
* @param {string} string | ||
* @param {Uint8Array} [bytes] | ||
* @returns {Uint8Array} | ||
* @param {Array|Uint8Array} bytes | ||
* @param {number} start | ||
* @returns {number} | ||
*/ | ||
function stringToUTF8(string, bytes) { | ||
const out = bytes || []; | ||
let p = 0; | ||
function writeUTF8(string, bytes, start = 0) { | ||
let p = start; | ||
for (let i = 0; i < string.length; i++) { | ||
let c = string.charCodeAt(i); | ||
if (c < 128) { | ||
out[p++] = c; | ||
bytes[p++] = c; | ||
} else if (c < 2048) { | ||
out[p++] = (c >> 6) | 192; | ||
out[p++] = (c & 63) | 128; | ||
bytes[p++] = (c >> 6) | 192; | ||
bytes[p++] = (c & 63) | 128; | ||
} else if ( | ||
@@ -156,19 +156,27 @@ (c & 0xfc00) === 0xd800 && | ||
c = 0x10000 + ((c & 0x03ff) << 10) + (string.charCodeAt(++i) & 0x03ff); | ||
out[p++] = (c >> 18) | 240; | ||
out[p++] = ((c >> 12) & 63) | 128; | ||
out[p++] = ((c >> 6) & 63) | 128; | ||
out[p++] = (c & 63) | 128; | ||
bytes[p++] = (c >> 18) | 240; | ||
bytes[p++] = ((c >> 12) & 63) | 128; | ||
bytes[p++] = ((c >> 6) & 63) | 128; | ||
bytes[p++] = (c & 63) | 128; | ||
} else { | ||
out[p++] = (c >> 12) | 224; | ||
out[p++] = ((c >> 6) & 63) | 128; | ||
out[p++] = (c & 63) | 128; | ||
bytes[p++] = (c >> 12) | 224; | ||
bytes[p++] = ((c >> 6) & 63) | 128; | ||
bytes[p++] = (c & 63) | 128; | ||
} | ||
} | ||
return p; | ||
} | ||
/** | ||
* @param {string} string | ||
* @param {Array|Uint8Array} bytes | ||
* @returns {Array|Uint8Array} | ||
*/ | ||
function stringToUTF8(string, bytes = []) { | ||
let length = writeUTF8(string, bytes); | ||
// zero out remaining bytes | ||
const { length } = out; | ||
while (p < length) { | ||
out[p++] = 0; | ||
while (length < bytes.length) { | ||
bytes[length++] = 0; | ||
} | ||
return out; | ||
return bytes; | ||
} | ||
@@ -222,3 +230,4 @@ | ||
stringToUTF8, | ||
writeUTF8, | ||
UTF8ToString, | ||
}; |
{ | ||
"name": "structurae", | ||
"version": "3.1.1", | ||
"version": "3.2.0", | ||
"description": "Data structures for performance-sensitive modern JavaScript applications.", | ||
@@ -48,8 +48,8 @@ "main": "index.js", | ||
"devDependencies": { | ||
"@types/jest": "^26.0.3", | ||
"@types/jest": "^26.0.4", | ||
"benchmark": "^2.1.4", | ||
"eslint": "^7.3.1", | ||
"eslint": "^7.4.0", | ||
"eslint-config-airbnb-base": "^14.2.0", | ||
"eslint-config-prettier": "^6.11.0", | ||
"eslint-plugin-import": "^2.21.2", | ||
"eslint-plugin-import": "^2.22.0", | ||
"jest": "^26.1.0", | ||
@@ -56,0 +56,0 @@ "jsdoc-to-markdown": "^6.0.1", |
@@ -292,3 +292,3 @@ # Structurae | ||
MapViews are useful for densely packing objects and arrays whose size my vary greatly. | ||
There are certain limitations involved: MapViews cannot be nested, and fields that were missing during instantiation cannot be set later. | ||
There is a limitation, though, since ArrayBuffers cannot be resized, optional fields that were absent upon creation of a map view cannot be set later, and those set cannot be resized. | ||
@@ -302,4 +302,4 @@ ```javascript | ||
properties: { | ||
id: { type: 'integer', btype: 'uint32' }, | ||
// notice that maxLength is not required in MapView | ||
id: { type: 'integer', btype: 'uint32', default: 10 }, | ||
// notice that maxLength is not required for optional fields in MapView | ||
// however, if set, MapView with truncate longer strings to fit the maxLength | ||
@@ -321,4 +321,12 @@ name: { type: 'string' }, | ||
}, | ||
// required fields are always present and can have default values | ||
required: ['id'], | ||
}); | ||
const person0 = Person.from({}); | ||
person.get('id') | ||
//=> 10 | ||
person.get('name') | ||
//=> name | ||
// create a person with one pet | ||
@@ -325,0 +333,0 @@ const person1 = Person.from({ id: 1, name: 'Artur', pets: [{ type: 'dog'}] }); |
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
225755
6213
1078