1.6.4 β€’ Published 1 year ago

typescript-binary v1.6.4

Weekly downloads
-
License
MIT
Repository
github
Last release
1 year ago

TypeScript Binary

Powerful, lightweight binary messages in TypeScript.

NPM version test test

πŸ“£ When to use?

TypeScript Binary is an optimal choice for real-time HTML5 and Node.js applications and games.

TypeScriptΒ BinaryFlatBuffersProtocolΒ BuffersRawΒ JSON
Serialization formatBinaryBinaryBinaryString
Schema definitionNative.fbs files.proto filesNative
TypeScript TypesNativeCode generationCode generationNative
External tooling dependenciesNonecmake and flatcNone*N/A
Reference data size†34 bytes68 bytes72 bytes175Β bytesΒ (minified)
Fast & efficientπŸŸ’πŸŸ’πŸŸ’πŸ”΄
16-bit floatsπŸŸ’πŸ”΄πŸ”΄πŸ”΄
Boolean-packingπŸŸ’πŸ”΄πŸ”΄πŸ”΄
Arbitrary JSONπŸŸ’πŸ”΄πŸ”΄πŸŸ’
Property manglingπŸŸ’πŸ”΄πŸ”΄πŸ”΄
Suitable for real-time dataπŸŸ’πŸŸ’πŸ”΄πŸ”΄
Suitable for web APIsπŸ”΄πŸ”΄πŸŸ’πŸŸ’
Supports HTML5 / Node.js🟒🟒🟒🟒
Cross-language (Java, C++, Python, etc.)πŸ”΄πŸŸ’πŸŸ’πŸŸ’

†Based on the Reference data formats and schemas

*When using protobufjs

Sample data (Minified JSON):

{
  "players": [
    {
      "id": 123,
      "position": {
        "x": 1.0,
        "y": 2.0,
        "z": 3.0
      },
      "velocity": {
        "x": 1.0,
        "y": 2.0,
        "z": 3.0
      },
      "health": 1.00
    },
    {
      "id": 456,
      "position": {
        "x": 1.0,
        "y": 2.0,
        "z": 3.0
      },
      "velocity": {
        "x": 1.0,
        "y": 2.0,
        "y": 3.0
      },
      "health": 0.50
    }
  ]
}

TypeScript Binary

const ExampleMessage = new BinaryCoder({
  players: [
    {
      id: Type.UInt,
      position: {
        x: Type.Float16,
        y: Type.Float16,
        z: Type.Float16
      },
      velocity: {
        x: Type.Float16,
        y: Type.Float16,
        y: Type.Float16
      },
      health: Type.UScalar
    },
  ],
});

FlatBuffers

// ExampleMessage.fbs

namespace ExampleNamespace;

table Vec3 {
  x: float;
  y: float;
  z: float;
}

table Player {
  id: uint;
  position: Vec3;
  velocity: Vec3;
  health: float;
}

table ExampleMessage {
  players: [Player];
}

root_type ExampleMessage;

Protocol Buffers (Proto3)

syntax = "proto3";

package example;

message Vec3 {
  float x = 1;
  float y = 2;
  float z = 3;
}

message Player {
  uint32 id = 1;
  Vec3 position = 2;
  Vec3 velocity = 3;
  float health = 4;
}

message ExampleMessage {
  repeated Player players = 1;
}

Install

npm install typescript-binary

yarn add typescript-binary

Usage

Define formats

Create a BinaryCoder like so:

import { BinaryCoder, Type } from "typescript-binary";

// Define your format:
const GameWorldData = new BinaryCoder({
  time: Type.UInt,
  players: [{
    id: Type.UInt,
    isJumping: Type.Boolean,
    position: {
      x: Type.Float,
      y: Type.Float
    }
  }]
});

// Encode:
const bytes = GameWorldData.encode({
  time: 123,
  players: [
    {
       id: 44,
       isJumping: true,  
       position: {
         x: 110.57345,
         y: -93.5366
       }
    }
  ]
});

bytes.byteLength
// 14

// Decode:
const data = GameWorldData.decode(bytes);

Inferred types

BinaryCoder will automatically infer the types for encode() and decode() from the schema provided (see the Types section below).

For example, the type T for GameWorldData.decode(...): T would be inferred as:

{
  timeRemaining: number,
  players: {
    id: string,
    health: number,
    isJumping: boolean,
    position?: {
      x: number,
      y: number
    }
  }[]
}

You can also use the Infer<T> helper type to use inferred types in any custom method/handler:

import { Infer } from "typescript-binary";

function updateGameWorld(data: Infer<typeof GameWorldData>) {
  // e.g. Access `data.players[0].position?.x`
}

✨ Parsing formats

By default, each BinaryCoder encodes a 2-byte identifier based on the shape of the data.

You can explicitly set Id in the BinaryCoder constructor to any 2-byte string or unsigned integer (or disable entirely by passing false).

Use BinaryFormatHandler

Handle multiple binary formats at once using a BinaryFormatHandler:

import { BinaryFormatHandler } from "typescript-binary";

const binaryHandler = new BinaryFormatHandler()
  .on(MyFormatA, (data) => handleMyFormatA(data))
  .on(MyFormatB, (data) => handleMyFormatB(data));

// Trigger handler (or throw UnhandledBinaryDecodeError)
binaryHandler.processBuffer(binary);

Note: Cannot be used with formats where Id is disabled.

Manual handling

You can manually read message identifers from incoming buffers with the static function BinaryCoder.peekIntId(...) (or BinaryCoder.peekStrId(...)):

if (BinaryCoder.peekStrId(incomingBinary) === MyMessageFormat.Id) {
  // Do something special.
}

πŸ’₯ Id Collisions

By default Id is based on a hash code of the encoding format. So the following two messages would have identical Ids:

const Person = new BinaryCoder({
  firstName: Type.String,
  lastName: Type.String
});

const FavoriteColor = new BinaryCoder({
  fullName: Type.String,
  color: Type.String
});

NameCoder.Id === ColorCoder.Id
  // true

If two identical formats with different handlers is a requirement, you can explicitly set unique identifiers.

const Person = new BinaryCoder({
  firstName: Type.String,
  lastName: Type.String
}, "PE");

const FavoriteColor = new BinaryCoder({
  fullName: Type.String,
  color: Type.String
}, "FC");

✨ Validation / Transforms

Validation

The great thing about binary encoders is that data is implicitly type-validated, however, you can also add custom validation rules using setValidation():

const UserMessage = new BinaryCoder({
  uuid: Type.String,
  // ...
})
.setValidation({
  uuid: (x) => {
    if (!isValidUUIDv4(x)) {
      throw new Error('Invalid UUIDv4: ' + x);
    }
  }
});

Transforms

You can also apply additional encode/decode transforms.

Here is an example where we're stripping out all whitespace:

const PositionMessage = new BinaryCoder({ name: Type.String })
  .setTransforms({ name: a => a.replace(/\s+/g, '') });

let binary = PositionMessage.encode({ name: 'Hello  There' })
let data = PositionMessage.decode(binary);

data.name
  // "HelloThere"

Unlike validation, transforms are applied asymmetrically.

The transform function is only applied on encode(), but you can provide two transform functions.

Here is an example which cuts the number of bytes required from 10 to 5:

const PercentMessage = new BinaryCoder({ value: Type.String }, false)
  .setTransforms({
    value: [
      (before) => before.replace(/\$|USD/g, '').trim(),
      (after) => '$' + after + ' USD'
    ]
  });

let binary = PercentMessage.encode({ value: ' $45.53 USD' })
let data = PercentMessage.decode(binary);

binary.byteLength
  // 5

data.value
  // "$45.53 USD"

Types

Here are all the ready-to-use types:

TypeJavaScript TypeBytesAbout
Type.Intnumber1-8*Integer between -Number.MAX_SAFE_INTEGER and Number.MAX_SAFE_INTEGER.
Type.Int8number1Integer between -127 to 128.
Type.Int16number2Integer between -32,767 to 32,767.
Type.Int32number4Integer between -2,147,483,647 to 2,147,483,647.
Type.UIntnumber1-8#Unsigned integer between 0 and Number.MAX_SAFE_INTEGER.
Type.UInt8number1Unsigned integer between 0 and 255.
Type.UInt16number2Unsigned integer between 0 and 65,535.
Type.UInt32number4Unsigned integer between 0 and 4,294,967,295.
Type.Scalarnumber1Signed scalar between -1.0 and 1.0.
Type.UScalarnumber1Unsigned scalar between 0.0 and 1.0.
Type.Float16number2A 16-bit "half-precision" floating point.Important Note: Low decimal precision. Max. large values Β±65,500.
Type.Float32number4A 32-bit "single-precision" floating point.
Type.Float64number8Default JavaScript number type. A 64-bit "double-precision" floating point.
Type.Stringstring1† +Β nA UTF-8 string.
Type.Booleanboolean1A single boolean.
Type.BooleanTupleboolean[]1ΒΆVariable-length array/tuple of boolean values packed into 1ΒΆ byte.
Type.Bitmask8boolean[]18 booleans.
Type.Bitmask16boolean[]216 booleans.
Type.Bitmask32boolean[]432 booleans.
Type.JSONany1† +Β nJSON format data, encoded as a UTF-8 string.
Type.BinaryArrayBuffer1† +Β nJavaScript ArrayBuffer data.
Type.RegExpRegExp1† +Β nΒ +Β 1JavaScript RegExp object.
Type.DateDate8JavaScript Date object.
Optional(T)T \| undefined1Any optional field. Use the Optional(...) helper. Array elements cannot be optional.
[T]Array<T>1† +Β nUse array syntax. Any array.
{}objectnoneUse object syntax. No overhead to using object types. Buffers are ordered, flattened structures.

*Int is a variable-length integer ("varint") which encodes <Β±64 = 1 byte, <Β±8,192 = 2 bytes, <Β±268,435,456 = 4 bytes, otherwise = 8 bytes.

#UInt is a variable-length unsigned integer ("varuint") which encodes <128 = 1 byte, <16,384 = 2 bytes, <536,870,912 = 4 bytes, otherwise = 8 bytes.

†Length of payload bytes as a UInt. Typically 1 byte, but could be 2-8 bytes for very large payloads.

ΒΆ2-bit overhead: 6 booleans per byte (i.e. 9 booleans would require 2 bytes).

Encoding guide

See docs/ENCODING.md for an overview on how most formats are encoded (including the dynamically sized integer types).

1.6.4

1 year ago

1.6.3

1 year ago

1.6.2

1 year ago

1.6.1

1 year ago

1.6.0

1 year ago

1.5.3

1 year ago

1.5.2

1 year ago

1.5.1

1 year ago

1.4.2

1 year ago

1.5.0

1 year ago

1.4.1

1 year ago

1.4.0

1 year ago

1.3.0

1 year ago

1.2.0

1 year ago

1.1.2

1 year ago

1.1.1

1 year ago

1.1.0

1 year ago

1.0.3

1 year ago

1.0.2

1 year ago

1.0.1

1 year ago

1.0.0

1 year ago