Skip to content

Repository files navigation

Safe Schema

A dependency-less solution to safely serialize your TypeScript models to and from ArrayBuffers.

Features

  • Uses your TypeScript object definitions as a starting point
  • Type Safe throughout
  • Broad model support
  • Lightning Fast due to AOT
  • First class support for Type Lookups and Enums
  • Supports custom serializations
  • Well tested
  • No external dependencies

Install

With npm:

$ npm install safe-schema --save

With yarn:

$ yarn add safe-schema

Basic Usage

// Importimport{makeSchema,generateSchema}from'safe-schema';// Define your modeltypeSimpleMessage={count: number};// Safely define your schema BASED on your modelconstsimpleMessageSchema=makeSchema<SimpleMessage>({count: 'uint8'});// ...// Initialize the AOT code generator (only once)constgenerator=generateSchema(simpleMessageSchema);// Turn your object into an ArrayBufferconstbuffer=generator.toBuffer({count: 12});assert(buffer.byteLength===1);// ...// Turn your ArrayBuffer back into your objectconstresult=generator.fromBuffer(buffer);// Use your 100% type safe object on the other side of the wireassert(result.count===12);

How It Works

You define your network schema just as you normally would using TypeScript types, then use SafeSchema to generate a runtime version of that schema. You will get full intellisense support when invoking makeSchema<SimpleMessage>({}), allowing you to easily define the DataTypes of your model (for instance number as uint16), as well as the ability to easily change your schema and have TypeScript throw the appropriate errors for missing values at compile time.

Calling generateSchema(simpleMessageSchema) generates JavaScript at runtime that is hand built to read and write your model to and from an ArrayBuffer. There is no switch case behind the scenes, every model generates unique JavaScript which executes lightning fast! Only exactly as many bytes will be sent over the wire as needed.

Take a look at the complexGameState, kitchenSink, and the other tests for complex and real-world examples.

Why It's Needed

Every other solution to this problem (protocol buffers, JSON, etc), did not allow me to define my models the way I wanted to, using TypeScript. TypeScript's modeling abilities are incredibly feature rich and I did not want to lose any of that functionality just because I needed to serialize my data. That's why I built in first class support for things like Discriminating Unions and Enums, so I can have a richly defined schema with the minimum amount of bytes used.

API Documentation

numbers

When you define your model to be a number in TypeScript you must tell the Schema what type and how big the number is. This is to save on memory over the wire and to not make assumptions.

Example:

typeSimpleMessage={count: number};constsimpleMessageSchema=makeSchema<SimpleMessage>({count: 'int32'});

TypeScript intellisense will only allow values that are valid. The valid values are:

  • uint8
  • uint16
  • uint32
  • int8
  • int16
  • int32
  • float32
  • float64

SafeSchema encodes strings into utf16 values, plus one uint16 for its length. It does not currently support strings over 65535 in length.

Example:

typeSimpleMessage={count: string};constsimpleMessageSchema=makeSchema<SimpleMessage>({count: 'string'});

SafeSchema encodes booleans into a single uint8 value

Example:

typeSimpleMessage={count: boolean};constsimpleMessageSchema=makeSchema<SimpleMessage>({count: 'boolean'});

SafeSchema allows any value to be optionally defined. This will send an extra byte over the wire to denote if the value is there or not. The type must be defined as optional in your model

Example:

typeSimpleMessage={count?: number};constsimpleMessageSchema=makeSchema<SimpleMessage>({count: {flag: 'optional',element: 'uint8',},});

SafeSchema can encode any type as an array. You must specify the max length of the array, either array-uint8, array-uint16, or array-uint32

Example:

typeSimpleMessage={count: boolean[]};constsimpleMessageSchema=makeSchema<SimpleMessage>({count: {flag: 'array-uint8',elements: 'boolean',},});
typeComplexMessage={count: {shoes: number}[]};constComplexMessageSchema=makeSchema<ComplexMessage>({count: {flag: 'array-uint8',elements: {shoes: 'float64'},},});

SafeSchema has first class support for type lookups. This is useful for Discriminating Unions in TypeScript.

Example:

typeSimpleMessage={type: 'run';duration: number}|{type: 'walk';speed: number};constsimpleMessageSchema=makeSchema<SimpleMessage>({flag: 'type-lookup',elements: {run: {duration: 'uint8'},walk: {speed: 'float32'},},});

SafeSchema has first class support TypeScript enums through string unions. It will only send a single byte over the wire.

Example:

typeSimpleMessage={weapon: 'sword'|'laser'|'shoe'};constsimpleMessageSchema=makeSchema<SimpleMessage>({flag: 'enum',sword: '0',laser: '1',shoe: '2',});

SafeSchema has first class support TypeScript enums through number unions. It will only send a single byte over the wire.

Example:

typeSimpleMessage={team: 1|2|3};constsimpleMessageSchema=makeSchema<SimpleMessage>({flag: 'enum',1: 1,2: 2,3: 3,});

In rare cases you may want to send a bitmasked value over the wire. You define this as a single object that only has boolean values. It will send a single byte over the wire, and be serialized back into the complex object.

Example:

typeBitMaskMessage={switcher: {up: boolean;down: boolean;left: boolean;right: boolean;};};constBitMaskMessageSchema=makeSchema<BitMaskMessage>({switcher: {flag: 'bitmask',up: 0,down: 1,left: 2,right: 3,},});

If these data types don't suit all of your needs, you can define your own custom schema type.

You must define a customSchemaType using makeCustom. The keys of the object you pass in will be the string you use in your schema. You must define how to read, write, and the size of the model. This customSchemaType can now be passed into makeSchema so it is aware of your custom keys.

Note that you must also pass customSchemaTypes into the generate function

Example:

import{makeCustomSchema,makeSchema,generateSchema}from'safe-schema';exportconstcustomSchemaTypes=makeCustomSchema({specialId: {// this turns the string 123-456 into two int16'sread: (buffer): string=>buffer.readInt16()+'-'+buffer.readInt16(),write: (model: string,buffer)=>{constspecialIdParse=/(-?\d*)-(-?\d*)/;constspecialIdResult=specialIdParse.exec(model);constx=parseInt(specialIdResult[1]);consty=parseInt(specialIdResult[2]);buffer.addInt16(x);buffer.addInt16(y);},size: (model: string)=>2+2,},});typeCustomTypeMessage={testId: string};constCustomTypeMessageSchema=makeSchema<CustomTypeMessage,typeofcustomSchemaTypes>({testId: 'specialId'});constgenerator=generateSchema(CustomTypeMessageSchema,customSchemaTypes);

About

A dependency-less solution to safely serialize your TypeScript models to and from ArrayBuffers.

Topics

Resources

Stars

26 stars

Watchers

1 watching

Forks

Releases

Packages

Contributors

Languages