@airplaneglue/pacpacpac
v0.1.2
Published
simple binary serialization library for the browser
Readme
PacPacPac
Lightweight binary encoding of JavaScript objects. PacPacPac is designed to be hard to say and easy to use.
The target use case is to serialize a small-to-medium-sized state object (< 100kb) in the following scenarios:
- persisting local state to a browser URL so that it's shareable
- persisting local state to session storage so that it persists on refresh
- passing an object to any Browser or Node API that accepts binary data or a string but not an object reference
API
type encode = <T>(schema: T, obj: TypeFromSchema<T>) => Uint8Array
type decode = <T>(schema: T, bytes: Uint8Array) => TypeFromSchema<T>
type compress = (bytes: Uint8Array) => Uint8Array
type decompress = (bytes: Uint8Array) => Uint8Array
type toBase64Url = (bytes: Uint8Array) => string
type fromBase64Url = (string: string) => Uint8Array
type S = {
/** 1-byte signed integer (-128 to 127) */
smallInt: SmallInt,
/** 4-byte signed integer (±2,147,483,647) */
int: Int,
/** 8-byte signed BigInt (±9,223,372,036,854,775,807) */
bigInt: BigInt,
/** 4-byte float (32-bit) */
float: Float,
/** 8-byte double (64-bit) */
double: Double,
/** Boolean (1 byte) */
bool: Boolean,
/** Single UTF-16 character (2 bytes) */
char: Char,
/** Variable-length UTF-8 string */
text: Text,
/** Enumeration of string values (varint index) */
enum: <T extends string[]>(value: T) => Enum<T>,
/** Fixed keys with typed values */
struct: <T extends Record<string, Schema>>(value: T) => Struct<T>,
/** Variable-length homogeneous array */
list: <T extends Schema>(value: T) => List<T>,
}Usage
This example models a Kibana-like analytics dashboard where each panel has a visualization type, query configuration, and layout. Enums are used heavily—they encode as single-byte indices instead of full strings, dramatically reducing payload size.
import { S, encode, decode, compress, decompress, toBase64Url, fromBase64Url } from 'pacpacpac'
// Define the dashboard schema
const dashboardSchema = S.struct({
id: S.int,
title: S.text,
timeRange: S.enum(['last15m', 'last1h', 'last6h', 'last24h', 'last7d', 'last30d']),
panels: S.list(S.struct({
vizType: S.enum(['line', 'bar', 'area', 'pie', 'table', 'metric', 'heatmap']),
title: S.text,
query: S.struct({
index: S.text,
aggregation: S.enum(['count', 'sum', 'avg', 'min', 'max', 'median', 'cardinality']),
field: S.text,
bucketBy: S.enum(['dateHistogram', 'terms', 'histogram', 'range']),
interval: S.enum(['auto', '1m', '5m', '15m', '1h', '1d', '1w']),
}),
style: S.struct({
palette: S.enum(['default', 'cool', 'warm', 'status', 'grayscale']),
legend: S.enum(['top', 'right', 'bottom', 'left', 'hidden']),
showGrid: S.bool,
}),
layout: S.struct({ x: S.smallInt, y: S.smallInt, w: S.smallInt, h: S.smallInt }),
}))
})
// Example data
const dashboard = {
id: 42,
title: 'System Metrics',
timeRange: 'last24h',
panels: [{
vizType: 'line',
title: 'CPU Usage',
query: { index: 'metrics-*', aggregation: 'avg', field: 'system.cpu.percent', bucketBy: 'dateHistogram', interval: '5m' },
style: { palette: 'cool', legend: 'right', showGrid: true },
layout: { x: 0, y: 0, w: 6, h: 4 },
}, {
vizType: 'area',
title: 'Memory Usage',
query: { index: 'metrics-*', aggregation: 'avg', field: 'system.memory.percent', bucketBy: 'dateHistogram', interval: '5m' },
style: { palette: 'warm', legend: 'right', showGrid: true },
layout: { x: 6, y: 0, w: 6, h: 4 },
}, {
vizType: 'bar',
title: 'Top Hosts by Traffic',
query: { index: 'logs-*', aggregation: 'sum', field: 'network.bytes', bucketBy: 'terms', interval: 'auto' },
style: { palette: 'default', legend: 'hidden', showGrid: false },
layout: { x: 0, y: 4, w: 12, h: 3 },
}]
}
// Encode as compressed base64url string
const encoded = toBase64Url(await compress(encode(dashboardSchema, dashboard)))
// Result: 192 chars vs 843 chars for JSON.stringify
// Persist to URL hash
window.location.hash = `state=${encoded}`
// Decode from URL
const hash = new URLSearchParams(window.location.hash.slice(1)).get('state')
const decoded = decode(dashboardSchema, await decompress(fromBase64Url(hash!)))