Skip to content

Commit

Permalink
v1.6.7 (#7)
Browse files Browse the repository at this point in the history
  • Loading branch information
reececomo authored Aug 2, 2024
1 parent c734dda commit bf5a74e
Show file tree
Hide file tree
Showing 11 changed files with 450 additions and 400 deletions.
366 changes: 200 additions & 166 deletions README.md

Large diffs are not rendered by default.

4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
{
"name": "tinybuf",
"version": "1.6.6",
"version": "1.6.7",
"author": "Reece Como <[email protected]>",
"authors": [
"Reece Como <[email protected]>",
"Sitegui <[email protected]>"
],
"license": "MIT",
"description": "Fast, lightweight binary encoders for JavaScript",
"description": "Fast, tiny binary serialization for Node.js and HTML5",
"main": "./dist/index.js",
"repository": {
"type": "git",
Expand Down
174 changes: 107 additions & 67 deletions src/core/BinaryCoder.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,6 @@
import * as coders from './lib/coders';
import { Field } from './Field';
import {
djb2HashUInt16,
hashCodeTo2CharStr,
strToHashCode
} from './lib/hashCode';
import { djb2HashUInt16, strToHashCode } from './lib/hashCode';
import { peekHeader, peekHeaderStr } from './lib/peek';
import { MutableArrayBuffer } from './MutableArrayBuffer';
import { ReadState } from './ReadState';
import {
Expand All @@ -16,52 +12,58 @@ import {
InferredTransformConfig,
InferredValidationConfig,
ValidationFn,
Transforms
Transforms,
FieldDefinition
} from './Type';

export type BinaryCoderHeader = string | number;

/**
* Infer the decoded type of a BinaryCoder.
*
* @example
* let onData = (data: Infer<typeof MyBinaryCoder>) => {...};
* Decoded type of a binary encoding.
* @example let onData = (data: Decoded<typeof MyBinaryCoder>) => {...};
*/
export type Decoded<FromBinaryCoder> = FromBinaryCoder extends BinaryCoder<infer EncoderType, any> ? InferredDecodedType<EncoderType> : never;

/** @deprecated use Decoded<T> */
export type Infer<T> = Decoded<T>;

/**
* BinaryCoder is a utility class for encoding and decoding binary data based
* on a provided encoding format.
*
* @see {Id}
* @see {header}
* @see {encode(data)}
* @see {decode(binary)}
*/
export class BinaryCoder<EncoderType extends EncoderDefinition, IdType extends string | number = number> {
export class BinaryCoder<EncoderType extends EncoderDefinition, HeaderType extends BinaryCoderHeader = number> {
/**
* A unique identifier encoded as the first 2 bytes (or `undefined` if headerless).
*
* @see {peekHeader(...)}
* @see {peekHeaderStr(...)}
* @see {BinaryCoder.hashCode}
*/
public readonly header?: HeaderType;

protected readonly type: Type;
protected readonly fields: Field[];

protected _hash?: number;
protected _format?: string;
protected _id?: IdType;

protected _transforms?: Transforms<any> | undefined;
protected _validationFn?: ValidationFn<any> | undefined;

/**
* @param encoderDefinition A defined encoding format.
* @param Id Defaults to hash code. Set `null` to disable. Must be a 16-bit unsigned integer.
*/
public constructor(
encoderDefinition: EncoderType,
Id?: IdType | null
header?: HeaderType | null
) {
if (
(typeof Id === 'number' && (Math.floor(Id) !== Id || Id < 0 || Id > 65_535))
|| (typeof Id === 'string' && new TextEncoder().encode(Id).byteLength !== 2)
|| (Id !== undefined && Id !== null && !['string', 'number'].includes(typeof Id))
(typeof header === 'number' && (Math.floor(header) !== header || header < 0 || header > 65_535))
|| (typeof header === 'string' && new TextEncoder().encode(header).byteLength !== 2)
|| (header !== undefined && header !== null && !['string', 'number'].includes(typeof header))
) {
throw new TypeError(`Id must be an unsigned 16-bit integer, a 2-byte string, or \`null\`. Received: ${Id}`);
throw new TypeError(`header must be an unsigned 16-bit integer, a 2-byte string, or \`null\`. Received: ${header}`);
}
else if (encoderDefinition instanceof OptionalType) {
throw new TypeError("Invalid type given. Root object must not be an Optional.");
Expand All @@ -79,60 +81,51 @@ export class BinaryCoder<EncoderType extends EncoderDefinition, IdType extends s
throw new TypeError("Invalid type given. Must be an object, or a known coder type.");
}

if (Id === null) {
this._id = undefined;
if (header === null) {
// explicitly disabled
this.header = undefined;
}
else if (Id === undefined && this.type === Type.Object) {
this._id = this.hashCode as IdType;
else if (header === undefined && this.type === Type.Object) {
// automatic
this.header = this.hashCode as HeaderType;
}
else {
this._id = Id;
// explicitly set
this.header = header;
}
}

// ----- Static methods: -----

/**
* Read the first two bytes of a buffer as an unsigned 16-bit integer.
*
* When passed an ArrayBufferView, accesses the underlying 'buffer' instance directly.
* Read the header of a buffer as a number.
*
* @see {BinaryCoder.Id}
* @see {BinaryCoder.header}
* @throws {RangeError} if buffer size < 2
*/
public static peekIntId(buffer: ArrayBuffer | ArrayBufferView): number {
const dataView = new DataView(buffer instanceof ArrayBuffer ? buffer : buffer.buffer);
return dataView.getUint16(0);
}
public static peekHeader = peekHeader;

/**
* Read the first two bytes of a buffer as a 2-character string.
*
* When passed an ArrayBufferView, accesses the underlying 'buffer' instance directly.
* Read the header of a buffer as a string.
*
* @see {BinaryCoder.Id}
* @see {BinaryCoder.header}
* @throws {RangeError} if buffer size < 2
*/
public static peekStrId(buffer: ArrayBuffer | ArrayBufferView): string {
return hashCodeTo2CharStr(this.peekIntId(buffer));
}
public static peekHeaderStr = peekHeaderStr;

/** @deprecated Use peekHeader */
public static peekIntId = peekHeader;
/** @deprecated Use peekHeader */
public static peekStrId = peekHeaderStr;

// ----- Public accessors: -----

/**
* A unique identifier as an unsigned 16-bit integer. Encoded as the first 2 bytes.
*
* @see {BinaryCoder.peekIntId(...)}
* @see {BinaryCoder.peekStrId(...)}
* @see {BinaryCoder.hashCode}
*/
public get Id(): IdType | undefined {
return this._id;
/** @deprecated use .header */
public get Id(): HeaderType | undefined {
return this.header;
}

/**
* @returns A hash code representing the encoding format. An unsigned 16-bit integer.
*/
/** A uint16 number representing the shape of the encoded format */
public get hashCode(): number {
if (this._hash === undefined) {
this._hash = djb2HashUInt16(this.format);
Expand Down Expand Up @@ -166,7 +159,7 @@ export class BinaryCoder<EncoderType extends EncoderDefinition, IdType extends s
const safeValue = this._preEncode(value);
const buffer = new MutableArrayBuffer();

this.writeId(buffer);
this.writeHeader(buffer);
this.write(safeValue, buffer, '');

return buffer.toArrayBuffer();
Expand All @@ -180,7 +173,7 @@ export class BinaryCoder<EncoderType extends EncoderDefinition, IdType extends s
public decode<DecodedType = InferredDecodedType<EncoderType>>(arrayBuffer: ArrayBuffer | ArrayBufferView): DecodedType {
return this.read(new ReadState(
arrayBuffer instanceof ArrayBuffer ? arrayBuffer : arrayBuffer.buffer,
this.Id === undefined ? 0 : 2
this.header === undefined ? 0 : 2
));
}

Expand Down Expand Up @@ -279,14 +272,14 @@ export class BinaryCoder<EncoderType extends EncoderDefinition, IdType extends s
}

/**
* Writes @see {Id} as the prefix of the buffer.
* Writes @see {header} as the prefix of the buffer.
*/
protected writeId(mutableArrayBuffer: MutableArrayBuffer): void {
if (this.Id === undefined) {
protected writeHeader(mutableArrayBuffer: MutableArrayBuffer): void {
if (this.header === undefined) {
return;
}

const idInt16 = typeof this.Id === 'string' ? strToHashCode(this.Id) : this.Id as number;
const idInt16 = typeof this.header === 'string' ? strToHashCode(this.header) : this.header as number;
coders.uint16Coder.write(idInt16, mutableArrayBuffer, '');
}

Expand All @@ -296,11 +289,11 @@ export class BinaryCoder<EncoderType extends EncoderDefinition, IdType extends s
protected getCoder(type: Type): coders.BinaryTypeCoder<any> {
switch (type) {
case Type.Binary: return coders.arrayBufferCoder;
case Type.Bitmask16: return coders.bitmask16Coder;
case Type.Bitmask32: return coders.bitmask32Coder;
case Type.Bitmask8: return coders.bitmask8Coder;
case Type.Boolean: return coders.booleanCoder;
case Type.BooleanTuple: return coders.booleanArrayCoder;
case Type.Bool: return coders.booleanCoder;
case Type.Bools: return coders.booleanArrayCoder;
case Type.Bools8: return coders.bitmask8Coder;
case Type.Bools16: return coders.bitmask16Coder;
case Type.Bools32: return coders.bitmask32Coder;
case Type.Date: return coders.dateCoder;
case Type.Float16: return coders.float16Coder;
case Type.Float32: return coders.float32Coder;
Expand Down Expand Up @@ -454,4 +447,51 @@ export class BinaryCoder<EncoderType extends EncoderDefinition, IdType extends s
}
}

export default BinaryCoder;
/**
* Parses and represents an object field.
*/
export class Field {
public readonly name: string;
public readonly coder: BinaryCoder<any>;
public readonly isOptional: boolean;
public readonly isArray: boolean;

protected _format?: string;

public constructor(name: string, rawType: FieldDefinition) {
this.isOptional = rawType instanceof OptionalType;

let type = rawType instanceof OptionalType ? rawType.type : rawType;

this.name = name;

if (Array.isArray(type)) {
if (type.length !== 1) {
throw new TypeError('Invalid array definition, it must have exactly one element');
}

type = type[0];
this.isArray = true;
}
else {
this.isArray = false;
}

this.coder = new BinaryCoder<any>(type);
}

/**
* @returns A string identifying the encoding format.
* @example "{str,uint16,bool}[]?"
*/
public get format(): string {
if (this._format === undefined) {
this._format = `${(this.coder as any).format}${this.isArray ? '[]' : ''}${this.isOptional ? '?' : ''}`;
}

return this._format;
}
}


export default BinaryCoder;
31 changes: 15 additions & 16 deletions src/core/BinaryFormatHandler.ts
Original file line number Diff line number Diff line change
@@ -1,20 +1,21 @@
import BinaryCoder from "./BinaryCoder";
import { EncoderDefinition, InferredDecodedType } from "./Type";
import { hashCodeTo2CharStr, strToHashCode } from "./lib/hashCode";
import { hashCodeToStr, strToHashCode } from "./lib/hashCode";
import { peekHeader } from "./lib/peek";

type BinaryCoderId = number;
type BCHeader = number;
type BinaryCoderOnDataHandler = (data: InferredDecodedType<any>) => any;

export class UnhandledBinaryDecodeError extends Error {}
export class BinaryCoderIdCollisionError extends Error {}
export class FormatHeaderCollisionError extends Error {}

/**
* A utility that facilitates the management and handling of multiple binary formats.
*
* It provides a central handler for encoding, decoding and routing.
*/
export class BinaryFormatHandler {
private coders = new Map<BinaryCoderId, [BinaryCoder<any, any>, BinaryCoderOnDataHandler]>();
private coders = new Map<BCHeader, [BinaryCoder<any, any>, BinaryCoderOnDataHandler]>();

/** All available coders. */
public get available(): Set<BinaryCoder<any, any>> {
Expand All @@ -28,17 +29,17 @@ export class BinaryFormatHandler {
coder: BinaryCoder<EncoderType, string | number>,
onDataHandler: (data: DecodedType) => any
): this {
if (coder.Id === undefined) {
throw new TypeError('Cannot register a BinaryCoder with Id disabled.');
if (coder.header === undefined) {
throw new TypeError('Cannot register a headerless encoding format.');
}

const intId = typeof coder.Id === 'string' ? strToHashCode(coder.Id) : coder.Id;
const intHeader = typeof coder.header === 'string' ? strToHashCode(coder.header) : coder.header;

if (this.coders.has(intId)) {
throw new BinaryCoderIdCollisionError(`Coder was already registered with matching Id: ${coder.Id}`);
if (this.coders.has(intHeader)) {
throw new FormatHeaderCollisionError(`Format with identical header was already registered: ${coder.header}`);
}

this.coders.set(intId, [coder, onDataHandler]);
this.coders.set(intHeader, [coder, onDataHandler]);

return this;
}
Expand All @@ -52,15 +53,13 @@ export class BinaryFormatHandler {
* @throws {RangeError} If buffer has < 2 bytes.
*/
public processBuffer(buffer: ArrayBuffer | ArrayBufferView): void {
const id: number = BinaryCoder.peekIntId(buffer);
const tuple = this.coders.get(id);
const header: number = peekHeader(buffer);

if (!tuple) {
const strId = hashCodeTo2CharStr(id);
throw new UnhandledBinaryDecodeError(`Failed to process buffer with Id ${id} ('${strId}').`);
if (!this.coders.has(header)) {
throw new UnhandledBinaryDecodeError(`Failed to process buffer. Header: ${header} ('${hashCodeToStr(header)}').`);
}

const [coder, onDataHandler] = tuple;
const [coder, onDataHandler] = this.coders.get(header);
const data = coder.decode(buffer);

onDataHandler(data);
Expand Down
Loading

0 comments on commit bf5a74e

Please sign in to comment.