Skip to content
Open
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
17 changes: 12 additions & 5 deletions javascript/packages/core/lib/fory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ import {
TypeId,
CustomSerializer,
} from "./type";
import { InputType, ResultType, TypeInfo } from "./typeInfo";
import { containerDeclaresElementTypes, InputType, ResultType, TypeInfo } from "./typeInfo";
import { Gen } from "./gen";
import { PlatformBuffer } from "./platformBuffer";
import { ReadContext, WriteContext } from "./context";
Expand Down Expand Up @@ -163,7 +163,12 @@ export default class Fory {
serializer = new Gen(this.typeResolver, {
customSerializer,
}).generateSerializer(typeInfo);
this.typeResolver.registerSerializer(typeInfo, serializer);
if (!containerDeclaresElementTypes(typeInfo)) {
// A declared-element container serializer is bound to this
// registration only; publishing it under the bare container type id
// would replace the dynamic container serializer.
this.typeResolver.registerSerializer(typeInfo, serializer);
}
}
return {
serializer,
Expand Down Expand Up @@ -223,9 +228,11 @@ export default class Fory {
}
const readContext = this.readContext;
const reader = readContext.reader;
const rootSerializer = TypeId.polymorphicType(serializer.getTypeId())
? serializer
: this.anySerializer;
const rootSerializer =
TypeId.polymorphicType(serializer.getTypeId()) ||
containerDeclaresElementTypes(serializer.getTypeInfo())
? serializer
: this.anySerializer;
const rootHeader = ConfigFlags.isCrossLanguageFlag;
rootDeserializer = (bytes: Uint8Array) => {
readContext.reset(bytes);
Expand Down
21 changes: 15 additions & 6 deletions javascript/packages/core/lib/gen/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
*/

import { TypeId, Serializer } from "../type";
import { TypeInfo } from "../typeInfo";
import { containerDeclaresElementTypes, TypeInfo } from "../typeInfo";
import { CodegenRegistry } from "./router";
import { CodecBuilder } from "./builder";
import { Scope } from "./scope";
Expand Down Expand Up @@ -139,11 +139,14 @@ export class Gen {
this.traversalContainer(x);
});
this.register(typeInfo, this.generate(typeInfo));
} else if (!this.isRegistered(typeInfo) && TypeId.structType(typeInfo.typeId)) {
// Forward reference to a struct type not yet fully defined — register a
// placeholder so that serializer factories can capture the object
// reference. The placeholder will be filled in via Object.assign
// when the real serializer is generated later.
} else if (
!this.isRegistered(typeInfo) &&
(TypeId.structType(typeInfo.typeId) || TypeId.extType(typeInfo.typeId))
) {
// Forward reference to a struct or ext type not yet fully defined —
// register a placeholder so that serializer factories can capture the
// object reference. The placeholder will be filled in via
// Object.assign when the real serializer is generated later.
this.register(typeInfo);
} else if (TypeId.enumType(typeInfo.typeId) && !this.isRegistered(typeInfo)) {
this.register(typeInfo, this.generate(typeInfo));
Expand Down Expand Up @@ -175,6 +178,12 @@ export class Gen {

generateSerializer(typeInfo: TypeInfo) {
this.traversalContainer(typeInfo);
if (containerDeclaresElementTypes(typeInfo)) {
// The type-id keyed registry only holds the dynamic container
// serializer; a container with declared element types gets a dedicated
// serializer for this registration.
return this.generate(typeInfo);

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.

Registering the container before its extension codec now leaves the generated serializer bound to undefined, even when all registration finishes before the first root operation:

class E {}
Type.ext(901)(E);

const fory = new Fory({ compatible: false });
const list = fory.register(Type.list(Type.ext(901)));
fory.register(E, { write() {}, read() {} });
list.serialize([new E()]);

traversalContainer() creates forward placeholders for structs but not extensions, so ExtSerializerGenerator.writeEmbed() captures the missing serializer in a factory-level constant. Registering E afterward cannot update that constant, and serialization fails at ext_ser.writeTypeInfo(null). Sets have the same issue. The previous dynamic root serializer resolved the codec at write time.

Please preserve registration ordering before the first operation by ensuring the generated container binds to the completed extension codec, and add a regression test for this order.

}
const serializer = this.typeResolver.getSerializerByTypeInfo(typeInfo);
if (serializer?._initialized) {
return serializer;
Expand Down
26 changes: 26 additions & 0 deletions javascript/packages/core/lib/typeInfo.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,32 @@ import { Decimal } from "./types/decimal";
const targetFields = new WeakMap<new () => any, { [key: string]: TypeInfo }>();
export const MAX_FIELD_ID = (1 << 29) - 1;

/**
* Whether this container TypeInfo declares concrete element types instead of
* dynamic `any` elements. Such a TypeInfo is a usage schema for one
* registration: it needs its own generated serializer and must not replace
* the dynamic container serializer in the type-id keyed registry.
*/
export function containerDeclaresElementTypes(typeInfo: TypeInfo): boolean {
if (typeInfo.typeId === TypeId.LIST) {
const inner = typeInfo.options?.inner;
return inner !== undefined && inner.typeId !== TypeId.UNKNOWN;
}
if (typeInfo.typeId === TypeId.SET) {
const inner = typeInfo.options?.key;
return inner !== undefined && inner.typeId !== TypeId.UNKNOWN;
}
if (typeInfo.typeId === TypeId.MAP) {
const key = typeInfo.options?.key;
const value = typeInfo.options?.value;
return (
(key !== undefined && key.typeId !== TypeId.UNKNOWN) ||
(value !== undefined && value.typeId !== TypeId.UNKNOWN)
);
}
return false;
}

export function checkFieldId(fieldId: number) {
if (Number.isFinite(fieldId) && fieldId < 0) {
throw new Error("field id must be non-negative");
Expand Down
50 changes: 50 additions & 0 deletions javascript/test/array.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,56 @@ describe("array", () => {
expect(deserialize(serialize({ c: [o, o] }))).toEqual({ c: [o, o] });
});

test("should root list use declared element type", () => {
// A root Type.list(...) registration previously fell back to the internal
// any-typed list serializer, silently discarding declared element types:
// declared float32 must narrow, while dynamic dispatch keeps float64.
const fory = new Fory({ compatible: false });
const { serialize, deserialize } = fory.register(Type.list(Type.float32()));
expect(deserialize(serialize([0.1]))).toEqual([Math.fround(0.1)]);

// The dynamic list serializer must stay untouched by the registration.
expect(fory.deserialize(fory.serialize([0.1, "a"]))).toEqual([0.1, "a"]);
});

test("should root set use declared element type", () => {
const fory = new Fory({ compatible: false });
const { serialize, deserialize } = fory.register(Type.set(Type.float32()));
expect(deserialize(serialize(new Set([0.1])))).toEqual(new Set([Math.fround(0.1)]));
expect(fory.deserialize(fory.serialize(new Set([0.1, "a"])))).toEqual(new Set([0.1, "a"]));
});

test("should root container registered before its ext codec work", () => {
// Registration order is free before the first root operation: the
// container's forward ext placeholder must be filled when the extension
// codec registers later, so the generated serializer binds to the
// completed codec instead of capturing undefined.
class ListedExtension {
constructor(public id = 0) {}
}
Type.ext(921)(ListedExtension);
const extCodec = {
write(context: any, value: ListedExtension) {
context.writeUint8(value.id);
},
read(context: any, result: ListedExtension) {
result.id = context.readUint8();
},
};

const listFory = new Fory({ compatible: false });
const list = listFory.register(Type.list(Type.ext(921)));
listFory.register(ListedExtension, extCodec);
const listResult = list.deserialize(list.serialize([new ListedExtension(7)]));
expect(listResult).toEqual([new ListedExtension(7)]);

const setFory = new Fory({ compatible: false });
const set = setFory.register(Type.set(Type.ext(921)));
setFory.register(ListedExtension, extCodec);
const setResult = set.deserialize(set.serialize(new Set([new ListedExtension(9)])));
expect(setResult).toEqual(new Set([new ListedExtension(9)]));
});

test("preserves a self-reference in a dynamic list", () => {
const fory = new Fory({ compatible: false, ref: true });
const value: any[] = [];
Expand Down
14 changes: 14 additions & 0 deletions javascript/test/map.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,20 @@ describe("map", () => {
});
});

test("should root map use declared key and value types", () => {
// A root Type.map(...) registration previously fell back to the internal
// any-typed map serializer, silently discarding declared key/value types:
// declared float32 must narrow, while dynamic dispatch keeps float64.
const fory = new Fory({ compatible: false });
const { serialize, deserialize } = fory.register(Type.map(Type.string(), Type.float32()));
expect(deserialize(serialize(new Map([["a", 0.1]])))).toEqual(
new Map([["a", Math.fround(0.1)]]),
);

// The dynamic map serializer must stay untouched by the registration.
expect(fory.deserialize(fory.serialize(new Map([[1, "x"]])))).toEqual(new Map([[1, "x"]]));
});

test("preserves shared dynamic map entries", () => {
const fory = new Fory({ compatible: false, ref: true });
@Type.struct(301, {
Expand Down
Loading