Skip to content

udamir/patchpack

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

73 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

PatchPack

npm npm CircleCI npm type definitions GitHub

A binary JsonPatch serializer based on schema. Efficiently encode state object and JsonPatch in to compact byte buffers and then decode them back in to objects on the receiver. Integrates very well with observable state and WebSockets.

Originally it was part of mosx State Management engine, but then it moved to separate package.

Motivation

I was working on an magx game server framework that used WebSockets to syncronize state between server and clients. Syncronization principle is simple: first server sends full state to clients then on every change sends patches in JsonPatch format. I have found the problem that sending a lot of patches without serialization is a waste of bandwidth.

As state's schema is known on server side it can be sent to the clients, then state and each patch can be encoded based on that schema on server side and decoded back on client side. State schema is not static that means it must be also syncronized with clients. This sophisticated approach can significantly reduce patch size and bandwidth usage.

Concept

Installation

npm install --save patchpack

Browser

A browser version of patchpack is also available:

<script src="https://cdn.jsdelivr.net/npm/[email protected]/browser/patchpack.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/[email protected]/browser/patchpack.js"></script>

Example

Server side

import { PatchPack } from "patchpack"

class Client {
  constructor(public name: string, public info = "") {}
}

// initial state
const state: any = {
  clients: {
    "1": new Client("Foo"),
    "2": new Client("Baz", "FooBaz"),
  },
  objects: [
    { id: 1, name: "Foo" },
    { id: 2, name: "Foo", foo: "Baz" },
  ],
  foo: { baz: false }
}

// create patchpack instance and define schema types
const ppServer = new PatchPack({
  State: ["clients", "objects", "foo"],
  Client, // it is recommended to use class in schema (better performance)
  Object: ["id", "name", "foo"],
  Foo: ["baz"]
})

// encoded state can include types definition
const encodedStateWithTypes = ppServer.encodeState(state)

const encodedState = ppServer.encodeState(state, false)

// add item

const client = new Client("FooBaz", "test" )
state.clients["3"] = client

const patch1 = { op: "add", path: "/clients/3", value: client }
const encodedPatch1 = ppServer.encodePatch(patch1)

// update value

state.foo.baz = true

const patch2 = { op: "replace", path: "/foo/baz", value: true }
const encodedPatch2 = ppServer.encodePatch(patch2)

Benchmark

Benchmark for encoded object size (byte):

PatchPack MessagePack JSON.stringify
state 60 107 (+78%) 165 (+175%)
patch1 22 53 (+140%) 72 (+227%)
patch2 5 33 (+560%) 47 (+840%)

Send encodedStateWithTypes, encodedPatch1 and encodedPatch2 to Client and decode them:

Client side

const ppClient = new PatchPack()

const decodedState = ppClient.decodeState(encodedStateWithTypes)
console.log(decodedState)

// {
//   clients: {
//     '1': { name: 'Foo', info: '' },
//     '2': { name: 'Baz', info: 'FooBaz' }
//   },
//   objects: [
//      { id: 1, name: 'Foo' },
//      { id: 2, name: 'Foo', foo: 'Baz' } ],
//   foo: { baz: false }
// }

const decodedPatch1 = ppClient.decodePatch(encodedPatch1)
console.log(decodedPatch1)

// {
//   op: 'add',
//   path: '/clients/3',
//   value: { name: 'FooBaz', info: 'test' }
// }

const decodedPatch2 = ppClient.decodePatch(encodedPatch2)
console.log(decodedPatch2)

// { op: 'replace', path: '/foo/baz', value: true }

Documentation

Patchpack

constructor

Return instance of PatchPack with defined schema types

constructor (types?: { [type: string]: string[] | Type<any> })

Types can be defined in 2 ways:

  • array of properties
  • Class name

Example:

class User {
  constructor (public name: string) {}
}

class Item {
  constructor (public id: number) {}
}

const state = {
  users: [ new User("John"), new User("Santa") ]
  item: new Item(123) 
}

const pp = new PatchPack({
  State: ["users", "item"],
  User,
  Item,
})

encodeState

Encode state and return in binary format

encodeState(state: any, includeTypes = true, updateSchema = true): Buffer

If parameter includeTypes = false used, decoder instance of PatchPack must be created with the same types.

First time state encoding must be with updateSchema = true. If you need to encode state with the same schema second time updateSchema can be set as false.

decodeState

Decode state from binary format to object

decodeState(buffer: Buffer): any

!Important. State can be decoded only once.

Example:

const pp = new PatchPack()
const state = pp.decodeState(encodedStatewWithTypes)

encodePatch

Encode JsonPatch and return in binary format

encodePatch(patch: IReversibleJsonPatch, updateSchema = true): Buffer

First time patch encoding must be with updateSchema = true. If you need to encode the same patch second time updateSchema must be set as false.

The following JsonPatch operation are supported:

  • add
  • replace
  • remove

ReversibleJsonPatch with oldValue is supported

Example:

// JsonPatch
const p1 = pp.encodePatch({
  op: "replace",
  path: "/a/b/c",
  value: "100",
})

// ReversibleJsonPatch
const p1 = pp.encodePatch({
  op: "replace",
  path: "/a/b/c",
  value: "100",
  oldValue: "99",
})

decodePatch

Decode patch from binary format to JsonPatch (or ReversibleJsonPatch).

decodePatch (buffer: Buffer): IReversibleJsonPatch

!Important. Patch can be decoded only once.

PatchPack.encode

Encode object to binary with last MessagePack specification.

static encode(value: any): Buffer

PatchPack.decode

Decode binary to object with last MessagePack specification.

static decode(buffer: Buffer): any

Specification

will be soon...

License

MIT