@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 CHANGED
@@ -1,3 +1,14 @@
1
- # Temporary Holding Version
1
+ # @zerodds/wire
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
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;
@@ -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;
@@ -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
+ }
@@ -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
+ }