@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/dist/wasm.d.ts ADDED
@@ -0,0 +1,203 @@
1
+ /**
2
+ * The WASM-Bindings Profile: handles, samples, and the layer that guards
3
+ * them.
4
+ *
5
+ * DDS-TS 1.0 Annex C. A TypeScript application talks to a DDS
6
+ * implementation compiled to WebAssembly through the operations of
7
+ * §C.2 — create a participant, a topic, a writer, a reader; write XCDR2
8
+ * octets; take them back. This file declares those operations, the
9
+ * handle and sample types they use, and {@link bindWasmBackend}, which
10
+ * wraps a raw module export table in the run-time invariants Annex C
11
+ * requires and the type system cannot state.
12
+ *
13
+ * # What crosses the boundary
14
+ *
15
+ * `Uint8Array`, always. §C.3.1 makes the XCDR2 octet sequence the
16
+ * canonical wire form and forbids exposing a JavaScript object graph as
17
+ * one: a binding that passed `{ x: 1 }` across and serialised on the far
18
+ * side would have two serialisers, and the one on the JavaScript side
19
+ * would be the one no other vendor implements. The generated codec turns
20
+ * a sample into octets before it gets here.
21
+ *
22
+ * A boxed `any` is no exception (§C.3.3). `boxAny` on the producing
23
+ * side, the generated serialiser, the octets, the generated
24
+ * deserialiser, `unboxAny` on the consuming side — the box is a
25
+ * TypeScript-side narrowing device and never a wire object of its own.
26
+ *
27
+ * # Why the brands are string literals
28
+ *
29
+ * The same reason as `./branded.js` gives for `Char`: a
30
+ * `unique symbol` brand is nominal across module boundaries, so a
31
+ * handle from vendor A's binding would not satisfy vendor B's parameter
32
+ * even though both are indices into the same resource table. The tags
33
+ * below — `"participant"`, `"topic"`, `"publisher"`, `"subscriber"`,
34
+ * `"writer"`, `"reader"` — and the property name `__dds_brand` are
35
+ * normative (§C.1.1).
36
+ *
37
+ * The brand is compile-time only. `0 as DataReaderHandle` type-checks,
38
+ * and 0 is the invalid-handle sentinel, so the check that matters is the
39
+ * run-time one in {@link bindWasmBackend}.
40
+ */
41
+ /** A domain participant (§C.1.1). */
42
+ export type ParticipantHandle = number & {
43
+ readonly __dds_brand: "participant";
44
+ };
45
+ /** A topic (§C.1.1). */
46
+ export type TopicHandle = number & {
47
+ readonly __dds_brand: "topic";
48
+ };
49
+ /** A publisher (§C.1.1). */
50
+ export type PublisherHandle = number & {
51
+ readonly __dds_brand: "publisher";
52
+ };
53
+ /** A subscriber (§C.1.1). */
54
+ export type SubscriberHandle = number & {
55
+ readonly __dds_brand: "subscriber";
56
+ };
57
+ /** A data writer (§C.1.1). */
58
+ export type DataWriterHandle = number & {
59
+ readonly __dds_brand: "writer";
60
+ };
61
+ /** A data reader (§C.1.1). */
62
+ export type DataReaderHandle = number & {
63
+ readonly __dds_brand: "reader";
64
+ };
65
+ /**
66
+ * An OMG DDSI-RTPS GUID: a 12-octet `GuidPrefix` and a 32-bit
67
+ * `EntityId`.
68
+ *
69
+ * DDS-TS 1.0 §C.1.2. The whole sixteen octets and not a 64-bit subset:
70
+ * the prefix carries the vendor id, the host id, the application id and
71
+ * the instance id (DDSI-RTPS 2.5 §8.2.4), and truncating it loses the
72
+ * vendor and host components — which is exactly what tells two writers
73
+ * apart in a multi-vendor or multi-host deployment.
74
+ */
75
+ export interface DdsGuid {
76
+ /** Twelve octets. */
77
+ readonly prefix: Uint8Array;
78
+ /** The entity id, as a 32-bit unsigned value. */
79
+ readonly entityId: number;
80
+ }
81
+ /**
82
+ * A GUID from its two parts, or a `RangeError`.
83
+ *
84
+ * DDS-TS 1.0 §C.1.2.
85
+ */
86
+ export declare function makeDdsGuid(prefix: Uint8Array, entityId: number): DdsGuid;
87
+ /**
88
+ * What DDS knows about a sample besides its octets.
89
+ *
90
+ * DDS-TS 1.0 §C.1.2. A strict subset of the OMG DDS 1.4 `SampleInfo`
91
+ * (§2.2.2.5.4): the fields this Profile exposes and no others. A
92
+ * `validData` of false is a sample that carries only an instance state
93
+ * change — a dispose, a writer leaving — and whose `bytes` say nothing.
94
+ */
95
+ export interface SampleInfo {
96
+ readonly validData: boolean;
97
+ readonly sampleState: "read" | "not_read";
98
+ readonly viewState: "new" | "not_new";
99
+ readonly instanceState: "alive" | "not_alive_disposed" | "not_alive_no_writers";
100
+ readonly sourceTimestampNs: bigint;
101
+ readonly sequenceNumber: bigint;
102
+ /** The full RTPS GUID of the writer that published it. */
103
+ readonly publicationHandle: DdsGuid;
104
+ /** Opaque, and 64 bits wide. */
105
+ readonly instanceHandle: bigint;
106
+ }
107
+ /**
108
+ * One sample: the XCDR2 octets exactly as they crossed the wire, and
109
+ * what DDS knows about them.
110
+ *
111
+ * DDS-TS 1.0 §C.1.2.
112
+ */
113
+ export interface Sample {
114
+ readonly bytes: Uint8Array;
115
+ readonly info: SampleInfo;
116
+ }
117
+ /** Called when a reader has data (§C.1.3). */
118
+ export type DataAvailableCallback = (r: DataReaderHandle) => void;
119
+ /**
120
+ * The raw module surface — a `.wasm` export table, an N-API `.node`
121
+ * module, or a reference implementation in TypeScript.
122
+ *
123
+ * Handles are plain numbers here: branding is the binding layer's, and
124
+ * a WebAssembly export cannot carry a TypeScript type anyway. Everything
125
+ * that Annex C requires *around* these calls — the invalid-handle
126
+ * sentinel, the cascade on delete, the double-delete, the deferral of
127
+ * listener delivery — is done by {@link bindWasmBackend} rather than
128
+ * asked of each module, so that a browser build and a Node build behave
129
+ * the same way without implementing the same rules twice (§C.4.2).
130
+ */
131
+ export interface WasmBackend {
132
+ createParticipant(domainId: number): number;
133
+ deleteParticipant(p: number): void;
134
+ createTopic(p: number, name: string, typeName: string): number;
135
+ deleteTopic(t: number): void;
136
+ createPublisher(p: number): number;
137
+ createSubscriber(p: number): number;
138
+ deletePublisher(pub: number): void;
139
+ deleteSubscriber(sub: number): void;
140
+ createDataWriter(pub: number, topic: number): number;
141
+ createDataReader(sub: number, topic: number): number;
142
+ writeSample(w: number, xcdr2: Uint8Array): void;
143
+ takeSamples(r: number, max: number): ReadonlyArray<Sample>;
144
+ deleteDataWriter(w: number): void;
145
+ deleteDataReader(r: number): void;
146
+ setDataAvailableListener(r: number, cb: ((r: number) => void) | null): void;
147
+ }
148
+ /**
149
+ * The operations of §C.2, with branded handles and the Annex C
150
+ * invariants enforced.
151
+ *
152
+ * The signatures are normative; the WebAssembly export names behind them
153
+ * are not. A Node.js binding built on N-API exposes this same interface
154
+ * so that an application retargets between browser and Node by changing
155
+ * an import and nothing else (§C.4.2).
156
+ */
157
+ export interface DdsBindings {
158
+ createParticipant(domainId: number): ParticipantHandle;
159
+ deleteParticipant(p: ParticipantHandle): void;
160
+ createTopic(p: ParticipantHandle, name: string, typeName: string): TopicHandle;
161
+ deleteTopic(t: TopicHandle): void;
162
+ createPublisher(p: ParticipantHandle): PublisherHandle;
163
+ createSubscriber(p: ParticipantHandle): SubscriberHandle;
164
+ deletePublisher(pub: PublisherHandle): void;
165
+ deleteSubscriber(sub: SubscriberHandle): void;
166
+ createDataWriter(pub: PublisherHandle, topic: TopicHandle): DataWriterHandle;
167
+ createDataReader(sub: SubscriberHandle, topic: TopicHandle): DataReaderHandle;
168
+ writeSample(w: DataWriterHandle, xcdr2: Uint8Array): void;
169
+ takeSamples(r: DataReaderHandle, max: number): ReadonlyArray<Sample>;
170
+ deleteDataWriter(w: DataWriterHandle): void;
171
+ deleteDataReader(r: DataReaderHandle): void;
172
+ setDataAvailableListener(r: DataReaderHandle, cb: DataAvailableCallback | null): void;
173
+ }
174
+ /**
175
+ * Wrap a raw module in the invariants Annex C states.
176
+ *
177
+ * Five of them, and each one is a rule the type system cannot carry:
178
+ *
179
+ * 1. **The zero sentinel** (§C.1.1). `0 as DataReaderHandle` compiles,
180
+ * so every operation checks the handle it receives and rejects 0 —
181
+ * and anything that is not a non-negative 32-bit integer — with a
182
+ * `RangeError`.
183
+ * 2. **Liveness** (§C.5.3). A handle that has been deleted throws when
184
+ * it is used again, naming the handle. A double delete throws for
185
+ * the same reason: silently succeeding would let a caller believe it
186
+ * had released something twice as often as it had.
187
+ * 3. **The cascade** (§C.5.3). Deleting a participant releases
188
+ * everything created from it — topics, publishers, subscribers, and
189
+ * the writers and readers under those — and a call naming any of
190
+ * them afterwards throws rather than reaching a module whose
191
+ * resource is gone.
192
+ * 4. **Borrowed buffers** (§C.3.2). The octets handed to `writeSample`
193
+ * are copied before they cross, so a module that retained a
194
+ * reference past the call would be holding a buffer the caller
195
+ * cannot observe; and samples coming back are copied again, so what
196
+ * the caller holds is owned by JavaScript and not a view into a
197
+ * WebAssembly memory that may be resized under it.
198
+ * 5. **Deferred delivery** (§C.2.4). A `DataAvailable` listener runs on
199
+ * the host event loop, never inside the synchronous `writeSample`
200
+ * that produced the sample. The wrapper queues a microtask, so the
201
+ * callback runs after the caller's current turn finishes.
202
+ */
203
+ export declare function bindWasmBackend(backend: WasmBackend): DdsBindings;
package/dist/wasm.js ADDED
@@ -0,0 +1,194 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ // Copyright 2026 ZeroDDS Contributors
3
+ /**
4
+ * A GUID from its two parts, or a `RangeError`.
5
+ *
6
+ * DDS-TS 1.0 §C.1.2.
7
+ */
8
+ export function makeDdsGuid(prefix, entityId) {
9
+ if (prefix.length !== 12) {
10
+ throw new RangeError("DdsGuid.prefix requires 12 bytes");
11
+ }
12
+ if (!Number.isInteger(entityId) || entityId < 0 || entityId > 0xffffffff) {
13
+ throw new RangeError("DdsGuid.entityId requires uint32");
14
+ }
15
+ return { prefix, entityId };
16
+ }
17
+ /** The table key. Two kinds may hold the same number, so both are in it. */
18
+ function slot(kind, value) {
19
+ return `${kind}:${value}`;
20
+ }
21
+ /**
22
+ * Wrap a raw module in the invariants Annex C states.
23
+ *
24
+ * Five of them, and each one is a rule the type system cannot carry:
25
+ *
26
+ * 1. **The zero sentinel** (§C.1.1). `0 as DataReaderHandle` compiles,
27
+ * so every operation checks the handle it receives and rejects 0 —
28
+ * and anything that is not a non-negative 32-bit integer — with a
29
+ * `RangeError`.
30
+ * 2. **Liveness** (§C.5.3). A handle that has been deleted throws when
31
+ * it is used again, naming the handle. A double delete throws for
32
+ * the same reason: silently succeeding would let a caller believe it
33
+ * had released something twice as often as it had.
34
+ * 3. **The cascade** (§C.5.3). Deleting a participant releases
35
+ * everything created from it — topics, publishers, subscribers, and
36
+ * the writers and readers under those — and a call naming any of
37
+ * them afterwards throws rather than reaching a module whose
38
+ * resource is gone.
39
+ * 4. **Borrowed buffers** (§C.3.2). The octets handed to `writeSample`
40
+ * are copied before they cross, so a module that retained a
41
+ * reference past the call would be holding a buffer the caller
42
+ * cannot observe; and samples coming back are copied again, so what
43
+ * the caller holds is owned by JavaScript and not a view into a
44
+ * WebAssembly memory that may be resized under it.
45
+ * 5. **Deferred delivery** (§C.2.4). A `DataAvailable` listener runs on
46
+ * the host event loop, never inside the synchronous `writeSample`
47
+ * that produced the sample. The wrapper queues a microtask, so the
48
+ * callback runs after the caller's current turn finishes.
49
+ */
50
+ export function bindWasmBackend(backend) {
51
+ const live = new Map();
52
+ /** Record a handle the module just produced. */
53
+ function born(kind, value, parents) {
54
+ if (!Number.isInteger(value) || value <= 0 || value > 0xffffffff) {
55
+ throw new RangeError(`${kind}: the backend answered with ${value}, which is not a handle`);
56
+ }
57
+ live.set(slot(kind, value), { kind, value, parents });
58
+ return value;
59
+ }
60
+ /** Check a handle a caller passed in, and answer with its number. */
61
+ function check(kind, handle) {
62
+ if (!Number.isInteger(handle) || handle < 0 || handle > 0xffffffff) {
63
+ throw new RangeError(`${kind} handle ${String(handle)} is not a handle`);
64
+ }
65
+ if (handle === 0) {
66
+ throw new RangeError(`${kind} handle 0 is the invalid-handle sentinel`);
67
+ }
68
+ if (!live.has(slot(kind, handle))) {
69
+ throw new RangeError(`${kind} handle ${handle} is not live`);
70
+ }
71
+ return handle;
72
+ }
73
+ /** Forget a handle and everything created from it. */
74
+ function bury(kind, handle) {
75
+ const key = slot(kind, handle);
76
+ live.delete(key);
77
+ for (const [child, resource] of [...live]) {
78
+ if (resource.parents.includes(key)) {
79
+ bury(resource.kind, resource.value);
80
+ }
81
+ }
82
+ }
83
+ /**
84
+ * A copy of the caller's octets.
85
+ *
86
+ * `slice` and not the array itself: §C.3.2 makes the buffer borrowed
87
+ * for the duration of the call, and the copy is what makes that true
88
+ * of a module that does not read the clause.
89
+ */
90
+ function borrowed(octets) {
91
+ return octets.slice();
92
+ }
93
+ return {
94
+ createParticipant(domainId) {
95
+ if (!Number.isInteger(domainId) || domainId < 0) {
96
+ throw new RangeError(`domain ${String(domainId)} is not a domain id`);
97
+ }
98
+ return born("participant", backend.createParticipant(domainId), []);
99
+ },
100
+ deleteParticipant(p) {
101
+ const handle = check("participant", p);
102
+ backend.deleteParticipant(handle);
103
+ bury("participant", handle);
104
+ },
105
+ createTopic(p, name, typeName) {
106
+ const participant = check("participant", p);
107
+ return born("topic", backend.createTopic(participant, name, typeName), [
108
+ slot("participant", participant),
109
+ ]);
110
+ },
111
+ deleteTopic(t) {
112
+ const handle = check("topic", t);
113
+ backend.deleteTopic(handle);
114
+ bury("topic", handle);
115
+ },
116
+ createPublisher(p) {
117
+ const participant = check("participant", p);
118
+ return born("publisher", backend.createPublisher(participant), [
119
+ slot("participant", participant),
120
+ ]);
121
+ },
122
+ createSubscriber(p) {
123
+ const participant = check("participant", p);
124
+ return born("subscriber", backend.createSubscriber(participant), [
125
+ slot("participant", participant),
126
+ ]);
127
+ },
128
+ deletePublisher(pub) {
129
+ const handle = check("publisher", pub);
130
+ backend.deletePublisher(handle);
131
+ bury("publisher", handle);
132
+ },
133
+ deleteSubscriber(sub) {
134
+ const handle = check("subscriber", sub);
135
+ backend.deleteSubscriber(handle);
136
+ bury("subscriber", handle);
137
+ },
138
+ createDataWriter(pub, topic) {
139
+ const publisher = check("publisher", pub);
140
+ const t = check("topic", topic);
141
+ return born("writer", backend.createDataWriter(publisher, t), [
142
+ slot("publisher", publisher),
143
+ slot("topic", t),
144
+ ]);
145
+ },
146
+ createDataReader(sub, topic) {
147
+ const subscriber = check("subscriber", sub);
148
+ const t = check("topic", topic);
149
+ return born("reader", backend.createDataReader(subscriber, t), [
150
+ slot("subscriber", subscriber),
151
+ slot("topic", t),
152
+ ]);
153
+ },
154
+ writeSample(w, xcdr2) {
155
+ backend.writeSample(check("writer", w), borrowed(xcdr2));
156
+ },
157
+ takeSamples(r, max) {
158
+ if (!Number.isInteger(max) || max < 0) {
159
+ throw new RangeError(`take of ${String(max)} samples is not a count`);
160
+ }
161
+ return backend.takeSamples(check("reader", r), max).map((sample) => ({
162
+ bytes: sample.bytes.slice(),
163
+ info: sample.info,
164
+ }));
165
+ },
166
+ deleteDataWriter(w) {
167
+ const handle = check("writer", w);
168
+ backend.deleteDataWriter(handle);
169
+ bury("writer", handle);
170
+ },
171
+ deleteDataReader(r) {
172
+ const handle = check("reader", r);
173
+ backend.deleteDataReader(handle);
174
+ bury("reader", handle);
175
+ },
176
+ setDataAvailableListener(r, cb) {
177
+ const handle = check("reader", r);
178
+ if (cb === null) {
179
+ backend.setDataAvailableListener(handle, null);
180
+ return;
181
+ }
182
+ backend.setDataAvailableListener(handle, (reader) => {
183
+ // The host event loop, not this turn. §C.2.4 forbids delivery
184
+ // inside the synchronous `writeSample` that produced the
185
+ // sample, and a backend that calls straight through would
186
+ // otherwise re-enter the application from the middle of its
187
+ // own write.
188
+ queueMicrotask(() => {
189
+ cb(reader);
190
+ });
191
+ });
192
+ },
193
+ };
194
+ }