structurae
Advanced tools
Comparing version 3.2.0 to 3.3.0
@@ -7,2 +7,5 @@ # Changelog | ||
## [3.3.0] - 2020-07-14 | ||
- Add VectorView | ||
## [3.2.0] - 2020-07-09 | ||
@@ -9,0 +12,0 @@ - Support required fields and default values in MapView. |
@@ -281,3 +281,3 @@ // Type definitions for structurae | ||
static littleEndian: true; | ||
static objectLength: number; | ||
static viewLength: number; | ||
static Views: Map<string, typeof TypeView>; | ||
@@ -322,2 +322,3 @@ static ArrayClass: typeof ArrayView; | ||
static getLength(size: number): number; | ||
static getOffset(size: number): number; | ||
static getSize(length: number): number; | ||
@@ -369,3 +370,3 @@ } | ||
static types: ObjectViewTypeDefs; | ||
static objectLength: number; | ||
static viewLength: number; | ||
private static defaultBuffer: ArrayBuffer; | ||
@@ -396,4 +397,2 @@ | ||
static masks: Int8Array; | ||
static encoder: TextEncoder; | ||
static decoder: TextDecoder; | ||
static ArrayClass: typeof ArrayView; | ||
@@ -415,8 +414,23 @@ | ||
trim(): StringView; | ||
static decode(bytes: Uint8Array): string; | ||
static encode( | ||
string: string, | ||
view: number[] | Uint8Array | DataView, | ||
start?: number, | ||
length?: number, | ||
): number; | ||
static from(...args: any[]): View; | ||
static toJSON(view: View, start?: number, length?: number): string; | ||
static getByteSize(string: string): number; | ||
static getLength(string: string): number; | ||
} | ||
export declare class MapView extends DataView { | ||
export declare class VariableView extends DataView { | ||
static maxLength: number; | ||
static maxView: DataView; | ||
static bufferView: DataView; | ||
} | ||
type AnyView = View | VariableView; | ||
export declare class MapView extends VariableView { | ||
static schema: object; | ||
@@ -431,7 +445,5 @@ static layout: ViewLayout; | ||
static Views: ViewTypes; | ||
static maxLength: number; | ||
static maxView: DataView; | ||
get(field: string): any; | ||
getView(field: string): View; | ||
getView(field: string): AnyView; | ||
private getLayout(field: string): [ViewType, number, number]; | ||
@@ -441,4 +453,5 @@ set(field: string, value: any): this; | ||
toJSON(): object; | ||
static from(value: object, view?: View, start?: number): View; | ||
static toJSON(view: View, start?: number): object; | ||
static encode(value: object, view: AnyView, start?: number): number; | ||
static from(value: object, view?: AnyView, start?: number): View; | ||
static toJSON(view: AnyView, start?: number): object; | ||
static getLength(value: any): number; | ||
@@ -460,2 +473,25 @@ static initialize(): void; | ||
export declare class VectorView extends VariableView { | ||
static View: ViewType | typeof VariableView; | ||
static Views: WeakMap<ViewType | typeof VariableView, typeof VectorView>; | ||
get(index: number): any; | ||
getView(index: number): AnyView; | ||
private getLayout(index: number): [number, number]; | ||
set(index: number, value: any): this; | ||
setView(index: number, value: AnyView): this; | ||
toJSON(): any[]; | ||
[Symbol.iterator](): IterableIterator<AnyView>; | ||
static encode(value: ArrayLike<any>, view: AnyView, start?: number): number; | ||
static from(value: ArrayLike<any>, view?: AnyView, start?: number): AnyView; | ||
static toJSON(view: AnyView, start?: number, length?: number): any[]; | ||
static getLength(value: any[]): number; | ||
static getSize(view: AnyView): number; | ||
} | ||
export declare function VectorViewMixin( | ||
ViewClass: ViewType | typeof VariableView, | ||
VectorViewClass?: typeof VectorView, | ||
): typeof VectorView; | ||
interface BinaryProtocolSchema { | ||
@@ -462,0 +498,0 @@ [propName: number]: typeof ObjectView; |
@@ -25,2 +25,4 @@ const BitField = require('./lib/bit-field'); | ||
const BooleanView = require('./lib/boolean-view'); | ||
const VariableView = require('./lib/variable-view'); | ||
const { VectorView, VectorViewMixin } = require('./lib/vector-view'); | ||
@@ -70,2 +72,5 @@ /** | ||
BooleanView, | ||
VariableView, | ||
VectorView, | ||
VectorViewMixin, | ||
}; |
@@ -24,3 +24,3 @@ const ArrayView = require('./array-view'); | ||
View.View = ViewClass; | ||
View.itemLength = ViewClass.objectLength || itemLength; | ||
View.itemLength = ViewClass.viewLength || itemLength; | ||
if (typeof itemLength !== 'number') ArrayViews.set(ViewClass, View); | ||
@@ -27,0 +27,0 @@ return View; |
@@ -16,3 +16,3 @@ /** | ||
const { View } = this.constructor; | ||
const offset = this.constructor.getLength(index); | ||
const offset = this.constructor.getOffset(index); | ||
return View.toJSON(this, offset); | ||
@@ -28,4 +28,4 @@ } | ||
getView(index) { | ||
const { itemLength, View } = this.constructor; | ||
const offset = this.constructor.getLength(index); | ||
const { View, itemLength } = this.constructor; | ||
const offset = this.constructor.getOffset(index); | ||
return new View(this.buffer, this.byteOffset + offset, itemLength); | ||
@@ -42,4 +42,4 @@ } | ||
set(index, value) { | ||
const { itemLength, View } = this.constructor; | ||
const offset = this.constructor.getLength(index); | ||
const { View, itemLength } = this.constructor; | ||
const offset = this.constructor.getOffset(index); | ||
View.from(value, this, this.byteOffset + offset, itemLength); | ||
@@ -58,3 +58,3 @@ return this; | ||
const { itemLength } = this.constructor; | ||
const offset = this.constructor.getLength(index); | ||
const offset = this.constructor.getOffset(index); | ||
new Uint8Array(this.buffer, this.byteOffset + offset, itemLength).set( | ||
@@ -105,3 +105,3 @@ new Uint8Array(value.buffer, value.byteOffset, value.byteLength), | ||
*/ | ||
static from(value, array, start = 0, length = this.getLength(value.length)) { | ||
static from(value, array, start = 0, length = this.getOffset(value.length)) { | ||
const view = array || this.of(value.length); | ||
@@ -113,3 +113,3 @@ new Uint8Array(view.buffer, view.byteOffset + start, length).fill(0); | ||
for (let i = 0; i < max; i++) { | ||
const offset = this.getLength(i); | ||
const offset = this.getOffset(i); | ||
View.from(value[i], view, start + offset, itemLength); | ||
@@ -121,28 +121,19 @@ } | ||
/** | ||
* Returns an array representation of a given array view. | ||
* Returns the byte length of an array view to hold a given amount of objects. | ||
* | ||
* @param {View} view | ||
* @param {number} [start=0] | ||
* @param {number} [length] | ||
* @returns {Object} | ||
* @param {number} size | ||
* @returns {number} | ||
*/ | ||
static toJSON(view, start, length) { | ||
const { View, itemLength } = this; | ||
const size = this.getSize(length); | ||
const array = new Array(size); | ||
for (let i = 0; i < size; i++) { | ||
const offset = this.getLength(i); | ||
array[i] = View.toJSON(view, start + offset, itemLength); | ||
} | ||
return array; | ||
static getLength(size) { | ||
return this.getOffset(size); | ||
} | ||
/** | ||
* Returns the byte length of an array view to hold a given amount of objects. | ||
* Returns the starting byte offset of an item in the array. | ||
* | ||
* @param {number} size | ||
* @param {number} index | ||
* @returns {number} | ||
*/ | ||
static getLength(size) { | ||
return (size * this.itemLength) | 0; | ||
static getOffset(index) { | ||
return (index * this.itemLength) | 0; | ||
} | ||
@@ -161,2 +152,21 @@ | ||
/** | ||
* Returns an array representation of a given array view. | ||
* | ||
* @param {View} view | ||
* @param {number} [start=0] | ||
* @param {number} [length] | ||
* @returns {Object} | ||
*/ | ||
static toJSON(view, start, length) { | ||
const { View, itemLength } = this; | ||
const size = this.getSize(length); | ||
const array = new Array(size); | ||
for (let i = 0; i < size; i++) { | ||
const offset = this.getOffset(i); | ||
array[i] = View.toJSON(view, start + offset, itemLength); | ||
} | ||
return array; | ||
} | ||
/** | ||
* Creates an empty array view of specified size. | ||
@@ -168,3 +178,3 @@ * | ||
static of(size = 1) { | ||
const buffer = new ArrayBuffer(this.getLength(size)); | ||
const buffer = new ArrayBuffer(this.getOffset(size)); | ||
return new this(buffer); | ||
@@ -171,0 +181,0 @@ } |
@@ -85,3 +85,3 @@ const { ObjectView, ObjectViewMixin } = require('./object-view'); | ||
if (!View) throw TypeError('No tag information is found.'); | ||
return new View(buffer, offset, View.objectLength); | ||
return new View(buffer, offset, View.viewLength); | ||
} | ||
@@ -100,4 +100,4 @@ | ||
if (!View) throw TypeError('No tag information is found.'); | ||
const buffer = arrayBuffer || new ArrayBuffer(View.objectLength); | ||
const view = new View(buffer, offset, View.objectLength); | ||
const buffer = arrayBuffer || new ArrayBuffer(View.viewLength); | ||
const view = new View(buffer, offset, View.viewLength); | ||
View.from(object, view); | ||
@@ -104,0 +104,0 @@ return view; |
@@ -0,10 +1,10 @@ | ||
const VariableView = require('./variable-view'); | ||
const { ObjectView, ObjectViewMixin } = require('./object-view'); | ||
const StringView = require('./string-view'); | ||
const ArrayViewMixin = require('./array-view-mixin'); | ||
const { writeUTF8 } = require('./utilities'); | ||
const { VectorViewMixin } = require('./vector-view'); | ||
/** | ||
* @extends DataView | ||
* @extends {VariableView} | ||
*/ | ||
class MapView extends DataView { | ||
class MapView extends VariableView { | ||
/** | ||
@@ -99,12 +99,7 @@ * Returns the JavaScript value at a given field. | ||
* @param {Object} value the object to take data from | ||
* @param {View} [view] the view to assign fields to | ||
* @param {View} view the view to assign fields to | ||
* @param {number} [start=0] | ||
* @returns {View} | ||
* @returns {number} | ||
*/ | ||
static from(value, view, start = 0) { | ||
const mapView = view || this.bufferView; | ||
const mapArray = new Uint8Array(mapView.buffer, mapView.byteOffset); | ||
if (this.defaultBuffer) { | ||
mapArray.set(this.defaultBuffer, start); | ||
} | ||
static encode(value, view, start = 0) { | ||
const { layout, requiredFields, optionalFields, lengthOffset } = this; | ||
@@ -116,3 +111,3 @@ for (let i = 0; i < requiredFields.length; i++) { | ||
const { View, length: maxLength, start: fieldStart } = layout[field]; | ||
View.from(fieldValue, mapView, start + fieldStart, maxLength); | ||
View.from(fieldValue, view, start + fieldStart, maxLength); | ||
} | ||
@@ -128,18 +123,32 @@ } | ||
const caret = start + end; | ||
if (View === StringView) { | ||
fieldLength = writeUTF8(fieldValue, mapArray, caret); | ||
} else if (View.prototype instanceof MapView) { | ||
View.from(fieldValue, mapView, caret); | ||
const fieldEnd = caret + View.lengthOffset; | ||
fieldLength = mapView.getUint32(fieldEnd, true); | ||
if (View.viewLength || View.itemLength) { | ||
fieldLength = View.getLength(fieldValue.length || 1); | ||
View.from(fieldValue, view, caret, fieldLength); | ||
} else { | ||
fieldLength = View.getLength(fieldValue.length || 1); | ||
View.from(fieldValue, mapView, caret, fieldLength); | ||
fieldLength = View.encode(fieldValue, view, caret); | ||
} | ||
fieldLength = Math.min(fieldLength, maxLength); | ||
} | ||
mapView.setUint32(start + fieldStart, end, true); | ||
view.setUint32(start + fieldStart, end, true); | ||
end += fieldLength; | ||
} | ||
mapView.setUint32(start + lengthOffset, end, true); | ||
view.setUint32(start + lengthOffset, end, true); | ||
return end; | ||
} | ||
/** | ||
* Creates a map view from a given object. | ||
* | ||
* @param {Object} value the object to take data from | ||
* @param {View} [view] the view to assign fields to | ||
* @param {number} [start=0] | ||
* @returns {View} | ||
*/ | ||
static from(value, view, start = 0) { | ||
const mapView = view || this.bufferView; | ||
const mapArray = new Uint8Array(mapView.buffer, mapView.byteOffset); | ||
if (this.defaultBuffer) { | ||
mapArray.set(this.defaultBuffer, start); | ||
} | ||
const end = this.encode(value, mapView, start); | ||
return view || new this(mapView.buffer.slice(0, end)); | ||
@@ -163,8 +172,8 @@ } | ||
const { View, length: maxLength } = layout[field]; | ||
if (View.prototype instanceof MapView) { | ||
if (View.viewLength) { | ||
fieldLength = View.viewLength; | ||
} else if (View.itemLength) { | ||
fieldLength = View.getLength(fieldValue.length); | ||
} else { | ||
fieldLength = View.getLength(fieldValue); | ||
} else if (View.getByteSize) { | ||
fieldLength = View.getByteSize(fieldValue); | ||
} else { | ||
fieldLength = View.getLength(fieldValue.length || 1); | ||
} | ||
@@ -213,2 +222,3 @@ length += Math.min(fieldLength, maxLength); | ||
if (objects[i].btype === 'map') { | ||
// eslint-disable-next-line no-use-before-define | ||
MapViewMixin(objects[i], this, ObjectViewClass); | ||
@@ -227,3 +237,3 @@ } else { | ||
const field = schema.properties[property]; | ||
const fieldLayout = this.getFieldLayout(field, offset, true); | ||
const fieldLayout = this.getFieldLayout(field, offset, true, property); | ||
layout[property] = fieldLayout; | ||
@@ -236,3 +246,3 @@ offset += fieldLayout.length; | ||
const field = schema.properties[property]; | ||
layout[property] = this.getFieldLayout(field, offset + (i << 2), false); | ||
layout[property] = this.getFieldLayout(field, offset + (i << 2), false, property); | ||
} | ||
@@ -243,3 +253,5 @@ this.lengthOffset = offset + (optional.length << 2); | ||
this.optionalFields = optional; | ||
if (offset) this.setDefaultBuffer(); | ||
if (offset) { | ||
this.defaultBuffer = ObjectViewClass.getDefaultBuffer.call(this, offset, required, layout); | ||
} | ||
} | ||
@@ -252,5 +264,6 @@ | ||
* @param {boolean} required | ||
* @param {string} name | ||
* @returns {Object} | ||
*/ | ||
static getFieldLayout(field, start, required) { | ||
static getFieldLayout(field, start, required, name) { | ||
let currentField = field; | ||
@@ -265,15 +278,20 @@ let View; | ||
} else { | ||
const sizes = []; | ||
const arrays = []; | ||
while (currentField && currentField.type === 'array') { | ||
sizes.push(currentField.maxItems); | ||
arrays.push(currentField); | ||
currentField = currentField.items; | ||
} | ||
View = ArrayViewMixin( | ||
this.ObjectViewClass.getViewFromSchema(currentField), | ||
currentField.maxLength, | ||
); | ||
let itemLength = View.getLength(sizes.pop()); | ||
for (let j = sizes.length - 1; j >= 0; j--) { | ||
View = ArrayViewMixin(View, itemLength); | ||
itemLength = View.getLength(sizes[j]); | ||
let currentArray = arrays.pop(); | ||
const isArray = currentArray.btype !== 'vector'; | ||
View = this.ObjectViewClass.getViewFromSchema(currentField); | ||
View = isArray ? ArrayViewMixin(View, currentField.maxLength) : VectorViewMixin(View); | ||
let itemLength = isArray ? View.getLength(currentArray.maxItems) : 0; | ||
for (let j = arrays.length - 1; j >= 0; j--) { | ||
currentArray = arrays[j]; | ||
if (currentArray.btype === 'vector') { | ||
View = VectorViewMixin(View); | ||
} else { | ||
View = ArrayViewMixin(View, itemLength); | ||
itemLength = View.getLength(currentArray.maxItems); | ||
} | ||
} | ||
@@ -284,3 +302,3 @@ length = itemLength; | ||
if (required && length === Infinity) | ||
throw new TypeError('The length of a required field is undefined.'); | ||
throw new TypeError(`The length of a required field "${name}" is undefined.`); | ||
const layout = { View, start, length, required }; | ||
@@ -290,31 +308,2 @@ if (Reflect.has(field, 'default')) layout.default = field.default; | ||
} | ||
/** | ||
* @private | ||
* @returns {void} | ||
*/ | ||
static setDefaultBuffer() { | ||
const { requiredFields, layout, optionalOffset } = this; | ||
const buffer = new ArrayBuffer(optionalOffset); | ||
const array = new Uint8Array(buffer); | ||
const view = new this(buffer); | ||
for (let i = 0; i < requiredFields.length; i++) { | ||
const name = requiredFields[i]; | ||
const field = layout[name]; | ||
if (Reflect.has(field, 'default')) { | ||
view.set(name, field.default); | ||
} else if (field.View.defaultBuffer) { | ||
array.set(new Uint8Array(field.View.defaultBuffer), field.start); | ||
} | ||
} | ||
this.defaultBuffer = array; | ||
} | ||
/** | ||
* @type {DataView} | ||
*/ | ||
static get bufferView() { | ||
if (!this.maxView) this.maxView = new DataView(new ArrayBuffer(this.maxLength)); | ||
return this.maxView; | ||
} | ||
} | ||
@@ -368,13 +357,2 @@ | ||
/** | ||
* @type {number} Maximum possible size of a map. | ||
*/ | ||
MapView.maxLength = 8192; | ||
/** | ||
* @protected | ||
* @type {DataView} | ||
*/ | ||
MapView.maxView = undefined; | ||
/** | ||
* Creates a MapView class with a given schema. | ||
@@ -381,0 +359,0 @@ * |
@@ -99,4 +99,4 @@ const StringView = require('./string-view'); | ||
*/ | ||
static from(object, view, start = 0, length = this.objectLength) { | ||
const objectView = view || new this(this.defaultBuffer.slice()); | ||
static from(object, view, start = 0, length = this.viewLength) { | ||
const objectView = view || new this(this.defaultBuffer.buffer.slice()); | ||
if (view) new Uint8Array(view.buffer, view.byteOffset + start, length).fill(0); | ||
@@ -134,7 +134,14 @@ const { fields, layout } = this; | ||
* @private | ||
* @returns {void} | ||
* @param {number} viewLength | ||
* @param {Array<string>} fields | ||
* @param {Object<string, ViewLayoutField>} layout | ||
* @returns {Uint8Array} | ||
*/ | ||
static setDefaultBuffer() { | ||
const { objectLength, fields, layout } = this; | ||
const buffer = new ArrayBuffer(objectLength); | ||
static getDefaultBuffer( | ||
viewLength = this.viewLength, | ||
fields = this.fields, | ||
layout = this.layout, | ||
) { | ||
const buffer = new ArrayBuffer(viewLength); | ||
const array = new Uint8Array(buffer); | ||
const view = new this(buffer); | ||
@@ -147,6 +154,6 @@ for (let i = 0; i < fields.length; i++) { | ||
} else if (field.View.defaultBuffer) { | ||
new Uint8Array(buffer).set(new Uint8Array(field.View.defaultBuffer), field.start); | ||
array.set(new Uint8Array(field.View.defaultBuffer), field.start); | ||
} | ||
} | ||
this.defaultBuffer = buffer; | ||
return array; | ||
} | ||
@@ -160,3 +167,3 @@ | ||
static getLength() { | ||
return this.objectLength; | ||
return this.viewLength; | ||
} | ||
@@ -178,5 +185,5 @@ | ||
const View = i === 0 ? this : class extends ParentViewClass {}; | ||
[View.layout, View.objectLength, View.fields] = this.getLayoutFromSchema(objectSchema); | ||
[View.layout, View.viewLength, View.fields] = this.getLayoutFromSchema(objectSchema); | ||
ObjectView.Views[id] = View; | ||
View.setDefaultBuffer(); | ||
View.defaultBuffer = View.getDefaultBuffer(); | ||
} | ||
@@ -341,4 +348,3 @@ } | ||
/** | ||
* @private | ||
* @type {Object<string, ViewLayoutField>} | ||
* @private {Object<string, ViewLayoutField>} | ||
*/ | ||
@@ -351,4 +357,3 @@ ObjectView.layout = undefined; | ||
/** | ||
* @private | ||
* @type {Array<string>} | ||
* @private {Array<string>} | ||
*/ | ||
@@ -358,10 +363,8 @@ ObjectView.fields = undefined; | ||
/** | ||
* @private | ||
* @type {number} | ||
* @private {number} | ||
*/ | ||
ObjectView.objectLength = 0; | ||
ObjectView.viewLength = 0; | ||
/** | ||
* @private | ||
* @type {ArrayBuffer} | ||
* @private {Uint8Array} | ||
*/ | ||
@@ -368,0 +371,0 @@ ObjectView.defaultBuffer = undefined; |
const ArrayView = require('./array-view'); | ||
const { UTF8ToString, stringToUTF8 } = require('./utilities'); | ||
@@ -290,3 +289,3 @@ /** | ||
toString() { | ||
return UTF8ToString(this); | ||
return this.constructor.decode(this); | ||
} | ||
@@ -298,3 +297,3 @@ | ||
toJSON() { | ||
return this.toString(); | ||
return this.constructor.decode(this); | ||
} | ||
@@ -319,2 +318,95 @@ | ||
/** | ||
* Converts a UTF8 byte array into a JS string. | ||
* Shamelessly stolen from Google Closure: | ||
* https://github.com/google/closure-library/blob/master/closure/goog/crypt/crypt.js | ||
* | ||
* @param {Uint8Array} bytes | ||
* @returns {string} | ||
*/ | ||
static decode(bytes) { | ||
const out = []; | ||
let pos = 0; | ||
let c = 0; | ||
while (pos < bytes.length) { | ||
const c1 = bytes[pos++]; | ||
// bail on zero byte | ||
if (c1 === 0) break; | ||
if (c1 < 128) { | ||
out[c++] = String.fromCharCode(c1); | ||
} else if (c1 > 191 && c1 < 224) { | ||
out[c++] = String.fromCharCode(((c1 & 31) << 6) | (bytes[pos++] & 63)); | ||
} else if (c1 > 239 && c1 < 365) { | ||
// Surrogate Pair | ||
const u = | ||
(((c1 & 7) << 18) | | ||
((bytes[pos++] & 63) << 12) | | ||
((bytes[pos++] & 63) << 6) | | ||
(bytes[pos++] & 63)) - | ||
0x10000; | ||
out[c++] = String.fromCharCode(0xd800 + (u >> 10)); | ||
out[c++] = String.fromCharCode(0xdc00 + (u & 1023)); | ||
} else { | ||
out[c++] = String.fromCharCode( | ||
((c1 & 15) << 12) | ((bytes[pos++] & 63) << 6) | (bytes[pos++] & 63), | ||
); | ||
} | ||
} | ||
return out.join(''); | ||
} | ||
/** | ||
* Converts a JS string into a UTF8 byte array. | ||
* Shamelessly stolen from Google Closure: | ||
* https://github.com/google/closure-library/blob/master/closure/goog/crypt/crypt.js | ||
* | ||
* TODO: use TextEncoder#encode/encodeInto when the following issues are resolved: | ||
* - https://bugs.chromium.org/p/v8/issues/detail?id=4383 | ||
* - https://bugs.webkit.org/show_bug.cgi?id=193274 | ||
* | ||
* @param {string} string | ||
* @param {Array|Uint8Array} view | ||
* @param {number} [start=0] | ||
* @param {number} [length] | ||
* @returns {number} | ||
*/ | ||
static encode(string, view, start = 0, length) { | ||
const bytes = | ||
view instanceof DataView | ||
? new Uint8Array(view.buffer, view.byteOffset + start, length || view.byteLength - start) | ||
: view; | ||
let p = 0; | ||
for (let i = 0; i < string.length; i++) { | ||
let c = string.charCodeAt(i); | ||
if (c < 128) { | ||
bytes[p++] = c; | ||
} else if (c < 2048) { | ||
bytes[p++] = (c >> 6) | 192; | ||
bytes[p++] = (c & 63) | 128; | ||
} else if ( | ||
(c & 0xfc00) === 0xd800 && | ||
i + 1 < string.length && | ||
(string.charCodeAt(i + 1) & 0xfc00) === 0xdc00 | ||
) { | ||
// Surrogate Pair | ||
c = 0x10000 + ((c & 0x03ff) << 10) + (string.charCodeAt(++i) & 0x03ff); | ||
bytes[p++] = (c >> 18) | 240; | ||
bytes[p++] = ((c >> 12) & 63) | 128; | ||
bytes[p++] = ((c >> 6) & 63) | 128; | ||
bytes[p++] = (c & 63) | 128; | ||
} else { | ||
bytes[p++] = (c >> 12) | 224; | ||
bytes[p++] = ((c >> 6) & 63) | 128; | ||
bytes[p++] = (c & 63) | 128; | ||
} | ||
} | ||
if (!length) return p; | ||
// zero out remaining bytes | ||
while (p < length) { | ||
bytes[p++] = 0; | ||
} | ||
return p; | ||
} | ||
/** | ||
* Creates a StringView from a string or an array like object. | ||
@@ -330,5 +422,8 @@ | ||
// no view is supplied | ||
if (!view) return new this(stringToUTF8(value)); | ||
const array = new Uint8Array(view.buffer, view.byteOffset + start, length || view.byteLength); | ||
stringToUTF8(value, array); | ||
if (!view) { | ||
const array = []; | ||
this.encode(value, array); | ||
return new this(array); | ||
} | ||
this.encode(value, view, start, length); | ||
return view; | ||
@@ -343,9 +438,19 @@ } | ||
* @param {length} [length] | ||
* @returns {Array<number>} | ||
* @returns {string} | ||
*/ | ||
static toJSON(view, start = 0, length) { | ||
return new this(view.buffer, view.byteOffset + start, length).toString(); | ||
return this.decode(new this(view.buffer, view.byteOffset + start, length)); | ||
} | ||
/* istanbul ignore next */ | ||
/** | ||
* @deprecated Use String.getLength instead. | ||
* @param {string} string | ||
* @returns {number} | ||
*/ | ||
static getByteSize(string) { | ||
return this.getLength(string); | ||
} | ||
/** | ||
* Returns the size in bytes of a given string without encoding it. | ||
@@ -359,3 +464,3 @@ * | ||
*/ | ||
static getByteSize(string) { | ||
static getLength(string) { | ||
let size = 0; | ||
@@ -387,2 +492,3 @@ for (let i = 0; i < string.length; i++) { | ||
/** | ||
* @deprecated | ||
* @type TextEncoder | ||
@@ -393,2 +499,3 @@ */ | ||
/** | ||
* @deprecated | ||
* @type TextDecoder | ||
@@ -395,0 +502,0 @@ */ |
@@ -43,3 +43,3 @@ const { typeGetters, typeSetters, typeOffsets } = require('./utilities'); | ||
static getLength() { | ||
return this.objectLength; | ||
return this.viewLength; | ||
} | ||
@@ -67,3 +67,3 @@ | ||
static of() { | ||
return new this(new ArrayBuffer(this.objectLength)); | ||
return new this(new ArrayBuffer(this.viewLength)); | ||
} | ||
@@ -100,3 +100,3 @@ | ||
*/ | ||
TypeView.objectLength = 1; | ||
TypeView.viewLength = 1; | ||
@@ -128,3 +128,3 @@ /** | ||
View.littleEndian = !!littleEndian; | ||
View.objectLength = 1 << View.offset; | ||
View.viewLength = 1 << View.offset; | ||
TypeViews.set(classId, View); | ||
@@ -131,0 +131,0 @@ return View; |
@@ -10,14 +10,2 @@ const ArrayView = require('./array-view'); | ||
/** | ||
* Returns a number at a given index. | ||
* | ||
* @param {number} index | ||
* @returns {number} | ||
*/ | ||
get(index) { | ||
const { View } = this.constructor; | ||
const offset = this.constructor.getLength(index); | ||
return View.toJSON(this, offset); | ||
} | ||
/** | ||
* Allows iterating over objects views stored in the array. | ||
@@ -35,9 +23,9 @@ * | ||
/** | ||
* Returns the byte length of an array view to hold a given amount of numbers. | ||
* Returns the starting byte offset of an item in the array. | ||
* | ||
* @param {number} size | ||
* @param {number} index | ||
* @returns {number} | ||
*/ | ||
static getLength(size) { | ||
return size << this.View.offset; | ||
static getOffset(index) { | ||
return index << this.View.offset; | ||
} | ||
@@ -44,0 +32,0 @@ |
@@ -125,96 +125,2 @@ const BigInt = globalThis.BigInt || Number; | ||
/** | ||
* Converts a JS string into a UTF8 byte array. | ||
* Shamelessly stolen from Google Closure: | ||
* https://github.com/google/closure-library/blob/master/closure/goog/crypt/crypt.js | ||
* | ||
* TODO: use TextEncoder#encode/encodeInto when the following issues are resolved: | ||
* - https://bugs.chromium.org/p/v8/issues/detail?id=4383 | ||
* - https://bugs.webkit.org/show_bug.cgi?id=193274 | ||
* | ||
* @param {string} string | ||
* @param {Array|Uint8Array} bytes | ||
* @param {number} start | ||
* @returns {number} | ||
*/ | ||
function writeUTF8(string, bytes, start = 0) { | ||
let p = start; | ||
for (let i = 0; i < string.length; i++) { | ||
let c = string.charCodeAt(i); | ||
if (c < 128) { | ||
bytes[p++] = c; | ||
} else if (c < 2048) { | ||
bytes[p++] = (c >> 6) | 192; | ||
bytes[p++] = (c & 63) | 128; | ||
} else if ( | ||
(c & 0xfc00) === 0xd800 && | ||
i + 1 < string.length && | ||
(string.charCodeAt(i + 1) & 0xfc00) === 0xdc00 | ||
) { | ||
// Surrogate Pair | ||
c = 0x10000 + ((c & 0x03ff) << 10) + (string.charCodeAt(++i) & 0x03ff); | ||
bytes[p++] = (c >> 18) | 240; | ||
bytes[p++] = ((c >> 12) & 63) | 128; | ||
bytes[p++] = ((c >> 6) & 63) | 128; | ||
bytes[p++] = (c & 63) | 128; | ||
} else { | ||
bytes[p++] = (c >> 12) | 224; | ||
bytes[p++] = ((c >> 6) & 63) | 128; | ||
bytes[p++] = (c & 63) | 128; | ||
} | ||
} | ||
return p; | ||
} | ||
/** | ||
* @param {string} string | ||
* @param {Array|Uint8Array} bytes | ||
* @returns {Array|Uint8Array} | ||
*/ | ||
function stringToUTF8(string, bytes = []) { | ||
let length = writeUTF8(string, bytes); | ||
// zero out remaining bytes | ||
while (length < bytes.length) { | ||
bytes[length++] = 0; | ||
} | ||
return bytes; | ||
} | ||
/** | ||
* Converts a UTF8 byte array into a JS string. | ||
* | ||
* @param {Uint8Array} bytes | ||
* @returns {string} | ||
*/ | ||
function UTF8ToString(bytes) { | ||
const out = []; | ||
let pos = 0; | ||
let c = 0; | ||
while (pos < bytes.length) { | ||
const c1 = bytes[pos++]; | ||
// bail on zero byte | ||
if (c1 === 0) break; | ||
if (c1 < 128) { | ||
out[c++] = String.fromCharCode(c1); | ||
} else if (c1 > 191 && c1 < 224) { | ||
out[c++] = String.fromCharCode(((c1 & 31) << 6) | (bytes[pos++] & 63)); | ||
} else if (c1 > 239 && c1 < 365) { | ||
// Surrogate Pair | ||
const u = | ||
(((c1 & 7) << 18) | | ||
((bytes[pos++] & 63) << 12) | | ||
((bytes[pos++] & 63) << 6) | | ||
(bytes[pos++] & 63)) - | ||
0x10000; | ||
out[c++] = String.fromCharCode(0xd800 + (u >> 10)); | ||
out[c++] = String.fromCharCode(0xdc00 + (u & 1023)); | ||
} else { | ||
out[c++] = String.fromCharCode( | ||
((c1 & 15) << 12) | ((bytes[pos++] & 63) << 6) | (bytes[pos++] & 63), | ||
); | ||
} | ||
} | ||
return out.join(''); | ||
} | ||
module.exports = { | ||
@@ -228,5 +134,2 @@ log2, | ||
typeOffsets, | ||
stringToUTF8, | ||
writeUTF8, | ||
UTF8ToString, | ||
}; |
{ | ||
"name": "structurae", | ||
"version": "3.2.0", | ||
"version": "3.3.0", | ||
"description": "Data structures for performance-sensitive modern JavaScript applications.", | ||
@@ -5,0 +5,0 @@ "main": "index.js", |
@@ -12,2 +12,3 @@ # Structurae | ||
- [MapView](https://github.com/zandaqo/structurae#MapView) - ObjectView with optional fields and fields of varying sizes. | ||
- [VectorView](https://github.com/zandaqo/structurae#VectorView) - ArrayView that supports optional and variable length elements, including MapViews. | ||
- [StringView](https://github.com/zandaqo/structurae#StringView) - extends Uint8Array to handle C-like representation of UTF-8 encoded strings. | ||
@@ -319,2 +320,9 @@ - [BinaryProtocol](https://github.com/zandaqo/structurae#BinaryProtocol) - a helper class that simplifies defining and operating on multiple tagged ObjectViews. | ||
}, | ||
names: { | ||
type: 'array', | ||
// uses VectorView for an array of variable length elements | ||
// if btype is set to vector | ||
btype: 'vector', | ||
items: { type: 'string' }, | ||
}, | ||
}, | ||
@@ -337,3 +345,3 @@ // required fields are always present and can have default values | ||
// create a person with no pets | ||
const person0 = PersonWithPets.from({ id: 1, name: 'Artur'}); | ||
const person0 = Person.from({ id: 1, name: 'Artur'}); | ||
person0.byteLength; | ||
@@ -346,7 +354,43 @@ //=> 18 | ||
//=> undefined | ||
const person2 = Person.from({ names: ['Arthur', 'Dent', '', 'Arthur Dent']}) | ||
person2.toJSON(); | ||
//=> { id: 10, names: ['Arthur', 'Dent', undefined, 'Arthur Dent']} | ||
``` | ||
For performance reasons, MapView uses a single buffer for serialization, thus, limiting the maximum size of a view. | ||
By default the size is 8192 bytes, if you expect bigger views, please set the desired size in `MapView.maxLength`. | ||
The buffer is inherited from `VariableView` class and the default is 8192 bytes, if you expect bigger views, please set the desired size in `VariableView.maxLength`. | ||
#### VectorView | ||
VectorView is an ArrayView that supports optional elements (i.e. `undefined`) and elements of variable length, such as MapView or StringView. | ||
VectorView stores offsets inside the view itself resulting in an overhead of 4 * (_n_ + 2) bytes where _n_ is the number of elements in the view. | ||
Like MapView, VectorView has limited editablity: the layout of an instance is calculated once upon creation, | ||
hence, setting absent elements or resizing existing elements is not possible. | ||
```javascript | ||
const { MapViewMixin, VectorViewMixin, TypeViewMixin } = require('structurae'); | ||
const SparseArrayView = VectorViewMixin(TypeViewMixin('uint8')); | ||
SparseArrayView.from([1, , 2, null]).toJSON(); | ||
//=> [1, undefined, 2, undefined] | ||
const MapVector = VectorViewMixin(MapViewMixin({ | ||
$id: 'SomeMap', | ||
btype: 'map', | ||
properties: { | ||
id: { type: 'integer' }, | ||
name: { type: 'string' }, | ||
}, | ||
})); | ||
const mapVector = MapVector.from([{ id: 1 }, null, { name: 'abc'}]); | ||
mapVector.size; | ||
//=> 3 | ||
mapVector.get(0); | ||
//=> { id: 1 }; | ||
mapVector.toJSON(); | ||
//=> [{ id: 1 }, undefined, { name: 'abc'}] | ||
``` | ||
Like MapView, VectorView uses for serialization the default buffer inherited from `VariableView`, if you expect your | ||
vectors to exceed the default 8192 bytes in length, please set the desired maximum length in `VariableView.maxLength`. | ||
#### StringView | ||
@@ -353,0 +397,0 @@ Encoding API (available both in modern browsers and Node.js) allows us to convert JavaScript strings to |
234648
35
6439
1122