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]