structurae
Advanced tools
Comparing version 2.3.0 to 3.0.0
@@ -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": { |
331
README.md
@@ -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 @@ ``` |
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
215010
5764
1064