Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions javascript/packages/core/lib/gen/collection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -241,6 +241,7 @@ export class CollectionAnySerializer {
this.writeElementsHeader(value);
if (isSame) {
serializer!.writeTypeInfo(sample);
this.writeContext.writer.reserve((serializer!.fixedSize + 1) * size);
if (trackingRef) {
for (const item of value) {
if (!serializer!.writeRefOrNull(item)) {
Expand All @@ -262,12 +263,16 @@ export class CollectionAnySerializer {
}
}
} else {
// Mixed-type elements resolve a serializer per item, so capacity is
// reserved per item; the upfront byte per element covers null flags.
this.writeContext.writer.reserve(size);
if (trackingRef) {
for (const item of value) {
if (item === null || item === undefined) {
this.writeContext.writer.writeInt8(RefFlags.NullFlag);
} else {
const serializer = this.writeContext.typeResolver.getSerializerByData(item);
this.writeContext.writer.reserve(serializer!.fixedSize);
serializer!.writeRef(item);
}
}
Expand All @@ -277,13 +282,15 @@ export class CollectionAnySerializer {
this.writeContext.writer.writeInt8(RefFlags.NullFlag);
} else {
const serializer = this.writeContext.typeResolver.getSerializerByData(item);
this.writeContext.writer.reserve(serializer!.fixedSize);
this.writeContext.writer.writeInt8(RefFlags.NotNullValueFlag);
serializer!.writeNoRef(item);
}
}
} else {
for (const item of value) {
const serializer = this.writeContext.typeResolver.getSerializerByData(item);
this.writeContext.writer.reserve(serializer!.fixedSize);
serializer!.writeNoRef(item);
}
}
Expand All @@ -307,6 +314,7 @@ export class CollectionAnySerializer {
}
}
this.writeContext.writer.writeUint8(flags);
this.writeContext.writer.reserve((serializer.fixedSize + 1) * size);
if (flags & CollectionFlags.TRACKING_REF) {
for (const item of value) {
if (!serializer.writeRefOrNull(item)) {
Expand Down
8 changes: 8 additions & 0 deletions javascript/packages/core/lib/gen/map.ts
Original file line number Diff line number Diff line change
Expand Up @@ -209,6 +209,11 @@ export class MapAnySerializer {
this.valueSerializer !== null
? this.valueSerializer
: this.writeContext.typeResolver.getSerializerByData(v);
this.writeContext.writer.reserve(
(keySerializer ? keySerializer.fixedSize : 1) +
(valueSerializer ? valueSerializer.fixedSize : 1) +
2,
);

const header = mapChunkWriter.next(
new ElementInfo(
Expand Down Expand Up @@ -421,6 +426,9 @@ export class MapSerializerGenerator extends BaseSerializerGenerator {

return `
${this.builder.writer.writeVarUint32Small7(`${accessor}.size`)}
${this.builder.writer.reserve(
`${this.keyGenerator.getFixedSize() + this.valueGenerator.getFixedSize() + 2} * ${accessor}.size`,
)};
let ${lastKeyIsNull} = false;
let ${lastValueIsNull} = false;
let ${chunkSize} = 0;
Expand Down
41 changes: 41 additions & 0 deletions javascript/test/array.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -323,6 +323,47 @@ describe("array", () => {
);
expect(containsBytes(bfloat16Bytes, [0x80, 0x3f, 0x00, 0xc0])).toBe(true);
});

test("should large any-typed list work", () => {
// The dynamic element write path must reserve writer capacity per item.
// Without it, single-byte writes past the buffer end were silent no-ops
// while the cursor advanced, so dump() returned uninitialized tail bytes.
const fory = new Fory({ compatible: false });
const { serialize, deserialize } = fory.register(Type.list(Type.any()));
const arr = new Array(150000).fill(1);

@chaokunyang chaokunyang Aug 30, 2026

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Because every element is 1, writeElementsHeader sets isSame = true, so this test exercises only the aggregate reserve. The per-item mixed-type reserves and the writeDeclared reserve can still be removed without this test failing. Please add a list larger than the initial writer buffer with mixed element types; if writeDeclared is part of this fix, cover the unknown-Struct declared-list reserialization path as well.

const result = deserialize(serialize(arr)) as number[];
expect(result.length).toBe(150000);
expect(result.every((x) => x === 1)).toBe(true);
});

test("should large mixed-type list work", () => {
// Mixed element types disable the same-type aggregate reserve, so this
// exercises the per-item reserves in the dynamic write loops, with and
// without null elements. Numeric elements only: string bodies reserve
// internally, which would mask a missing per-item reserve.
const fory = new Fory({ compatible: false });
const { serialize, deserialize } = fory.register(Type.list(Type.any()));
const arr: (number | bigint | null)[] = [];
for (let i = 0; i < 50000; i++) {
arr.push(i, BigInt(i), i % 100 === 0 ? null : -i);
}
expect(deserialize(serialize(arr))).toEqual(arr);

const noNulls = arr.filter((x) => x !== null);
expect(deserialize(serialize(noNulls))).toEqual(noNulls);
});

test("should reserialize unknown struct with a large declared list", () => {
// Reserializing an unknown compatible struct writes declared list fields
// through CollectionAnySerializer.writeDeclared, which must reserve
// writer capacity for the whole list body.
const writerFory = new Fory({ compatible: true });
const readerFory = new Fory({ compatible: true });
const writer = writerFory.register(Type.struct(7501, { values: Type.list(Type.int32()) }));
const values = new Array(30000).fill(123456789);
const unknown = readerFory.deserialize(writer.serialize({ values }));
expect(writer.deserialize(readerFory.serialize(unknown))).toEqual({ values });
});
});

function containsBytes(bytes: Uint8Array, needle: number[]) {
Expand Down
32 changes: 32 additions & 0 deletions javascript/test/map.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -213,4 +213,36 @@ describe("map", () => {
expect(serializer.deserialize(valid)).toEqual(value);
}
});

test("should large declared map work", () => {
// The generated map write must reserve writer capacity for its entries.
// Without it, unchecked DataView writes past the buffer end threw a
// RangeError once the map body outgrew the initial buffer.
const fory = new Fory({ compatible: false });
const { serialize, deserialize } = fory.register(
Type.struct(
{ namespace: "example", typeName: "BigMap" },
{ m: Type.map(Type.int32({ encoding: "fixed" }), Type.int32({ encoding: "fixed" })) },

@chaokunyang chaokunyang Aug 30, 2026

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This declared int32 map goes through writeSpecificType, so it does not execute the new reserve in MapAnySerializer.write. Please add a Type.map(Type.any(), Type.any()) case large enough to exceed the initial writer buffer, so removing that reserve makes the regression test fail.

),
);
const m = new Map<number, number>();
for (let i = 0; i < 30000; i++) {
m.set(i, i + 1);
}
expect(deserialize(serialize({ m })).m.get(29999)).toBe(30000);
});

test("should large any-typed map work", () => {
// A map with dynamic key/value types writes through MapAnySerializer,
// which must reserve writer capacity per entry.
// Numeric entries only: string bodies reserve internally, which would
// mask a missing per-entry reserve.
const fory = new Fory({ compatible: false });
const { serialize, deserialize } = fory.register(Type.map(Type.any(), Type.any()));
const m = new Map<any, any>();
for (let i = 0; i < 30000; i++) {
m.set(i, i % 2 === 0 ? BigInt(i) : i * 3);
}
expect(deserialize(serialize(m))).toEqual(m);
});
});
Loading