Files
bun.sh/test/js/web/structured-clone-fastpath.test.ts
SUZUKI Sosuke fa78d2b408 perf(structuredClone): add fast path for arrays containing simple objects (#26818)
## Summary

Adds a **DenseArray fast path** for `structuredClone` / `postMessage`
that completely skips byte-buffer serialization when an
`ArrayWithContiguous` array contains **flat objects** (whose property
values are only primitives or strings).

This builds on #26814 which added fast paths for Int32/Double/Contiguous
arrays of primitives and strings. The main remaining slow case was
**arrays of objects** — the most common real-world pattern (e.g.
`[{name: "Alice", age: 30}, {name: "Bob", age: 25}]`). Previously, these
fell back to the full serialization path because the Contiguous fast
path rejected non-string cell elements.

## How it works

### Serialization
The existing Contiguous array handler is extended to recognize object
elements that pass `isObjectFastPathCandidate` (FinalObject, no
getters/setters, no indexed properties, all enumerable). For qualifying
objects, properties are collected into a `SimpleCloneableObject` struct
(reusing the existing `SimpleInMemoryPropertyTableEntry` type). The
result is stored as a `FixedVector<DenseArrayElement>` where
`DenseArrayElement = std::variant<JSValue, String,
SimpleCloneableObject>`.

If no object elements are found, the existing `SimpleArray` path is used
(no regression).

### Deserialization
A **Structure cache** avoids repeated Structure transitions when the
array contains many same-shape objects (the common case). The first
object is built via `constructEmptyObject` + `putDirect`, and its final
Structure + Identifiers are cached. Subsequent objects with matching
property names are created directly with `JSFinalObject::create(vm,
cachedStructure)`, skipping all transitions.

Safety guards:
- Cache is only used when property count AND all property names match
- Cache is disabled when `outOfLineCapacity() > 0` (properties exceed
`maxInlineCapacity`), since `JSFinalObject::create` cannot allocate a
butterfly

### Fallback conditions

| Condition | Behavior |
|-----------|----------|
| Elements are only primitives/strings | SimpleArray (existing) |
| Elements include `isObjectFastPathCandidate` objects | **DenseArray
(NEW)** |
| Object property value is an object/array | Fallback to normal path |
| Elements include Date, RegExp, Map, Set, ArrayBuffer, etc. | Fallback
to normal path |
| Array has holes | Fallback to normal path |

## Benchmarks

Apple M4 Max, release build vs system Bun v1.3.8 and Node.js v24.12:

| Benchmark | Node.js v24.12 | Bun v1.3.8 | **This PR** | vs Bun | vs
Node |

|-----------|---------------|------------|-------------|--------|---------|
| `[10 objects]` | 2.83 µs | 2.72 µs | **1.56 µs** | **1.7x** | **1.8x**
|
| `[100 objects]` | 24.51 µs | 25.98 µs | **14.11 µs** | **1.8x** |
**1.7x** |

## Test coverage

28 new edge-case tests covering:
- **Property value variants**: empty objects, special numbers (NaN,
Infinity, -0), null/undefined values, empty string keys, boolean-only
values, numeric string keys
- **Structure cache correctness**: alternating shapes, objects
interleaved with primitives, >maxInlineCapacity properties (100+), 1000
same-shape objects (stress test), repeated clone independence
- **Fallback correctness**: array property values, nested objects,
Date/RegExp/Map/Set/ArrayBuffer elements, getters, non-enumerable
properties, `Object.create(null)`, class instances
- **Frozen/sealed**: clones are mutable regardless of source
- **postMessage via MessageChannel**: mixed arrays with objects, empty
object arrays

## Changed files

- `src/bun.js/bindings/webcore/SerializedScriptValue.h` —
`SimpleCloneableObject`, `DenseArrayElement`, `FastPath::DenseArray`,
factory/constructor/member
- `src/bun.js/bindings/webcore/SerializedScriptValue.cpp` — serialize,
deserialize, `computeMemoryCost`
- `test/js/web/structured-clone-fastpath.test.ts` — 28 new tests
- `bench/snippets/structuredClone.mjs` — object array benchmarks

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
2026-02-09 21:54:41 -08:00

887 lines
30 KiB
TypeScript

import { describe, expect, test } from "bun:test";
describe("Structured Clone Fast Path", () => {
test("structuredClone should work with empty object", () => {
const object = {};
const cloned = structuredClone(object);
expect(cloned).toStrictEqual({});
});
test("structuredClone should work with empty string", () => {
const string = "";
const cloned = structuredClone(string);
expect(cloned).toStrictEqual("");
});
const deOptimizations = [
{
get accessor() {
return 1;
},
},
Object.create(Object.prototype, {
data: {
value: 1,
writable: false,
configurable: false,
},
}),
Object.create(Object.prototype, {
data: {
value: 1,
writable: true,
configurable: false,
},
}),
Object.create(Object.prototype, {
data: {
get: () => 1,
configurable: true,
},
}),
Object.create(Object.prototype, {
data: {
set: () => {},
enumerable: true,
configurable: true,
},
}),
];
for (const deOptimization of deOptimizations) {
test("structuredCloneDeOptimization", () => {
structuredClone(deOptimization);
});
}
test("structuredClone should use a constant amount of memory for string inputs", () => {
const clones: Array<string> = [];
// Create a 512KB string to test fast path
const largeString = Buffer.alloc(512 * 1024, "a").toString();
for (let i = 0; i < 100; i++) {
clones.push(structuredClone(largeString));
}
Bun.gc(true);
const rss = process.memoryUsage.rss();
for (let i = 0; i < 10000; i++) {
clones.push(structuredClone(largeString));
}
Bun.gc(true);
const rss2 = process.memoryUsage.rss();
const delta = rss2 - rss;
expect(delta).toBeLessThan(1024 * 1024 * 8);
expect(clones.length).toBe(10000 + 100);
});
test("structuredClone should use a constant amount of memory for simple object inputs", () => {
// Create a 512KB string to test fast path
const largeValue = { property: Buffer.alloc(512 * 1024, "a").toString() };
for (let i = 0; i < 100; i++) {
structuredClone(largeValue);
}
Bun.gc(true);
const rss = process.memoryUsage.rss();
for (let i = 0; i < 10000; i++) {
structuredClone(largeValue);
}
Bun.gc(true);
const rss2 = process.memoryUsage.rss();
const delta = rss2 - rss;
expect(delta).toBeLessThan(1024 * 1024);
});
// === Array fast path tests ===
test("structuredClone should work with empty array", () => {
expect(structuredClone([])).toEqual([]);
});
test("structuredClone should work with array of numbers", () => {
const input = [1, 2, 3, 4, 5];
expect(structuredClone(input)).toEqual(input);
});
test("structuredClone should work with array of strings", () => {
const input = ["hello", "world", ""];
expect(structuredClone(input)).toEqual(input);
});
test("structuredClone should work with array of mixed primitives", () => {
const input = [1, "hello", true, false, null, undefined, 3.14];
const cloned = structuredClone(input);
expect(cloned).toEqual(input);
});
test("structuredClone should work with array of special numbers", () => {
const cloned = structuredClone([-0, NaN, Infinity, -Infinity]);
expect(Object.is(cloned[0], -0)).toBe(true);
expect(cloned[1]).toBeNaN();
expect(cloned[2]).toBe(Infinity);
expect(cloned[3]).toBe(-Infinity);
});
test("structuredClone should work with large array of numbers", () => {
const input = Array.from({ length: 10000 }, (_, i) => i);
expect(structuredClone(input)).toEqual(input);
});
test("structuredClone should work with array of simple objects", () => {
const input = [
{ a: 1, b: "hello" },
{ a: 2, b: "world" },
];
expect(structuredClone(input)).toEqual(input);
});
test("structuredClone should work with large array of same-shape objects", () => {
const input = Array.from({ length: 100 }, (_, i) => ({ id: i, name: `item-${i}`, active: i % 2 === 0 }));
expect(structuredClone(input)).toEqual(input);
});
test("structuredClone should work with array of mixed elements and objects", () => {
const input = [1, "hello", { a: 1 }, true, { b: "world" }];
expect(structuredClone(input)).toEqual(input);
});
test("structuredClone should work with array of objects with different shapes", () => {
const input = [{ a: 1 }, { b: "hello", c: true }, { x: 42 }];
expect(structuredClone(input)).toEqual(input);
});
test("structuredClone should fallback for array with nested objects inside objects", () => {
// nested object inside object → normal path (still correct)
const input = [{ a: { b: 1 } }];
expect(structuredClone(input)).toEqual(input);
});
test("structuredClone creates independent copies of objects in array", () => {
const input = [{ a: 1 }, { a: 2 }];
const cloned = structuredClone(input);
cloned[0].a = 999;
expect(input[0].a).toBe(1);
});
test("postMessage with array of objects via MessageChannel", async () => {
const { port1, port2 } = new MessageChannel();
const input = [
{ id: 1, name: "Alice" },
{ id: 2, name: "Bob" },
];
const { promise, resolve } = Promise.withResolvers();
port2.onmessage = (e: MessageEvent) => resolve(e.data);
port1.postMessage(input);
const result = await promise;
expect(result).toEqual(input);
port1.close();
port2.close();
});
test("structuredClone should fallback for arrays with holes", () => {
const input = [1, , 3]; // sparse
const cloned = structuredClone(input);
// structured clone spec: holes become undefined
expect(cloned[0]).toBe(1);
expect(cloned[1]).toBe(undefined);
expect(cloned[2]).toBe(3);
});
test("structuredClone should work with array of doubles", () => {
const input = [1.5, 2.7, 3.14, 0.1 + 0.2];
const cloned = structuredClone(input);
expect(cloned).toEqual(input);
});
test("structuredClone creates independent copy of array", () => {
const input = [1, 2, 3];
const cloned = structuredClone(input);
cloned[0] = 999;
expect(input[0]).toBe(1);
});
test("structuredClone should preserve named properties on arrays", () => {
const input: any = [1, 2, 3];
input.foo = "bar";
const cloned = structuredClone(input);
expect(cloned.foo).toBe("bar");
expect(Array.from(cloned)).toEqual([1, 2, 3]);
});
test("postMessage should work with array fast path", async () => {
const { port1, port2 } = new MessageChannel();
const input = [1, 2, 3, "hello", true];
const { promise, resolve } = Promise.withResolvers();
port2.onmessage = (e: MessageEvent) => resolve(e.data);
port1.postMessage(input);
const result = await promise;
expect(result).toEqual(input);
port1.close();
port2.close();
});
// === Edge case tests ===
test("structuredClone of frozen array should produce a non-frozen clone", () => {
const input = Object.freeze([1, 2, 3]);
const cloned = structuredClone(input);
expect(cloned).toEqual([1, 2, 3]);
expect(Object.isFrozen(cloned)).toBe(false);
cloned[0] = 999;
expect(cloned[0]).toBe(999);
});
test("structuredClone of sealed array should produce a non-sealed clone", () => {
const input = Object.seal([1, 2, 3]);
const cloned = structuredClone(input);
expect(cloned).toEqual([1, 2, 3]);
expect(Object.isSealed(cloned)).toBe(false);
cloned.push(4);
expect(cloned).toEqual([1, 2, 3, 4]);
});
test("structuredClone of array with deleted element (hole via delete)", () => {
const input = [1, 2, 3];
delete (input as any)[1];
const cloned = structuredClone(input);
expect(cloned[0]).toBe(1);
expect(cloned[1]).toBe(undefined);
expect(cloned[2]).toBe(3);
expect(1 in cloned).toBe(false); // holes remain holes after structuredClone
});
test("structuredClone of array with length > actual elements", () => {
const input = [1, 2, 3];
input.length = 6;
const cloned = structuredClone(input);
expect(cloned.length).toBe(6);
expect(cloned[0]).toBe(1);
expect(cloned[1]).toBe(2);
expect(cloned[2]).toBe(3);
expect(cloned[3]).toBe(undefined);
});
test("structuredClone of single element arrays", () => {
expect(structuredClone([42])).toEqual([42]);
expect(structuredClone([3.14])).toEqual([3.14]);
expect(structuredClone(["hello"])).toEqual(["hello"]);
expect(structuredClone([true])).toEqual([true]);
expect(structuredClone([null])).toEqual([null]);
});
test("structuredClone of array with named properties on Int32 array", () => {
const input: any = [1, 2, 3]; // Int32 indexing
input.name = "test";
input.count = 42;
const cloned = structuredClone(input);
expect(cloned.name).toBe("test");
expect(cloned.count).toBe(42);
expect(Array.from(cloned)).toEqual([1, 2, 3]);
});
test("structuredClone of array with named properties on Double array", () => {
const input: any = [1.1, 2.2, 3.3]; // Double indexing
input.label = "doubles";
const cloned = structuredClone(input);
expect(cloned.label).toBe("doubles");
expect(Array.from(cloned)).toEqual([1.1, 2.2, 3.3]);
});
test("structuredClone of array that transitions Int32 to Double", () => {
const input = [1, 2, 3]; // starts as Int32
input.push(4.5); // transitions to Double
const cloned = structuredClone(input);
expect(cloned).toEqual([1, 2, 3, 4.5]);
});
test("structuredClone of array with modified prototype", () => {
const input = [1, 2, 3];
Object.setPrototypeOf(input, {
customMethod() {
return 42;
},
});
const cloned = structuredClone(input);
// Clone should have standard Array prototype, not the custom one
expect(Array.from(cloned)).toEqual([1, 2, 3]);
expect(cloned).toBeInstanceOf(Array);
expect((cloned as any).customMethod).toBeUndefined();
});
test("structuredClone of array with prototype indexed properties and holes", () => {
const proto = Object.create(Array.prototype);
proto[1] = "from proto";
const input = new Array(3);
Object.setPrototypeOf(input, proto);
input[0] = "a";
input[2] = "c";
// structuredClone only copies own properties; prototype values are not included
const cloned = structuredClone(input);
expect(cloned[0]).toBe("a");
expect(1 in cloned).toBe(false); // hole, not "from proto"
expect(cloned[2]).toBe("c");
expect(cloned).toBeInstanceOf(Array);
});
test("postMessage with Int32 array via MessageChannel", async () => {
const { port1, port2 } = new MessageChannel();
const input = [10, 20, 30, 40, 50];
const { promise, resolve } = Promise.withResolvers();
port2.onmessage = (e: MessageEvent) => resolve(e.data);
port1.postMessage(input);
const result = await promise;
expect(result).toEqual(input);
port1.close();
port2.close();
});
test("postMessage with Double array via MessageChannel", async () => {
const { port1, port2 } = new MessageChannel();
const input = [1.1, 2.2, 3.3];
const { promise, resolve } = Promise.withResolvers();
port2.onmessage = (e: MessageEvent) => resolve(e.data);
port1.postMessage(input);
const result = await promise;
expect(result).toEqual(input);
port1.close();
port2.close();
});
test("structuredClone of array multiple times produces independent copies", () => {
const input = [1, 2, 3];
const clones = Array.from({ length: 10 }, () => structuredClone(input));
clones[0][0] = 999;
clones[5][1] = 888;
// All other clones and the original should be unaffected
expect(input).toEqual([1, 2, 3]);
for (let i = 1; i < 10; i++) {
if (i === 5) {
expect(clones[i]).toEqual([1, 888, 3]);
} else {
expect(clones[i]).toEqual([1, 2, 3]);
}
}
});
test("structuredClone of Array subclass loses subclass identity", () => {
class MyArray extends Array {
customProp = "hello";
sum() {
return this.reduce((a: number, b: number) => a + b, 0);
}
}
const input = new MyArray(1, 2, 3);
input.customProp = "world";
const cloned = structuredClone(input);
// structuredClone spec: result is a plain Array, not a subclass
expect(Array.from(cloned)).toEqual([1, 2, 3]);
expect(cloned).toBeInstanceOf(Array);
expect((cloned as any).sum).toBeUndefined();
});
test("structuredClone of array with only undefined values", () => {
const input = [undefined, undefined, undefined];
const cloned = structuredClone(input);
expect(cloned).toEqual([undefined, undefined, undefined]);
expect(cloned.length).toBe(3);
// Ensure they are actual values, not holes
expect(0 in cloned).toBe(true);
expect(1 in cloned).toBe(true);
expect(2 in cloned).toBe(true);
});
test("structuredClone of array with only null values", () => {
const input = [null, null, null];
const cloned = structuredClone(input);
expect(cloned).toEqual([null, null, null]);
});
test("structuredClone of dense double array preserves -0 and NaN", () => {
const input = [-0, NaN, -0, NaN];
const cloned = structuredClone(input);
expect(Object.is(cloned[0], -0)).toBe(true);
expect(cloned[1]).toBeNaN();
expect(Object.is(cloned[2], -0)).toBe(true);
expect(cloned[3]).toBeNaN();
});
test("structuredClone on object with simple properties can exceed JSFinalObject::maxInlineCapacity", () => {
let largeValue = {};
for (let i = 0; i < 100; i++) {
largeValue["property" + i] = i;
}
for (let i = 0; i < 100; i++) {
expect(structuredClone(largeValue)).toStrictEqual(largeValue);
}
Bun.gc(true);
for (let i = 0; i < 100; i++) {
expect(structuredClone(largeValue)).toStrictEqual(largeValue);
}
});
// === DenseArray fast path edge case tests ===
test("array of empty objects", () => {
const input = [{}, {}, {}];
const cloned = structuredClone(input);
expect(cloned).toEqual(input);
expect(cloned[0]).not.toBe(input[0]);
});
test("array with single object element", () => {
const input = [{ key: "value" }];
const cloned = structuredClone(input);
expect(cloned).toEqual(input);
cloned[0].key = "modified";
expect(input[0].key).toBe("value");
});
test("objects with special number property values", () => {
const input = [{ a: NaN, b: Infinity, c: -Infinity, d: -0 }];
const cloned = structuredClone(input);
expect(cloned[0].a).toBeNaN();
expect(cloned[0].b).toBe(Infinity);
expect(cloned[0].c).toBe(-Infinity);
expect(Object.is(cloned[0].d, -0)).toBe(true);
});
test("objects with null and undefined property values", () => {
const input = [
{ a: null, b: undefined },
{ a: null, b: undefined },
];
const cloned = structuredClone(input);
expect(cloned).toEqual(input);
expect(cloned[0].a).toBeNull();
expect(cloned[0].b).toBeUndefined();
expect("b" in cloned[0]).toBe(true);
});
test("objects with empty string keys and values", () => {
const input = [{ "": "" }, { "": "nonempty" }];
const cloned = structuredClone(input);
expect(cloned).toEqual(input);
});
test("objects with many properties exceeding maxInlineCapacity", () => {
const obj: Record<string, number> = {};
for (let i = 0; i < 100; i++) obj[`p${i}`] = i;
const input = [obj, { ...obj }];
const cloned = structuredClone(input);
expect(cloned).toEqual(input);
expect(Object.keys(cloned[0]).length).toBe(100);
});
test("alternating object shapes (cache invalidation)", () => {
// shape A, shape B, shape A, shape B — cache should not corrupt results
const input = [{ x: 1, y: 2 }, { name: "hello" }, { x: 3, y: 4 }, { name: "world" }];
const cloned = structuredClone(input);
expect(cloned).toEqual(input);
});
test("objects followed by primitives followed by objects", () => {
const input = [{ a: 1 }, 42, null, { b: "two" }, "str", { c: true }];
const cloned = structuredClone(input);
expect(cloned).toEqual(input);
});
test("fallback: array with object containing array property value", () => {
const input = [{ foo: [1, 2, 3] }];
const cloned = structuredClone(input);
expect(cloned).toEqual(input);
cloned[0].foo[0] = 999;
expect(input[0].foo[0]).toBe(1);
});
test("fallback: array with object containing nested object property value", () => {
const input = [{ a: 1 }, { b: { nested: true } }];
const cloned = structuredClone(input);
expect(cloned).toEqual(input);
cloned[1].b.nested = false;
expect(input[1].b.nested).toBe(true);
});
test("fallback: array with Date object", () => {
const now = new Date();
const input = [{ a: 1 }, now];
const cloned = structuredClone(input);
expect(cloned[0]).toEqual({ a: 1 });
expect(cloned[1]).toEqual(now);
expect(cloned[1]).toBeInstanceOf(Date);
});
test("fallback: array with RegExp object", () => {
const input = [{ a: 1 }, /test/gi];
const cloned = structuredClone(input);
expect(cloned[0]).toEqual({ a: 1 });
expect(cloned[1]).toEqual(/test/gi);
});
test("fallback: array with Map", () => {
const input = [new Map([["key", "value"]])];
const cloned = structuredClone(input);
expect(cloned[0].get("key")).toBe("value");
});
test("fallback: array with Set", () => {
const input = [new Set([1, 2, 3])];
const cloned = structuredClone(input);
expect(cloned[0]).toEqual(new Set([1, 2, 3]));
});
test("fallback: object with getter in array", () => {
const obj = Object.defineProperty({}, "x", { get: () => 42, enumerable: true, configurable: true });
const input = [obj];
const cloned = structuredClone(input);
expect(cloned[0].x).toBe(42);
});
test("fallback: object with non-enumerable property in array", () => {
const obj = Object.defineProperty({ a: 1 }, "hidden", { value: 2, enumerable: false });
const input = [obj];
const cloned = structuredClone(input);
expect(cloned[0].a).toBe(1);
// non-enumerable property should not be cloned by structuredClone
expect(cloned[0].hidden).toBeUndefined();
});
test("frozen objects in array produce non-frozen clones", () => {
const input = [Object.freeze({ a: 1, b: "hello" }), Object.freeze({ a: 2, b: "world" })];
const cloned = structuredClone(input);
expect(cloned).toEqual([
{ a: 1, b: "hello" },
{ a: 2, b: "world" },
]);
expect(Object.isFrozen(cloned[0])).toBe(false);
cloned[0].a = 999;
expect(cloned[0].a).toBe(999);
});
test("sealed objects in array produce non-sealed clones", () => {
const input = [Object.seal({ x: 10 })];
const cloned = structuredClone(input);
expect(cloned).toEqual([{ x: 10 }]);
expect(Object.isSealed(cloned[0])).toBe(false);
});
test("object with numeric string keys in array", () => {
const input = [{ "0": "a", "1": "b", "2": "c" }];
const cloned = structuredClone(input);
expect(cloned).toEqual(input);
});
test("repeated structuredClone of same array of objects", () => {
const input = [
{ id: 1, name: "Alice" },
{ id: 2, name: "Bob" },
];
const clones = Array.from({ length: 10 }, () => structuredClone(input));
// Mutate one clone
clones[0][0].id = 999;
clones[3][1].name = "Charlie";
// Original and other clones unaffected
expect(input[0].id).toBe(1);
expect(input[1].name).toBe("Bob");
expect(clones[1][0].id).toBe(1);
expect(clones[5][1].name).toBe("Bob");
});
test("postMessage with mixed array of objects and primitives", async () => {
const { port1, port2 } = new MessageChannel();
const input = [42, { x: "hello" }, true, { y: 3.14 }, null];
const { promise, resolve } = Promise.withResolvers();
port2.onmessage = (e: MessageEvent) => resolve(e.data);
port1.postMessage(input);
const result = await promise;
expect(result).toEqual(input);
port1.close();
port2.close();
});
test("postMessage with array of empty objects", async () => {
const { port1, port2 } = new MessageChannel();
const input = [{}, {}, {}];
const { promise, resolve } = Promise.withResolvers();
port2.onmessage = (e: MessageEvent) => resolve(e.data);
port1.postMessage(input);
const result = await promise;
expect(result).toEqual(input);
port1.close();
port2.close();
});
test("fallback: array with ArrayBuffer", () => {
const buf = new ArrayBuffer(8);
new Uint8Array(buf).set([1, 2, 3, 4, 5, 6, 7, 8]);
const input = [{ a: 1 }, buf];
const cloned = structuredClone(input);
expect(cloned[0]).toEqual({ a: 1 });
expect(new Uint8Array(cloned[1] as ArrayBuffer)).toEqual(new Uint8Array([1, 2, 3, 4, 5, 6, 7, 8]));
});
test("fallback: object created with Object.create(null) in array", () => {
const obj = Object.create(null);
obj.a = 1;
obj.b = "hello";
const input = [obj];
const cloned = structuredClone(input);
expect(cloned[0].a).toBe(1);
expect(cloned[0].b).toBe("hello");
});
test("fallback: class instance in array", () => {
class Foo {
constructor(public x: number) {}
}
const input = [new Foo(42)];
const cloned = structuredClone(input);
expect(cloned[0].x).toBe(42);
expect(cloned[0]).not.toBeInstanceOf(Foo);
});
test("object with boolean property values in array", () => {
const input = [
{ enabled: true, visible: false },
{ enabled: false, visible: true },
];
const cloned = structuredClone(input);
expect(cloned).toEqual(input);
expect(cloned[0].enabled).toBe(true);
expect(cloned[1].visible).toBe(true);
});
test("object with only string values in array", () => {
const input = [
{ first: "Alice", last: "Smith" },
{ first: "Bob", last: "Jones" },
];
const cloned = structuredClone(input);
expect(cloned).toEqual(input);
});
test("large array of objects with same shape (structure cache stress)", () => {
const input = Array.from({ length: 1000 }, (_, i) => ({ id: i, value: `v${i}` }));
const cloned = structuredClone(input);
expect(cloned).toEqual(input);
cloned[500].id = -1;
expect(input[500].id).toBe(500);
});
// === Additional DenseArray edge case tests ===
test("shared object reference in source array preserves identity", () => {
const shared = { x: 1, y: "hello" };
const input = [shared, shared, shared];
const cloned = structuredClone(input);
expect(cloned).toEqual(input);
// structuredClone preserves shared references per the HTML spec
expect(cloned[0]).toBe(cloned[1]);
expect(cloned[1]).toBe(cloned[2]);
expect(cloned[0]).not.toBe(shared);
// Mutating one should affect all shared references
cloned[0].x = 999;
expect(cloned[1].x).toBe(999);
expect(cloned[2].x).toBe(999);
});
test("objects with long string property values", () => {
const longStr = Buffer.alloc(10000, "x").toString();
const input = [
{ data: longStr, id: 1 },
{ data: longStr, id: 2 },
];
const cloned = structuredClone(input);
expect(cloned).toEqual(input);
expect(cloned[0].data.length).toBe(10000);
expect(cloned[1].data.length).toBe(10000);
});
test("objects with unicode property names and values", () => {
const input = [
{ "\u{1F600}": "smile", name: "\u{1F4A9}" },
{ "\u{1F600}": "grin", name: "\u{2764}" },
];
const cloned = structuredClone(input);
expect(cloned).toEqual(input);
});
test("first element is primitive, objects appear later in array", () => {
// Ensures structure cache initializes correctly when first element is not an object
const input = [1, 2, "hello", { a: 1, b: 2 }, { a: 3, b: 4 }, { a: 5, b: 6 }];
const cloned = structuredClone(input);
expect(cloned).toEqual(input);
});
test("structure cache miss: first shape has many props, second has few", () => {
const big = { a: 1, b: 2, c: 3, d: 4, e: 5 };
const small = { x: 1 };
const input = [big, small, big, small];
const cloned = structuredClone(input);
expect(cloned).toEqual(input);
});
test("structure cache miss: same property count but different names", () => {
const input = [
{ a: 1, b: 2 },
{ x: 1, y: 2 },
{ a: 3, b: 4 },
{ x: 3, y: 4 },
];
const cloned = structuredClone(input);
expect(cloned).toEqual(input);
});
test("fallback: object with Symbol property in array", () => {
const sym = Symbol("test");
const obj: any = { a: 1 };
obj[sym] = "symbol-value";
const input = [obj];
// structuredClone should handle this correctly (Symbols are not cloned)
const cloned = structuredClone(input);
expect(cloned[0].a).toBe(1);
expect(cloned[0][sym]).toBeUndefined();
});
test("fallback: object with BigInt property value in array", () => {
const input = [{ value: 42n }];
const cloned = structuredClone(input);
expect(cloned[0].value).toBe(42n);
});
test("array of objects survives GC", () => {
const input = Array.from({ length: 100 }, (_, i) => ({ id: i, name: `item-${i}`, flag: i % 2 === 0 }));
const clones: any[] = [];
for (let i = 0; i < 50; i++) {
clones.push(structuredClone(input));
if (i % 10 === 0) Bun.gc(true);
}
Bun.gc(true);
// Verify all clones are still valid after GC
for (const clone of clones) {
expect(clone.length).toBe(100);
expect(clone[0]).toEqual({ id: 0, name: "item-0", flag: true });
expect(clone[99]).toEqual({ id: 99, name: "item-99", flag: false });
}
});
test("structuredClone of array of objects does not crash under repeated GC", () => {
const input = Array.from({ length: 50 }, (_, i) => ({
id: i,
name: `item-${i}`,
active: i % 2 === 0,
}));
for (let i = 0; i < 200; i++) {
const cloned = structuredClone(input);
expect(cloned.length).toBe(50);
if (i % 20 === 0) Bun.gc(true);
}
});
test("SerializedScriptValue can be deserialized multiple times (postMessage to two ports)", async () => {
// Verify the serialized value is not consumed after first deserialization
const { port1: p1a, port2: p1b } = new MessageChannel();
const { port1: p2a, port2: p2b } = new MessageChannel();
const input = [
{ id: 1, val: "one" },
{ id: 2, val: "two" },
];
const { promise: promise1, resolve: resolve1 } = Promise.withResolvers();
const { promise: promise2, resolve: resolve2 } = Promise.withResolvers();
p1b.onmessage = (e: MessageEvent) => resolve1(e.data);
p2b.onmessage = (e: MessageEvent) => resolve2(e.data);
// structuredClone for each postMessage creates separate serialized values,
// but let's verify concurrent postMessage works
p1a.postMessage(input);
p2a.postMessage(input);
const [result1, result2] = await Promise.all([promise1, promise2]);
expect(result1).toEqual(input);
expect(result2).toEqual(input);
p1a.close();
p1b.close();
p2a.close();
p2b.close();
});
test("array of objects with all-integer property values", () => {
const input = Array.from({ length: 20 }, (_, i) => ({ a: i, b: i * 2, c: i * 3 }));
const cloned = structuredClone(input);
expect(cloned).toEqual(input);
});
test("array of objects with all-string property values", () => {
const input = Array.from({ length: 20 }, (_, i) => ({
first: `first-${i}`,
last: `last-${i}`,
}));
const cloned = structuredClone(input);
expect(cloned).toEqual(input);
});
test("array of objects with all-boolean property values", () => {
const input = Array.from({ length: 20 }, (_, i) => ({
a: i % 2 === 0,
b: i % 3 === 0,
}));
const cloned = structuredClone(input);
expect(cloned).toEqual(input);
});
test("fallback: array containing both simple objects and TypedArray", () => {
const input = [{ a: 1 }, new Uint8Array([1, 2, 3])];
const cloned = structuredClone(input);
expect(cloned[0]).toEqual({ a: 1 });
expect(new Uint8Array(cloned[1] as any)).toEqual(new Uint8Array([1, 2, 3]));
});
test("fallback: array containing object with function-like structure", () => {
// Proxy objects that look object-like but aren't FinalObjectType
const input = [
{ a: 1 },
new (class MyObj {
x = 42;
})(),
];
const cloned = structuredClone(input);
expect(cloned[0]).toEqual({ a: 1 });
expect(cloned[1]).toEqual({ x: 42 });
});
test("object property ordering is preserved after clone", () => {
const input = [
{ z: 1, a: 2, m: 3 },
{ z: 4, a: 5, m: 6 },
];
const cloned = structuredClone(input);
expect(Object.keys(cloned[0])).toEqual(["z", "a", "m"]);
expect(Object.keys(cloned[1])).toEqual(["z", "a", "m"]);
expect(cloned).toEqual(input);
});
test("postMessage with large array of same-shape objects via MessageChannel", async () => {
const { port1, port2 } = new MessageChannel();
const input = Array.from({ length: 500 }, (_, i) => ({ id: i, name: `item-${i}` }));
const { promise, resolve } = Promise.withResolvers();
port2.onmessage = (e: MessageEvent) => resolve(e.data);
port1.postMessage(input);
const result = await promise;
expect(result).toEqual(input);
port1.close();
port2.close();
});
test("postMessage with alternating shapes via MessageChannel", async () => {
const { port1, port2 } = new MessageChannel();
const input = [{ a: 1 }, { x: "hello", y: true }, { a: 2 }, { x: "world", y: false }];
const { promise, resolve } = Promise.withResolvers();
port2.onmessage = (e: MessageEvent) => resolve(e.data);
port1.postMessage(input);
const result = await promise;
expect(result).toEqual(input);
port1.close();
port2.close();
});
});