Skip to content

lilnasy/es-codec

Repository files navigation

es-codec

es-codec is an efficient, zero-dependency, compact serialization library for Node, Deno, and browsers. It is tailor-made for client-server communication where both the client and server are modern js environments. It supports a subset of objects supported by structuredClone that are avaialable on all browser, server, and edge runtimes.

Getting Started

npm install es-codec
pnpm add es-codec
yarn add es-codec
deno cache https://deno.land/x/escodec/es-codec.ts

Usage

// browser
// import { encode, decode, NotSerializable } from 'https://cdn.jsdelivr.net/npm/es-codec/es-codec.js'
// import { encode, decode, NotSerializable } from 'https://esm.sh/es-codec/es-codec.js'
// import { encode, decode, NotSerializable } from 'https://unpkg.com/es-codec/es-codec.js'

// deno
// import { encode, decode, NotSerializable } from 'https://deno.land/x/escodec/es-codec.ts'

// node
import { encode, decode, NotSerializable } from 'es-codec'

const object        = { foo: 'bar' }
const encodedObject = encode(object) satisfies ArrayBuffer
const decodedObject = decode(encodedObject) as typeof object

const array        = [1, true, null, "foo"]
const encodedArray = encode(array) satisfies ArrayBuffer
const decodedArray = decode(encodedArray) as typeof array

const map        = new Map([['foo', 'bar']])
const encodedMap = encode(map) satisfies ArrayBuffer
const decodedMap = decode(encodedMap) as typeof map

const set        = new Set([1, true, null, "foo"])
const encodedSet = encode(set) satisfies ArrayBuffer 
const decodedSet = decode(encodedSet) as typeof set

const date        = new Date()
const encodedDate = encode(date) satisfies ArrayBuffer
const decodedDate = decode(encodedDate) as typeof date

const regExp        = /([A-Z])+/gm
const encodedRegExp = encode(regExp) satisfies ArrayBuffer
const decodedRegExp = decode(encodedRegExp) as typeof regExp

const error        = new Error('foo')
const encodedError = encode(error) satisfies ArrayBuffer
const decodedError = decode(encodedError) as typeof error

const byteArray        = new Uint8Array([1, 2, 3])
const encodedByteArray = encode(byteArray) satisfies ArrayBuffer
const decodedByteArray = decode(encodedByteArray) as typeof byteArray

API

// throws NotSerializable if object is not serializable symbols, functions, class instances, etc.
function encode(x: Serializable): ArrayBuffer

function decode(buffer: ArrayBuffer): Serializable

class NotSerializable extends Error {
    value: unknown
}

Advanced: Extensions API

If you want to serialize objects not natively supported by es-codec, you can create your own codec. Here's an example of how you would add support for URL.

import { createCodec } from "es-codec"

const { encode, decode } = createCodec([
    {
        name: "URL",
        when: x => x.constructor === URL,
        encode: url => url.href,
        decode: href => new URL(href)
    }
])

The createCodec function accepts an array of extensions, where each extension is an object with the following properties:

  • name: This will be used as the tag in the serialized representation. When deserializing, the tag is used to identify the extension that should be used for decoding.
  • when: This is a function that receives an unsupported object as the argument. It should return true if the extension can encode the provided object.
  • encode: This is a function that receives all unsupported objects for which when returned true. You can "reduce" your custom type in terms of other types that are supported. For example, you can encode a Graph as { edges: Array<Edge>, nodes: Array<Node> }. Another extension can encode an Edge as [ from : number, to: number ].
  • decode: This is a function that receives the "reduced" representation created by the extension's encode and reconstructs your custom type from it.

Note that you can only provide implementations for types that es-codec does not support by default; it is not possible to change how the native types are encoded or decoded.

Type-safety

For better type-safety and convenience, a helper function is provided that can automatically infer the types from your extension.

import { createCodec, defineExtension } from "es-codec"

const urlExtension = defineExtension({

    name: "URL",
    
    // `x is URL` is a type predicate, it is required for
    // defineExtension's type inference
    when: (x): x is URL => x.constructor === URL,
    
    // `url` is inferred as URL
    encode: url => url.href,
    
    // `href` is inferred as string
    // return type is inferred as URL
    decode: href => new URL(href)
})

const { encode, decode } = createCodec([ urlExtension ])

// No type error! URL is now a valid argument type for encode.
const encodedURL: ArrayBuffer = encode(new URL("https://example.com"))

Stability

The binary format is subject to change until v1. For now, you will have to ensure that you are using the same version of es-codec on both the client and server.

Limitations

Generally, es-codec is more strict than structuredClone. It does not support serializing the following types:

  • null-prototype objects: structuredClone returns a plain object instead of a null-prototype one. Implicit replacement of object prototypes is probably a bad idea.
  • arrays with properties: Supporting this would cause either serialization to become much slower or the binary representation to become much larger.
  • new Boolean(), new Number(), and new String(): The distinction is not made between primitives and their object counterparts created using the new keyword. These objects are serialized as if they were primitives.

Benchmarks

TODO: include a benchmark comparing es-codec to JSON, devalue, msgpack, and protobuf for the objects supported by all formats.

About

An efficient, compact, extensible, zero-dependency binary serialization library for browser-server communication.

Resources

License

Stars

Watchers

Forks

Packages

No packages published