This is an automated email from the ASF dual-hosted git repository.
wangweipeng pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/fory.git
The following commit(s) were added to refs/heads/main by this push:
new b734daa81 feat(JavaScript): support ForyField (#3314)
b734daa81 is described below
commit b734daa81398f34c105ae75ab98522401b832e04
Author: weipeng <[email protected]>
AuthorDate: Mon Feb 9 17:01:48 2026 +0800
feat(JavaScript): support ForyField (#3314)
## Why?
## What does this PR do?
1. Support `ForyField` decoration for config `nullable` and
`trackingRef`
2. More testcase were passed, the current progress is 23/39.
## Related issues
## Does this PR introduce any user-facing change?
- [ ] Does this PR introduce any public API change?
- [ ] Does this PR introduce any binary protocol compatibility change?
## Benchmark
---
javascript/packages/fory/index.ts | 2 +
javascript/packages/fory/lib/fory.ts | 1 -
javascript/packages/fory/lib/gen/struct.ts | 4 +-
javascript/packages/fory/lib/meta/TypeMeta.ts | 20 +++++---
javascript/packages/fory/lib/type.ts | 1 -
javascript/packages/fory/lib/typeInfo.ts | 29 +++++++++++-
javascript/test/crossLanguage.test.ts | 68 +++++----------------------
7 files changed, 57 insertions(+), 68 deletions(-)
diff --git a/javascript/packages/fory/index.ts
b/javascript/packages/fory/index.ts
index a383a5548..00ea9b978 100644
--- a/javascript/packages/fory/index.ts
+++ b/javascript/packages/fory/index.ts
@@ -22,6 +22,7 @@ import {
TypeInfo,
ArrayTypeInfo,
Type,
+ ForyField,
} from "./lib/typeInfo";
import { Serializer, Mode } from "./lib/type";
import Fory from "./lib/fory";
@@ -36,6 +37,7 @@ export {
Type,
Mode,
BinaryWriter,
+ ForyField,
BinaryReader,
};
diff --git a/javascript/packages/fory/lib/fory.ts
b/javascript/packages/fory/lib/fory.ts
index fbef0e3ff..18ea876bb 100644
--- a/javascript/packages/fory/lib/fory.ts
+++ b/javascript/packages/fory/lib/fory.ts
@@ -59,7 +59,6 @@ export default class {
useSliceString: Boolean(config?.useSliceString),
hooks: config?.hooks || {},
mode: config?.mode || Mode.SchemaConsistent,
- classVersionHash: config?.classVersionHash || false,
};
}
diff --git a/javascript/packages/fory/lib/gen/struct.ts
b/javascript/packages/fory/lib/gen/struct.ts
index f86ab2180..6f8627442 100644
--- a/javascript/packages/fory/lib/gen/struct.ts
+++ b/javascript/packages/fory/lib/gen/struct.ts
@@ -164,7 +164,7 @@ class StructSerializerGenerator extends
BaseSerializerGenerator {
write(accessor: string): string {
const hash = this.typeMeta.computeStructHash();
return `
- ${this.builder.fory.config.classVersionHash ?
this.builder.writer.int32(hash) : ""}
+ ${!this.builder.fory.isCompatible() ? this.builder.writer.int32(hash) :
""}
${this.sortedProps.map(({ key, typeInfo }) => {
const InnerGeneratorClass = CodegenRegistry.get(typeInfo.typeId);
if (!InnerGeneratorClass) {
@@ -182,7 +182,7 @@ class StructSerializerGenerator extends
BaseSerializerGenerator {
const result = this.scope.uniqueName("result");
const hash = this.typeMeta.computeStructHash();
return `
- ${this.builder.fory.config.classVersionHash
+ ${!this.builder.fory.isCompatible()
? `
if(${this.builder.reader.int32()} !== ${hash}) {
throw new Error("Read class version is not consistent with ${hash} ")
diff --git a/javascript/packages/fory/lib/meta/TypeMeta.ts
b/javascript/packages/fory/lib/meta/TypeMeta.ts
index 8b5dce8d3..d132b5afe 100644
--- a/javascript/packages/fory/lib/meta/TypeMeta.ts
+++ b/javascript/packages/fory/lib/meta/TypeMeta.ts
@@ -132,6 +132,7 @@ interface InnerFieldInfo {
trackingRef: boolean;
nullable: boolean;
options?: InnerFieldInfoOptions;
+ fieldId?: number;
}
class FieldInfo {
constructor(
@@ -141,6 +142,7 @@ class FieldInfo {
public trackingRef = false,
public nullable = false,
public options: InnerFieldInfoOptions = {},
+ public fieldId?: number
) {
}
@@ -153,11 +155,11 @@ class FieldInfo {
}
hasFieldId() {
- return false; // todo not impl yet.
+ return typeof this.fieldId === "number";
}
getFieldId() {
- return 0;
+ return this.fieldId;
}
static writeTypeId(writer: BinaryWriter, typeInfo: InnerFieldInfo,
writeFlags = false) {
@@ -231,7 +233,10 @@ export class TypeMeta {
computeStructFingerprint(fields: FieldInfo[]) {
let fieldInfos = [];
for (const field of fields) {
- const typeId = field.getTypeId();
+ let typeId = field.getTypeId();
+ if (TypeId.userDefinedType(typeId)) {
+ typeId = TypeId.UNKNOWN;
+ }
let fieldIdentifier = "";
if (field.getFieldId()) {
fieldIdentifier = `${field.getFieldId()}`;
@@ -270,7 +275,7 @@ export class TypeMeta {
} else if (fieldTypeId === TypeId.NAMED_UNION || fieldTypeId ===
TypeId.TYPED_UNION) {
fieldTypeId = TypeId.UNION;
}
- const { trackingRef, nullable } =
structTypeInfo.options.fieldInfo?.[fieldName] || {};
+ const { trackingRef, nullable, id } =
structTypeInfo.options.fieldInfo?.[fieldName] || {};
return new FieldInfo(
fieldName,
fieldTypeId,
@@ -278,6 +283,7 @@ export class TypeMeta {
trackingRef,
nullable,
typeInfo.options,
+ id
);
});
}
@@ -355,7 +361,7 @@ export class TypeMeta {
}
// Read type ID
- const { typeId, userTypeId, trackingRef, nullable, options } =
this.readTypeId(reader);
+ const { typeId, userTypeId, trackingRef, nullable, options, fieldId } =
this.readTypeId(reader);
let fieldName: string;
if (encodingFlags === 3) {
@@ -367,7 +373,7 @@ export class TypeMeta {
fieldName = fieldDecoder.decode(reader, size + 1, encoding ||
Encoding.UTF_8);
}
- return new FieldInfo(fieldName, typeId, userTypeId, trackingRef, nullable,
options);
+ return new FieldInfo(fieldName, typeId, userTypeId, trackingRef, nullable,
options, fieldId);
}
private static readTypeId(reader: BinaryReader, readFlag = false):
InnerFieldInfo {
@@ -536,7 +542,7 @@ export class TypeMeta {
let encoded: Uint8Array | null = null;
if (fieldInfo.hasFieldId()) {
- size = fieldInfo.getFieldId();
+ size = fieldInfo.getFieldId()!;
encodingFlags = 3; // TAG_ID encoding
} else {
// Convert camelCase to snake_case for xlang compatibility
diff --git a/javascript/packages/fory/lib/type.ts
b/javascript/packages/fory/lib/type.ts
index 278157732..10d54d943 100644
--- a/javascript/packages/fory/lib/type.ts
+++ b/javascript/packages/fory/lib/type.ts
@@ -253,7 +253,6 @@ export interface Config {
afterCodeGenerated?: (code: string) => string;
};
mode: Mode;
- classVersionHash?: boolean;
}
export interface WithForyClsInfo {
diff --git a/javascript/packages/fory/lib/typeInfo.ts
b/javascript/packages/fory/lib/typeInfo.ts
index f3b846f9a..88696b15f 100644
--- a/javascript/packages/fory/lib/typeInfo.ts
+++ b/javascript/packages/fory/lib/typeInfo.ts
@@ -20,6 +20,26 @@
import Fory from "./fory";
import { ForyTypeInfoSymbol, TypeId, Mode } from "./type";
+const targetFieldInfo = new WeakMap<new () => any, { [key: string]:
StructFieldInfo }>();
+
+export const ForyField = (fieldInfo: {
+ nullable?: boolean,
+ trackingRef?: boolean,
+ id?: number,
+}) => {
+ return (target: any, key: string | {name?: string}) => {
+ const creator = target.constructor;
+ if (!targetFieldInfo.has(creator)) {
+ targetFieldInfo.set(creator, {});
+ }
+ const keyString = typeof key === "string" ? key : key?.name;
+ if (!keyString) {
+ throw new Error("Decorators can only be placed on classes and fields");
+ }
+ targetFieldInfo.get(creator)![keyString] = fieldInfo;
+ };
+}
+
const initMeta = (target: new () => any, typeInfo: TypeInfo) => {
if (!target.prototype) {
target.prototype = {};
@@ -32,6 +52,13 @@ const initMeta = (target: new () => any, typeInfo: TypeInfo)
=> {
if (!typeInfo.options.props) {
typeInfo.options.props = {}
}
+ if(targetFieldInfo.has(target)) {
+ const structTypeInfo = (typeInfo as StructTypeInfo);
+ if (!structTypeInfo.options.fieldInfo) {
+ structTypeInfo.options.fieldInfo = {};
+ }
+ Object.assign(structTypeInfo.options.fieldInfo,
targetFieldInfo.get(target));
+ }
Object.assign(typeInfo.options.props, targetFields.get(target) || {})
Object.defineProperties(target.prototype, {
[ForyTypeInfoSymbol]: {
@@ -340,7 +367,7 @@ export class TypeInfo<T = unknown> extends
ExtensibleFunction {
}
}
-type StructFieldInfo = {nullable?: boolean, trackingRef?: boolean}
+type StructFieldInfo = {nullable?: boolean, trackingRef?: boolean, id?: number}
export interface StructTypeInfo extends TypeInfo {
options: {
props?: { [key: string]: TypeInfo };
diff --git a/javascript/test/crossLanguage.test.ts
b/javascript/test/crossLanguage.test.ts
index f5650b91e..5c16b7782 100644
--- a/javascript/test/crossLanguage.test.ts
+++ b/javascript/test/crossLanguage.test.ts
@@ -21,6 +21,7 @@ import Fory, {
BinaryReader,
BinaryWriter,
Mode,
+ ForyField,
Type,
} from "../packages/fory/index";
import { describe, expect, test } from "@jest/globals";
@@ -182,40 +183,12 @@ describe("bool", () => {
writeToFile(writer.dump() as Buffer);
});
test("test_murmurhash3", () => {
- if (Boolean("1")) { return; }
+ const { x64hash128 } = require("../packages/fory/lib/murmurHash3");
const reader = new BinaryReader({});
reader.reset(content);
-
- // Read the two hash values written by Java
- const hash1Bytes = new Uint8Array(16);
- for (let i = 0; i < 16; i++) {
- hash1Bytes[i] = reader.uint8();
- }
-
- const hash2Bytes = new Uint8Array(16);
- for (let i = 0; i < 16; i++) {
- hash2Bytes[i] = reader.uint8();
- }
-
- // Import murmurHash3 function
- const { x64hash128 } = require("../packages/fory/lib/murmurHash3");
-
- // Test hash1: hash of [1, 2, 8] with seed 47
- const testData1 = new Uint8Array([1, 2, 8]);
- const result1 = x64hash128(testData1, 47);
- const result1Bytes = new Uint8Array(result1.buffer);
-
- // Test hash2: hash of "01234567890123456789" with seed 47
- const testData2 = new TextEncoder().encode("01234567890123456789");
- const result2 = x64hash128(testData2, 47);
- const result2Bytes = new Uint8Array(result2.buffer);
-
- // Write our computed hashes back
- const writer = new BinaryWriter();
- writer.reserve(32);
- writer.buffer(result1Bytes);
- writer.buffer(result2Bytes);
- writeToFile(writer.dump() as Buffer);
+ let dataview = x64hash128(new Uint8Array([1, 2, 8]), 47);
+ expect(reader.int64()).toEqual(dataview.getBigInt64(0));
+ expect(reader.int64()).toEqual(dataview.getBigInt64(8));
});
test("test_string_serializer", () => {
const fory = new Fory({
@@ -698,7 +671,6 @@ describe("bool", () => {
test("test_consistent_named", () => {
const fory = new Fory({
mode: Mode.SchemaConsistent,
- classVersionHash: true,
});
// Define and register Color enum
@@ -758,18 +730,18 @@ describe("bool", () => {
});
test("test_struct_version_check", () => {
- if (Boolean("1")) { return; }
const fory = new Fory({
- mode: Mode.Compatible
+ mode: Mode.SchemaConsistent,
});
@Type.struct(201, {
- f1: Type.int32(),
+ f1: Type.varInt32(),
f2: Type.string(),
f3: Type.float64()
})
class VersionCheckStruct {
f1: number = 0;
+ @ForyField({ nullable: true })
f2: string | null = null;
f3: number = 0;
}
@@ -918,22 +890,19 @@ describe("bool", () => {
writeToFile(writer.dump() as Buffer);
});
test("test_one_string_field_schema", () => {
- if (Boolean("1")) { return; }
const fory = new Fory({
- mode: Mode.Compatible
+ mode: Mode.SchemaConsistent
});
@Type.struct(200, {
f1: Type.string()
})
class OneStringFieldStruct {
+ @ForyField({nullable: true})
f1: string | null = null;
}
fory.registerSerializer(OneStringFieldStruct);
- const reader = new BinaryReader({});
- reader.reset(content);
-
// Deserialize struct from Java
let cursor = 0;
const deserializedStruct = fory.deserialize(content.subarray(cursor));
@@ -944,7 +913,6 @@ describe("bool", () => {
writeToFile(serializedData as Buffer);
});
test("test_one_string_field_compatible", () => {
- if (Boolean("1")) { return; }
const fory = new Fory({
mode: Mode.Compatible
});
@@ -953,6 +921,7 @@ describe("bool", () => {
f1: Type.string()
})
class OneStringFieldStruct {
+ @ForyField({nullable: true})
f1: string | null = null;
}
fory.registerSerializer(OneStringFieldStruct);
@@ -971,7 +940,6 @@ describe("bool", () => {
});
test("test_two_string_field_compatible", () => {
- if (Boolean("1")) { return; }
const fory = new Fory({
mode: Mode.Compatible
});
@@ -986,9 +954,6 @@ describe("bool", () => {
}
fory.registerSerializer(TwoStringFieldStruct);
- const reader = new BinaryReader({});
- reader.reset(content);
-
// Deserialize struct from Java
let cursor = 0;
const deserializedStruct = fory.deserialize(content.subarray(cursor));
@@ -1022,9 +987,8 @@ describe("bool", () => {
writeToFile(serializedData as Buffer);
});
test("test_one_enum_field_schema", () => {
- if (Boolean("1")) { return; }
const fory = new Fory({
- mode: Mode.Compatible
+ mode: Mode.SchemaConsistent
});
// Define and register TestEnum
@@ -1057,7 +1021,6 @@ describe("bool", () => {
});
test("test_one_enum_field_compatible", () => {
- if (Boolean("1")) { return; }
const fory = new Fory({
mode: Mode.Compatible
});
@@ -1078,9 +1041,6 @@ describe("bool", () => {
}
fory.registerSerializer(OneEnumFieldStruct);
- const reader = new BinaryReader({});
- reader.reset(content);
-
// Deserialize struct from Java
let cursor = 0;
const deserializedStruct = fory.deserialize(content.subarray(cursor));
@@ -1092,7 +1052,6 @@ describe("bool", () => {
});
test("test_two_enum_field_compatible", () => {
- if (Boolean("1")) { return; }
const fory = new Fory({
mode: Mode.Compatible
});
@@ -1115,9 +1074,6 @@ describe("bool", () => {
}
fory.registerSerializer(TwoEnumFieldStruct);
- const reader = new BinaryReader({});
- reader.reset(content);
-
// Deserialize struct from Java
let cursor = 0;
const deserializedStruct = fory.deserialize(content.subarray(cursor));
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]