@unikvs/v8-serde
Explains how to use the Transformer plugin that serializes values fast with Node.js v8.serialize.
Overview
@unikvs/v8-serde is a Transformer plugin that converts JavaScript values to byte sequences with Node.js v8.serialize. Transformer Node.js only
It converts values that structured clone can handle, fast with the native implementation. Writes convert to bytes with encode and reads restore the original value with decode, so you can read and write without thinking about serialization details.
The storage format is V8-specific and is not compatible with other runtimes or other languages. It fits snapshots and caches shared between Node.js peers.
This transformer is always open. No explicit open or close is needed; isOpen always returns true and name is always "V8Serde".
Install it as follows.
npm install @unikvs/v8-serdepnpm add @unikvs/v8-serdeyarn add @unikvs/v8-serdebun add @unikvs/v8-serdenub add @unikvs/v8-serdeaube add @unikvs/v8-serdeSupported Values
The values you can convert are as follows.
| Value | Encoding | After decoding |
|---|---|---|
null |
Supported | null |
| Boolean | Supported | boolean |
number |
Supported | number. NaN, Infinity, -Infinity, and -0 round-trip as is. |
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. Holes in sparse arrays are preserved too. |
| Plain object | Supported | plain object |
Date |
Supported | Date. An Invalid Date is restored as a Date too. |
Map |
Supported | Map. Non-string keys are restored too. |
Set |
Supported | Set |
| Typed arrays | Supported | The same type, such as Uint8Array or Float64Array |
ArrayBuffer |
Supported | ArrayBuffer |
DataView |
Supported | DataView |
Buffer |
Supported | Buffer |
RegExp |
Supported | RegExp |
Error |
Supported | Error and subclasses. The kind and message are preserved. |
| Circular references | Supported | Restored with the reference relationships intact. |
The limitations are as follows.
- Functions, symbols,
WeakMap,WeakSet,Promise, andSharedArrayBuffercannot be encoded. Passing one at the root or nested throwsV8SerdeEncodeError. - Properties keyed by a symbol are removed together with their values.
- Class instances become plain objects and lose their prototype.
Usage
The V8Serde constructor takes no arguments.
import { V8Serde } from "@unikvs/v8-serde";
const v8serde = new V8Serde();
Its main members are as follows. encode, decode, getEncodable, and getDecodable are all async, so call them with await.
| Member | Summary |
|---|---|
new V8Serde() |
Initializes with no arguments. |
name |
Always returns "V8Serde". |
isOpen |
Always returns true. |
encode({ data }) |
Converts any value to a byte Uint8Array<ArrayBuffer>. |
decode({ data }) |
Converts bytes back to a value. If passed bytes holding multiple concatenated values, it returns only the first value. |
getEncodable() |
Returns a TransformStream that converts values to bytes. |
getDecodable() |
Returns a TransformStream that converts bytes to values. |
import { V8Serde } from "@unikvs/v8-serde";
const v8serde = new V8Serde();
const bytes = await v8serde.encode({ data: { message: "hello" } });
const value = await v8serde.decode({ data: bytes });
Register it with UniKvs via appendTransformer. It becomes the upstream of subsequently registered storages.
import { V8Serde } from "@unikvs/v8-serde";
import UniKvs from "unikvs";
const kvs = UniKvs.config()
.appendTransformer(new V8Serde())
.appendStorage(storage)
.create();
await kvs.open();
await kvs.set("data", { message: "hello" });
const output = await kvs.get("data");
set converts to bytes for storage, and get converts them back to the original value automatically.
Streams
It supports both single values and streams.
- Single values are converted with
encodeanddecode. - Stream values are converted with the
TransformStreamfromgetEncodableandgetDecodable. Obtaining them is async, so call them withawait.
getEncodable encodes each input chunk as one independent value and emits its bytes in order. The output is a stream-only format.
getDecodable decodes bytes incrementally across arbitrary chunk boundaries.
- As soon as one value completes, its value is emitted immediately. If a chunk contains multiple values, the completed ones are emitted in order.
- A value 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 incomplete data remains when the stream ends, it throws
V8SerdeDecodeError. Completed values are emitted beforehand. - If the input is not valid for this package’s format, it throws
V8SerdeDecodeErrorat that point.
import { V8Serde } from "@unikvs/v8-serde";
const v8serde = new V8Serde();
const source = new ReadableStream<unknown>({
start(controller) {
controller.enqueue(new Date());
controller.enqueue(new Map([["a", 1]]));
controller.close();
},
});
const decoded = source
.pipeThrough(await v8serde.getEncodable())
.pipeThrough(await v8serde.getDecodable());
for await (const value of decoded) {
console.log(value);
}
Errors
| Error | Error name | Meaning |
|---|---|---|
V8SerdeEncodeError |
UniKvsV8SerdeEncodeError |
A value cannot be converted to bytes. The original error is kept in cause. |
V8SerdeDecodeError |
UniKvsV8SerdeDecodeError |
A byte sequence cannot be interpreted as a value. The original error is kept in cause. |
UnsupportedRuntimeError |
UniKvsUnsupportedRuntimeError |
Used on a runtime other than Node.js. A shared error from @unikvs/core. |
Conversion failures in encode and getEncodable become V8SerdeEncodeError, and those in decode and getDecodable become V8SerdeDecodeError. Both keep the original error in cause. Messages support Japanese display. On a runtime other than Node.js, all four methods throw UnsupportedRuntimeError.
import { V8Serde, V8SerdeDecodeError, V8SerdeEncodeError } from "@unikvs/v8-serde";
const v8serde = new V8Serde();
try {
await v8serde.encode({ data: () => 1 });
} catch (error) {
if (error instanceof V8SerdeEncodeError) {
console.log(error.cause);
} else {
throw error;
}
}
try {
await v8serde.decode({ data: new Uint8Array(0) });
} catch (error) {
if (error instanceof V8SerdeDecodeError) {
console.log(error.cause);
} else {
throw error;
}
}
Examples
An example with Memory storage. Date, Map, Set, bigint, and undefined round-trip with their types intact.
import { V8Serde } from "@unikvs/v8-serde";
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 V8Serde())
.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();