Skip to content
UniKVS
English
Esc
↑↓navigate↵open⌘Jpreview
On this page

@unikvs/cbor

Explains how to use the Transformer plugin that transparently converts values to and from CBOR sequences.

Overview

@unikvs/cbor is a Transformer plugin that converts JavaScript values to and from CBOR (RFC 8949) byte sequences. Transformer It wraps cbor-x to fit ITransformer.

Writes convert with encode and reads convert with decode, so you can read and write without thinking about serialization details.

The encoded output is valid CBOR, but rich types such as Date and Map use cbor-x extension tags (258, 259, 64, 27, and so on). Other CBOR decoders that do not understand these tags may expose them as raw tags.

This transformer is always open. No explicit open or close is needed; isOpen always returns true and name is always "Cbor".

Install it as follows.

npm install @unikvs/cbor
pnpm add @unikvs/cbor
yarn add @unikvs/cbor
bun add @unikvs/cbor
nub add @unikvs/cbor
aube add @unikvs/cbor

Supported Values

The values you can convert to CBOR are as follows.

Value Encoding After decoding
null Supported null
Boolean Supported boolean
number Supported number. NaN, Infinity, and -Infinity round-trip too. -0 becomes 0.
bigint Supported bigint. It is restored as a bigint regardless of size.
string Supported string, including Unicode.
undefined Supported undefined. Preserved both at the root and nested.
Array Supported array
Plain object Supported plain object
Date Supported Date
Map Supported Map. Non-string keys are restored too.
Set Supported Set
Typed arrays Supported The same type, such as Uint8Array or Float64Array
RegExp Supported RegExp
Error Supported Error. The message is preserved.

The limitations are as follows.

  • A function or symbol at the root or nested cannot be encoded. Passing one throws CborEncodeError.
  • Properties keyed by a symbol are removed together with their values.
  • Indefinite-length byte strings and text strings cannot be decoded. Passing one throws CborDecodeError.
  • A standard CBOR map (major type 5) decodes to a plain object, and non-string keys are converted to strings. Tag 259 is required to restore a Map; this package’s encode writes Map with tag 259.

Usage

The Cbor constructor takes no arguments.

import { Cbor } from "@unikvs/cbor";

const cbor = new Cbor();

Its main members are as follows.

Member Summary
new Cbor() Initializes with no arguments.
name Always returns "Cbor".
isOpen Always returns true.
encode({ data }) Converts any value to a CBOR Uint8Array<ArrayBuffer>.
decode({ data }) Converts a CBOR data item back to a value.
getEncodable() Returns a TransformStream that converts values to a CBOR sequence.
getDecodable() Returns a TransformStream that converts a CBOR sequence to values.

Register it with UniKvs via appendTransformer. It becomes the upstream of subsequently registered storages.

import { Cbor } from "@unikvs/cbor";
import UniKvs from "unikvs";

const kvs = UniKvs.config()
  .appendTransformer(new Cbor())
  .appendStorage(storage)
  .create();

await kvs.open();

await kvs.set("data", { message: "hello" });

const output = await kvs.get("data");

set stores values converted to CBOR, and get converts them back to the original values automatically.

Streams

@unikvs/cbor supports both single values and streams.

  • Single values are converted with encode and decode. decode accepts exactly one data item. An empty byte sequence or bytes trailing a value are rejected.
  • Stream values are converted with the TransformStream from getEncodable and getDecodable.

getEncodable encodes each input chunk as one independent value and emits its bytes in order. The output is a CBOR sequence (RFC 8742): multiple data items concatenated, with no enclosing array or header.

getDecodable decodes a CBOR sequence incrementally across arbitrary chunk boundaries.

  • As soon as one data item completes, its value is emitted immediately. If a chunk contains multiple items, the completed ones are emitted in order.
  • A data item cut off mid-way is buffered internally until it completes. It waits for the next chunk, so partial data is never emitted as a value by mistake.
  • An empty stream and zero-length chunks produce zero values and no error.
  • If an incomplete data item remains when the stream ends, it throws CborDecodeError. Completed values are emitted beforehand.
  • If the input is not valid CBOR, it throws CborDecodeError at that point.

Errors

Error Error name Meaning
CborEncodeError UniKvsCborEncodeError A value cannot be converted to CBOR. The original error is kept in cause.
CborDecodeError UniKvsCborDecodeError A byte sequence cannot be interpreted as a CBOR data item. The original error is kept in cause.

Conversion failures in encode and getEncodable become CborEncodeError, and those in decode and getDecodable become CborDecodeError. Both keep the original error in cause.

import { Cbor, CborDecodeError, CborEncodeError } from "@unikvs/cbor";

const cbor = new Cbor();

try {
  cbor.encode({ data: () => 1 });
} catch (error) {
  if (error instanceof CborEncodeError) {
    console.log(error.cause);
  }
}

try {
  cbor.decode({ data: new Uint8Array(0) });
} catch (error) {
  if (error instanceof CborDecodeError) {
    console.log(error.cause);
  }
}

Examples

An example with Memory storage. Date, Map, Set, bigint, and undefined round-trip with their types intact.

import { Cbor } from "@unikvs/cbor";
import { Memory } from "@unikvs/memory";
import UniKvs, { type PlainValue } from "unikvs";

type Snapshot = {
  createdAt: Date;
  counts: Map<string, number>;
  flags: Set<string>;
  revision: bigint;
  note: string | undefined;
};

const kvs = UniKvs.config<{ snapshot: PlainValue<Snapshot> }>()
  .appendTransformer(new Cbor())
  .appendStorage(new Memory())
  .create();

await kvs.open();

const snapshot: Snapshot = {
  createdAt: new Date("2024-01-02T03:04:05.678Z"),
  counts: new Map([
    ["read", 1],
    ["write", 2],
  ]),
  flags: new Set(["dirty"]),
  revision: 1n,
  note: undefined,
};

await kvs.set("snapshot", snapshot);

const restored = await kvs.get("snapshot");
console.log(restored.createdAt instanceof Date);
console.log(restored.counts instanceof Map);
console.log(restored.flags instanceof Set);
console.log(typeof restored.revision);
console.log("note" in restored);

await kvs.close();

Was this page helpful?