This is an automated email from the ASF dual-hosted git repository.

chaokunyang 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 bda7d80b2 fix(javascript): fix javascript schema idl tests (#3604)
bda7d80b2 is described below

commit bda7d80b27059e0175598916da1939677c27d34a
Author: Shawn Yang <[email protected]>
AuthorDate: Wed Apr 22 17:33:07 2026 +0800

    fix(javascript): fix javascript schema idl tests (#3604)
    
    ## Why?
    
    
    
    ## What does this PR do?
    
    
    
    ## Related issues
    
    #3394
    
    ## AI Contribution Checklist
    
    
    
    - [ ] Substantial AI assistance was used in this PR: `yes` / `no`
    - [ ] If `yes`, I included a completed [AI Contribution
    
Checklist](https://github.com/apache/fory/blob/main/AI_POLICY.md#9-contributor-checklist-for-ai-assisted-prs)
    in this PR description and the required `AI Usage Disclosure`.
    - [ ] If `yes`, my PR description includes the required `ai_review`
    summary and screenshot evidence of the final clean AI review results
    from both fresh reviewers on the current PR diff or current HEAD after
    the latest code changes.
    
    
    
    ## 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
---
 .../idl_tests/javascript/roundtrip.ts              | 611 +++++++++++++++++---
 .../idl_tests/javascript/test/roundtrip.test.ts    | 637 ++++++++++++++-------
 integration_tests/idl_tests/run_java_tests.sh      |   8 +-
 .../idl_tests/run_javascript_tests.sh              |   5 +-
 javascript/packages/core/lib/context.ts            |  69 ++-
 javascript/packages/core/lib/gen/any.ts            |  13 +-
 javascript/packages/core/lib/meta/TypeMeta.ts      | 153 ++++-
 javascript/test/typemeta.test.ts                   | 319 +++++++++--
 8 files changed, 1429 insertions(+), 386 deletions(-)

diff --git a/integration_tests/idl_tests/javascript/roundtrip.ts 
b/integration_tests/idl_tests/javascript/roundtrip.ts
index 92d3d1678..d91bd9a0c 100644
--- a/integration_tests/idl_tests/javascript/roundtrip.ts
+++ b/integration_tests/idl_tests/javascript/roundtrip.ts
@@ -20,140 +20,569 @@
 /**
  * Cross-language roundtrip program for JavaScript IDL tests.
  *
- * This script is invoked by the Java IdlRoundTripTest as a peer process.
- * It reads binary data files (written by Java), deserializes them,
- * re-serializes the objects, and writes the bytes back to the same files.
- * Java then reads the files back and verifies the roundtrip integrity.
- *
- * Environment variables:
- *   IDL_COMPATIBLE  - "true" for compatible mode, "false" for 
schema_consistent
- *   DATA_FILE       - AddressBook binary data file path
- *   DATA_FILE_AUTO_ID - Envelope (auto-id) binary data file path
- *   DATA_FILE_PRIMITIVES - PrimitiveTypes binary data file path
- *   DATA_FILE_COLLECTION - NumericCollections binary data file path
- *   DATA_FILE_COLLECTION_UNION - NumericCollectionUnion binary data file path
- *   DATA_FILE_COLLECTION_ARRAY - NumericCollectionsArray binary data file path
- *   DATA_FILE_COLLECTION_ARRAY_UNION - NumericCollectionArrayUnion binary 
data file path
- *   DATA_FILE_OPTIONAL_TYPES - OptionalHolder binary data file path
- *   DATA_FILE_TREE - TreeNode binary data file path (ref tracking)
- *   DATA_FILE_GRAPH - Graph binary data file path (ref tracking)
- *   DATA_FILE_FLATBUFFERS_MONSTER - Monster binary data file path
- *   DATA_FILE_FLATBUFFERS_TEST2 - Container binary data file path
+ * This peer is invoked by the existing Java `IdlRoundTripTest` flow. It
+ * mirrors the Go roundtrip test shape: build the expected local message,
+ * deserialize the Java-written payload, compare it against the local value,
+ * then serialize the decoded value back to the temp file.
  */
 
+import * as assert from "assert/strict";
 import * as fs from "fs";
-import Fory, { Type } from "@apache-fory/core";
 
-import { registerAddressbookTypes } from "./generated/addressbook";
-import { registerAutoIdTypes } from "./generated/auto_id";
-import { registerComplexPbTypes } from "./generated/complex_pb";
-import { registerCollectionTypes } from "./generated/collection";
-import { registerOptionalTypesTypes } from "./generated/optional_types";
-import { registerTreeTypes } from "./generated/tree";
-import { registerGraphTypes } from "./generated/graph";
-import { registerMonsterTypes } from "./generated/monster";
-import { registerComplexFbsTypes } from "./generated/complex_fbs";
+import Fory, { Type } from "@apache-fory/core";
+import { AnyHelper } from "@apache-fory/core/dist/lib/gen/any";
+import { ConfigFlags, RefFlags, type Serializer } from 
"@apache-fory/core/dist/lib/type";
+
+import {
+  AddressBook,
+  AnimalCase,
+  Cat,
+  Dog,
+  Person,
+  registerAddressbookTypes,
+} from "./generated/addressbook";
+import {
+  Envelope,
+  Status as AutoIdStatus,
+  registerAutoIdTypes,
+} from "./generated/auto_id";
+import {
+  NumericCollections,
+  NumericCollectionsArray,
+  NumericCollectionArrayUnion,
+  NumericCollectionArrayUnionCase,
+  NumericCollectionUnion,
+  NumericCollectionUnionCase,
+  registerCollectionTypes,
+} from "./generated/collection";
+import {
+  Container,
+  PayloadCase,
+  ScalarPack,
+  Status as ComplexFbsStatus,
+  registerComplexFbsTypes,
+} from "./generated/complex_fbs";
+import { PrimitiveTypes } from "./generated/complex_pb";
+import {
+  Graph,
+  registerGraphTypes,
+} from "./generated/graph";
+import {
+  AllOptionalTypes,
+  OptionalHolder,
+  OptionalUnionCase,
+  registerOptionalTypesTypes,
+} from "./generated/optional_types";
+import {
+  Monster,
+  Color,
+  registerMonsterTypes,
+} from "./generated/monster";
+import {
+  TreeNode,
+  registerTreeTypes,
+} from "./generated/tree";
+
+type RegisterFn = (fory: Fory, type: typeof Type) => void;
+type AssertFn<T> = (expected: T, actual: unknown) => void;
+
+function resolveCompatibleModes(): boolean[] {
+  const value = process.env.IDL_COMPATIBLE;
+  if (value == null || value.trim() === "") {
+    return [false, true];
+  }
+  const normalized = value.trim().toLowerCase();
+  if (normalized === "1" || normalized === "true" || normalized === "yes") {
+    return [true];
+  }
+  if (normalized === "0" || normalized === "false" || normalized === "no") {
+    return [false];
+  }
+  throw new Error(`Unsupported IDL_COMPATIBLE value: ${value}`);
+}
 
-const compatible = process.env["IDL_COMPATIBLE"] === "true";
+function buildFory(compatible: boolean, ref: boolean, registerFns: 
ReadonlyArray<RegisterFn>): Fory {
+  const fory = new Fory({
+    compatible,
+    ref,
+  });
+  for (const registerFn of registerFns) {
+    registerFn(fory, Type);
+  }
+  return fory;
+}
 
-// ---------------------------------------------------------------------------
-// Roundtrip helper: read file, deserialize, re-serialize, write back
-// ---------------------------------------------------------------------------
+function resolveRootSerializer(fory: Fory, bytes: Uint8Array): Serializer {
+  fory.readContext.reset(bytes);
+  const reader = fory.readContext.reader;
+  const bitmap = reader.readUint8();
+  if ((bitmap & ConfigFlags.isNullFlag) === ConfigFlags.isNullFlag) {
+    throw new Error("IDL roundtrip does not support null root payloads");
+  }
+  if ((bitmap & ConfigFlags.isCrossLanguageFlag) !== 
ConfigFlags.isCrossLanguageFlag) {
+    throw new Error("support crosslanguage mode only");
+  }
+  if ((bitmap & ConfigFlags.isOutOfBandFlag) === ConfigFlags.isOutOfBandFlag) {
+    throw new Error("outofband mode is not supported now");
+  }
+  const refFlag = fory.readContext.readRefFlag();
+  if (refFlag === RefFlags.NullFlag) {
+    throw new Error("IDL roundtrip does not support null root payloads");
+  }
+  if (refFlag === RefFlags.RefFlag) {
+    throw new Error("IDL roundtrip root payload must not be a back reference");
+  }
+  // Generated JS IDL outputs are plain interfaces, so generic serialize(value)
+  // cannot rediscover the root struct serializer. Reuse the runtime's existing
+  // serializer detection from the payload, then map back to the local
+  // registered serializer when available.
+  const detectedSerializer = AnyHelper.detectSerializer(fory.readContext);
+  return 
fory.typeResolver.getSerializerByTypeInfo(detectedSerializer.getTypeInfo()) ?? 
detectedSerializer;
+}
 
-function fileRoundTrip(
+function runFileRoundTrip<T>(
   envVar: string,
-  rootTypeId: number,
-  registerFn: (fory: any, type: any) => void,
-  foryOptions: { refTracking?: boolean | null; compatible?: boolean; union?: 
boolean }
+  fory: Fory,
+  expected: T,
+  assertFn: AssertFn<T>,
 ): void {
   const filePath = process.env[envVar];
   if (!filePath) {
     return;
   }
-
   console.log(`Processing ${envVar}: ${filePath}`);
+  const payload = new Uint8Array(fs.readFileSync(filePath));
+  const serializer = resolveRootSerializer(fory, payload);
+  const decoded = fory.deserialize(payload, serializer);
+  assertFn(expected, decoded);
+  const roundTripBytes = fory.serialize(decoded, serializer);
+  fs.writeFileSync(filePath, roundTripBytes);
+  console.log(`  OK: roundtrip complete for ${envVar}`);
+}
 
-  const fory = new Fory({
-    compatible: foryOptions.compatible ?? false,
-    ref: foryOptions.refTracking ?? false,
-  });
+function normalizeAcyclic(value: unknown): unknown {
+  if (value instanceof Date) {
+    return { __dateMs: value.getTime() };
+  }
+  if (value instanceof Map) {
+    const entries = Array.from(value.entries()).map(([key, itemValue]) => (
+      [normalizeAcyclic(key), normalizeAcyclic(itemValue)] as const
+    ));
+    entries.sort((left, right) => 
String(left[0]).localeCompare(String(right[0])));
+    return entries;
+  }
+  if (ArrayBuffer.isView(value)) {
+    if (value instanceof DataView) {
+      return Array.from(new Uint8Array(value.buffer, value.byteOffset, 
value.byteLength));
+    }
+    return Array.from(value as unknown as ArrayLike<unknown>, (item) => 
normalizeAcyclic(item));
+  }
+  if (Array.isArray(value)) {
+    return value.map((item) => normalizeAcyclic(item));
+  }
+  if (value != null && typeof value === "object") {
+    const entries = Object.entries(value as Record<string, unknown>);
+    entries.sort(([left], [right]) => left.localeCompare(right));
+    return Object.fromEntries(entries.map(([key, itemValue]) => [key, 
normalizeAcyclic(itemValue)]));
+  }
+  return value;
+}
 
-  registerFn(fory, Type);
+function assertAcyclicEqual<T>(label: string, expected: T, actual: unknown): 
void {
+  assert.deepStrictEqual(
+    normalizeAcyclic(actual),
+    normalizeAcyclic(expected),
+    `${label} mismatch`,
+  );
+}
 
-  const rootTypeInfo = foryOptions.union
-    ? Type.union(rootTypeId)
-    : Type.struct(rootTypeId);
-  const serializer = fory.typeResolver.getSerializerByTypeInfo(rootTypeInfo);
+function buildDog(): Dog {
+  return { name: "Rex", barkVolume: 5 };
+}
 
-  // Read binary data
-  const data = fs.readFileSync(filePath);
-  const bytes = new Uint8Array(data);
+function buildCat(): Cat {
+  return { name: "Mimi", lives: 9 };
+}
 
-  // Deserialize
-  const obj = fory.deserialize(bytes, serializer);
+function buildPhoneNumber(number_: string, phoneType: Person.PhoneType): 
Person.PhoneNumber {
+  return { number_, phoneType };
+}
 
-  // Re-serialize
-  const result = fory.serialize(obj, serializer);
+function buildAddressBook(): AddressBook {
+  const person: Person = {
+    name: "Alice",
+    id: 123,
+    email: "[email protected]",
+    tags: ["friend", "colleague"],
+    scores: new Map([
+      ["math", 100],
+      ["science", 98],
+    ]),
+    salary: 120000.5,
+    phones: [
+      buildPhoneNumber("555-0100", Person.PhoneType.MOBILE),
+      buildPhoneNumber("555-0111", Person.PhoneType.WORK),
+    ],
+    pet: {
+      case: AnimalCase.CAT,
+      value: buildCat(),
+    },
+  };
+  return {
+    people: [person],
+    peopleByName: new Map([[person.name, person]]),
+  };
+}
 
-  // Write back
-  fs.writeFileSync(filePath, result);
-  console.log(`  OK: roundtrip complete for ${envVar}`);
+function buildAutoIdEnvelope(): Envelope {
+  const payload: Envelope.Payload = { value: 42 };
+  return {
+    id: "env-1",
+    payload,
+    detail: { case: Envelope.DetailCase.PAYLOAD, value: payload },
+    status: AutoIdStatus.OK,
+  };
 }
 
-// ---------------------------------------------------------------------------
-// Process each data file type using generated code
-// ---------------------------------------------------------------------------
+function buildPrimitiveTypes(): PrimitiveTypes {
+  return {
+    boolValue: true,
+    int8Value: 12,
+    int16Value: 1234,
+    int32Value: -123456,
+    varint32Value: -12345,
+    int64Value: -123456789n,
+    varint64Value: -987654321n,
+    taggedInt64Value: 123456789n,
+    uint8Value: 200,
+    uint16Value: 60000,
+    uint32Value: 1234567890,
+    varUint32Value: 1234567890,
+    uint64Value: 9876543210n,
+    varUint64Value: 12345678901n,
+    taggedUint64Value: 2222222222n,
+    float32Value: 2.5,
+    float64Value: 3.5,
+    contact: {
+      case: PrimitiveTypes.ContactCase.PHONE,
+      value: 12345,
+    },
+  };
+}
 
+function buildNumericCollections(): NumericCollections {
+  return {
+    int8Values: [1, -2, 3],
+    int16Values: [100, -200, 300],
+    int32Values: [1000, -2000, 3000],
+    int64Values: [10000n, -20000n, 30000n],
+    uint8Values: [200, 250],
+    uint16Values: [50000, 60000],
+    uint32Values: [2000000000, 2100000000],
+    uint64Values: [9000000000n, 12000000000n],
+    float32Values: [1.5, 2.5],
+    float64Values: [3.5, 4.5],
+  };
+}
 
-fileRoundTrip("DATA_FILE", 103, registerAddressbookTypes, { compatible });
+function buildNumericCollectionUnion(): NumericCollectionUnion {
+  return {
+    case: NumericCollectionUnionCase.INT32_VALUES,
+    value: [7, 8, 9],
+  };
+}
 
+function buildNumericCollectionsArray(): NumericCollectionsArray {
+  return {
+    int8Values: [1, -2, 3],
+    int16Values: [100, -200, 300],
+    int32Values: [1000, -2000, 3000],
+    int64Values: [10000n, -20000n, 30000n],
+    uint8Values: [200, 250],
+    uint16Values: [50000, 60000],
+    uint32Values: [2000000000, 2100000000],
+    uint64Values: [9000000000n, 12000000000n],
+    float32Values: [1.5, 2.5],
+    float64Values: [3.5, 4.5],
+  };
+}
 
-fileRoundTrip("DATA_FILE_AUTO_ID", 3022445236, registerAutoIdTypes, { 
compatible });
+function buildNumericCollectionArrayUnion(): NumericCollectionArrayUnion {
+  return {
+    case: NumericCollectionArrayUnionCase.UINT16_VALUES,
+    value: [1000, 2000, 3000],
+  };
+}
+
+function buildMonster(): Monster {
+  return {
+    pos: {
+      x: 1.0,
+      y: 2.0,
+      z: 3.0,
+    },
+    mana: 200,
+    hp: 80,
+    name: "Orc",
+    friendly: true,
+    inventory: [1, 2, 3],
+    color: Color.Blue,
+  };
+}
 
+function buildContainer(): Container {
+  const scalars: ScalarPack = {
+    b: -8,
+    ub: 200,
+    s: -1234,
+    us: 40000,
+    i: -123456,
+    ui: 123456,
+    l: -123456789n,
+    ul: 987654321n,
+    f: 1.5,
+    d: 2.5,
+    ok: true,
+  };
+  return {
+    id: 9876543210n,
+    status: ComplexFbsStatus.STARTED,
+    bytes: [1, 2, 3],
+    numbers: [10, 20, 30],
+    scalars,
+    names: ["alpha", "beta"],
+    flags: [true, false],
+    payload: {
+      case: PayloadCase.METRIC,
+      value: { value: 42.0 },
+    },
+  };
+}
+
+function buildLocalDate(year: number, month: number, day: number): Date {
+  return new Date(year, month - 1, day, 0, 0, 0, 0);
+}
+
+function buildOptionalHolder(): OptionalHolder {
+  const allTypes: AllOptionalTypes = {
+    boolValue: true,
+    int8Value: 12,
+    int16Value: 1234,
+    int32Value: -123456,
+    fixedInt32Value: -123456,
+    varint32Value: -12345,
+    int64Value: -123456789n,
+    fixedInt64Value: -123456789n,
+    varint64Value: -987654321n,
+    taggedInt64Value: 123456789n,
+    uint8Value: 200,
+    uint16Value: 60000,
+    uint32Value: 1234567890,
+    fixedUint32Value: 1234567890,
+    varUint32Value: 1234567890,
+    uint64Value: 9876543210n,
+    fixedUint64Value: 9876543210n,
+    varUint64Value: 12345678901n,
+    taggedUint64Value: 2222222222n,
+    float32Value: 2.5,
+    float64Value: 3.5,
+    stringValue: "optional",
+    bytesValue: new Uint8Array([1, 2, 3]),
+    dateValue: buildLocalDate(2024, 1, 2),
+    timestampValue: new Date("2024-01-02T03:04:05Z"),
+    int32List: [1, 2, 3],
+    stringList: ["alpha", "beta"],
+    int64Map: new Map([
+      ["alpha", 10n],
+      ["beta", 20n],
+    ]),
+  };
+  return {
+    allTypes,
+    choice: {
+      case: OptionalUnionCase.NOTE,
+      value: "optional",
+    },
+  };
+}
+
+function buildTree(): TreeNode {
+  const childA: TreeNode = {
+    id: "child-a",
+    name: "child-a",
+    children: [],
+  };
+  const childB: TreeNode = {
+    id: "child-b",
+    name: "child-b",
+    children: [],
+  };
+  childA.parent = childB;
+  childB.parent = childA;
+  return {
+    id: "root",
+    name: "root",
+    children: [childA, childA, childB],
+  };
+}
+
+function buildGraph(): Graph {
+  const nodeA = {
+    id: "node-a",
+    outEdges: [],
+    inEdges: [],
+  } as unknown as Graph["nodes"][number];
+  const nodeB = {
+    id: "node-b",
+    outEdges: [],
+    inEdges: [],
+  } as unknown as Graph["nodes"][number];
+  const edge = {
+    id: "edge-1",
+    weight: 1.5,
+    from_: nodeA,
+    to: nodeB,
+  } as Graph["edges"][number];
+  nodeA.outEdges = [edge];
+  nodeA.inEdges = [edge];
+  nodeB.inEdges = [edge];
+  nodeB.outEdges = [];
+  return {
+    nodes: [nodeA, nodeB],
+    edges: [edge],
+  };
+}
 
-fileRoundTrip("DATA_FILE_PRIMITIVES", 200, registerComplexPbTypes, { 
compatible });
+function assertAddressBookEqual(expected: AddressBook, actual: unknown): void {
+  assertAcyclicEqual("addressbook", expected, actual);
+}
 
+function assertAutoIdEnvelopeEqual(expected: Envelope, actual: unknown): void {
+  assertAcyclicEqual("auto_id envelope", expected, actual);
+}
 
-fileRoundTrip("DATA_FILE_COLLECTION", 210, registerCollectionTypes, { 
compatible });
+function assertPrimitiveTypesEqual(expected: PrimitiveTypes, actual: unknown): 
void {
+  assertAcyclicEqual("primitive types", expected, actual);
+}
 
-// DATA_FILE_COLLECTION_UNION: NumericCollectionUnion (type ID 211)
-fileRoundTrip("DATA_FILE_COLLECTION_UNION", 211, registerCollectionTypes, { 
compatible, union: true });
+function assertNumericCollectionsEqual(expected: NumericCollections, actual: 
unknown): void {
+  assertAcyclicEqual("numeric collections", expected, actual);
+}
 
-// DATA_FILE_COLLECTION_ARRAY: NumericCollectionsArray (type ID 212)
-fileRoundTrip("DATA_FILE_COLLECTION_ARRAY", 212, registerCollectionTypes, { 
compatible });
+function assertNumericCollectionUnionEqual(expected: NumericCollectionUnion, 
actual: unknown): void {
+  assertAcyclicEqual("numeric collection union", expected, actual);
+}
 
-// DATA_FILE_COLLECTION_ARRAY_UNION: NumericCollectionArrayUnion (type ID 213)
-fileRoundTrip("DATA_FILE_COLLECTION_ARRAY_UNION", 213, 
registerCollectionTypes, { compatible, union: true });
+function assertNumericCollectionsArrayEqual(expected: NumericCollectionsArray, 
actual: unknown): void {
+  assertAcyclicEqual("numeric collections array", expected, actual);
+}
 
+function assertNumericCollectionArrayUnionEqual(expected: 
NumericCollectionArrayUnion, actual: unknown): void {
+  assertAcyclicEqual("numeric collection array union", expected, actual);
+}
 
-fileRoundTrip("DATA_FILE_OPTIONAL_TYPES", 122, registerOptionalTypesTypes, { 
compatible });
+function assertMonsterEqual(expected: Monster, actual: unknown): void {
+  assertAcyclicEqual("monster", expected, actual);
+}
 
+function assertContainerEqual(expected: Container, actual: unknown): void {
+  assertAcyclicEqual("complex_fbs container", expected, actual);
+}
 
-fileRoundTrip("DATA_FILE_TREE", 2251833438, registerTreeTypes, {
-  refTracking: true,
-  compatible,
-});
+function assertOptionalHolderEqual(expected: OptionalHolder, actual: unknown): 
void {
+  assertAcyclicEqual("optional holder", expected, actual);
+}
 
+function assertTreeEqual(expected: TreeNode, actualValue: unknown): void {
+  assert.ok(actualValue != null && typeof actualValue === "object", "tree 
payload must decode to an object");
+  const actual = actualValue as TreeNode;
+  assert.equal(actual.id, expected.id, "tree root id mismatch");
+  assert.equal(actual.name, expected.name, "tree root name mismatch");
+  assert.equal(actual.children.length, expected.children.length, "tree 
children size mismatch");
+  assert.equal(expected.children.length, 3, "expected tree fixture drift");
+  assert.strictEqual(expected.children[0], expected.children[1], "expected 
tree shared child drift");
+  assert.notStrictEqual(expected.children[0], expected.children[2], "expected 
tree distinct child drift");
+  assert.equal(actual.children[0].id, expected.children[0].id, "tree first 
child id mismatch");
+  assert.equal(actual.children[0].name, expected.children[0].name, "tree first 
child name mismatch");
+  assert.equal(actual.children[2].id, expected.children[2].id, "tree third 
child id mismatch");
+  assert.equal(actual.children[2].name, expected.children[2].name, "tree third 
child name mismatch");
+  assert.strictEqual(actual.children[0], actual.children[1], "tree shared 
child mismatch");
+  assert.notStrictEqual(actual.children[0], actual.children[2], "tree distinct 
child mismatch");
+  assert.strictEqual(actual.children[0].parent, actual.children[2], "tree 
parent back-pointer mismatch");
+  assert.strictEqual(actual.children[2].parent, actual.children[0], "tree 
parent reverse mismatch");
+}
 
-fileRoundTrip("DATA_FILE_GRAPH", 2373163777, registerGraphTypes, {
-  refTracking: true,
-  compatible,
-});
+function assertGraphEqual(expected: Graph, actualValue: unknown): void {
+  assert.ok(actualValue != null && typeof actualValue === "object", "graph 
payload must decode to an object");
+  const actual = actualValue as Graph;
+  assert.equal(actual.nodes.length, expected.nodes.length, "graph node size 
mismatch");
+  assert.equal(actual.edges.length, expected.edges.length, "graph edge size 
mismatch");
+  assert.equal(expected.nodes.length, 2, "expected graph fixture drift");
+  assert.equal(expected.edges.length, 1, "expected graph edge fixture drift");
+  const actualNodeA = actual.nodes[0];
+  const actualNodeB = actual.nodes[1];
+  const actualEdge = actual.edges[0];
+  assert.equal(actualNodeA.id, expected.nodes[0].id, "graph node-a id 
mismatch");
+  assert.equal(actualNodeB.id, expected.nodes[1].id, "graph node-b id 
mismatch");
+  assert.equal(actualEdge.id, expected.edges[0].id, "graph edge id mismatch");
+  assert.equal(actualEdge.weight, expected.edges[0].weight, "graph edge weight 
mismatch");
+  assert.strictEqual(actualNodeA.outEdges[0], actualNodeA.inEdges[0], "graph 
shared edge mismatch");
+  assert.strictEqual(actualEdge, actualNodeA.outEdges[0], "graph edge link 
mismatch");
+  assert.strictEqual(actualEdge.from_, actualNodeA, "graph edge from 
mismatch");
+  assert.strictEqual(actualEdge.to, actualNodeB, "graph edge to mismatch");
+}
 
+function runStandardRoundTrip(compatible: boolean): void {
+  // AddressBook registration already includes the shared complex_pb types.
+  const fory = buildFory(compatible, false, [
+    registerAddressbookTypes,
+    registerAutoIdTypes,
+    registerMonsterTypes,
+    registerComplexFbsTypes,
+    registerCollectionTypes,
+    registerOptionalTypesTypes,
+  ]);
+
+  runFileRoundTrip("DATA_FILE", fory, buildAddressBook(), 
assertAddressBookEqual);
+  runFileRoundTrip("DATA_FILE_AUTO_ID", fory, buildAutoIdEnvelope(), 
assertAutoIdEnvelopeEqual);
+  runFileRoundTrip("DATA_FILE_PRIMITIVES", fory, buildPrimitiveTypes(), 
assertPrimitiveTypesEqual);
+  runFileRoundTrip("DATA_FILE_COLLECTION", fory, buildNumericCollections(), 
assertNumericCollectionsEqual);
+  runFileRoundTrip(
+    "DATA_FILE_COLLECTION_UNION",
+    fory,
+    buildNumericCollectionUnion(),
+    assertNumericCollectionUnionEqual,
+  );
+  runFileRoundTrip(
+    "DATA_FILE_COLLECTION_ARRAY",
+    fory,
+    buildNumericCollectionsArray(),
+    assertNumericCollectionsArrayEqual,
+  );
+  runFileRoundTrip(
+    "DATA_FILE_COLLECTION_ARRAY_UNION",
+    fory,
+    buildNumericCollectionArrayUnion(),
+    assertNumericCollectionArrayUnionEqual,
+  );
+  runFileRoundTrip("DATA_FILE_OPTIONAL_TYPES", fory, buildOptionalHolder(), 
assertOptionalHolderEqual);
+  runFileRoundTrip("DATA_FILE_FLATBUFFERS_MONSTER", fory, buildMonster(), 
assertMonsterEqual);
+  runFileRoundTrip("DATA_FILE_FLATBUFFERS_TEST2", fory, buildContainer(), 
assertContainerEqual);
+}
 
-fileRoundTrip(
-  "DATA_FILE_FLATBUFFERS_MONSTER",
-  438716985,
-  registerMonsterTypes,
-  { compatible }
-);
+function runRefRoundTrip(compatible: boolean): void {
+  const refFory = buildFory(compatible, true, [
+    registerTreeTypes,
+    registerGraphTypes,
+  ]);
 
+  runFileRoundTrip("DATA_FILE_TREE", refFory, buildTree(), assertTreeEqual);
+  runFileRoundTrip("DATA_FILE_GRAPH", refFory, buildGraph(), assertGraphEqual);
+}
 
-fileRoundTrip(
-  "DATA_FILE_FLATBUFFERS_TEST2",
-  372413680,
-  registerComplexFbsTypes,
-  { compatible }
-);
+for (const compatible of resolveCompatibleModes()) {
+  runStandardRoundTrip(compatible);
+  runRefRoundTrip(compatible);
+}
 
 console.log("JavaScript roundtrip finished.");
diff --git a/integration_tests/idl_tests/javascript/test/roundtrip.test.ts 
b/integration_tests/idl_tests/javascript/test/roundtrip.test.ts
index f21535024..ebb1eb41d 100644
--- a/integration_tests/idl_tests/javascript/test/roundtrip.test.ts
+++ b/integration_tests/idl_tests/javascript/test/roundtrip.test.ts
@@ -17,275 +17,522 @@
  * under the License.
  */
 
-/**
- * Integration tests for JavaScript IDL-generated code.
- *
- * These tests verify that:
- * 1. Generated JavaScript types compile correctly
- * 2. Objects can be constructed conforming to the generated interfaces
- * 3. Roundtrip serialization works via the Fory JS runtime
- */
-
-import Fory, { Type } from '@apache-fory/core';
+import Fory, { Decimal, Type } from '@apache-fory/core';
 import {
   AddressBook,
-  Person,
   Animal,
   AnimalCase,
-  Dog,
   Cat,
-  
-  
+  Dog,
+  Person,
   registerAddressbookTypes,
 } from '../generated/addressbook';
-import {
-  AllOptionalTypes,
-  registerOptionalTypesTypes,
-} from '../generated/optional_types';
-import { TreeNode } from '../generated/tree';
 import {
   Envelope,
-  
-  Status,
-  
+  Status as AutoIdStatus,
+  Wrapper,
   WrapperCase,
   registerAutoIdTypes,
 } from '../generated/auto_id';
+import {
+  NumericCollections,
+  NumericCollectionsArray,
+  NumericCollectionArrayUnion,
+  NumericCollectionArrayUnionCase,
+  NumericCollectionUnion,
+  NumericCollectionUnionCase,
+  registerCollectionTypes,
+} from '../generated/collection';
+import {
+  Container,
+  PayloadCase,
+  ScalarPack,
+  Status as ComplexFbsStatus,
+  registerComplexFbsTypes,
+} from '../generated/complex_fbs';
+import { PrimitiveTypes, registerComplexPbTypes } from 
'../generated/complex_pb';
+import {
+  Graph,
+  registerGraphTypes,
+} from '../generated/graph';
+import {
+  AllOptionalTypes,
+  OptionalHolder,
+  OptionalUnionCase,
+  registerOptionalTypesTypes,
+} from '../generated/optional_types';
+import {
+  Color,
+  Monster,
+  registerMonsterTypes,
+} from '../generated/monster';
+import {
+  TreeNode,
+  registerTreeTypes,
+} from '../generated/tree';
+
+type RegisterFn = (fory: Fory, type: typeof Type) => void;
+type RegisteredTypeInfo = ReturnType<typeof Type.struct> | ReturnType<typeof 
Type.union>;
+
+const MODES = [
+  { title: 'schema-consistent', compatible: false },
+  { title: 'compatible', compatible: true },
+] as const;
+
+function buildFory(
+  compatible: boolean,
+  ref: boolean,
+  registerFns: ReadonlyArray<RegisterFn>,
+): Fory {
+  const fory = new Fory({ compatible, ref });
+  for (const registerFn of registerFns) {
+    registerFn(fory, Type);
+  }
+  return fory;
+}
+
+function getSerializer(fory: Fory, typeInfo: RegisteredTypeInfo) {
+  const serializer = fory.typeResolver.getSerializerByTypeInfo(typeInfo);
+  if (!serializer) {
+    throw new Error(`Missing serializer for type id ${typeInfo.typeId}`);
+  }
+  return serializer;
+}
+
+function roundTripValue<T>(fory: Fory, typeInfo: RegisteredTypeInfo, value: 
T): unknown {
+  const serializer = getSerializer(fory, typeInfo);
+  const bytes = fory.serialize(value, serializer);
+  return fory.deserialize(bytes, serializer);
+}
+
+function roundTripStruct<T>(fory: Fory, typeId: number, value: T): unknown {
+  return roundTripValue(fory, Type.struct(typeId), value);
+}
+
+function roundTripUnion<T>(fory: Fory, typeId: number, value: T): unknown {
+  return roundTripValue(fory, Type.union(typeId), value);
+}
+
+function normalize(value: unknown): unknown {
+  if (value instanceof Decimal) {
+    return { __decimal: value.toString() };
+  }
+  if (value instanceof Date) {
+    return { __dateMs: value.getTime() };
+  }
+  if (value instanceof Map) {
+    const entries = Array.from(value.entries()).map(([key, itemValue]) => (
+      [normalize(key), normalize(itemValue)] as const
+    ));
+    entries.sort((left, right) => 
JSON.stringify(left[0]).localeCompare(JSON.stringify(right[0])));
+    return entries;
+  }
+  if (value instanceof Set) {
+    return Array.from(value.values()).map((item) => normalize(item)).sort();
+  }
+  if (ArrayBuffer.isView(value)) {
+    if (value instanceof DataView) {
+      return Array.from(new Uint8Array(value.buffer, value.byteOffset, 
value.byteLength));
+    }
+    return Array.from(value as unknown as ArrayLike<unknown>, (item) => 
normalize(item));
+  }
+  if (Array.isArray(value)) {
+    return value.map((item) => normalize(item));
+  }
+  if (value != null && typeof value === 'object') {
+    const entries = Object.entries(value as Record<string, unknown>);
+    entries.sort(([left], [right]) => left.localeCompare(right));
+    return Object.fromEntries(entries.map(([key, itemValue]) => [key, 
normalize(itemValue)]));
+  }
+  return value;
+}
+
+function expectAcyclicEqual(expected: unknown, actual: unknown): void {
+  expect(normalize(actual)).toEqual(normalize(expected));
+}
+
+function asGeneratedNumberArray<T extends ArrayBufferView>(value: T): number[] 
{
+  return value as unknown as number[];
+}
 
-// ---------------------------------------------------------------------------
-// Helper: build test objects that conform to generated interfaces
-// ---------------------------------------------------------------------------
+function asGeneratedBigIntArray<T extends BigInt64Array | 
BigUint64Array>(value: T): (bigint | number)[] {
+  return value as unknown as (bigint | number)[];
+}
 
-function builddog(): Dog {
+function buildDog(): Dog {
   return { name: 'Rex', barkVolume: 5 };
 }
 
-function buildcat(): Cat {
+function buildCat(): Cat {
   return { name: 'Mimi', lives: 9 };
 }
 
-function buildPersonPhoneNumber(num: string, pt: Person.PhoneType): 
Person.PhoneNumber {
-  return { number_: num, phoneType: pt };
+function buildPhoneNumber(number_: string, phoneType: Person.PhoneType): 
Person.PhoneNumber {
+  return { number_, phoneType };
 }
 
-function buildperson(): Person {
+function buildPerson(): Person {
   return {
     name: 'Alice',
     id: 123,
     email: '[email protected]',
     tags: ['friend', 'colleague'],
-    scores: new Map([['math', 100], ['science', 98]]),
+    scores: new Map([
+      ['math', 100],
+      ['science', 98],
+    ]),
     salary: 120000.5,
     phones: [
-      buildPersonPhoneNumber('555-0100', Person.PhoneType.MOBILE),
-      buildPersonPhoneNumber('555-0111', Person.PhoneType.WORK),
+      buildPhoneNumber('555-0100', Person.PhoneType.MOBILE),
+      buildPhoneNumber('555-0111', Person.PhoneType.WORK),
     ],
-    pet: { case: AnimalCase.CAT, value: buildcat() },
+    pet: {
+      case: AnimalCase.CAT,
+      value: buildCat(),
+    },
   };
 }
 
-function buildaddressBook(): AddressBook {
-  const person = buildperson();
+function buildAddressBook(): AddressBook {
+  const person = buildPerson();
   return {
     people: [person],
     peopleByName: new Map([[person.name, person]]),
   };
 }
 
-function buildtreeNode(): TreeNode {
-  const child1: TreeNode = {
-    id: 'child-1',
-    name: 'Child 1',
-    children: [],
-    parent: undefined,
-  };
-  const child2: TreeNode = {
-    id: 'child-2',
-    name: 'Child 2',
-    children: [],
-    parent: undefined,
-  };
-  return {
-    id: 'root',
-    name: 'Root',
-    children: [child1, child2],
-    parent: undefined,
-  };
-}
-
-function buildAutoIdenvelope(): Envelope {
+function buildAutoIdEnvelope(): Envelope {
   const payload: Envelope.Payload = { value: 42 };
   return {
     id: 'env-1',
     payload,
     detail: { case: Envelope.DetailCase.PAYLOAD, value: payload },
-    status: Status.OK,
+    status: AutoIdStatus.OK,
   };
 }
 
-// ---------------------------------------------------------------------------
-// 1. Compilation & type-construction tests
-//    (If these tests run at all, the generated types compile correctly.)
-// ---------------------------------------------------------------------------
-
-describe('Generated types compile and construct correctly', () => {
-  test('addressBook type construction', () => {
-    const book = buildaddressBook();
-    expect(book.people).toHaveLength(1);
-    expect(book.people[0].name).toBe('Alice');
-    expect(book.people[0].id).toBe(123);
-    expect(book.people[0].email).toBe('[email protected]');
-    expect(book.people[0].tags).toEqual(['friend', 'colleague']);
-    expect(book.people[0].salary).toBe(120000.5);
-    expect(book.people[0].phones).toHaveLength(2);
-    expect(book.people[0].phones[0].phoneType).toBe(Person.PhoneType.MOBILE);
-    expect(book.people[0].phones[1].phoneType).toBe(Person.PhoneType.WORK);
-    expect(book.peopleByName.get('Alice')).toBe(book.people[0]);
-  });
+function buildPrimitiveTypes(): PrimitiveTypes {
+  return {
+    boolValue: true,
+    int8Value: 12,
+    int16Value: 1234,
+    int32Value: -123456,
+    varint32Value: -12345,
+    int64Value: -123456789n,
+    varint64Value: -987654321n,
+    taggedInt64Value: 123456789n,
+    uint8Value: 200,
+    uint16Value: 60000,
+    uint32Value: 1234567890,
+    varUint32Value: 1234567890,
+    uint64Value: 9876543210n,
+    varUint64Value: 12345678901n,
+    taggedUint64Value: 2222222222n,
+    float32Value: 2.5,
+    float64Value: 3.5,
+    contact: {
+      case: PrimitiveTypes.ContactCase.PHONE,
+      value: 12345,
+    },
+  };
+}
 
-  test('Union (animal) type construction', () => {
-    const doganimal: Animal = {
-      case: AnimalCase.DOG,
-      value: builddog(),
-    };
-    expect(doganimal.case).toBe(AnimalCase.DOG);
-    expect((doganimal.value as Dog).name).toBe('Rex');
+function buildNumericCollections(): NumericCollections {
+  return {
+    int8Values: asGeneratedNumberArray(new Int8Array([1, -2, 3])),
+    int16Values: asGeneratedNumberArray(new Int16Array([100, -200, 300])),
+    int32Values: asGeneratedNumberArray(new Int32Array([1000, -2000, 3000])),
+    int64Values: asGeneratedBigIntArray(new BigInt64Array([10000n, -20000n, 
30000n])),
+    uint8Values: asGeneratedNumberArray(new Uint8Array([200, 250])),
+    uint16Values: asGeneratedNumberArray(new Uint16Array([50000, 60000])),
+    uint32Values: asGeneratedNumberArray(new Uint32Array([2000000000, 
2100000000])),
+    uint64Values: asGeneratedBigIntArray(new BigUint64Array([9000000000n, 
12000000000n])),
+    float32Values: asGeneratedNumberArray(new Float32Array([1.5, 2.5])),
+    float64Values: asGeneratedNumberArray(new Float64Array([3.5, 4.5])),
+  };
+}
 
-    const catanimal: Animal = {
-      case: AnimalCase.CAT,
-      value: buildcat(),
-    };
-    expect(catanimal.case).toBe(AnimalCase.CAT);
-    expect((catanimal.value as Cat).lives).toBe(9);
-  });
+function buildNumericCollectionsArray(): NumericCollectionsArray {
+  return {
+    int8Values: asGeneratedNumberArray(new Int8Array([1, -2, 3])),
+    int16Values: asGeneratedNumberArray(new Int16Array([100, -200, 300])),
+    int32Values: asGeneratedNumberArray(new Int32Array([1000, -2000, 3000])),
+    int64Values: asGeneratedBigIntArray(new BigInt64Array([10000n, -20000n, 
30000n])),
+    uint8Values: asGeneratedNumberArray(new Uint8Array([200, 250])),
+    uint16Values: asGeneratedNumberArray(new Uint16Array([50000, 60000])),
+    uint32Values: asGeneratedNumberArray(new Uint32Array([2000000000, 
2100000000])),
+    uint64Values: asGeneratedBigIntArray(new BigUint64Array([9000000000n, 
12000000000n])),
+    float32Values: asGeneratedNumberArray(new Float32Array([1.5, 2.5])),
+    float64Values: asGeneratedNumberArray(new Float64Array([3.5, 4.5])),
+  };
+}
 
-  test('Enum values are correct', () => {
-    expect(Person.PhoneType.MOBILE).toBe(0);
-    expect(Person.PhoneType.HOME).toBe(1);
-    expect(Person.PhoneType.WORK).toBe(2);
+function buildNumericCollectionUnion(): NumericCollectionUnion {
+  return {
+    case: NumericCollectionUnionCase.INT64_VALUES,
+    value: asGeneratedBigIntArray(new BigInt64Array([10000n, -20000n, 
30000n])),
+  };
+}
 
-    expect(AnimalCase.DOG).toBe(1);
-    expect(AnimalCase.CAT).toBe(2);
-  });
+function buildNumericCollectionArrayUnion(): NumericCollectionArrayUnion {
+  return {
+    case: NumericCollectionArrayUnionCase.FLOAT64_VALUES,
+    value: asGeneratedNumberArray(new Float64Array([3.5, 4.5, 5.5])),
+  };
+}
 
-  test('treeNode type construction with optional parent', () => {
-    const tree = buildtreeNode();
-    expect(tree.id).toBe('root');
-    expect(tree.children).toHaveLength(2);
-    expect(tree.parent).toBeUndefined();
-    expect(tree.children[0].name).toBe('Child 1');
-  });
+function buildMonster(): Monster {
+  return {
+    pos: {
+      x: 1.0,
+      y: 2.0,
+      z: 3.0,
+    },
+    mana: 200,
+    hp: 80,
+    name: 'Orc',
+    friendly: true,
+    inventory: asGeneratedNumberArray(new Uint8Array([1, 2, 3])),
+    color: Color.Blue,
+  };
+}
 
-  test('AutoId types type construction', () => {
-    const myEnvelope = buildAutoIdenvelope();
-    expect(myEnvelope.id).toBe('env-1');
-    expect(myEnvelope.payload?.value).toBe(42);
-    expect(myEnvelope.status).toBe(Status.OK);
+function buildContainer(): Container {
+  const scalars: ScalarPack = {
+    b: -8,
+    ub: 200,
+    s: -1234,
+    us: 40000,
+    i: -123456,
+    ui: 123456,
+    l: -123456789n,
+    ul: 987654321n,
+    f: 1.5,
+    d: 2.5,
+    ok: true,
+  };
+  return {
+    id: 9876543210n,
+    status: ComplexFbsStatus.STARTED,
+    bytes: asGeneratedNumberArray(new Int8Array([1, 2, 3])),
+    numbers: asGeneratedNumberArray(new Int32Array([10, 20, 30])),
+    scalars,
+    names: ['alpha', 'beta'],
+    flags: [true, false],
+    payload: {
+      case: PayloadCase.METRIC,
+      value: { value: 42.0 },
+    },
+  };
+}
 
-    expect(Status.UNKNOWN).toBe(4096);
-    expect(Status.OK).toBe(8192);
+function buildLocalDate(year: number, month: number, day: number): Date {
+  return new Date(year, month - 1, day, 0, 0, 0, 0);
+}
 
-    expect(WrapperCase.ENVELOPE).toBe(1);
-    expect(WrapperCase.RAW).toBe(2);
+function buildOptionalHolder(): OptionalHolder {
+  const allTypes: AllOptionalTypes = {
+    boolValue: true,
+    int8Value: 12,
+    int16Value: 1234,
+    int32Value: -123456,
+    fixedInt32Value: -123456,
+    varint32Value: -12345,
+    int64Value: -123456789n,
+    fixedInt64Value: -123456789n,
+    varint64Value: -987654321n,
+    taggedInt64Value: 123456789n,
+    uint8Value: 200,
+    uint16Value: 60000,
+    uint32Value: 1234567890,
+    fixedUint32Value: 1234567890,
+    varUint32Value: 1234567890,
+    uint64Value: 9876543210n,
+    fixedUint64Value: 9876543210n,
+    varUint64Value: 12345678901n,
+    taggedUint64Value: 2222222222n,
+    float32Value: 2.5,
+    float64Value: 3.5,
+    stringValue: 'optional',
+    bytesValue: new Uint8Array([1, 2, 3]),
+    dateValue: buildLocalDate(2024, 1, 2),
+    timestampValue: new Date('2024-01-02T03:04:05Z'),
+    int32List: asGeneratedNumberArray(new Int32Array([1, 2, 3])),
+    stringList: ['alpha', 'beta'],
+    int64Map: new Map([
+      ['alpha', 10n],
+      ['beta', 20n],
+    ]),
+  };
+  return {
+    allTypes,
+    choice: {
+      case: OptionalUnionCase.NOTE,
+      value: 'optional',
+    },
+  };
+}
 
-    expect(Envelope.DetailCase.PAYLOAD).toBe(1);
-    expect(Envelope.DetailCase.NOTE).toBe(2);
-  });
-});
+function buildTree(): TreeNode {
+  const childA: TreeNode = {
+    id: 'child-a',
+    name: 'child-a',
+    children: [],
+  };
+  const childB: TreeNode = {
+    id: 'child-b',
+    name: 'child-b',
+    children: [],
+  };
+  childA.parent = childB;
+  childB.parent = childA;
+  return {
+    id: 'root',
+    name: 'root',
+    children: [childA, childA, childB],
+  };
+}
 
-// ---------------------------------------------------------------------------
-// 2. Serialization roundtrip tests using the Fory JS runtime
-//    We manually build TypeInfo objects matching the generated interfaces.
-// ---------------------------------------------------------------------------
-
-describe('Serialization roundtrip', () => {
-  test('dog struct roundtrip', () => {
-    const fory = new Fory();
-    registerAddressbookTypes(fory, Type);
-    const serializer = fory.typeResolver.getSerializerByTypeInfo(
-      Type.struct(104), 
-    );
+function buildGraph(): Graph {
+  const nodeA = {
+    id: 'node-a',
+    outEdges: [],
+    inEdges: [],
+  } as unknown as Graph['nodes'][number];
+  const nodeB = {
+    id: 'node-b',
+    outEdges: [],
+    inEdges: [],
+  } as unknown as Graph['nodes'][number];
+  const edge = {
+    id: 'edge-1',
+    weight: 1.5,
+    from_: nodeA,
+    to: nodeB,
+  } as Graph['edges'][number];
+  nodeA.outEdges = [edge];
+  nodeA.inEdges = [edge];
+  nodeB.inEdges = [edge];
+  nodeB.outEdges = [];
+  return {
+    nodes: [nodeA, nodeB],
+    edges: [edge],
+  };
+}
 
-    const dog: Dog = builddog();
-    const bytes = fory.serialize(dog, serializer);
-    const result = fory.deserialize(bytes, serializer) as Dog;
+function expectTreeEqual(expected: TreeNode, actualValue: unknown): void {
+  const actual = actualValue as TreeNode;
+  expect(actual.id).toBe(expected.id);
+  expect(actual.name).toBe(expected.name);
+  expect(actual.children).toHaveLength(expected.children.length);
+  expect(expected.children).toHaveLength(3);
+  expect(expected.children[0]).toBe(expected.children[1]);
+  expect(expected.children[0]).not.toBe(expected.children[2]);
+  expect(actual.children[0].id).toBe(expected.children[0].id);
+  expect(actual.children[0].name).toBe(expected.children[0].name);
+  expect(actual.children[2].id).toBe(expected.children[2].id);
+  expect(actual.children[2].name).toBe(expected.children[2].name);
+  expect(actual.children[0]).toBe(actual.children[1]);
+  expect(actual.children[0]).not.toBe(actual.children[2]);
+  expect(actual.children[0].parent).toBe(actual.children[2]);
+  expect(actual.children[2].parent).toBe(actual.children[0]);
+}
 
-    expect(result).toEqual(dog);
-  });
+function expectGraphEqual(expected: Graph, actualValue: unknown): void {
+  const actual = actualValue as Graph;
+  expect(actual.nodes).toHaveLength(expected.nodes.length);
+  expect(actual.edges).toHaveLength(expected.edges.length);
+  expect(actual.nodes[0].id).toBe(expected.nodes[0].id);
+  expect(actual.nodes[1].id).toBe(expected.nodes[1].id);
+  expect(actual.edges[0].id).toBe(expected.edges[0].id);
+  expect(actual.edges[0].weight).toBe(expected.edges[0].weight);
+  expect(actual.nodes[0].outEdges[0]).toBe(actual.nodes[0].inEdges[0]);
+  expect(actual.edges[0]).toBe(actual.nodes[0].outEdges[0]);
+  expect(actual.edges[0].from_).toBe(actual.nodes[0]);
+  expect(actual.edges[0].to).toBe(actual.nodes[1]);
+}
 
-  test('cat struct roundtrip', () => {
-    const fory = new Fory();
-    registerAddressbookTypes(fory, Type);
-    const serializer = fory.typeResolver.getSerializerByTypeInfo(
-      Type.struct(105),
-    );
+describe.each(MODES)('generated IDL local roundtrip ($title)', ({ compatible 
}) => {
+  test('round-trips addressbook messages and root animal unions', () => {
+    const fory = buildFory(compatible, false, [registerAddressbookTypes]);
 
-    const cat: Cat = buildcat();
-    const bytes = fory.serialize(cat, serializer);
-    const result = fory.deserialize(bytes, serializer) as Cat;
+    expectAcyclicEqual(buildAddressBook(), roundTripStruct(fory, 103, 
buildAddressBook()));
 
-    expect(result).toEqual(cat);
+    const dogAnimal: Animal = {
+      case: AnimalCase.DOG,
+      value: buildDog(),
+    };
+    const catAnimal: Animal = {
+      case: AnimalCase.CAT,
+      value: buildCat(),
+    };
+    expectAcyclicEqual(dogAnimal, roundTripUnion(fory, 106, dogAnimal));
+    expectAcyclicEqual(catAnimal, roundTripUnion(fory, 106, catAnimal));
   });
 
-  test('person.phoneNumber struct roundtrip', () => {
-    const fory = new Fory();
-    registerAddressbookTypes(fory, Type);
-    const serializer = fory.typeResolver.getSerializerByTypeInfo(
-      Type.struct(102),
-    );
+  test('round-trips auto_id messages and root wrapper unions', () => {
+    const fory = buildFory(compatible, false, [registerAutoIdTypes]);
 
-    const phone: Person.PhoneNumber = buildPersonPhoneNumber('555-0100', 
Person.PhoneType.MOBILE);
-    const bytes = fory.serialize(phone, serializer);
-    const result = fory.deserialize(bytes, serializer) as Person.PhoneNumber;
+    const envelope = buildAutoIdEnvelope();
+    const wrapperEnvelope: Wrapper = {
+      case: WrapperCase.ENVELOPE,
+      value: envelope,
+    };
+    const wrapperRaw: Wrapper = {
+      case: WrapperCase.RAW,
+      value: 'raw-payload',
+    };
 
-    expect(result).toEqual(phone);
+    expectAcyclicEqual(envelope, roundTripStruct(fory, 3022445236, envelope));
+    expectAcyclicEqual(wrapperEnvelope, roundTripUnion(fory, 1471345060, 
wrapperEnvelope));
+    expectAcyclicEqual(wrapperRaw, roundTripUnion(fory, 1471345060, 
wrapperRaw));
   });
 
-  test('envelope.payload (autoId) struct roundtrip', () => {
-    const fory = new Fory();
-    registerAutoIdTypes(fory, Type);
-    const serializer = fory.typeResolver.getSerializerByTypeInfo(
-      Type.struct(2862577837),
+  test('round-trips primitive and collection generated messages and unions', 
() => {
+    const fory = buildFory(compatible, false, [
+      registerComplexPbTypes,
+      registerCollectionTypes,
+    ]);
+
+    expectAcyclicEqual(buildPrimitiveTypes(), roundTripStruct(fory, 200, 
buildPrimitiveTypes()));
+    expectAcyclicEqual(buildNumericCollections(), roundTripStruct(fory, 210, 
buildNumericCollections()));
+    expectAcyclicEqual(
+      buildNumericCollectionsArray(),
+      roundTripStruct(fory, 212, buildNumericCollectionsArray()),
+    );
+    expectAcyclicEqual(
+      buildNumericCollectionUnion(),
+      roundTripUnion(fory, 211, buildNumericCollectionUnion()),
     );
+    expectAcyclicEqual(
+      buildNumericCollectionArrayUnion(),
+      roundTripUnion(fory, 213, buildNumericCollectionArrayUnion()),
+    );
+  });
 
-    const payload: Envelope.Payload = { value: 42 };
-    const bytes = fory.serialize(payload, serializer);
-    const result = fory.deserialize(bytes, serializer) as Envelope.Payload;
+  test('round-trips flatbuffers and optional generated messages', () => {
+    const flatbufferFory = buildFory(compatible, false, [
+      registerMonsterTypes,
+      registerComplexFbsTypes,
+      registerOptionalTypesTypes,
+    ]);
 
-    expect(result).toEqual(payload);
+    expectAcyclicEqual(buildMonster(), roundTripStruct(flatbufferFory, 
438716985, buildMonster()));
+    expectAcyclicEqual(buildContainer(), roundTripStruct(flatbufferFory, 
372413680, buildContainer()));
+    expectAcyclicEqual(buildOptionalHolder(), roundTripStruct(flatbufferFory, 
122, buildOptionalHolder()));
   });
 });
 
-// ---------------------------------------------------------------------------
-// 3. Optional field tests — use generated registration helpers, not manual
-//    TypeInfo construction, so we validate the generated code end-to-end.
-// ---------------------------------------------------------------------------
-
-describe('Optional field handling', () => {
-  test('allOptionalTypes roundtrip with present and absent optional fields', 
() => {
-    const fory = new Fory();
-    registerOptionalTypesTypes(fory, Type);
-    const serializer = fory.typeResolver.getSerializerByTypeInfo(
-      Type.struct(120),
-    );
+describe.each(MODES)('generated IDL local ref roundtrip ($title)', ({ 
compatible }) => {
+  test('round-trips tree and preserves shared-node topology', () => {
+    const fory = buildFory(compatible, true, [registerTreeTypes]);
+    const tree = buildTree();
+    expectTreeEqual(tree, roundTripStruct(fory, 2251833438, tree));
+  });
 
-    // All optional fields set
-    const full: AllOptionalTypes = {
-      boolValue: true,
-      int32Value: 42,
-      stringValue: 'hello',
-    };
-    const bytes1 = fory.serialize(full, serializer);
-    const result1 = fory.deserialize(bytes1, serializer) as AllOptionalTypes;
-    expect(result1.boolValue).toBe(true);
-    expect(result1.int32Value).toBe(42);
-    expect(result1.stringValue).toBe('hello');
-
-    // Optional fields absent (undefined)
-    const sparse: AllOptionalTypes = {};
-    const bytes2 = fory.serialize(sparse, serializer);
-    const result2 = fory.deserialize(bytes2, serializer) as AllOptionalTypes;
-    expect(result2.stringValue).toBeNull();
-    expect(result2.int32Value).toBeNull();
+  test('round-trips graph and preserves edge/node references', () => {
+    const fory = buildFory(compatible, true, [registerGraphTypes]);
+    const graph = buildGraph();
+    expectGraphEqual(graph, roundTripStruct(fory, 2373163777, graph));
   });
 });
diff --git a/integration_tests/idl_tests/run_java_tests.sh 
b/integration_tests/idl_tests/run_java_tests.sh
index 107fc0d66..f945a5d5d 100755
--- a/integration_tests/idl_tests/run_java_tests.sh
+++ b/integration_tests/idl_tests/run_java_tests.sh
@@ -21,8 +21,14 @@ set -euo pipefail
 
 SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
 ROOT_DIR="$(cd "${SCRIPT_DIR}/../.." && pwd)"
+JAVA_TEST_PATTERN="${IDL_JAVA_TEST_PATTERN:-}"
 
 python "${SCRIPT_DIR}/generate_idl.py" --lang java
 
 cd "${ROOT_DIR}/java"
-ENABLE_FORY_DEBUG_OUTPUT=1 mvn -T16 --no-transfer-progress test -f 
"${ROOT_DIR}/integration_tests/idl_tests/java/pom.xml"
+MAVEN_ARGS=(-T16 --no-transfer-progress)
+if [[ -n "${JAVA_TEST_PATTERN}" ]]; then
+  MAVEN_ARGS+=("-Dtest=${JAVA_TEST_PATTERN}")
+fi
+MAVEN_ARGS+=(test -f "${ROOT_DIR}/integration_tests/idl_tests/java/pom.xml")
+ENABLE_FORY_DEBUG_OUTPUT=1 mvn "${MAVEN_ARGS[@]}"
diff --git a/integration_tests/idl_tests/run_javascript_tests.sh 
b/integration_tests/idl_tests/run_javascript_tests.sh
index e0e6a6716..bb3226f4b 100755
--- a/integration_tests/idl_tests/run_javascript_tests.sh
+++ b/integration_tests/idl_tests/run_javascript_tests.sh
@@ -20,12 +20,15 @@
 set -euo pipefail
 
 SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
+ROOT_DIR="$(cd "${SCRIPT_DIR}/../.." && pwd)"
 
 python "${SCRIPT_DIR}/generate_idl.py" --lang javascript
 
+cd "${ROOT_DIR}/java"
+ENABLE_FORY_DEBUG_OUTPUT=1 mvn -T16 --no-transfer-progress install -DskipTests
+
 cd "${SCRIPT_DIR}/javascript"
 npm install
 ENABLE_FORY_DEBUG_OUTPUT=1 npx jest --ci
 
 IDL_PEER_LANG=javascript "${SCRIPT_DIR}/run_java_tests.sh"
-
diff --git a/javascript/packages/core/lib/context.ts 
b/javascript/packages/core/lib/context.ts
index 25ceadcdf..b355c275a 100644
--- a/javascript/packages/core/lib/context.ts
+++ b/javascript/packages/core/lib/context.ts
@@ -387,7 +387,6 @@ export class ReadContext {
   readonly metaStringReader: MetaStringReader;
 
   private typeMeta: TypeMeta[] = [];
-  private typeMetaCount = 0;
   /** Persistent cross-message cache keyed by 8-byte type meta header. */
   private typeMetaCache: Map<bigint, TypeMeta> = new Map();
   private _depth = 0;
@@ -411,7 +410,7 @@ export class ReadContext {
     this.reader.reset(bytes);
     this.refReader.reset();
     this.metaStringReader.reset();
-    this.typeMetaCount = 0;
+    this.typeMeta = [];
     this._depth = 0;
   }
 
@@ -466,44 +465,62 @@ export class ReadContext {
   readTypeMeta(): TypeMeta {
     const idOrLen = this.reader.readVarUInt32();
     if (idOrLen & 1) {
-      return this.typeMeta[idOrLen >> 1];
+      const typeMeta = this.typeMeta[idOrLen >> 1];
+      if (!typeMeta) {
+        throw new Error(`missing TypeMeta reference ${idOrLen >> 1}`);
+      }
+      return typeMeta;
     }
+    const dynamicTypeId = idOrLen >> 1;
     // Read the 8-byte header to check the cross-message cache.
-    const [headerLong, metaSize] = TypeMeta.readHeader(this.reader);
-    const cached = this.typeMetaCache.get(headerLong);
+    const header = TypeMeta.readHeader(this.reader);
+    const cached = this.typeMetaCache.get(header);
     let typeMeta: TypeMeta;
     if (cached) {
-      TypeMeta.skipBody(this.reader, metaSize);
+      TypeMeta.skipBody(this.reader, header);
       typeMeta = cached;
     } else {
-      typeMeta = TypeMeta.fromBytesAfterHeader(this.reader);
-      this.typeMetaCache.set(headerLong, typeMeta);
-    }
-    if (this.typeMetaCount < this.typeMeta.length) {
-      this.typeMeta[this.typeMetaCount] = typeMeta;
-    } else {
-      this.typeMeta.push(typeMeta);
+      typeMeta = TypeMeta.fromBytesAfterHeader(this.reader, header);
+      this.typeMetaCache.set(header, typeMeta);
     }
-    this.typeMetaCount++;
+    this.typeMeta[dynamicTypeId] = typeMeta;
     return typeMeta;
   }
 
-  private fieldInfoToTypeInfo(fieldInfo: InnerFieldInfo): TypeInfo {
+  private fieldInfoToTypeInfo(fieldInfo: InnerFieldInfo, fallbackTypeInfo?: 
TypeInfo): TypeInfo {
     switch (fieldInfo.typeId) {
       case TypeId.MAP:
         return Type.map(
-          this.fieldInfoToTypeInfo(fieldInfo.options!.key!),
-          this.fieldInfoToTypeInfo(fieldInfo.options!.value!)
+          this.fieldInfoToTypeInfo(fieldInfo.options!.key!, 
fallbackTypeInfo?.options?.key),
+          this.fieldInfoToTypeInfo(fieldInfo.options!.value!, 
fallbackTypeInfo?.options?.value)
         );
       case TypeId.LIST:
-        return Type.array(this.fieldInfoToTypeInfo(fieldInfo.options!.inner!));
+        return Type.array(this.fieldInfoToTypeInfo(fieldInfo.options!.inner!, 
fallbackTypeInfo?.options?.inner));
       case TypeId.SET:
-        return Type.set(this.fieldInfoToTypeInfo(fieldInfo.options!.key!));
+        return Type.set(this.fieldInfoToTypeInfo(fieldInfo.options!.key!, 
fallbackTypeInfo?.options?.key));
       default: {
+        // Remote TypeMeta only carries the nested user-defined type kind, not 
the
+        // concrete named type or custom serializer identity. Reuse the local 
field
+        // declaration when available. When the local field is absent, still 
prefer
+        // any generic serializer available for that kind (for example enums) 
before
+        // falling back to `any`.
+        if (TypeId.userDefinedType(fieldInfo.typeId)) {
+          if (fallbackTypeInfo) {
+            return fallbackTypeInfo.clone();
+          }
+          const serializer = 
this.typeResolver.getSerializerById(fieldInfo.typeId, fieldInfo.userTypeId);
+          if (serializer) {
+            return serializer.getTypeInfo().clone();
+          }
+          return Type.any();
+        }
         const serializer = 
this.typeResolver.getSerializerById(fieldInfo.typeId, fieldInfo.userTypeId);
         if (serializer) {
           return serializer.getTypeInfo().clone();
         }
+        if (fallbackTypeInfo) {
+          return fallbackTypeInfo.clone();
+        }
         return Type.any();
       }
     }
@@ -514,6 +531,14 @@ export class ReadContext {
     if (!TypeId.structType(typeId)) {
       throw new Error("only support reconstructor struct type");
     }
+    if (!original) {
+      if (TypeId.isNamedType(typeId)) {
+        const named = `${typeMeta.getNs()}$${typeMeta.getTypeName()}`;
+        original = this.typeResolver.getSerializerByName(named);
+      } else {
+        original = this.typeResolver.getSerializerById(typeId, 
typeMeta.getUserTypeId());
+      }
+    }
     let typeInfo: TypeInfo;
     if (original) {
       typeInfo = original.getTypeInfo().clone();
@@ -522,8 +547,10 @@ export class ReadContext {
     } else {
       typeInfo = Type.struct({ typeName: typeMeta.getTypeName(), namespace: 
typeMeta.getNs() });
     }
-    const props = Object.fromEntries(typeMeta.getFieldInfo().map((fieldInfo) 
=> {
-      const fieldTypeInfo = this.fieldInfoToTypeInfo(fieldInfo)
+    const localProps = original?.getTypeInfo().options?.props;
+    const props = 
Object.fromEntries(typeMeta.remapFieldNames(localProps).map((fieldInfo) => {
+      const localFieldTypeInfo = localProps?.[fieldInfo.getFieldName()];
+      const fieldTypeInfo = this.fieldInfoToTypeInfo(fieldInfo, 
localFieldTypeInfo)
         .setNullable(fieldInfo.nullable)
         .setTrackingRef(fieldInfo.trackingRef)
         .setId(fieldInfo.fieldId);
diff --git a/javascript/packages/core/lib/gen/any.ts 
b/javascript/packages/core/lib/gen/any.ts
index c02073e56..2486e93f3 100644
--- a/javascript/packages/core/lib/gen/any.ts
+++ b/javascript/packages/core/lib/gen/any.ts
@@ -73,8 +73,19 @@ export class AnyHelper {
           serializer = typeResolver.getSerializerByName(buildNamedTypeKey(ns, 
typeName));
         }
         break;
-      case TypeId.NAMED_STRUCT:
       case TypeId.NAMED_EXT:
+        if (readContext.isCompatible()) {
+          const typeMeta = readContext.readTypeMeta();
+          const ns = typeMeta.getNs();
+          const typeName = typeMeta.getTypeName();
+          serializer = typeResolver.getSerializerByName(buildNamedTypeKey(ns, 
typeName));
+        } else {
+          const ns = readContext.readNamespace();
+          const typeName = readContext.readTypeName();
+          serializer = typeResolver.getSerializerByName(buildNamedTypeKey(ns, 
typeName));
+        }
+        break;
+      case TypeId.NAMED_STRUCT:
       case TypeId.NAMED_COMPATIBLE_STRUCT:
         if (readContext.isCompatible() || typeId === 
TypeId.NAMED_COMPATIBLE_STRUCT) {
           const typeMeta = readContext.readTypeMeta();
diff --git a/javascript/packages/core/lib/meta/TypeMeta.ts 
b/javascript/packages/core/lib/meta/TypeMeta.ts
index 83f832e17..78110c8ca 100644
--- a/javascript/packages/core/lib/meta/TypeMeta.ts
+++ b/javascript/packages/core/lib/meta/TypeMeta.ts
@@ -46,8 +46,9 @@ const typeNameDecoder = new MetaStringDecoder("$", ".");
 // so NAMED_COMPATIBLE_STRUCT output is byte-compatible cross-binding.
 const COMPRESS_META_FLAG = 1n << 9n;
 const HAS_FIELDS_META_FLAG = 1n << 8n;
-const META_SIZE_MASKS = 0xFF;
+const META_SIZE_MASKS = 0xFF; // low 8 bits
 const NUM_HASH_BITS = 50;
+const HASH_SHIFT_BITS = 64n - BigInt(NUM_HASH_BITS);
 const BIG_NAME_THRESHOLD = 0b111111;
 
 const PRIMITIVE_TYPE_IDS = [
@@ -210,16 +211,26 @@ const pkgNameEncoding = [Encoding.UTF_8, 
Encoding.ALL_TO_LOWER_SPECIAL, Encoding
 const fieldNameEncoding = [Encoding.UTF_8, Encoding.ALL_TO_LOWER_SPECIAL, 
Encoding.LOWER_UPPER_DIGIT_SPECIAL];
 const typeNameEncoding = [Encoding.UTF_8, Encoding.ALL_TO_LOWER_SPECIAL, 
Encoding.LOWER_UPPER_DIGIT_SPECIAL, Encoding.FIRST_TO_LOWER_SPECIAL];
 export class TypeMeta {
+  private headerHash: number | null;
+  private readonly hasFieldsMeta: boolean;
+  private readonly compressed: boolean;
+
   private constructor(private fields: FieldInfo[], private type: {
     typeId: number;
     typeName: string;
     namespace: string;
     userTypeId: number;
-  }) {
+  }, headerHash?: number, hasFieldsMeta?: boolean, compressed = false) {
+    this.headerHash = headerHash ?? null;
+    this.hasFieldsMeta = hasFieldsMeta ?? fields.length > 0;
+    this.compressed = compressed;
   }
 
   getHash() {
-    return this.fields.length;
+    if (this.headerHash === null) {
+      this.toBytes();
+    }
+    return this.headerHash!;
   }
 
   computeStructFingerprint(fields: FieldInfo[]) {
@@ -230,7 +241,7 @@ export class TypeMeta {
         typeId = TypeId.UNKNOWN;
       }
       let fieldIdentifier = "";
-      if (field.getFieldId()) {
+      if (field.hasFieldId()) {
         fieldIdentifier = `${field.getFieldId()}`;
       } else {
         fieldIdentifier = TypeMeta.toSnakeCase(field.getFieldName());
@@ -292,37 +303,37 @@ export class TypeMeta {
 
   /**
    * Read the 8-byte type meta header and extract the body size.
-   * Returns [headerLong, metaSize] without advancing past the body.
+   * Returns the raw header without advancing past the body.
    */
-  static readHeader(reader: BinaryReader): [bigint, number] {
-    const headerLong = reader.readInt64();
-    let metaSize = Number(headerLong & BigInt(META_SIZE_MASKS));
-    if (metaSize === META_SIZE_MASKS) {
-      metaSize += reader.readVarUInt32();
-    }
-    return [headerLong, metaSize];
+  static readHeader(reader: BinaryReader): bigint {
+    return reader.readUint64();
   }
 
   /**
    * Skip the type meta body bytes after the header has already been read.
    */
-  static skipBody(reader: BinaryReader, metaSize: number) {
-    reader.readSkip(metaSize);
+  static skipBody(reader: BinaryReader, header: bigint) {
+    reader.readSkip(TypeMeta.readMetaSize(reader, header));
   }
 
   static fromBytes(reader: BinaryReader): TypeMeta {
-    // Read header with hash and flags
-    const [headerLong, metaSize] = TypeMeta.readHeader(reader);
-    void headerLong;
-    void metaSize;
-    return TypeMeta.fromBytesAfterHeader(reader);
+    return TypeMeta.fromBytesAfterHeader(reader, TypeMeta.readHeader(reader));
   }
 
   /**
    * Parse the type meta body after the header has already been consumed
    * by readHeader(). Used by ReadContext to avoid re-reading the header.
    */
-  static fromBytesAfterHeader(reader: BinaryReader): TypeMeta {
+  static fromBytesAfterHeader(reader: BinaryReader, header: bigint): TypeMeta {
+    const compressed = (header & COMPRESS_META_FLAG) !== 0n;
+    if (compressed) {
+      throw new Error("compressed TypeMeta is not supported yet");
+    }
+    const hasFieldsMeta = (header & HAS_FIELDS_META_FLAG) !== 0n;
+    const metaSize = TypeMeta.readMetaSize(reader, header);
+    const headerHash = Number(header >> HASH_SHIFT_BITS);
+
+    const bodyStart = reader.readGetCursor();
     // Read class header
     const classHeader = reader.readUint8();
     let numFields = classHeader & SMALL_NUM_FIELDS_THRESHOLD;
@@ -361,7 +372,26 @@ export class TypeMeta {
       userTypeId,
     };
 
-    return new TypeMeta(fields, typeInfo);
+    const consumed = reader.readGetCursor() - bodyStart;
+    if (consumed !== metaSize) {
+      throw new Error(`unexpected TypeMeta body size: expected ${metaSize}, 
consumed ${consumed}`);
+    }
+
+    return new TypeMeta(
+      fields,
+      typeInfo,
+      headerHash,
+      hasFieldsMeta,
+      compressed
+    );
+  }
+
+  private static readMetaSize(reader: BinaryReader, header: bigint): number {
+    let metaSize = Number(header & BigInt(META_SIZE_MASKS));
+    if (metaSize === META_SIZE_MASKS) {
+      metaSize += reader.readVarUInt32();
+    }
+    return metaSize;
   }
 
   private static readFieldInfo(reader: BinaryReader): FieldInfo {
@@ -376,12 +406,13 @@ export class TypeMeta {
     }
 
     // Read type ID
-    const { typeId, userTypeId, options, fieldId } = this.readTypeId(reader);
+    const { typeId, userTypeId, options } = this.readTypeId(reader);
 
     let fieldName: string;
+    let fieldId: number | undefined;
     if (encodingFlags === 3) {
-      // TAG_ID encoding - field name is the size value
-      fieldName = size.toString();
+      fieldId = size;
+      fieldName = `$tag${fieldId}`;
     } else {
       // Read field name
       const encoding = FieldInfo.u8ToEncoding(encodingFlags);
@@ -478,7 +509,59 @@ export class TypeMeta {
     return this.fields;
   }
 
+  remapFieldNames(localProps: Record<string, TypeInfo> | undefined): 
FieldInfo[] {
+    if (!localProps) {
+      return this.fields;
+    }
+
+    const localNameById = new Map<number, string>();
+    const localNameByNormalized = new Map<string, string>();
+    for (const [fieldName, typeInfo] of Object.entries(localProps)) {
+      if (typeof typeInfo.id === "number") {
+        localNameById.set(typeInfo.id, fieldName);
+      }
+      const normalized = TypeMeta.toSnakeCase(fieldName);
+      if (!localNameByNormalized.has(normalized)) {
+        localNameByNormalized.set(normalized, fieldName);
+      }
+    }
+
+    return this.fields.map((fieldInfo) => {
+      let resolvedName = fieldInfo.getFieldName();
+      if (fieldInfo.hasFieldId()) {
+        const localName = localNameById.get(fieldInfo.getFieldId()!);
+        if (localName) {
+          resolvedName = localName;
+        }
+      } else if (localProps[resolvedName]) {
+        resolvedName = fieldInfo.getFieldName();
+      } else {
+        const normalized = TypeMeta.toSnakeCase(resolvedName);
+        const localName = localNameByNormalized.get(normalized);
+        if (localName) {
+          resolvedName = localName;
+        }
+      }
+      if (resolvedName === fieldInfo.getFieldName()) {
+        return fieldInfo;
+      }
+      return new FieldInfo(
+        resolvedName,
+        fieldInfo.typeId,
+        fieldInfo.userTypeId,
+        fieldInfo.trackingRef,
+        fieldInfo.nullable,
+        fieldInfo.options,
+        fieldInfo.fieldId
+      );
+    });
+  }
+
   toBytes() {
+    if (this.compressed) {
+      throw new Error("compressed TypeMeta is not supported yet");
+    }
+
     const writer = new BinaryWriter({});
     writer.writeUint8(-1); // placeholder for header, update later
     let currentClassHeader = this.fields.length;
@@ -552,7 +635,7 @@ export class TypeMeta {
   private writeFieldsInfo(writer: BinaryWriter, fields: FieldInfo[]) {
     for (const fieldInfo of fields) {
       // header: 2 bits field name encoding + 4 bits size + nullability flag + 
ref tracking flag
-      let header = fieldInfo.trackingRef ? 1 : 0; // Simplified - no ref 
tracking or nullability for now
+      let header = fieldInfo.trackingRef ? 1 : 0;
       header |= fieldInfo.nullable ? 0b10 : 0b00;
       let size: number;
       let encodingFlags: number;
@@ -649,8 +732,7 @@ export class TypeMeta {
     return result;
   }
 
-  private prependHeader(buffer: Uint8Array, isCompressed: boolean, 
hasFieldsMeta: boolean): Uint8Array {
-    const metaSize = buffer.length;
+  private static buildHeader(buffer: Uint8Array, isCompressed: boolean, 
hasFieldsMeta: boolean) {
     const hash = x64hash128(buffer, 47);
     // Read the high 64 bits of the 128-bit MurmurHash3 as a SIGNED
     // int64 to match pyfory (`hash_buffer()[0]` unpacks `int64_t[0]`),
@@ -672,19 +754,28 @@ export class TypeMeta {
       header = -header;
     }
     header = header & 0x7FFFFFFFFFFFFFFFn;
-
     if (isCompressed) {
       header |= COMPRESS_META_FLAG;
     }
     if (hasFieldsMeta) {
       header |= HAS_FIELDS_META_FLAG;
     }
-    header |= BigInt(Math.min(metaSize, META_SIZE_MASKS));
+    header |= BigInt(Math.min(buffer.length, META_SIZE_MASKS));
+    return {
+      header: BigInt.asUintN(64, header),
+      headerHash: Number(header >> HASH_SHIFT_BITS),
+    };
+  }
+
+  private prependHeader(buffer: Uint8Array, isCompressed: boolean, 
hasFieldsMeta: boolean): Uint8Array {
+    const metaSize = buffer.length;
+    const { header, headerHash } = TypeMeta.buildHeader(buffer, isCompressed, 
hasFieldsMeta);
+    this.headerHash = headerHash;
 
     const writer = new BinaryWriter({});
-    writer.writeInt64(header);
+    writer.writeUint64(header);
 
-    if (metaSize > META_SIZE_MASKS) {
+    if (metaSize >= META_SIZE_MASKS) {
       writer.writeVarUInt32(metaSize - META_SIZE_MASKS);
     }
 
diff --git a/javascript/test/typemeta.test.ts b/javascript/test/typemeta.test.ts
index 8cccadd35..d34438cf3 100644
--- a/javascript/test/typemeta.test.ts
+++ b/javascript/test/typemeta.test.ts
@@ -18,59 +18,288 @@
  */
 
 import Fory, { Type } from '../packages/core/index';
-import {describe, expect, test} from '@jest/globals';
-import * as beautify from 'js-beautify';
+import { TypeMeta } from '../packages/core/lib/meta/TypeMeta';
+import { BinaryReader } from '../packages/core/lib/reader';
+import { describe, expect, test } from '@jest/globals';
 
+const HAS_FIELDS_META_FLAG = 1n << 8n;
+const COMPRESS_META_FLAG = 1n << 9n;
+const META_SIZE_MASK = 0xFFn;
+const HASH_SHIFT_BITS = 14n;
 
 describe('typemeta', () => {
-  test('should evaluation scheme work', () => {
-    
-    const fory = new Fory({
-        compatible: true
-    });    
-
-    @Type.struct("example.foo")
-    class Foo {
-        @Type.string()
-        bar: string;
-
-        @Type.int32()
-        bar2: number;
-
-        setBar(bar: string) {
-            this.bar = bar;
-            return this;
-        }
-
-        setBar2(bar2: number) {
-            this.bar2 = bar2;
-            return this;
-        }
+  test('writes TypeMeta header bits in the xlang layout', () => {
+    const typeInfo = Type.struct(7001, {
+      fullName: Type.string().setId(1),
+      age: Type.int32().setId(2),
+    });
+
+    const bytes = TypeMeta.fromTypeInfo(typeInfo).toBytes();
+    const header = new DataView(bytes.buffer, bytes.byteOffset, 
bytes.byteLength).getBigUint64(0, true);
+
+    expect(Number(header & META_SIZE_MASK)).toBe(bytes.length - 8);
+    expect((header & HAS_FIELDS_META_FLAG) !== 0n).toBe(true);
+    expect((header & COMPRESS_META_FLAG) !== 0n).toBe(false);
+    expect(header >> HASH_SHIFT_BITS).toBeGreaterThan(0n);
+  });
+
+  test('writes the zero size extension when the TypeMeta body is exactly 0xFF 
bytes', () => {
+    const typeMeta = TypeMeta.fromTypeInfo(Type.struct(7003, {})) as any;
+    const body = new Uint8Array(0xFF);
+    const bytes = typeMeta.prependHeader(body, false, false) as Uint8Array;
+    const reader = new BinaryReader({});
+
+    expect(bytes).toHaveLength(8 + 1 + body.length);
+    expect(bytes[8]).toBe(0);
+
+    reader.reset(bytes);
+    const header = TypeMeta.readHeader(reader);
+    TypeMeta.skipBody(reader, header);
+    expect(reader.readGetCursor()).toBe(bytes.length);
+  });
+
+  test('regenerates compatible named serializers when schema changes but field 
count stays the same', () => {
+    const writerFory = new Fory({ compatible: true });
+    const readerFory = new Fory({ compatible: true });
+
+    const writerType = Type.struct('example.item', {
+      value: Type.string(),
+    });
+    const readerType = Type.struct('example.item', {
+      value: Type.int32(),
+    });
+
+    const bytes = writerFory.register(writerType).serialize({ value: 'hello' 
});
+    const result = readerFory.register(readerType).deserialize(bytes);
+
+    expect(result).toEqual({ value: 'hello' });
+  });
+
+  test('remaps compatible tag-id fields onto local property names during 
regeneration', () => {
+    const writerFory = new Fory({ compatible: true });
+    const readerFory = new Fory({ compatible: true });
+
+    const writerType = Type.struct(7002, {
+      fullName: Type.string().setId(1),
+      note: Type.string().setId(2),
+    });
+    const readerType = Type.struct(7002, {
+      name: Type.string().setId(1),
+      alias: Type.int32().setId(2),
+    });
+
+    const bytes = writerFory.register(writerType).serialize({
+      fullName: 'Alice',
+      note: 'ally',
+    });
+    const result = readerFory.register(readerType).deserialize(bytes);
+
+    expect(result).toEqual({
+      name: 'Alice',
+      alias: 'ally',
+    });
+  });
+
+  test('keeps compatible named schema evolution working when field count 
differs', () => {
+    const writerFory = new Fory({ compatible: true });
+    const readerFory = new Fory({ compatible: true });
+
+    const writerType = Type.struct('example.foo', {
+      bar: Type.string(),
+      bar2: Type.int32(),
+    });
+    const readerType = Type.struct('example.foo', {
+      bar: Type.string(),
+    });
+
+    const bytes = writerFory.register(writerType).serialize({
+      bar: 'hello',
+      bar2: 123,
+    });
+    const result = readerFory.register(readerType).deserialize(bytes);
+
+    expect(result).toEqual({
+      bar: 'hello',
+      bar2: 123,
+    });
+  });
+
+  test('remaps regenerated compatible field names onto local snake_case 
properties', () => {
+    const writerFory = new Fory({ compatible: true });
+    const readerFory = new Fory({ compatible: true });
+
+    class WriterHolder {
+      animalMap = new Map<string, number>();
+      marker = 0;
+    }
+    Type.struct(7004, {
+      animalMap: Type.map(Type.string(), Type.any()),
+      marker: Type.int32(),
+    })(WriterHolder);
+
+    class ReaderHolder {
+      animal_map = new Map<string, number>();
+    }
+    Type.struct(7004, {
+      animal_map: Type.map(Type.string(), Type.any()),
+    })(ReaderHolder);
+
+    const writerReg = writerFory.register(WriterHolder);
+    const readerReg = readerFory.register(ReaderHolder);
+
+    const value = new WriterHolder();
+    value.animalMap.set('dog', 7);
+    value.marker = 99;
+
+    const result = readerReg.deserialize(writerReg.serialize(value));
+
+    expect(result).toBeInstanceOf(ReaderHolder);
+    expect(result.animal_map.get('dog')).toBe(7);
+    expect((result as ReaderHolder & { animalMap?: Map<string, number> 
}).animalMap).toBeUndefined();
+    expect((result as ReaderHolder & { marker?: number }).marker).toBe(99);
+  });
+
+  test('skips unknown named custom fields by falling back to any when no local 
field exists', () => {
+    const writerFory = new Fory({ compatible: true });
+    const readerFory = new Fory({ compatible: true });
+
+    class MyExt {
+      id = 0;
+    }
+    Type.ext('my_ext')(MyExt);
+
+    const customSerializer = {
+      write: (writeContext: any, value: MyExt) => {
+        writeContext.writeVarInt32(value.id);
+      },
+      read: (readContext: any, result: MyExt) => {
+        result.id = readContext.readVarInt32();
+      },
+    };
+
+    writerFory.register(MyExt, customSerializer);
+    readerFory.register(MyExt, customSerializer);
+
+    class WriterWrapper {
+      note = '';
+      myExt = new MyExt();
+    }
+    Type.struct('example.wrapper', {
+      note: Type.string(),
+      myExt: Type.ext('my_ext'),
+    })(WriterWrapper);
+
+    class EmptyWrapper {}
+    Type.struct('example.wrapper', {})(EmptyWrapper);
+
+    const writerReg = writerFory.register(WriterWrapper);
+    const readerReg = readerFory.register(EmptyWrapper);
+
+    const value = new WriterWrapper();
+    value.note = 'hello';
+    value.myExt.id = 42;
+
+    const result = readerReg.deserialize(writerReg.serialize(value));
+
+    expect(result).toBeInstanceOf(EmptyWrapper);
+  });
+
+  test('skips unknown compatible enum fields when regenerating an empty 
reader', () => {
+    const writerFory = new Fory({ compatible: true });
+    const readerFory = new Fory({ compatible: true });
+
+    const TestEnum = {
+      VALUE_A: 0,
+      VALUE_B: 1,
+      VALUE_C: 2,
+    };
+    writerFory.register(Type.enum(7101, TestEnum));
+    readerFory.register(Type.enum(7101, TestEnum));
+
+    class WriterStruct {
+      f1 = TestEnum.VALUE_A;
+      f2 = TestEnum.VALUE_B;
     }
+    Type.struct(7102, {
+      f1: Type.enum(7101, TestEnum),
+      f2: Type.enum(7101, TestEnum),
+    })(WriterStruct);
 
-    const { serialize } = fory.register(Foo);
-    const bin = serialize(new Foo().setBar("hello").setBar2(123));
+    class EmptyStruct {}
+    Type.struct(7102, {})(EmptyStruct);
 
+    const writerReg = writerFory.register(WriterStruct);
+    const readerReg = readerFory.register(EmptyStruct);
 
-    @Type.struct("example.foo")
-    class Foo2 {
-        @Type.string()
-        bar: string;
+    const value = new WriterStruct();
+    const result = readerReg.deserialize(writerReg.serialize(value));
+
+    expect(result).toBeInstanceOf(EmptyStruct);
+  });
+
+  test('skips unknown enum and named custom fields together during compatible 
regeneration', () => {
+    const writerFory = new Fory({ compatible: true });
+    const readerFory = new Fory({ compatible: true });
+
+    const Color = {
+      Green: 0,
+      Red: 1,
+      Blue: 2,
+      White: 3,
+    };
+    writerFory.register(Type.enum('color', Color));
+    readerFory.register(Type.enum('color', Color));
+
+    class MyExt {
+      id = 0;
+    }
+    Type.ext('my_ext')(MyExt);
+
+    const customSerializer = {
+      write: (writeContext: any, value: MyExt) => {
+        writeContext.writeVarInt32(value.id);
+      },
+      read: (readContext: any, result: MyExt) => {
+        result.id = readContext.readVarInt32();
+      },
+    };
+
+    writerFory.register(MyExt, customSerializer);
+    readerFory.register(MyExt, customSerializer);
+
+    class MyStruct {
+      id = 0;
+    }
+    Type.struct('my_struct', {
+      id: Type.varInt32(),
+    })(MyStruct);
+
+    writerFory.register(MyStruct);
+    readerFory.register(MyStruct);
+
+    class WriterWrapper {
+      color = Color.White;
+      myStruct = new MyStruct();
+      myExt = new MyExt();
     }
+    Type.struct('my_wrapper', {
+      color: Type.enum('color', Color),
+      myStruct: Type.struct('my_struct'),
+      myExt: Type.ext('my_ext'),
+    })(WriterWrapper);
+
+    class EmptyWrapper {}
+    Type.struct('my_wrapper', {})(EmptyWrapper);
+
+    const writerReg = writerFory.register(WriterWrapper);
+    const readerReg = readerFory.register(EmptyWrapper);
+
+    const value = new WriterWrapper();
+    value.myStruct.id = 42;
+    value.myExt.id = 43;
+
+    const result = readerReg.deserialize(writerReg.serialize(value));
 
-    const fory2 = new Fory({
-        compatible: true,
-        hooks: {
-            afterCodeGenerated: (code: string) => {
-                return beautify.js(code, { indent_size: 2, 
space_in_empty_paren: true, indent_empty_lines: true });
-              }        
-            }
-    });    
-    const { deserialize  } = fory2.register(Foo2);
-    const r = deserialize(bin);
-    expect(r).toEqual({
-        bar: "hello",
-        bar2: 123,
-    })
+    expect(result).toBeInstanceOf(EmptyWrapper);
   });
 });


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to