@chelonia/serdes
v1.0.1
Published
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`, `MessagePor
Readme
@chelonia/serdes
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.
Why?
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.
Install
npm install @chelonia/serdesQuick Start
import { 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)API
serializer(data, noFn?)
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 |
deserializer(data)
Reconstructs serialized data on the receiving side.
const original = deserializer(received.data)deserializer.register(Constructor)
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)Symbols
| 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 |
Supported Types
| 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 |
Custom Types
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)Memory Management
- 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.
Build
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/Development
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.
License
MIT — okTurtles Foundation, Inc.
