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 2.3.0 to 3.0.0

lib/map-view.js

9

CHANGELOG.md

@@ -7,2 +7,11 @@ # Changelog

## [3.0.0] - 2020-03-30
### Changed
(breaking changes)
- ObjectView and all related *View classes use JSON Schema for schema definition.
- All *View classes use little endian encoding default.
- `ObjectView#get`, `ArrayView#get`, `MapView#get` return JavaScript values, use `*View.getView` methods to get views.
- CollectionView is replaced by MapView
- TypedArrayView extends ArrayView, and TypedArrayViewMixin is replaced with ArrayViewMixin
## [2.3.0] - 2020-03-10

@@ -9,0 +18,0 @@ ### Added

141

index.d.ts

@@ -200,11 +200,13 @@ // Type definitions for structurae

type ViewType = typeof ArrayView | typeof ObjectView | typeof TypedArrayView | typeof StringView | typeof TypeView;
type PrimitiveFieldType = 'int8' | 'uint8' | 'int16' | 'uint16'
| 'int32' | 'uint32' | 'float32' | 'float64' | 'bigint64' | 'biguint64';
type View = ObjectView | ArrayView | TypedArrayView | StringView | TypeView;
type ViewType = typeof ArrayView | typeof ObjectView | typeof StringView | typeof TypeView;
type View = ObjectView | ArrayView | StringView | TypeView;
declare class TypeView extends DataView {
static isPrimitive: true;
private static offset: number;
private static littleEndian: true;
private static objectLength: number;
static offset: number;
static littleEndian: true;
static objectLength: number;

@@ -215,4 +217,4 @@ get(): number;

static getLength(): number;
static from(value: number, view?: View, start?: number): View;
static toJSON(view: View, start: number): number;
static from(value: any, view?: View, start?: number): View;
static toJSON(view: View, start?: number): any;
static of(): TypeView;

@@ -222,4 +224,4 @@ }

export declare class BooleanView extends TypeView {
static from(value: boolean, view?: View, start?: number): View;
static toJSON(view: View, start: number): boolean;
static from(value: number|boolean, view?: View, start?: number): View;
static toJSON(view: View, start?: number): boolean;
}

@@ -232,53 +234,61 @@

static itemLength: number;
static View: typeof ObjectView | typeof StringView;
static View: ViewType;
get(index: number): ObjectView;
getValue(index: number): object;
set(index: number, value: object): this;
setView(index: number, value: ObjectView): this;
toJSON(): object[];
[Symbol.iterator](): IterableIterator<ObjectView>;
static from(value: ArrayLike<object>, array?: View, start?: number, length?: number): View;
get(index: number): any;
getView(index: number): View;
set(index: number, value: any): this;
setView(index: number, value: View): this;
toJSON(): any[];
[Symbol.iterator](): IterableIterator<View | number>;
static from(value: ArrayLike<any>, array?: View, start?: number, length?: number): View;
static toJSON(view: View, start: number, length: number): any[];
static of(size: number): ArrayView;
static of(size?: number): ArrayView;
static getLength(size: number): number;
static getSize(length: number): number;
}
export declare function ArrayViewMixin(ObjectViewClass: typeof ObjectView | typeof StringView,
itemLength?: number): typeof ArrayView;
declare class TypedArrayView extends ArrayView {
static View: typeof TypeView;
type PrimitiveFieldType = 'int8' | 'uint8' | 'int16' | 'uint16'
| 'int32' | 'uint32' | 'float32' | 'float64' | 'bigint64' | 'biguint64';
get(index: number): number;
set(index: number, value: number): this;
toJSON(): Array<number>;
[Symbol.iterator](): IterableIterator<number>;
static from(value: ArrayLike<number>, array?: View, start?: number, length?: number): View;
static toJSON(view: View, start: number, length: number): number[];
static of(size?: number): TypedArrayView;
}
type ObjectViewFieldType = PrimitiveFieldType| string | ViewType;
export declare function ArrayViewMixin(ObjectViewClass: ViewType | PrimitiveFieldType,
itemLength?: number | boolean): typeof ArrayView;
interface ObjectViewField {
type: ObjectViewFieldType;
size?: number;
littleEndian?: boolean;
interface ViewLayoutField {
View: ViewType;
start?: number;
length?: number;
View?: ViewType;
default?: any;
}
interface ObjectViewSchema {
[propName: string]: ObjectViewField;
interface ViewLayout {
[propName: string]: ViewLayoutField;
}
interface ViewTypes {
[propName: string]: ViewType;
}
interface ObjectViewTypeDefs {
[propName: string]: (field: ObjectViewField) => void;
[propName: string]: (field: ViewLayoutField) => void;
}
export declare class ObjectView extends DataView {
static schema: object;
static layout: ViewLayout;
static fields: string[];
static Views: ViewTypes;
static types: ObjectViewTypeDefs;
static schema: ObjectViewSchema;
static isInitialized: boolean;
static isPrimitive: false;
private static fields: string[];
private static objectLength: number;
static objectLength: number;
private static defaultBuffer: ArrayBuffer;
get(field: string): number | View;
getValue(field: string): any;
get(field: string): any;
getView(field: string): View;

@@ -289,8 +299,12 @@ set(field: string, value: any): this;

static from(object: object, view?: View, start?: number, length?: number): View;
static toJSON(view: View, start: number): any[];
static toJSON(view: View, start?: number): object;
static getLength(): number;
static initialize(): void;
private static setDefaultBuffer(): void;
static getSchemaOrdering(schema: object): object[];
static getLayoutFromSchema(schema: object): [ViewLayout, number, string[]];
static getViewFromSchema(schema: object): ViewType;
}
export declare function ObjectViewMixin(schema: ObjectViewSchema, ObjectViewClass?: typeof ObjectView): typeof ObjectView;
export declare function ObjectViewMixin(schema: object, ObjectViewClass?: typeof ObjectView): typeof ObjectView;

@@ -322,33 +336,24 @@ export declare class StringView extends Uint8Array {

declare class TypedArrayView extends DataView {
size: number;
static View: TypeView;
static itemLength: number;
export declare class MapView extends DataView {
static schema: object;
static layout: ViewLayout;
static fields: string[];
static Views: ViewTypes;
get(index: number): number;
set(index: number, value: number): this;
toJSON(): Array<number>;
[Symbol.iterator](): IterableIterator<number>;
static getLength(size: number): number;
static from(value: ArrayLike<number>, array?: View, start?: number, length?: number): View;
static toJSON(view: View, start: number, length: number): number[];
static of(size: number): TypedArrayView;
get(field: string): any;
getView(field: string): View;
private getLayout(field: string): [ViewType, number, number];
set(field: string, value: any): this;
setView(field: string, value: View): this;
toJSON(): object;
static from(value: object): MapView;
static toJSON(view: View, start?: number): object;
static getLength(value: any, getOffsets?: boolean): number | [number, number[]];
static initialize(): void;
}
export declare function TypedArrayViewMixin(type: PrimitiveFieldType, littleEndian: boolean): typeof TypedArrayView;
export declare function MapViewMixin(schema: object, MapViewClass?: typeof MapView): typeof MapView;
export declare class CollectionView extends DataView {
static schema: ViewType[];
get(index: number): View;
set(index: number, value: object): this;
toJSON(): object[];
[Symbol.iterator](): IterableIterator<View>;
static from(value: object[], array?: CollectionView): CollectionView;
static getLength(sizes: number[]): number;
static of(sizes: number[]): CollectionView;
}
interface BinaryProtocolSchema {
[propName: number]: object|typeof ObjectView;
[propName: number]: typeof ObjectView;
}

@@ -361,3 +366,3 @@

constructor(views: BinaryProtocolSchema, tagName?: string, tagType?: string);
constructor(views: object, tagName?: string, tagType?: string);
view(buffer: ArrayBuffer, offset?: number): ObjectView;

@@ -364,0 +369,0 @@ encode(object: object, arrayBuffer?: ArrayBuffer, offset?: number): ObjectView;

@@ -17,4 +17,4 @@ const BitField = require('./lib/bit-field');

const ArrayView = require('./lib/array-view');
const { ArrayViewMixin, TypedArrayViewMixin } = require('./lib/array-view-mixin');
const CollectionView = require('./lib/collection-view');
const ArrayViewMixin = require('./lib/array-view-mixin');
const { MapView, MapViewMixin } = require('./lib/map-view');
const { ObjectView, ObjectViewMixin } = require('./lib/object-view');

@@ -59,7 +59,7 @@ const StringView = require('./lib/string-view');

ArrayViewMixin,
CollectionView,
MapView,
MapViewMixin,
ObjectView,
ObjectViewMixin,
StringView,
TypedArrayViewMixin,
BinaryProtocol,

@@ -66,0 +66,0 @@ TypeViewMixin,

@@ -7,3 +7,3 @@ const ArrayView = require('./array-view');

* @private
* @type {WeakMap<ViewType, (Class<ArrayView>|Class<TypedArrayView>)>}
* @type {WeakMap<Class<View>, Class<ArrayView>>}
*/

@@ -15,39 +15,24 @@ const ArrayViews = new WeakMap();

*
* @param {ViewType} ObjectViewClass
* @param {number} [itemLength]
* @param {Class<View>|PrimitiveFieldType} ObjectViewClass
* @param {number|boolean} [itemLength]
* @returns {Class<ArrayView>}
*/
function ArrayViewMixin(ObjectViewClass, itemLength) {
if (!itemLength && ArrayViews.has(ObjectViewClass)) return ArrayViews.get(ObjectViewClass);
if (ObjectViewClass.initialize && !ObjectViewClass.isInitialized) {
ObjectViewClass.initialize();
let ViewClass = ObjectViewClass;
if (typeof ObjectViewClass === 'string') {
const littleEndian = itemLength === undefined ? true : !!itemLength;
ViewClass = TypeViewMixin(ObjectViewClass, littleEndian);
if (ArrayViews.has(ViewClass)) return ArrayViews.get(ViewClass);
} else {
if (!itemLength && ArrayViews.has(ViewClass)) return ArrayViews.get(ViewClass);
if (ViewClass.initialize && !ViewClass.layout) ViewClass.initialize();
}
const ArrayViewClass = ObjectViewClass.isPrimitive ? TypedArrayView : ArrayView;
const ArrayViewClass = Reflect.has(ViewClass, 'offset') ? TypedArrayView : ArrayView;
const View = class extends ArrayViewClass {};
View.View = ObjectViewClass;
View.itemLength = ObjectViewClass.objectLength || itemLength;
ArrayViews.set(ObjectViewClass, View);
View.View = ViewClass;
View.itemLength = ViewClass.objectLength || itemLength;
ArrayViews.set(ViewClass, View);
return View;
}
/**
* Creates a TypedArrayView class for a given TypeView class.
*
* @param {PrimitiveFieldType} type
* @param {boolean} [littleEndian]
* @returns {Class<TypedArrayView>}
*/
function TypedArrayViewMixin(type, littleEndian) {
const TypeViewClass = TypeViewMixin(type, !!littleEndian);
if (ArrayViews.has(TypeViewClass)) return ArrayViews.get(TypeViewClass);
const View = class extends TypedArrayView {};
View.View = TypeViewClass;
View.itemLength = TypeViewClass.objectLength;
ArrayViews.set(TypeViewClass, View);
return View;
}
module.exports = {
TypedArrayViewMixin,
ArrayViewMixin,
};
module.exports = ArrayViewMixin;

@@ -9,23 +9,23 @@ /**

/**
* Returns an object view at a given index.
* Returns an object at a given index.
*
* @param {number} index
* @returns {ObjectView}
* @returns {Object}
*/
get(index) {
const { itemLength, View } = this.constructor;
return new View(
this.buffer, this.byteOffset + (index * itemLength), itemLength,
);
const { View } = this.constructor;
const offset = this.constructor.getLength(index);
return View.toJSON(this, offset);
}
/**
* Returns an object at a given index.
* Returns an object view at a given index.
*
* @param {number} index
* @returns {Object}
* @returns {ObjectView}
*/
getValue(index) {
getView(index) {
const { itemLength, View } = this.constructor;
return View.toJSON(this, index * itemLength);
const offset = this.constructor.getLength(index);
return new View(this.buffer, this.byteOffset + offset, itemLength);
}

@@ -42,3 +42,4 @@

const { itemLength, View } = this.constructor;
View.from(value, this, this.byteOffset + index * itemLength, itemLength);
const offset = this.constructor.getLength(index);
View.from(value, this, this.byteOffset + offset, itemLength);
return this;

@@ -56,3 +57,4 @@ }

const { itemLength } = this.constructor;
new Uint8Array(this.buffer, this.byteOffset + (index * itemLength), itemLength)
const offset = this.constructor.getLength(index);
new Uint8Array(this.buffer, this.byteOffset + offset, itemLength)
.set(new Uint8Array(value.buffer, value.byteOffset, value.byteLength));

@@ -68,3 +70,3 @@ return this;

get size() {
return this.byteLength / this.constructor.itemLength;
return this.constructor.getSize(this.byteLength);
}

@@ -74,3 +76,3 @@

/**
* Allows iterating over objects views stored in the array.
* Allows iterating over object views stored in the array.
*

@@ -82,3 +84,3 @@ * @returns {Iterable<ObjectView>}

for (let i = 0; i < size; i++) {
yield this.get(i);
yield this.getView(i);
}

@@ -109,7 +111,7 @@ }

const { View, itemLength } = this;
const size = length / itemLength;
const size = this.getSize(length);
const max = (size < value.length ? size : value.length);
for (let i = 0; i < max; i++) {
const startOffset = start + i * itemLength;
View.from(value[i], view, startOffset, itemLength);
const offset = this.getLength(i);
View.from(value[i], view, start + offset, itemLength);
}

@@ -124,3 +126,3 @@ return view;

* @param {number} [start=0]
* @param {length} [length]
* @param {number} [length]
* @returns {Object}

@@ -130,7 +132,7 @@ */

const { View, itemLength } = this;
const size = length / itemLength;
const size = this.getSize(length);
const array = new Array(size);
for (let i = 0; i < size; i++) {
const startOffset = start + i * itemLength;
array[i] = View.toJSON(view, startOffset, itemLength);
const offset = this.getLength(i);
array[i] = View.toJSON(view, start + offset, itemLength);
}

@@ -147,6 +149,16 @@ return array;

static getLength(size) {
return size * this.itemLength;
return (size * this.itemLength) | 0;
}
/**
* Calculates the size of an array from it's byte length.
*
* @param {number} length
* @returns {number}
*/
static getSize(length) {
return (length / this.itemLength) | 0;
}
/**
* Creates an empty array view of specified size.

@@ -153,0 +165,0 @@ *

@@ -31,3 +31,3 @@ const { ObjectView, ObjectViewMixin } = require('./object-view');

/**
* @param {Object<number, object|ObjectView>} views a hash of tag values
* @param {Object<number, object|string>} views a hash of tag values
* and corresponding views or schemas

@@ -43,14 +43,17 @@ * @param {string} [tagName=tag] a custom name for the tag field

const schema = views[tag];
if (schema.prototype instanceof ObjectView) {
if (!Reflect.has(schema.schema, tagName)
|| schema.schema[tagName].default !== tag) {
if (typeof schema === 'string') {
const View = ObjectView.Views[schema];
if (!View) throw Error(`View "${schema}" is not found.`);
const defaultTag = View.layout[tagName];
if (!defaultTag || defaultTag.default !== tag) {
throw TypeError('The tag definition in the View is incorrect.');
}
this.Views[tag] = schema;
this.Views[tag] = View;
continue;
}
this.Views[tag] = ObjectViewMixin({
[tagName]: { type: tagType, default: tag },
...schema,
});
schema.properties = {
[tagName]: { type: 'number', btype: tagType, default: tag },
...schema.properties,
};
this.Views[tag] = ObjectViewMixin(schema);
}

@@ -57,0 +60,0 @@

const TypeViewMixin = require('./type-view');
/**
* @extends {TypeView}
*/
class BooleanView extends TypeViewMixin('uint8') {

@@ -4,0 +6,0 @@ /**

const StringView = require('./string-view');
const { ArrayViewMixin, TypedArrayViewMixin } = require('./array-view-mixin');
const ArrayViewMixin = require('./array-view-mixin');
const TypeViewMixin = require('./type-view');

@@ -7,11 +7,6 @@ const BooleanView = require('./boolean-view');

/**
* @typedef {Class<ArrayView>|Class<ObjectView>|Class<TypedArrayView>
* |Class<StringView>|Class<TypeView>} ViewType
* @typedef {ArrayView|ObjectView|StringView|TypeView} View
*/
/**
* @typedef {ArrayView|ObjectView|TypedArrayView|StringView|TypeView} View
*/
/**
* @typedef {'int8'|'uint8'|'int16'|'uint16'|'int32'

@@ -22,13 +17,6 @@ * |'uint32'|'float32'|'float64'|'bigint64'|'biguint64'} PrimitiveFieldType

/**
* @typedef {(PrimitiveFieldType|string|ViewType)} ObjectViewFieldType
*/
/**
* @typedef {Object} ObjectViewField
* @property {ObjectViewFieldType} type
* @property {number} [size] the maximum size in bytes for a string type
* @property {boolean} [littleEndian]
* @typedef {Object} ViewLayoutField
* @property {Class<View>} View
* @property {number} [start]
* @property {number} [length]
* @property {number} [start]
* @property {ViewType} [View]
* @property {*} [default]

@@ -38,10 +26,2 @@ */

/**
* @typedef {Object<string, ObjectViewField>} ObjectViewSchema
*/
/**
* @typedef {Object<string, Function>} ObjectViewTypeDefs
*/
/**
* A DataView based C-like struct to store JavaScript objects in ArrayBuffer.

@@ -53,3 +33,3 @@ *

/**
* Returns a number for primitive fields or a view for all other fields.
* Returns the JavaScript value of a given field.
*

@@ -60,15 +40,3 @@ * @param {string} field the name of the field

get(field) {
const { start, View, length } = this.constructor.schema[field];
return View.isPrimitive ? View.toJSON(this, start)
: new View(this.buffer, this.byteOffset + start, length);
}
/**
* Returns the JavaScript value of a given field.
*
* @param {string} field the name of the field
* @returns {*} value of the field
*/
getValue(field) {
const { start, View, length } = this.constructor.schema[field];
const { start, View, length } = this.constructor.layout[field];
return View.toJSON(this, start, length);

@@ -81,6 +49,6 @@ }

* @param {string} field the name of the field
* @returns {*} view of the field
* @returns {View} view of the field
*/
getView(field) {
const { View, start, length } = this.constructor.schema[field];
const { View, start, length } = this.constructor.layout[field];
return new View(this.buffer, this.byteOffset + start, length);

@@ -97,3 +65,3 @@ }

set(field, value) {
const { start, View, length } = this.constructor.schema[field];
const { start, View, length } = this.constructor.layout[field];
View.from(value, this, start, length);

@@ -104,3 +72,3 @@ return this;

/**
* Sets an View to a field.
* Copies a given view into a field.
*

@@ -112,3 +80,3 @@ * @param {string} field the name of the field

setView(field, value) {
const { start } = this.constructor.schema[field];
const { start } = this.constructor.layout[field];
new Uint8Array(this.buffer, this.byteOffset, this.byteLength)

@@ -143,7 +111,7 @@ .set(

if (view) new Uint8Array(view.buffer, view.byteOffset + start, length).fill(0);
const { fields, schema } = this;
const { fields, layout } = this;
for (let i = 0; i < fields.length; i++) {
const name = fields[i];
if (Reflect.has(object, name)) {
const { View, start: fieldStart, length: fieldLength } = schema[name];
const { View, start: fieldStart, length: fieldLength } = layout[name];
View.from(object[name], objectView, start + fieldStart, fieldLength);

@@ -158,12 +126,12 @@ }

*
* @param {View} view
* @param {number} [start=0]
* @param {View} view a given view
* @param {number} [start=0] starting offset
* @returns {Object}
*/
static toJSON(view, start = 0) {
const { fields, schema } = this;
const { fields, layout } = this;
const result = {};
for (let i = 0; i < fields.length; i++) {
const name = fields[i];
const { View, start: fieldStart, length: fieldLength } = schema[name];
const { View, start: fieldStart, length: fieldLength } = layout[name];
result[name] = View.toJSON(view, start + fieldStart, fieldLength);

@@ -179,3 +147,3 @@ }

static setDefaultBuffer() {
const { objectLength, fields, schema } = this;
const { objectLength, fields, layout } = this;
const buffer = new ArrayBuffer(objectLength);

@@ -185,3 +153,3 @@ const view = new this(buffer);

const name = fields[i];
const field = schema[name];
const field = layout[name];
if (Reflect.has(field, 'default')) {

@@ -208,78 +176,171 @@ view.set(name, field.default);

*
* @param {Class<ObjectView>} ParentViewClass
* @returns {void}
*/
static initialize() {
static initialize(ParentViewClass = ObjectView) {
const { schema } = this;
const fields = Object.keys(schema);
const schemas = this.getSchemaOrdering(schema);
for (let i = schemas.length - 1; i >= 0; i--) {
const objectSchema = schemas[i];
const id = objectSchema.$id;
if (Reflect.has(ObjectView.Views, id)) continue;
const View = i === 0 ? this : class extends ParentViewClass {};
[View.layout, View.objectLength, View.fields] = this.getLayoutFromSchema(objectSchema);
ObjectView.Views[id] = View;
View.setDefaultBuffer();
}
}
/**
* @param {Object} schema
* @returns {Array<Object>}
*/
static getSchemaOrdering(schema) {
// create graph
let object = schema;
let id = object.$id;
const objects = { [id]: object };
const adjacency = { [id]: [] };
const indegrees = { [id]: 0 };
const processing = [id];
while (processing.length) {
id = processing.pop();
object = objects[id];
const properties = Object.keys(object.properties);
for (const property of properties) {
let field = object.properties[property];
if (field.type === 'array') {
while (field.type === 'array') field = field.items;
}
const { $id, $ref } = field;
if ($id) {
objects[$id] = field;
adjacency[id].push($id);
adjacency[$id] = [];
indegrees[$id] = indegrees[$id] ? indegrees[$id] + 1 : 1;
processing.push($id);
} else if ($ref) {
const refId = $ref.slice(1);
indegrees[refId] = indegrees[refId] ? indegrees[refId] + 1 : 1;
adjacency[id].push(refId);
}
}
}
// topologically sort the graph
let visited = 0;
const order = [];
processing.push(schema.$id);
while (processing.length) {
id = processing.shift();
const children = adjacency[id];
if (!children) continue; // $ref no external links
order.push(objects[id]);
for (const child of children) {
indegrees[child] -= 1;
if (indegrees[child] === 0) processing.push(child);
}
visited++;
}
// check for recursive links
if (visited !== Object.keys(objects).length) {
throw Error('The schema has recursive references.');
}
return order;
}
/**
* @private
* @param {Object} schema
* @returns {Array<*>}
*/
static getLayoutFromSchema(schema) {
const properties = Object.keys(schema.properties);
const layout = {};
let lastOffset = 0;
for (let i = 0; i < fields.length; i++) {
const name = fields[i];
const field = schema[name];
field.start = lastOffset;
const type = typeof field.type !== 'string' ? 'object' : field.type;
if (!Reflect.has(this.types, type)) throw TypeError(`Type "${field.type}" is not a valid type.`);
const definition = typeof this.types[type] === 'string' ? this.types[this.types[type]] : this.types[type];
if (definition) definition(field);
lastOffset += field.length;
for (const property of properties) {
let field = schema.properties[property];
const start = lastOffset;
let View;
let length = 0;
let defaultValue;
if (field.type !== 'array') {
View = this.getViewFromSchema(field);
length = field.maxLength || View.getLength();
defaultValue = field.default;
} else {
const sizes = [];
defaultValue = field.default;
while (field && field.type === 'array') {
sizes.push(field.maxItems);
field = field.items;
}
View = ArrayViewMixin(this.getViewFromSchema(field), field.maxLength);
let itemLength = View.getLength(sizes.pop());
for (let i = sizes.length - 1; i >= 0; i--) {
View = ArrayViewMixin(View, itemLength);
itemLength = View.getLength(sizes[i]);
}
length = itemLength;
}
lastOffset += length;
layout[property] = { start, View, length };
if (defaultValue !== undefined) layout[property].default = defaultValue;
}
this.objectLength = lastOffset;
this.fields = fields;
this.isInitialized = true;
this.setDefaultBuffer();
return [layout, lastOffset, properties];
}
/**
* @param {Object} schema
* @returns {Class<View>}
*/
static getViewFromSchema(schema) {
const { $ref, type } = schema;
let { $id } = schema;
if ($ref) $id = $ref.slice(1);
if ($id) {
if (!Reflect.has(ObjectView.Views, $id)) throw Error(`View "${$id}" is not found.`);
return ObjectView.Views[$id];
}
if (Reflect.has(this.types, type)) {
return this.types[type](schema);
}
throw Error(`Type "${type}" is not supported.`);
}
}
/**
* @type {ObjectViewTypeDefs}
* @type {Object<string, Function>}
*/
ObjectView.types = {
int8: 'number',
uint8: 'number',
int16: 'number',
uint16: 'number',
int32: 'number',
uint32: 'number',
float32: 'number',
float64: 'number',
bigint64: 'number',
biguint64: 'number',
/**
* @param {ObjectViewField} field
* @returns {void}
* @returns {Class<BooleanView>}
*/
boolean(field) {
const { size } = field;
field.View = size ? ArrayViewMixin(BooleanView) : BooleanView;
field.length = field.View.getLength(size || 1);
boolean() {
return BooleanView;
},
/**
* @param {ObjectViewField} field
* @returns {void}
* @param {object} field
* @returns {Class<TypeViewMixin>}
*/
number(field) {
const { type, littleEndian, size } = field;
field.View = size ? TypedArrayViewMixin(type, littleEndian) : TypeViewMixin(type, littleEndian);
field.length = field.View.getLength(size || 1);
const { btype = 'float64', littleEndian = true } = field;
return TypeViewMixin(btype, littleEndian);
},
/**
* @param {ObjectViewField} field
* @returns {void}
* @param {object} field
* @returns {Class<TypeViewMixin>}
*/
string(field) {
const { size, length } = field;
field.View = size ? ArrayViewMixin(StringView, length) : StringView;
field.length = size ? field.View.getLength(size) : field.length;
integer(field) {
const { btype = 'int32', littleEndian = true } = field;
return TypeViewMixin(btype, littleEndian);
},
/**
* @param {ObjectViewField} field
* @returns {void}
* @returns {Class<StringView>}
*/
object(field) {
const { type, size } = field;
field.View = size ? ArrayViewMixin(type) : type;
field.length = field.View.getLength(size);
string() {
return StringView;
},

@@ -289,12 +350,10 @@ };

/**
* @type {ObjectViewSchema}
* @private
* @type {Object<string, ViewLayoutField>}
*/
ObjectView.layout = undefined;
/** @type {object} */
ObjectView.schema = undefined;
/** @type {boolean} */
ObjectView.isInitialized = false;
/** @type {boolean} */
ObjectView.isPrimitive = false;
/**

@@ -319,2 +378,7 @@ * @private

/**
* @type {Object<string, Class<View>>}
*/
ObjectView.Views = {};
/**
* Creates an ObjectView class with a given schema.

@@ -327,2 +391,4 @@ *

function ObjectViewMixin(schema, ObjectViewClass = ObjectView) {
const id = schema.$id;
if (ObjectView.Views[id]) return ObjectView.Views[id];
class Base extends ObjectViewClass {}

@@ -329,0 +395,0 @@ Base.schema = schema;

@@ -14,3 +14,3 @@ const { typeGetters, typeSetters, typeOffsets } = require('./utilities');

*/
function TypeViewMixin(type, littleEndian) {
function TypeViewMixin(type, littleEndian = true) {
const classId = type + +!!littleEndian;

@@ -23,2 +23,5 @@ if (TypeViews.has(classId)) return TypeViews.get(classId);

/**
* @extends {DataView}
*/
class TypeView extends DataView {

@@ -67,3 +70,3 @@ /**

* @param {number} value
* @param {View} view
* @param {View} [view]
* @param {number} [start=0]

@@ -101,7 +104,2 @@ * @returns {View}

/**
* @type {boolean}
*/
TypeView.isPrimitive = true;
/**
* @type {number}

@@ -108,0 +106,0 @@ */

@@ -0,1 +1,3 @@

const ArrayView = require('./array-view');
/**

@@ -6,3 +8,3 @@ * A DataView based TypedArray that supports endianness and can be set at any offset.

*/
class TypedArrayView extends DataView {
class TypedArrayView extends ArrayView {
/**

@@ -16,31 +18,9 @@ * Returns a number at a given index.

const { View } = this.constructor;
return View.toJSON(this, index << View.offset);
const offset = this.constructor.getLength(index);
return View.toJSON(this, offset);
}
/**
* Sets a number at a given index.
* Allows iterating over objects views stored in the array.
*
* @param {number} index
* @param {number} value
* @returns {TypedArrayView}
*/
set(index, value) {
const { View } = this.constructor;
View.from(value, this, index << View.offset);
return this;
}
/**
* Returns the amount of available numbers in the array.
*
* @type {number}
*/
get size() {
return this.byteLength >> this.constructor.View.offset;
}
/**
* Allows iterating over numbers stored in the instance.
*
* @returns {Iterable<number>}

@@ -56,11 +36,2 @@ */

/**
* Returns an array representation of the array view.
*
* @returns {Array<number>}
*/
toJSON() {
return this.constructor.toJSON(this, 0, this.byteLength);
}
/**
* Returns the byte length of an array view to hold a given amount of numbers.

@@ -76,49 +47,10 @@ *

/**
* Creates an array view from a given array of numbers.
* Calculates the size of an array from it's byte length.
*
* @param {ArrayLike<number>} value
* @param {View} [array]
* @param {number} [start=0]
* @param {number} [length]
* @returns {TypedArrayView}
* @param {number} length
* @returns {number}
*/
static from(value, array, start = 0, length = this.getLength(value.length)) {
const view = array || this.of(value.length);
new Uint8Array(view.buffer, view.byteOffset + start, length).fill(0);
const { View } = this;
const size = length >> View.offset;
for (let i = 0; i < size; i++) {
View.from(value[i], view, start + (i << View.offset));
}
return view;
static getSize(length) {
return length >> this.View.offset;
}
/**
* Returns an array representation of a given view.
*
* @param {View} view
* @param {number} [start=0]
* @param {length} [length]
* @returns {Array<number>}
*/
static toJSON(view, start = 0, length) {
const { View } = this;
const size = length >> View.offset;
const array = new Array(size);
for (let i = 0; i < size; i++) {
array[i] = View.toJSON(view, start + (i << View.offset));
}
return array;
}
/**
* Creates an empty array view of specified size.
*
* @param {number} size
* @returns {TypedArrayView}
*/
static of(size = 1) {
const buffer = new ArrayBuffer(this.getLength(size));
return new this(buffer);
}
}

@@ -125,0 +57,0 @@

{
"name": "structurae",
"version": "2.3.0",
"version": "3.0.0",
"description": "Data structures for performance-sensitive modern JavaScript applications.",

@@ -10,2 +10,3 @@ "main": "index.js",

"binary",
"binary protocol",
"data structures",

@@ -51,7 +52,7 @@ "sorted",

"eslint": "^6.8.0",
"eslint-config-airbnb-base": "^14.0.0",
"eslint-plugin-import": "^2.20.1",
"jest": "^25.1.0",
"eslint-config-airbnb-base": "^14.1.0",
"eslint-plugin-import": "^2.20.2",
"jest": "^25.2.4",
"jsdoc-to-markdown": "^5.0.3",
"json-schema-faker": "^0.5.0-rc23"
"json-schema-faker": "^0.5.0-rcv.24"
},

@@ -58,0 +59,0 @@ "jest": {

@@ -10,6 +10,5 @@ # Structurae

- [ObjectView](https://github.com/zandaqo/structurae#ObjectView) - extends DataView to implement C-like struct.
- [ArrayView](https://github.com/zandaqo/structurae#ArrayView) - an array of C-like structs, ObjectViews.
- [CollectionView](https://github.com/zandaqo/structurae#CollectionView) - an array of ObjectViews and ArrayViews of different types that support optional members.
- [ArrayView](https://github.com/zandaqo/structurae#ArrayView) - DataView based array of ObjectViews, strings, numbers, etc.
- [MapView](https://github.com/zandaqo/structurae#MapView) - ObjectView with optional fields and fields of varying sizes.
- [StringView](https://github.com/zandaqo/structurae#StringView) - extends Uint8Array to handle C-like representation of UTF-8 encoded strings.
- [TypedArrayView](https://github.com/zandaqo/structurae#TypedArrayView) - a DataView based TypedArray that supports endianness and can be set at any offset.
- [BinaryProtocol](https://github.com/zandaqo/structurae#BinaryProtocol) - a helper class that simplifies defining and operating on multiple tagged ObjectViews.

@@ -53,3 +52,3 @@ - Bit Structures:

Useful on their own, when combined, these classes form the basis for a simple binary protocol that is smaller and faster than
schema-less binary formats (e.g. BSON, MessagePac) and supports zero-copy operations. Unlike other schema-based formats
schema-less binary formats (e.g. BSON, MessagePack) and supports zero-copy operations. Unlike other schema-based formats
(e.g. Flatbuffers), these interfaces are native to JavaScript, hence, supported in all modern browsers and Node.js,

@@ -59,6 +58,6 @@ and do not require compilation.

#### ObjectView
Extends DataView to store a JavaScript object in ArrayBuffer akin to C-like struct.
The fields are defined in ObjectView.schema and can be of any primitive type supported by DataView,
their arrays, booleans, strings, or other objects and arrays of objects. The data is laid out sequentially with fixed sizes, hence,
variable length arrays and optional fields are not supported (for those check out [CollectionView](https://github.com/zandaqo/structurae#CollectionView)).
ObjectView extends DataView to store a JavaScript object in an ArrayBuffer akin to C-like struct.
Fields of an ObjectView are defined using a subset of JSON Schema. ObjectView supports all JavaScript values, that is, numbers, strings,
booleans, objects, and arrays. Internally, the data is laid out sequentially with fixed sizes, hence,
variable length arrays and optional fields are not supported (for those check out [MapView](https://github.com/zandaqo/structurae#MapView)).

@@ -68,16 +67,48 @@ ```javascript

const House = ObjectViewMixin({
size: { type: 'uint32', default: 100 }, // a primitive type (unsigned 32-bit integer) that defaults to 100
});
const Pet = ObjectViewMixin({
type: { type: 'string', length: 10 }, // // string with max length of 10 bytes
});
const Person = ObjectViewMixin({
name: { type: 'string', length: 10 },
fullName: { type: 'string', size: 2, length: 10 }, // an array of 2 strings 10 bytes long each
scores: { type: 'uint32', size: 10 }, // a an array of 10 numbers
house: { type: House }, // nested object view
pets: { type: Pet, size: 3 }, // an array of 3 pet objects
$id: 'Person', // each object requires a unique id
type: 'object',
properties: {
name: { type: 'string', maxLength: 10 }, // the size of a string field is required and defined by maxLength
fullName: {
type: 'array',
maxItems: 2, // the size of an array is required and defined by maxItems
items: { type: 'string', maxLength: 10 }, // all items have to be the same type
},
bestFriend: { $ref: '#Friend' }, // objects can be referenced with $ref using their $id
friends: {
type: 'array',
maxItems: 3,
items: {
$id: 'Friend',
type: 'object',
properties: {
name: { type: 'string', maxLength: 10 },
},
},
},
scores: {
type: 'array',
maxItems: 10,
items: { type: 'integer', btype: 'int16' },
},
house: {
$id: 'House',
type: 'object',
properties: {
size: { type: 'number', btype: 'float32', default: 100 }, // default values are applied upon creation of a view
},
},
pets: {
type: 'array',
maxItems: 3,
items: {
$id: 'Pet',
type: 'object',
properties: {
type: { type: 'string', maxLength: 10 },
}
},
},
},
});

@@ -98,12 +129,12 @@

//=> 64
person.get('scores').get(0)
person.get('scores');
//=> 1
person.get('name')
person.get('name');
//=> Zaphod
person.getView('name')
//=> StringView [10]
person.getValue('name');
//=> Zaphod
person.getValue('scores')
person.get('scores')
//=> [1, 2, 3, 0, 0, 0, 0, 0, 0, 0,]
person.set('house', { size: 5 });
person.getValue('house');
person.get('house');
//=> { size: 5 }

@@ -115,6 +146,19 @@ person.toJSON()

There are certain requirements for a JSON Schema used for ObjectViews:
- Each object should have a unique id defined with `$id` field. Upon initialization, the view class is stored in `ObjectView.Views`
and accessed with the id used as the key. References made with `$ref` are also resolved against the id.
- Sizes of strings and arrays should be defined using `maxLength` and `maxItems` properties respectfully.
- `$ref` can be used to reference objects and only objects by their `$id`. The referenced object should be defined either in the
same schema or in a schema of an ObjectView class initialized previously.
- Type `number` by default resolves to `float64` and type `integer` to `int32`, you can use any other type by specifying it in
`btype` property.
ObjectView supports setting default values of fields. Default values are applied upon creation of a view:
```javascript
const House = ObjectViewMixin({
size: { type: 'uint32', default: 100 }
$id: 'House',
type: 'object',
properties: {
size: { type: 'integer', btype: 'uint32', default: 100 }
},
});

@@ -127,7 +171,11 @@

Default values of a ObjectView can be overridden when the view is used as a field inside other views:
Default values of an ObjectView can be overridden when the view is used as a field inside other views:
```javascript
const Neighborhood = ObjectViewMixin({
house: { type: House },
biggerHouse: { type: House, default: { size: 200 } },
$id: 'Neighborhood',
type: 'object',
properties: {
house: { $ref: '#House' },
biggerHouse: { $ref: '#House', default: { size: 200 } },
},
});

@@ -146,7 +194,8 @@

class DateView extends TypeViewMixin('float64', true) {
static get(position, view) {
return new Date(super.get(position, view));
static from(value, view, start) {
super.from(+value, view, start);
}
static set(position, value, view) {
super.set(position, +value, view);
static toJSON(view, start) {
return new Date(super.toJSON(view, start));
}

@@ -156,16 +205,18 @@ }

class View extends ObjectView {}
View.schema = {
a: { type: 'date' },
};
View.types = {
...ObjectView.types,
date(field) {
field.View = DateView;
field.length = 8;
return DateView;
},
};
View.initialize();
const date = View.from({ a: new Date(0) });
date.getValue('a')
const ViewWithDate = ObjectViewMixin({
$id: 'ViewWithDate',
type: 'object',
properties: {
a: { type: 'date' },
},
}, View);
const date = ViewWithDate.from({ a: new Date(0) });
date.get('a')
//=> Thu Jan 01 1970 00:00:00 GMT+0000

@@ -178,9 +229,17 @@ date.set('a', new Date(1e8));

#### ArrayView
DataView based array of objects or more precisely ObjectViews:
DataView based array of "views": objects, number, strings, etc:
```javascript
const { ObjectViewMixin, ArrayViewMixin } = require('structurae');
const { ObjectViewMixin, ArrayViewMixin, StringView } = require('structurae');
const Int32ArrayView = ArrayViewMixin('int32', true); // create an ArrayView class for int32 values with little endian encoding
const Int32ArrayViewBE = ArrayViewMixin('int32', false); // big endian int32 values
const StringsView = ArrayViewMixin(StringView, 20); // an ArrayView class for strings with maximum length of 20 bytes each
const Person = ObjectViewMixin({
id: { type: 'uint32' },
name: { type: 'string', length: 10 },
$id: 'Person', // each object requires a unique id
type: 'object',
properties: {
id: { type: 'integer', btype: 'uint32' },
name: { type: 'string', maxLength: 10 },
},
});

@@ -200,6 +259,6 @@

// get a view of the first object
hitchhikers.get(0);
hitchhikers.getView(0);
//=> Person [14]
// get the value of the first object
hitchhikers.getValue(0);
hitchhikers.get(0);
//=> { id: 1, name: 'Arthur' }

@@ -209,3 +268,3 @@

hitchhikers.set(0, { id: 3, name: 'Trillian' });
hitchhikers.get(0).toJSON();
hitchhikers.get(0);
//=> { id: 3, name: 'Trillian' }

@@ -217,32 +276,70 @@

#### CollectionView
Whereas a single ArrayView can only hold objects of a single ObjectView class, CollectionView allows holding objects and arrays
of different types as well as them being optional, i.e. it does not allocate space upon creation for missing members.
TypedArrays in JavaScript have two limitations that make them cumbersome to use in conjunction with DataView.
First, there is no way to specify the endianness of numbers in TypedArrays unlike DataView.
Second, TypedArrays require their offset (byteOffset) to be a multiple of their element size (BYTES_PER_ELEMENT),
which means that they often cannot "view" into existing ArrayBuffer starting from certain positions.
ArrayViews for numbers are essentially TypedArrays that circumvent both issues by using the DataView interface.
You can specify endianness and instantiate them at any position in an existing ArrayBuffer.
```javascript
const { ObjectViewMixin, ArrayViewMixin, CollectionView } = require('structurae');
const { ArrayViewMixin } = require('structurae');
const Person = ObjectViewMixin({
id: { type: 'uint32' },
name: { type: 'string', length: 10 },
});
// create a class for little endian doubles
const Float64View = ArrayViewMixin('float64', true);
const buffer = new ArrayBuffer(11);
const doubles = new Float64View(buffer, 3, 8);
doubles.byteLength
//=> 20
doubles.byteOffset
//=> 3
doubles.set(0, 5).set(1, 10);
[...doubles]
//=> [5, 10]
```
const Pet = ObjectViewMixin({
type: { type: 'string', length: 10 }, // string with max length of 10 bytes
});
const Pets = ArrayViewMixin(Pet);
#### MapView
MapView is an ObjectView that supports optional fields and fields of variable size. The size and layout of each MapView instance
is calculated upon creation and stored within the instance (unlike ObjectViews, where each instance have the same size and layout).
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.
class PersonWithPets extends CollectionView {}
PersonWithPets.schema = [Person, Pets];
```javascript
const { MapViewMixin } = require('structurae');
const Person = MapViewMixin({
$id: 'Person',
type: 'object',
properties: {
id: { type: 'integer', btype: 'uint32' },
name: { type: 'string' }, // notice that maxLength is not required in MapView
pets: {
type: 'array',
// maxItems is also not required for MapView
items: {
$id: 'Pet',
type: 'object',
properties: {
// however both maxLengh and maxItems are required in nested objects and arrays
type: { type: 'string', maxLength: 10 }
},
},
},
},
});
// create a person with one pet
const arthur = PersonWithPets.from([{ id: 1, name: 'Artur'}, [{ type: 'dog'}]]);
arthur.byteLength
//=> 24
const person1 = Person.from({ id: 1, name: 'Artur', pets: [{ type: 'dog'}] });
person1.byteLength;
//=> 31
// create a person with no pets
const arthur = PersonWithPets.from([{ id: 1, name: 'Artur'}, undefined]);
arthur.byteLength
//=> 14
const person0 = PersonWithPets.from({ id: 1, name: 'Artur'});
person0.byteLength;
//=> 18
person0.get('pets');
//=> undefined
person0.set('pets', [{ type: 'dog'}]);
person0.get('pets');
//=> undefined
```

@@ -298,26 +395,2 @@

#### TypedArrayView
TypedArrays in JavaScript have two limitations that make them cumbersome to use in conjunction with DataView.
First, there is no way to specify the endianness of numbers in TypedArrays unlike DataView,
second, TypedArrays require their offset (byteOffset) to be a multiple of their element size (BYTES_PER_ELEMENT),
which means that they often cannot "view" into existing ArrayBuffer starting from certain positions.
TypedArrayViews are essentially TypedArrays that circumvent both issues by using the DataView interface.
You can specify endianness and instantiate them at any position in an existing ArrayBuffer.
TypedArrayViews are internally used by ObjectView to handle arrays of numbers, although, they can be used on their own:
```javascript
const { TypedArrayViewMixin } = require('structurae');
// create a class for little endian doubles
const Float64View = TypedArrayViewMixin('float64', true);
const buffer = new ArrayBuffer(11);
const doubles = new Float64View(buffer, 3, 8);
doubles.byteLength
//=> 20
doubles.byteOffset
//=> 3
doubles.set(0, 5).set(1, 10);
[...doubles]
//=> [5, 10]
```
#### BinaryProtocol

@@ -338,8 +411,20 @@ When transferring our buffers encoded with views we can often rely on meta information to know what kind of ObjectView

0: {
age: { type: 'int8' },
name: { type: 'string', length: 10 },
$id: 'Person',
type: 'object',
properties: {
age: { type: 'integer', btype: 'int8' },
name: { type: 'string', length: 10 },
}
},
1: {
id: { type: 'uint32' },
items: { type: 'string', size: 3, length: 10 },
$id: 'Items',
type: 'object',
properties: {
id: { type: 'integer', btype: 'uint32' },
items: {
type: 'array',
maxItems: 3,
items: { type: 'string', maxLength: 10 },
},
},
},

@@ -361,8 +446,16 @@ });

We can define ObjectViews separately, however, we will have to specify the tag field by ourselves in that case.
We can use references to existing ObjectViews, however, those views should have a tag field and appropriate default value specified.
```javascript
const View = ObjectViewMixin({
tag: { type: 'uint8', default: 1 },
id: { type: 'uint32' },
items: { type: 'string', size: 3, length: 10 },
$id: 'Items',
type: 'object',
properties: {
tag: { type: 'integer', btype: 'uint8', default: 1 },
id: { type: 'integer', btype: 'uint32' },
items: {
type: 'array',
maxItems: 3,
items: { type: 'string', maxLength: 10 },
},
},
});

@@ -372,6 +465,10 @@

0: {
age: { type: 'int8' },
name: { type: 'string', length: 10 },
$id: 'Person',
type: 'object',
properties: {
age: { type: 'integer', btype: 'int8' },
name: { type: 'string', length: 10 },
}
},
1: View,
1: { $ref: '#Items' },
});

@@ -383,5 +480,13 @@ ```

const View = ObjectViewMixin({
tagId: { type: 'uint32', default: 1 },
id: { type: 'uint32' },
items: { type: 'string', size: 3, length: 10 },
$id: 'Items',
type: 'object',
properties: {
tagId: { type: 'integer', btype: 'uint32', default: 1 },
id: { type: 'integer', btype: 'uint32' },
items: {
type: 'array',
maxItems: 3,
items: { type: 'string', maxLength: 10 },
},
},
});

@@ -391,6 +496,10 @@

0: {
age: { type: 'int8' },
name: { type: 'string', length: 10 },
$id: 'Person',
type: 'object',
properties: {
age: { type: 'integer', btype: 'int8' },
name: { type: 'string', length: 10 },
}
},
1: View,
1: { $ref: '#Items' },
}, 'tagId', 'uint32');

@@ -397,0 +506,0 @@ ```

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