A TypeScript library for serializing and deserializing complex JavaScript objects that neither structuredClone nor JSON support. It enables sharing custom objects — including functions, Map, Set, Error, Blob, File, ArrayBuffer, MessagePort, and more — across MessagePort boundaries.
structuredClone does not support custom objects (whatwg/html#7428). JSON cannot handle undefined, Map, Set, Error, binary data, or functions. This library bridges the gap by providing an augmented deep-clone mechanism that works with postMessage and MessagePort.
npm install @chelonia/serdesimport { serializer, deserializer } from '@chelonia/serdes'
const source = {
name: 'example',
tags: new Set(['a', 'b']),
metadata: new Map([['key', 'value']]),
optional: undefined
}
// Serialize
const { data, transferables, revokables } = serializer(source)
// Send via MessagePort
port.postMessage(data, transferables)
// On the receiving side, reconstruct the original object
const reconstructed = deserializer(data)Serializes data into a form suitable for structuredClone / postMessage.
data— Any value to serialize.noFn(optional) — Iftrue, disables function serialization (useful for memory management).
Returns { data, transferables, revokables }:
| Field | Type | Description |
|---|---|---|
data |
unknown |
The serialized payload, safe for postMessage |
transferables |
Transferables[] |
Objects to pass as the second argument to postMessage |
revokables |
MessagePort[] |
Ports that must be closed when no longer needed to prevent memory leaks |
Reconstructs serialized data on the receiving side.
const original = deserializer(received.data)Registers a custom class for deserialization. Must be called on the receiving side for every custom type that may appear in messages.
deserializer.register(MyClass)| Export | Purpose |
|---|---|
serdesTagSymbol |
Symbol key for a class's unique tag string |
serdesSerializeSymbol |
Symbol key for a class's static serialize method |
serdesDeserializeSymbol |
Symbol key for a class's static deserialize method |
| Type | Encoding |
|---|---|
undefined |
['_', '_'] |
Map |
['_', 'Map', entries] |
Set |
['_', 'Set', values] |
Blob / File |
Stored verbatim via _ref |
Error |
['_', '_err', ref, name] — preserves .name and recursively serializes .cause |
MessagePort / ReadableStream / WritableStream / ArrayBuffer / ArrayBufferView |
Stored verbatim and added to transferables |
| Functions | Converted to MessagePort pairs (['_', '_fn', port]) |
| Custom classes | ['_', '_custom', tag, serializedData] via the Symbol protocol |
Make any class serializable by implementing three static Symbol-keyed members:
import {
serdesTagSymbol,
serdesSerializeSymbol,
serdesDeserializeSymbol,
deserializer
} from '@chelonia/serdes'
class Coordinate {
x: number
y: number
constructor (x: number, y: number) {
this.x = x
this.y = y
}
static get [serdesTagSymbol] () { return 'Coordinate' }
static [serdesSerializeSymbol] (instance: Coordinate) {
return { x: instance.x, y: instance.y }
}
static [serdesDeserializeSymbol] (data: { x: number, y: number }) {
return new Coordinate(data.x, data.y)
}
}
// Register on the receiving side
deserializer.register(Coordinate)- Close revokables: The
serializerreturns arevokablesarray ofMessagePorts. Close them when no longer needed to prevent memory leaks. noFnparameter: Passtrueto disable function serialization when you don't need it.- Automatic cleanup:
FinalizationRegistryis used to automatically closeMessagePorts when deserialized function proxies are garbage collected.
The library ships both ESM and UMD formats:
npm run build # Build both formats
npm run build:esm # ESM only → dist/esm/
npm run build:umd # UMD only → dist/umd/npm install
npm test # Lint + tests
npm run lint # Lint onlyTests use the Node.js built-in test runner (node:test) and node:assert/strict. The --expose-gc flag is required for memory-leak tests that rely on FinalizationRegistry.
MIT — okTurtles Foundation, Inc.