Huge News!Announcing our $40M Series B led by Abstract Ventures.Learn More
Socket
Sign inDemoInstall
Socket

structurae

Package Overview
Dependencies
Maintainers
1
Versions
75
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

structurae - npm Package Compare versions

Comparing version 3.1.1 to 3.2.0

10

CHANGELOG.md

@@ -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'}] });

SocketSocket SOC 2 Logo

Product

  • Package Alerts
  • Integrations
  • Docs
  • Pricing
  • FAQ
  • Roadmap
  • Changelog

Packages

npm

Stay in touch

Get open source security insights delivered straight into your inbox.


  • Terms
  • Privacy
  • Security

Made with ⚡️ by Socket Inc