@zerodds/wire 0.0.0-stage → 1.0.0-rc.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +13 -2
- package/dist/branded.d.ts +98 -0
- package/dist/branded.js +64 -0
- package/dist/dds_any.d.ts +47 -0
- package/dist/dds_any.js +41 -0
- package/dist/endianness.d.ts +22 -0
- package/dist/endianness.js +35 -0
- package/dist/equal.d.ts +46 -0
- package/dist/equal.js +273 -0
- package/dist/error.d.ts +52 -0
- package/dist/error.js +78 -0
- package/dist/float128.d.ts +15 -0
- package/dist/float128.js +181 -0
- package/dist/index.d.ts +31 -0
- package/dist/index.js +33 -0
- package/dist/operations.d.ts +180 -0
- package/dist/operations.js +74 -0
- package/dist/policy.d.ts +133 -0
- package/dist/policy.js +184 -0
- package/dist/reader.d.ts +605 -0
- package/dist/reader.js +1345 -0
- package/dist/registry.d.ts +84 -0
- package/dist/registry.js +166 -0
- package/dist/test_backend.d.ts +9 -0
- package/dist/test_backend.js +173 -0
- package/dist/types.d.ts +246 -0
- package/dist/types.js +3 -0
- package/dist/wasm.d.ts +203 -0
- package/dist/wasm.js +194 -0
- package/dist/writer.d.ts +662 -0
- package/dist/writer.js +1333 -0
- package/package.json +27 -4
package/README.md
CHANGED
|
@@ -1,3 +1,14 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @zerodds/wire
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
The canonical TypeScript XCDR and descriptor runtime imported by generated
|
|
4
|
+
ZeroDDS modules. Consumers declare `@zerodds/wire` at the same version as
|
|
5
|
+
their compiler, currently `1.0.0-rc.7`. It is independent of the optional
|
|
6
|
+
`@zerodds/node` native DDS binding.
|
|
7
|
+
|
|
8
|
+
`npm pack` builds and packages JavaScript and declaration files from `src`
|
|
9
|
+
into `dist`. Install the resulting tarball in a separate consumer project
|
|
10
|
+
to verify package resolution. No repository path mapping is required.
|
|
11
|
+
|
|
12
|
+
The package manifest prepares publication but does not assert registry
|
|
13
|
+
availability. Package ownership and Trusted Publishing for `@zerodds/wire`
|
|
14
|
+
must be established before a separately authorized upload.
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The IDL types TypeScript has no structural form for.
|
|
3
|
+
*
|
|
4
|
+
* DDS-TS 1.0 §7.3.2, §7.3.3, §8.5 and §8.6. Three of them:
|
|
5
|
+
* a `char` holding exactly one ISO 8859-1 character, a `wchar` holding
|
|
6
|
+
* exactly one Unicode scalar, and a `long double` holding sixteen octets
|
|
7
|
+
* of binary128.
|
|
8
|
+
*
|
|
9
|
+
* # Why the brands are string literals
|
|
10
|
+
*
|
|
11
|
+
* A brand made from a module-local `unique symbol` is nominal across
|
|
12
|
+
* module boundaries: a `Char` produced by one vendor's runtime would not
|
|
13
|
+
* satisfy another vendor's `Char` parameter even though both hold the
|
|
14
|
+
* same character. The tags below — `"char"`, `"wchar"`, `"long_double"`
|
|
15
|
+
* — and the property name `__dds_brand` are normative for exactly that
|
|
16
|
+
* reason, and two conformant runtimes produce assignment-compatible
|
|
17
|
+
* types without importing each other.
|
|
18
|
+
*
|
|
19
|
+
* The brand is a compile-time marker with no run-time effect: a `Char`
|
|
20
|
+
* *is* a string at run time. What the factories below add is the check
|
|
21
|
+
* the type cannot make.
|
|
22
|
+
*/
|
|
23
|
+
/**
|
|
24
|
+
* An IDL `char`: one ISO 8859-1 character.
|
|
25
|
+
*
|
|
26
|
+
* DDS-TS 1.0 §7.3.2. Construct with {@link makeChar}.
|
|
27
|
+
*/
|
|
28
|
+
export type Char = string & {
|
|
29
|
+
readonly __dds_brand: "char";
|
|
30
|
+
};
|
|
31
|
+
/**
|
|
32
|
+
* An IDL `wchar`: one Unicode scalar.
|
|
33
|
+
*
|
|
34
|
+
* DDS-TS 1.0 §7.3.2. Construct with {@link makeWChar}.
|
|
35
|
+
*/
|
|
36
|
+
export type WChar = string & {
|
|
37
|
+
readonly __dds_brand: "wchar";
|
|
38
|
+
};
|
|
39
|
+
/**
|
|
40
|
+
* One ISO 8859-1 character, or a `RangeError`.
|
|
41
|
+
*
|
|
42
|
+
* OMG IDL 4.2 §7.2.6.2 makes `char` an 8-bit quantity holding a single
|
|
43
|
+
* ISO 8859-1 character, so the value space is `U+0000`..`U+00FF` and
|
|
44
|
+
* nothing above it. A string of length one whose code point is
|
|
45
|
+
* `U+20AC` (the euro sign) is a perfectly good JavaScript string and is
|
|
46
|
+
* not a `char`: it needs three octets in UTF-8 and there is no octet
|
|
47
|
+
* that means it.
|
|
48
|
+
*
|
|
49
|
+
* The length check is on UTF-16 code units, which for this range is the
|
|
50
|
+
* same as on code points — every scalar at or below `U+00FF` is one code
|
|
51
|
+
* unit. {@link makeWChar} cannot use the same check, and that difference
|
|
52
|
+
* is the whole reason these are two functions.
|
|
53
|
+
*/
|
|
54
|
+
export declare function makeChar(s: string): Char;
|
|
55
|
+
/**
|
|
56
|
+
* One Unicode scalar, or a `RangeError`.
|
|
57
|
+
*
|
|
58
|
+
* OMG IDL 4.2 §7.2.6.2 makes `wchar` a full Unicode scalar:
|
|
59
|
+
* `U+0000`..`U+10FFFF` less the surrogate range
|
|
60
|
+
* `U+D800`..`U+DFFF`, which is not a scalar value but half of one.
|
|
61
|
+
*
|
|
62
|
+
* A JavaScript string is a sequence of UTF-16 **code units**, so every
|
|
63
|
+
* scalar above the BMP is two of them: `"\u{1F600}".length` is 2. A
|
|
64
|
+
* length check on `s.length === 1` would therefore reject every
|
|
65
|
+
* astral-plane character, which is why this counts code points — the
|
|
66
|
+
* string iterator yields those — and not units. DDS-TS 1.0 §7.3.2 states
|
|
67
|
+
* that as a MUST NOT.
|
|
68
|
+
*/
|
|
69
|
+
export declare function makeWChar(s: string): WChar;
|
|
70
|
+
/**
|
|
71
|
+
* An IDL `long double`, as the sixteen octets it is.
|
|
72
|
+
*
|
|
73
|
+
* DDS-TS 1.0 §7.3.3 and §8.6. A TypeScript `number` is an IEEE 754
|
|
74
|
+
* binary64 (ECMA-262 §6.1.6.1) and drops sixty bits of a binary128's
|
|
75
|
+
* significand, so a `number`-typed member would have lost the value
|
|
76
|
+
* before any octet was written. The octets are the one form that carries
|
|
77
|
+
* it without deciding what it means.
|
|
78
|
+
*
|
|
79
|
+
* The byte order *within* `bytes` is implementation-defined; what
|
|
80
|
+
* crosses a DDS boundary is governed by OMG X-Types 1.3 §7.4.3 and is
|
|
81
|
+
* decided by the codec, not here. `runtime/typescript/src/float128.ts`
|
|
82
|
+
* holds the conversion from a decimal string to those sixteen octets,
|
|
83
|
+
* least-significant first.
|
|
84
|
+
*/
|
|
85
|
+
export interface LongDouble {
|
|
86
|
+
readonly __dds_brand: "long_double";
|
|
87
|
+
/** Sixteen octets. Any other length is a `RangeError`. */
|
|
88
|
+
readonly bytes: Uint8Array;
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Sixteen octets as a {@link LongDouble}, or a `RangeError`.
|
|
92
|
+
*
|
|
93
|
+
* The length is the whole check: a binary128 has no invalid bit pattern
|
|
94
|
+
* — every one of the 2^128 is a number, an infinity or a NaN — so a
|
|
95
|
+
* factory that inspected the exponent would refuse values IEEE 754
|
|
96
|
+
* defines.
|
|
97
|
+
*/
|
|
98
|
+
export declare function makeLongDouble(bytes: Uint8Array): LongDouble;
|
package/dist/branded.js
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
// Copyright 2026 ZeroDDS Contributors
|
|
3
|
+
/**
|
|
4
|
+
* One ISO 8859-1 character, or a `RangeError`.
|
|
5
|
+
*
|
|
6
|
+
* OMG IDL 4.2 §7.2.6.2 makes `char` an 8-bit quantity holding a single
|
|
7
|
+
* ISO 8859-1 character, so the value space is `U+0000`..`U+00FF` and
|
|
8
|
+
* nothing above it. A string of length one whose code point is
|
|
9
|
+
* `U+20AC` (the euro sign) is a perfectly good JavaScript string and is
|
|
10
|
+
* not a `char`: it needs three octets in UTF-8 and there is no octet
|
|
11
|
+
* that means it.
|
|
12
|
+
*
|
|
13
|
+
* The length check is on UTF-16 code units, which for this range is the
|
|
14
|
+
* same as on code points — every scalar at or below `U+00FF` is one code
|
|
15
|
+
* unit. {@link makeWChar} cannot use the same check, and that difference
|
|
16
|
+
* is the whole reason these are two functions.
|
|
17
|
+
*/
|
|
18
|
+
export function makeChar(s) {
|
|
19
|
+
const first = s.codePointAt(0);
|
|
20
|
+
if (s.length !== 1 || first === undefined || first > 0xff) {
|
|
21
|
+
throw new RangeError("Char requires single ISO 8859-1 code point");
|
|
22
|
+
}
|
|
23
|
+
return s;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* One Unicode scalar, or a `RangeError`.
|
|
27
|
+
*
|
|
28
|
+
* OMG IDL 4.2 §7.2.6.2 makes `wchar` a full Unicode scalar:
|
|
29
|
+
* `U+0000`..`U+10FFFF` less the surrogate range
|
|
30
|
+
* `U+D800`..`U+DFFF`, which is not a scalar value but half of one.
|
|
31
|
+
*
|
|
32
|
+
* A JavaScript string is a sequence of UTF-16 **code units**, so every
|
|
33
|
+
* scalar above the BMP is two of them: `"\u{1F600}".length` is 2. A
|
|
34
|
+
* length check on `s.length === 1` would therefore reject every
|
|
35
|
+
* astral-plane character, which is why this counts code points — the
|
|
36
|
+
* string iterator yields those — and not units. DDS-TS 1.0 §7.3.2 states
|
|
37
|
+
* that as a MUST NOT.
|
|
38
|
+
*/
|
|
39
|
+
export function makeWChar(s) {
|
|
40
|
+
// The iterator yields code points, so a surrogate pair counts once.
|
|
41
|
+
const points = [...s];
|
|
42
|
+
if (points.length !== 1) {
|
|
43
|
+
throw new RangeError("WChar requires exactly one Unicode scalar");
|
|
44
|
+
}
|
|
45
|
+
const point = s.codePointAt(0);
|
|
46
|
+
if (point === undefined || (point >= 0xd800 && point <= 0xdfff)) {
|
|
47
|
+
throw new RangeError("WChar must not be an unpaired surrogate");
|
|
48
|
+
}
|
|
49
|
+
return s;
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Sixteen octets as a {@link LongDouble}, or a `RangeError`.
|
|
53
|
+
*
|
|
54
|
+
* The length is the whole check: a binary128 has no invalid bit pattern
|
|
55
|
+
* — every one of the 2^128 is a number, an infinity or a NaN — so a
|
|
56
|
+
* factory that inspected the exponent would refuse values IEEE 754
|
|
57
|
+
* defines.
|
|
58
|
+
*/
|
|
59
|
+
export function makeLongDouble(bytes) {
|
|
60
|
+
if (bytes.length !== 16) {
|
|
61
|
+
throw new RangeError("LongDouble requires 16 bytes");
|
|
62
|
+
}
|
|
63
|
+
return { __dds_brand: "long_double", bytes };
|
|
64
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The boxed IDL `any`, and the two functions that open and close it.
|
|
3
|
+
*
|
|
4
|
+
* DDS-TS 1.0 §7.3.4 and §8.4. An IDL `any` carries a value together with
|
|
5
|
+
* an explicit run-time type identity (OMG IDL 4.2 §7.4.14). TypeScript
|
|
6
|
+
* `interface` declarations have none — they are erased — so an unwrapped
|
|
7
|
+
* payload gives a consumer nothing to discover the type from. The
|
|
8
|
+
* mapping therefore boxes: `{ typeId, value }`, where `typeId` is the
|
|
9
|
+
* qualified IDL name and indexes the registry.
|
|
10
|
+
*
|
|
11
|
+
* Mapping `any` to TypeScript `any` or `unknown` is forbidden by the
|
|
12
|
+
* spec, and the reason is the same one: `unknown` says the type is not
|
|
13
|
+
* known, where the IDL construct says the type is carried.
|
|
14
|
+
*
|
|
15
|
+
* Both functions check the descriptor's type guard, and that check is
|
|
16
|
+
* what makes the box worth having. A producer that boxed the wrong value
|
|
17
|
+
* and a consumer that read a box it did not expect fail here, naming the
|
|
18
|
+
* two type ids, rather than at whatever line first reads a property that
|
|
19
|
+
* is not there.
|
|
20
|
+
*/
|
|
21
|
+
import type { DdsAny, DdsTypeDescriptor } from "./types.js";
|
|
22
|
+
/**
|
|
23
|
+
* Box a value with the identity of its type.
|
|
24
|
+
*
|
|
25
|
+
* DDS-TS 1.0 §8.7 (`boxAny`). The guard is checked first: a box whose
|
|
26
|
+
* `typeId` does not describe its `value` is worse than no box, because
|
|
27
|
+
* every consumer downstream trusts it.
|
|
28
|
+
*
|
|
29
|
+
* @throws TypeError when `descriptor.typeGuard(value)` is false.
|
|
30
|
+
*/
|
|
31
|
+
export declare function boxAny<T>(descriptor: DdsTypeDescriptor<T>, value: T): DdsAny;
|
|
32
|
+
/**
|
|
33
|
+
* Narrow a box to the type a descriptor describes.
|
|
34
|
+
*
|
|
35
|
+
* DDS-TS 1.0 §8.7 (`unboxAny`). Two conditions, both required: the
|
|
36
|
+
* box's `typeId` is the descriptor's name, and the descriptor's guard
|
|
37
|
+
* accepts the payload. The second is not redundant — a box built by hand
|
|
38
|
+
* or arriving from another process carries whatever `typeId` its
|
|
39
|
+
* producer wrote.
|
|
40
|
+
*
|
|
41
|
+
* The spec spells the first parameter `any`; it is named `boxed` here
|
|
42
|
+
* because the identifier `any` beside a type annotation reads as the
|
|
43
|
+
* TypeScript keyword. The shape is unchanged.
|
|
44
|
+
*
|
|
45
|
+
* @throws TypeError when either condition fails, naming which.
|
|
46
|
+
*/
|
|
47
|
+
export declare function unboxAny<T>(boxed: DdsAny, descriptor: DdsTypeDescriptor<T>): T;
|
package/dist/dds_any.js
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
// Copyright 2026 ZeroDDS Contributors
|
|
3
|
+
/**
|
|
4
|
+
* Box a value with the identity of its type.
|
|
5
|
+
*
|
|
6
|
+
* DDS-TS 1.0 §8.7 (`boxAny`). The guard is checked first: a box whose
|
|
7
|
+
* `typeId` does not describe its `value` is worse than no box, because
|
|
8
|
+
* every consumer downstream trusts it.
|
|
9
|
+
*
|
|
10
|
+
* @throws TypeError when `descriptor.typeGuard(value)` is false.
|
|
11
|
+
*/
|
|
12
|
+
export function boxAny(descriptor, value) {
|
|
13
|
+
if (!descriptor.typeGuard(value)) {
|
|
14
|
+
throw new TypeError(`boxAny: the value is not a \`${descriptor.name}\``);
|
|
15
|
+
}
|
|
16
|
+
return { typeId: descriptor.name, value };
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Narrow a box to the type a descriptor describes.
|
|
20
|
+
*
|
|
21
|
+
* DDS-TS 1.0 §8.7 (`unboxAny`). Two conditions, both required: the
|
|
22
|
+
* box's `typeId` is the descriptor's name, and the descriptor's guard
|
|
23
|
+
* accepts the payload. The second is not redundant — a box built by hand
|
|
24
|
+
* or arriving from another process carries whatever `typeId` its
|
|
25
|
+
* producer wrote.
|
|
26
|
+
*
|
|
27
|
+
* The spec spells the first parameter `any`; it is named `boxed` here
|
|
28
|
+
* because the identifier `any` beside a type annotation reads as the
|
|
29
|
+
* TypeScript keyword. The shape is unchanged.
|
|
30
|
+
*
|
|
31
|
+
* @throws TypeError when either condition fails, naming which.
|
|
32
|
+
*/
|
|
33
|
+
export function unboxAny(boxed, descriptor) {
|
|
34
|
+
if (boxed.typeId !== descriptor.name) {
|
|
35
|
+
throw new TypeError(`unboxAny: the box holds a \`${boxed.typeId}\`, not a \`${descriptor.name}\``);
|
|
36
|
+
}
|
|
37
|
+
if (!descriptor.typeGuard(boxed.value)) {
|
|
38
|
+
throw new TypeError(`unboxAny: the box says \`${descriptor.name}\` and the value is not one`);
|
|
39
|
+
}
|
|
40
|
+
return boxed.value;
|
|
41
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Byte order.
|
|
3
|
+
*
|
|
4
|
+
* The counterpart of `crates/wire/src/endianness.rs` and of
|
|
5
|
+
* `runtime/python/zerodds_wire/endianness.py`. A module of its own
|
|
6
|
+
* because the writer and the reader both need it and neither should
|
|
7
|
+
* own it.
|
|
8
|
+
*/
|
|
9
|
+
/** Which end of a multi-byte value goes out first. */
|
|
10
|
+
export declare enum Endianness {
|
|
11
|
+
Little = "le",
|
|
12
|
+
Big = "be"
|
|
13
|
+
}
|
|
14
|
+
/** Whether the low octet goes out first. */
|
|
15
|
+
export declare function isLittle(order: Endianness): boolean;
|
|
16
|
+
/**
|
|
17
|
+
* The command line's spelling, or `undefined` for anything else.
|
|
18
|
+
*
|
|
19
|
+
* `undefined` and not a default: a typo in a wire-affecting flag has to
|
|
20
|
+
* be loud, because the octets it produces look plausible.
|
|
21
|
+
*/
|
|
22
|
+
export declare function parseEndianness(name: string): Endianness | undefined;
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
// Copyright 2026 ZeroDDS Contributors
|
|
3
|
+
/**
|
|
4
|
+
* Byte order.
|
|
5
|
+
*
|
|
6
|
+
* The counterpart of `crates/wire/src/endianness.rs` and of
|
|
7
|
+
* `runtime/python/zerodds_wire/endianness.py`. A module of its own
|
|
8
|
+
* because the writer and the reader both need it and neither should
|
|
9
|
+
* own it.
|
|
10
|
+
*/
|
|
11
|
+
/** Which end of a multi-byte value goes out first. */
|
|
12
|
+
export var Endianness;
|
|
13
|
+
(function (Endianness) {
|
|
14
|
+
Endianness["Little"] = "le";
|
|
15
|
+
Endianness["Big"] = "be";
|
|
16
|
+
})(Endianness || (Endianness = {}));
|
|
17
|
+
/** Whether the low octet goes out first. */
|
|
18
|
+
export function isLittle(order) {
|
|
19
|
+
return order === Endianness.Little;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* The command line's spelling, or `undefined` for anything else.
|
|
23
|
+
*
|
|
24
|
+
* `undefined` and not a default: a typo in a wire-affecting flag has to
|
|
25
|
+
* be loud, because the octets it produces look plausible.
|
|
26
|
+
*/
|
|
27
|
+
export function parseEndianness(name) {
|
|
28
|
+
if (name === "le" || name === "little") {
|
|
29
|
+
return Endianness.Little;
|
|
30
|
+
}
|
|
31
|
+
if (name === "be" || name === "big") {
|
|
32
|
+
return Endianness.Big;
|
|
33
|
+
}
|
|
34
|
+
return undefined;
|
|
35
|
+
}
|
package/dist/equal.d.ts
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import type { DdsTypeDescriptor, DdsTypeRef } from "./types.js";
|
|
2
|
+
/**
|
|
3
|
+
* Whether two key values described by `keyType` are the same key.
|
|
4
|
+
*
|
|
5
|
+
* DDS-TS 1.0 §8.7 (`equalKey`). The rules, in the order they apply:
|
|
6
|
+
*
|
|
7
|
+
* - Primitives, strings, `bigint`s and branded characters compare by
|
|
8
|
+
* **value**, with two departures from `===` that are both normative.
|
|
9
|
+
* `NaN` equals itself, because a key that never matches itself would
|
|
10
|
+
* make every sample its own instance; and `+0` equals `-0`, because
|
|
11
|
+
* `Object.is` separates them and nothing on the wire does.
|
|
12
|
+
* - A `ref` resolves through `lookupType` and compares member by
|
|
13
|
+
* member. This is the case that overrides JavaScript's default: two
|
|
14
|
+
* distinct objects holding the same members are one key.
|
|
15
|
+
* - An `any` compares its `typeId` and then its payload through the
|
|
16
|
+
* descriptor that `typeId` names.
|
|
17
|
+
* - A map, a sequence and an array are **not** IDL key types
|
|
18
|
+
* (X-Types 1.3 §7.2.2.4.5); reaching one is a malformed descriptor
|
|
19
|
+
* and throws rather than answering.
|
|
20
|
+
*
|
|
21
|
+
* Floating-point keys are allowed here and are a poor idea in a topic
|
|
22
|
+
* design: two writers that computed the same value by different routes
|
|
23
|
+
* can differ in the last place, and then they are two instances.
|
|
24
|
+
*
|
|
25
|
+
* @throws TypeError for a key type IDL does not permit, for a `ref` the
|
|
26
|
+
* registry does not answer for, and for a value graph deeper than
|
|
27
|
+
* {@link MAX_DEPTH}.
|
|
28
|
+
*
|
|
29
|
+
* zerodds-lint: recursion-depth 64 (key nesting; bounded by `MAX_DEPTH`)
|
|
30
|
+
*/
|
|
31
|
+
export declare function equalKey(a: unknown, b: unknown, keyType: DdsTypeRef): boolean;
|
|
32
|
+
/**
|
|
33
|
+
* Whether two descriptors describe the same type.
|
|
34
|
+
*
|
|
35
|
+
* DDS-TS 1.0 §8.7 (`registerType`). Deep value equality over every
|
|
36
|
+
* field, with `typeGuard` compared by reference identity — a function is
|
|
37
|
+
* not deep-equals comparable, and two closures that behave identically
|
|
38
|
+
* are still two closures.
|
|
39
|
+
*
|
|
40
|
+
* What this is for: a module imported twice in one bundle runs its
|
|
41
|
+
* `registerType` call twice with the same descriptor object, and that
|
|
42
|
+
* has to be silent. Two different IDL types compiled to one qualified
|
|
43
|
+
* name is the case that must not be, and it is the one this separates
|
|
44
|
+
* out.
|
|
45
|
+
*/
|
|
46
|
+
export declare function descriptorsEqual(a: DdsTypeDescriptor, b: DdsTypeDescriptor): boolean;
|
package/dist/equal.js
ADDED
|
@@ -0,0 +1,273 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
// Copyright 2026 ZeroDDS Contributors
|
|
3
|
+
/**
|
|
4
|
+
* Equality, twice: over a key value and over a descriptor.
|
|
5
|
+
*
|
|
6
|
+
* DDS-TS 1.0 §8.7. Two different questions that JavaScript answers the
|
|
7
|
+
* same wrong way.
|
|
8
|
+
*
|
|
9
|
+
* `a === b` on two objects is reference equality, so two samples with
|
|
10
|
+
* identical key members compare unequal and a DDS instance would split
|
|
11
|
+
* in two. {@link equalKey} walks the key's {@link DdsTypeRef} instead
|
|
12
|
+
* and compares the values the type describes.
|
|
13
|
+
*
|
|
14
|
+
* {@link descriptorsEqual} answers the other one: whether a second
|
|
15
|
+
* `registerType` call carries the same type as the first. The registry
|
|
16
|
+
* needs it to tell a harmless double-import from two different types
|
|
17
|
+
* sharing a name, and the second has to throw.
|
|
18
|
+
*/
|
|
19
|
+
import { lookupType } from "./registry.js";
|
|
20
|
+
/**
|
|
21
|
+
* How deep a value graph is walked before it is taken to be malformed.
|
|
22
|
+
*
|
|
23
|
+
* A descriptor graph has no cycles — `DdsTypeRef.kind === "ref"` carries
|
|
24
|
+
* a name and not a reference, which is what §8.2 says it is for — so a
|
|
25
|
+
* chain longer than this is a value that was built by hand and is
|
|
26
|
+
* pointing at itself. Recursing into one would end the process rather
|
|
27
|
+
* than report anything.
|
|
28
|
+
*/
|
|
29
|
+
const MAX_DEPTH = 64;
|
|
30
|
+
/** The synthetic member id a union's discriminator carries (§7.5.1). */
|
|
31
|
+
const DISCRIMINATOR_ID = 0xffffffff;
|
|
32
|
+
/**
|
|
33
|
+
* Whether two key values described by `keyType` are the same key.
|
|
34
|
+
*
|
|
35
|
+
* DDS-TS 1.0 §8.7 (`equalKey`). The rules, in the order they apply:
|
|
36
|
+
*
|
|
37
|
+
* - Primitives, strings, `bigint`s and branded characters compare by
|
|
38
|
+
* **value**, with two departures from `===` that are both normative.
|
|
39
|
+
* `NaN` equals itself, because a key that never matches itself would
|
|
40
|
+
* make every sample its own instance; and `+0` equals `-0`, because
|
|
41
|
+
* `Object.is` separates them and nothing on the wire does.
|
|
42
|
+
* - A `ref` resolves through `lookupType` and compares member by
|
|
43
|
+
* member. This is the case that overrides JavaScript's default: two
|
|
44
|
+
* distinct objects holding the same members are one key.
|
|
45
|
+
* - An `any` compares its `typeId` and then its payload through the
|
|
46
|
+
* descriptor that `typeId` names.
|
|
47
|
+
* - A map, a sequence and an array are **not** IDL key types
|
|
48
|
+
* (X-Types 1.3 §7.2.2.4.5); reaching one is a malformed descriptor
|
|
49
|
+
* and throws rather than answering.
|
|
50
|
+
*
|
|
51
|
+
* Floating-point keys are allowed here and are a poor idea in a topic
|
|
52
|
+
* design: two writers that computed the same value by different routes
|
|
53
|
+
* can differ in the last place, and then they are two instances.
|
|
54
|
+
*
|
|
55
|
+
* @throws TypeError for a key type IDL does not permit, for a `ref` the
|
|
56
|
+
* registry does not answer for, and for a value graph deeper than
|
|
57
|
+
* {@link MAX_DEPTH}.
|
|
58
|
+
*
|
|
59
|
+
* zerodds-lint: recursion-depth 64 (key nesting; bounded by `MAX_DEPTH`)
|
|
60
|
+
*/
|
|
61
|
+
export function equalKey(a, b, keyType) {
|
|
62
|
+
return equalKeyAt(a, b, keyType, 0);
|
|
63
|
+
}
|
|
64
|
+
/** {@link equalKey}, carrying the depth it has reached. */
|
|
65
|
+
function equalKeyAt(a, b, keyType, depth) {
|
|
66
|
+
if (depth > MAX_DEPTH) {
|
|
67
|
+
throw new TypeError(`equalKey: the key nests past ${MAX_DEPTH}`);
|
|
68
|
+
}
|
|
69
|
+
switch (keyType.kind) {
|
|
70
|
+
case "primitive":
|
|
71
|
+
case "bitfield":
|
|
72
|
+
case "string":
|
|
73
|
+
return sameValue(a, b);
|
|
74
|
+
case "ref":
|
|
75
|
+
return equalRef(a, b, keyType.name, depth);
|
|
76
|
+
case "any":
|
|
77
|
+
return equalBoxed(a, b, depth);
|
|
78
|
+
case "sequence":
|
|
79
|
+
case "array":
|
|
80
|
+
case "map":
|
|
81
|
+
throw new TypeError(`equalKey: a \`${keyType.kind}\` is not an IDL key type`);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Value equality with the two departures §8.7 requires.
|
|
86
|
+
*
|
|
87
|
+
* `===` for everything, then `NaN` against itself. `+0 === -0` is
|
|
88
|
+
* already true, so the second departure needs no code — it needs
|
|
89
|
+
* `Object.is` **not** to be used, which is what this comment records.
|
|
90
|
+
*/
|
|
91
|
+
function sameValue(a, b) {
|
|
92
|
+
if (a === b) {
|
|
93
|
+
return true;
|
|
94
|
+
}
|
|
95
|
+
return (typeof a === "number" && typeof b === "number" && Number.isNaN(a) && Number.isNaN(b));
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* Two values of a named type, compared through its descriptor.
|
|
99
|
+
*
|
|
100
|
+
* The descriptor's `kind` decides how, because the TypeScript surface of
|
|
101
|
+
* a named IDL type is not always an object: an enumeration is a string
|
|
102
|
+
* literal, an alias is whatever it aliases, and only a struct, an
|
|
103
|
+
* exception, a bitset or a union is a property bag.
|
|
104
|
+
*/
|
|
105
|
+
function equalRef(a, b, name, depth) {
|
|
106
|
+
const descriptor = lookupType(name);
|
|
107
|
+
if (descriptor === undefined) {
|
|
108
|
+
throw new TypeError(`equalKey: no descriptor is registered for \`${name}\``);
|
|
109
|
+
}
|
|
110
|
+
switch (descriptor.kind) {
|
|
111
|
+
// A string-literal union or a number at the TypeScript surface.
|
|
112
|
+
case "enum":
|
|
113
|
+
case "bitmask":
|
|
114
|
+
return sameValue(a, b);
|
|
115
|
+
// One member named `value`, carrying the aliased type.
|
|
116
|
+
case "alias": {
|
|
117
|
+
const aliased = descriptor.fields[0];
|
|
118
|
+
if (aliased === undefined) {
|
|
119
|
+
throw new TypeError(`equalKey: the alias \`${name}\` describes no type`);
|
|
120
|
+
}
|
|
121
|
+
return equalKeyAt(a, b, aliased.type, depth + 1);
|
|
122
|
+
}
|
|
123
|
+
case "union":
|
|
124
|
+
return equalUnion(a, b, descriptor, depth);
|
|
125
|
+
case "struct":
|
|
126
|
+
case "exception":
|
|
127
|
+
case "bitset":
|
|
128
|
+
return equalMembers(a, b, descriptor.fields, depth);
|
|
129
|
+
// An interface has no data form, so no value of one can be a key.
|
|
130
|
+
case "interface":
|
|
131
|
+
throw new TypeError(`equalKey: \`${name}\` is an interface and has no value`);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
/**
|
|
135
|
+
* Two property bags, compared over the members a descriptor lists.
|
|
136
|
+
*
|
|
137
|
+
* A member absent from both is equal — that is what `@optional` means —
|
|
138
|
+
* and a member absent from one only is not. Comparing with `in` rather
|
|
139
|
+
* than against `undefined` keeps those two apart: a member explicitly
|
|
140
|
+
* set to `undefined` is present, and a `@optional` member that is not
|
|
141
|
+
* set is not.
|
|
142
|
+
*/
|
|
143
|
+
function equalMembers(a, b, members, depth) {
|
|
144
|
+
if (!isRecord(a) || !isRecord(b)) {
|
|
145
|
+
return false;
|
|
146
|
+
}
|
|
147
|
+
for (const member of members) {
|
|
148
|
+
const inA = member.name in a;
|
|
149
|
+
const inB = member.name in b;
|
|
150
|
+
if (inA !== inB) {
|
|
151
|
+
return false;
|
|
152
|
+
}
|
|
153
|
+
if (!inA) {
|
|
154
|
+
continue;
|
|
155
|
+
}
|
|
156
|
+
if (!equalKeyAt(a[member.name], b[member.name], member.type, depth + 1)) {
|
|
157
|
+
return false;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
return true;
|
|
161
|
+
}
|
|
162
|
+
/**
|
|
163
|
+
* Two union values: the discriminator first, then the selected case.
|
|
164
|
+
*
|
|
165
|
+
* The discriminator's **property name** comes out of the descriptor
|
|
166
|
+
* rather than being written in here. It is the member carrying the
|
|
167
|
+
* synthetic id `0xFFFFFFFF` (§7.5.1), and reading it from the side-table
|
|
168
|
+
* is what keeps this function agreeing with whatever the generator
|
|
169
|
+
* spelled it.
|
|
170
|
+
*/
|
|
171
|
+
function equalUnion(a, b, descriptor, depth) {
|
|
172
|
+
const discriminator = descriptor.fields.find((field) => field.id === DISCRIMINATOR_ID);
|
|
173
|
+
if (discriminator === undefined) {
|
|
174
|
+
throw new TypeError(`equalKey: the union \`${descriptor.name}\` has no discriminator member`);
|
|
175
|
+
}
|
|
176
|
+
if (!isRecord(a) || !isRecord(b)) {
|
|
177
|
+
return false;
|
|
178
|
+
}
|
|
179
|
+
if (!sameValue(a[discriminator.name], b[discriminator.name])) {
|
|
180
|
+
return false;
|
|
181
|
+
}
|
|
182
|
+
// Whichever case the discriminator selected is the one present; the
|
|
183
|
+
// others are absent in both and `equalMembers` passes over them.
|
|
184
|
+
const cases = descriptor.fields.filter((field) => field.id !== DISCRIMINATOR_ID);
|
|
185
|
+
return equalMembers(a, b, cases, depth);
|
|
186
|
+
}
|
|
187
|
+
/**
|
|
188
|
+
* Two boxed `any` values: the identity first, then the payload.
|
|
189
|
+
*
|
|
190
|
+
* The payload is compared through the descriptor `typeId` names, which
|
|
191
|
+
* is the whole reason the box carries one.
|
|
192
|
+
*/
|
|
193
|
+
function equalBoxed(a, b, depth) {
|
|
194
|
+
if (!isRecord(a) || !isRecord(b)) {
|
|
195
|
+
return false;
|
|
196
|
+
}
|
|
197
|
+
const typeId = a["typeId"];
|
|
198
|
+
if (typeof typeId !== "string" || typeId !== b["typeId"]) {
|
|
199
|
+
return false;
|
|
200
|
+
}
|
|
201
|
+
return equalRef(a["value"], b["value"], typeId, depth + 1);
|
|
202
|
+
}
|
|
203
|
+
/** Whether a value can be indexed by a member name. */
|
|
204
|
+
function isRecord(value) {
|
|
205
|
+
return typeof value === "object" && value !== null;
|
|
206
|
+
}
|
|
207
|
+
/**
|
|
208
|
+
* Whether two descriptors describe the same type.
|
|
209
|
+
*
|
|
210
|
+
* DDS-TS 1.0 §8.7 (`registerType`). Deep value equality over every
|
|
211
|
+
* field, with `typeGuard` compared by reference identity — a function is
|
|
212
|
+
* not deep-equals comparable, and two closures that behave identically
|
|
213
|
+
* are still two closures.
|
|
214
|
+
*
|
|
215
|
+
* What this is for: a module imported twice in one bundle runs its
|
|
216
|
+
* `registerType` call twice with the same descriptor object, and that
|
|
217
|
+
* has to be silent. Two different IDL types compiled to one qualified
|
|
218
|
+
* name is the case that must not be, and it is the one this separates
|
|
219
|
+
* out.
|
|
220
|
+
*/
|
|
221
|
+
export function descriptorsEqual(a, b) {
|
|
222
|
+
if (a === b) {
|
|
223
|
+
return true;
|
|
224
|
+
}
|
|
225
|
+
if (a.typeGuard !== b.typeGuard) {
|
|
226
|
+
return false;
|
|
227
|
+
}
|
|
228
|
+
return deepEqual(withoutGuard(a), withoutGuard(b), 0);
|
|
229
|
+
}
|
|
230
|
+
/** A descriptor's data, without the one member that is not data. */
|
|
231
|
+
function withoutGuard(descriptor) {
|
|
232
|
+
const { typeGuard: _guard, ...data } = descriptor;
|
|
233
|
+
return data;
|
|
234
|
+
}
|
|
235
|
+
/**
|
|
236
|
+
* Deep value equality over the JSON-shaped half of a descriptor.
|
|
237
|
+
*
|
|
238
|
+
* Arrays, plain objects, and the primitives — plus `Uint8Array`, which a
|
|
239
|
+
* `@default` on a `long double` member carries and which is an object
|
|
240
|
+
* whose `===` is reference equality.
|
|
241
|
+
*
|
|
242
|
+
* A property present with the value `undefined` and a property that is
|
|
243
|
+
* absent compare equal here: `exactOptionalPropertyTypes` keeps the two
|
|
244
|
+
* apart in the type system, and a generator that wrote `topic: undefined`
|
|
245
|
+
* where another omitted `topic` did not emit a different type.
|
|
246
|
+
*
|
|
247
|
+
* zerodds-lint: recursion-depth 64 (descriptor nesting; bounded by `MAX_DEPTH`)
|
|
248
|
+
*/
|
|
249
|
+
function deepEqual(a, b, depth) {
|
|
250
|
+
if (depth > MAX_DEPTH) {
|
|
251
|
+
throw new TypeError(`registerType: the descriptor nests past ${MAX_DEPTH}`);
|
|
252
|
+
}
|
|
253
|
+
if (sameValue(a, b)) {
|
|
254
|
+
return true;
|
|
255
|
+
}
|
|
256
|
+
if (a instanceof Uint8Array && b instanceof Uint8Array) {
|
|
257
|
+
return a.length === b.length && a.every((octet, at) => octet === b[at]);
|
|
258
|
+
}
|
|
259
|
+
if (Array.isArray(a) && Array.isArray(b)) {
|
|
260
|
+
return (a.length === b.length &&
|
|
261
|
+
a.every((element, at) => deepEqual(element, b[at], depth + 1)));
|
|
262
|
+
}
|
|
263
|
+
if (isRecord(a) && isRecord(b) && !Array.isArray(a) && !Array.isArray(b)) {
|
|
264
|
+
const keys = new Set([...Object.keys(a), ...Object.keys(b)]);
|
|
265
|
+
for (const key of keys) {
|
|
266
|
+
if (!deepEqual(a[key], b[key], depth + 1)) {
|
|
267
|
+
return false;
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
return true;
|
|
271
|
+
}
|
|
272
|
+
return false;
|
|
273
|
+
}
|