structured-clone-preflight
v0.1.0
Published
Explain exactly why a JavaScript value cannot cross a structured-clone boundary.
Maintainers
Readme
structured-clone-preflight
Explain exactly why a JavaScript value cannot cross a structured-clone boundary.
import { preflight } from "structured-clone-preflight";
const payload = {
jobs: [{ id: 1 }, { id: 2, callback: () => "done" }],
};
const report = preflight(payload, { target: "worker" });
report.ok;
// false
report.errors;
// [{
// code: "function-not-cloneable",
// path: "$.jobs[1].callback",
// message: "Functions cannot be structured-cloned.",
// valueType: "Function"
// }]Native structuredClone(), postMessage(), Workers, IndexedDB, and history APIs can fail with an unhelpful DataCloneError. This package reports the failing path, warns about values that silently change meaning, and prepares a safe transfer list without detaching anything.
Install
npm install structured-clone-preflight- Zero runtime dependencies
- ESM and CommonJS
- TypeScript declarations included
- Node.js 18+, modern browsers, Bun, Deno, and Workers
- Iterative, cycle-safe, and resource-bounded
Transfer planning
Some values must be transferred rather than copied. Others, such as ArrayBuffer, can be copied or transferred.
const bytes = new Uint8Array(1024 * 1024);
const report = preflight(
{ bytes },
{
target: "worker",
transfer: "all",
},
);
if (report.ok) {
worker.postMessage({ bytes }, report.transferables);
}Preflight itself never detaches a buffer or moves a port. Detachment occurs only if the caller later passes report.transferables to a transfer operation.
Transfer lists are deduplicated. Multiple typed-array views backed by the same ArrayBuffer produce one entry.
Node.js Buffers
Buffer requires special care:
const report = preflight(Buffer.from("secret"), {
target: "worker",
transfer: "all",
});
report.transferables;
// []The package deliberately refuses to auto-transfer a Buffer's backing store. Pooled buffers can share a larger allocation containing unrelated bytes. It also warns that a cloned Buffer arrives as a plain Uint8Array.
Silent semantic losses
A value can clone successfully and still arrive differently:
class User {
constructor(readonly name: string) {}
greet() {
return `Hello ${this.name}`;
}
}
const report = preflight(new User("Ada"));
report.ok;
// true
report.warnings;
// [{ code: "prototype-dropped", path: "$", ... }]Preflight warns about:
- Custom prototypes and class identity being dropped
- Enumerable symbol-keyed properties being dropped
- Non-enumerable properties being dropped
- Accessors becoming plain writable data properties
RegExp.lastIndexbeing reset- Node.js
BufferbecomingUint8Array - Runtime-dependent
Errorfields - Custom properties attached to built-in values being dropped
- Shared memory remaining shared
- Values that will be detached by a returned transfer plan
Warnings do not make report.ok false. They describe successful but potentially surprising transfers.
Targets
structured-clone
The default. Models structuredClone(value, { transfer }) in the current runtime.
preflight(value);worker
Enforces Worker and postMessage transfer requirements and creates a transfer plan.
preflight(value, { target: "worker", transfer: "required" });storage
Models structured serialization for persistent storage and history state. Transfer-only values and SharedArrayBuffer are rejected.
preflight(value, { target: "storage" });Individual storage APIs may impose additional quotas or platform-object restrictions. Preflight covers structured-serialization compatibility, not database quotas or transaction state.
Transfer modes
| Mode | Behavior |
| ---------- | ------------------------------------------------------------------------ |
| none | Reject values that require a transfer list |
| required | Include only transfer-only values such as MessagePort; this is default |
| all | Also include safely discoverable optional values such as ArrayBuffer |
all is an explicit performance decision. It changes ownership: transferred values become detached or otherwise unusable on the sending side.
APIs
preflight(value, options?)
Returns an immutable report and does not throw for payload problems.
interface PreflightReport {
readonly ok: boolean;
readonly target: "structured-clone" | "worker" | "storage";
readonly transferMode: "none" | "required" | "all";
readonly errors: readonly PreflightError[];
readonly warnings: readonly PreflightWarning[];
readonly transferables: readonly object[];
readonly stats: {
readonly nodesVisited: number;
readonly deepestPath: number;
readonly cycles: number;
readonly nativeProbes: number;
readonly truncated: boolean;
};
}Invalid library options throw PreflightOptionError. Untrusted payloads are represented in the report.
isCloneable(value, options?)
Boolean convenience API:
if (isCloneable(message, { target: "worker" })) {
// The graph passed preflight.
}Use preflight when transfer lists or diagnostics matter.
assertCloneable(value, options?)
Returns the successful report or throws StructuredClonePreflightError. The error exposes its immutable report property.
const report = assertCloneable(message);Options
preflight(value, {
target: "structured-clone",
transfer: "required",
maxDepth: 100,
maxNodes: 10_000,
maxIssues: 100,
nativeOracle: true,
maxNativeProbes: 64,
});Resource budgets
Object graphs are walked iteratively, so deep input does not consume the JavaScript call stack. The default budgets bound CPU, memory, diagnostics, and native probes.
Exceeding any traversal budget is an error and sets stats.truncated to true. An incomplete inspection never returns a false green.
Native oracle
JavaScript cannot portably inspect every host object's internal slots or detect a Proxy. When available, the current runtime's native structuredClone is used as a final bounded oracle. If it rejects an otherwise valid-looking graph, preflight probes bounded subvalues to locate the opaque failure.
The oracle clones the graph for validation and can therefore read enumerable accessors again. Set nativeOracle: false when a single inspection pass is required.
Disable this only when deterministic static inspection is more important than detecting runtime-specific objects:
preflight(value, { nativeOracle: false });Error codes
| Code | Meaning |
| ------------------------------- | ------------------------------------------------------ |
| function-not-cloneable | A function was found |
| symbol-not-cloneable | A Symbol value was found |
| weak-collection-not-cloneable | WeakMap, WeakSet, WeakRef, or FinalizationRegistry |
| promise-not-cloneable | A Promise was found |
| property-access-threw | An enumerable getter or Error cause threw |
| introspection-threw | A Proxy or exotic object rejected safe inspection |
| native-clone-failed | The current runtime rejected an opaque value |
| transfer-required | Transfer mode forbids a required transferable |
| storage-unsupported | A value cannot use persistent structured serialization |
| max-depth-exceeded | The depth budget was exceeded |
| max-nodes-exceeded | The node budget was exceeded |
| max-issues-exceeded | The diagnostics budget was exceeded |
Codes are stable API. Human-readable messages may become clearer in minor releases.
Property getters and proxies
The structured-clone algorithm reads enumerable properties, so author-defined getters can execute during a real clone. Preflight follows the same observable boundary and captures thrown getter errors. Do not preflight an object if merely reading its enumerable properties is unsafe.
Proxy traps can also execute during inspection. Every trap interaction is contained; payload failures become report errors rather than escaping from preflight.
What this package does not do
- It does not clone, serialize, sanitize, or repair values.
- It does not make functions, Promises, DOM nodes, or weak collections transferable.
- It does not guarantee that a destination runtime exposes the same platform interfaces.
- It does not measure IndexedDB quotas, browser history size limits, or Worker availability.
- It does not transfer anything automatically.
The report answers one question: can this graph cross the selected structured-clone boundary, what will change, and which values must move?
Security
Resource limits are part of the public contract. Keep them bounded for untrusted graphs. See SECURITY.md for the supported versions and private reporting process.
Contributing
Bug reports should include the runtime and version, the smallest reproducible value, the selected target, and the native error when available. See CONTRIBUTING.md.
License
MIT
