@statewalker/webrun-msgpack 0.1.1 → 0.2.2

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,8 +1,31 @@
1
1
  # @statewalker/webrun-msgpack
2
2
 
3
- Length-prefixed MessagePack frame codec for async iterables. Streams-safe `encode`/`decode` for arbitrary values, plus zero-copy specialisations for `Float32Array`.
3
+ MessagePack on the wire, in **two distinct shapes**:
4
4
 
5
- ## Why it exists
5
+ - a **stream codec** — `encodeMsgpack` / `decodeMsgpack`, length-prefixed, for a transport with no
6
+ message boundaries (plus zero-copy specialisations for `Float32Array`);
7
+ - a **message codec** — `msgpackCodec`, a `PortCodec` for `@statewalker/webrun-rpc`'s
8
+ `multiplexPort`, with no length prefix, for a transport that already frames.
9
+
10
+ They are not interchangeable, and reaching for the wrong one is easy. The table below is the whole
11
+ decision.
12
+
13
+ ## Which codec
14
+
15
+ | | `encodeMsgpack` / `decodeMsgpack` | `msgpackCodec` |
16
+ | --- | --- | --- |
17
+ | Kind | stream codec | message codec (`PortCodec`) |
18
+ | Shape | `AsyncIterable<T>` ⇄ `AsyncIterable<Uint8Array>` | one `PortEnvelope` ⇄ one `postMessage` |
19
+ | Framing | **4-byte big-endian length prefix**, added by this package | **none** — the transport's own message boundaries are the framing |
20
+ | Use it when | the transport is a byte *stream*: a TCP-like socket, a `ReadableStream`, a file, anything where chunk boundaries are arbitrary | the transport preserves *message* boundaries: a WebSocket, an `RTCDataChannel`, a LiveKit data packet |
21
+ | Malformed input | a truncated trailing frame is never emitted; the consumer ends without yielding a partial value | dropped, never thrown — a bad frame from a peer cannot take the multiplexer down |
22
+ | Depends on | `@ygoe/msgpack` only | `@ygoe/msgpack`, plus **type-only** `@statewalker/webrun-rpc` |
23
+
24
+ Adding a length prefix on a transport that already frames is redundant framing; relying on message
25
+ boundaries where there are none is the truncation bug the stream codec exists to prevent. Pick by
26
+ the transport, not by taste.
27
+
28
+ ## Why the stream codec exists
6
29
 
7
30
  Consumers that pipe values across transports (scanners writing chunks to a store, chat pipelines streaming embeddings, etc.) need a way to serialise a stream of objects into a byte stream and reassemble it on the other side without truncation surprises.
8
31
 
@@ -10,13 +33,34 @@ A raw MessagePack stream has no frame boundaries: a decoder can only succeed if
10
33
 
11
34
  Previously the codec lived inside `@repo/streams` (private, unpublished). It's been extracted here so (a) consumers that only need framing don't pull in the broader `webrun-streams` surface, and (b) the `@ygoe/msgpack` dependency lives in exactly one place.
12
35
 
13
- ## How to use
36
+ ## Why the port codec exists
37
+
38
+ `@statewalker/webrun-rpc`'s `multiplexPort` runs many virtual ports over one transport, and it needs
39
+ a codec to put its envelopes on the wire. `structuredCodec` (in `webrun-rpc`) passes them through
40
+ unencoded, which works only where messages are *structured values* — a `MessagePort`, a worker, an
41
+ iframe. `msgpackCodec` is the byte-transport sibling: one envelope becomes one msgpack frame and one
42
+ `postMessage`.
43
+
44
+ ## Install
14
45
 
15
46
  ```sh
16
47
  npm install @statewalker/webrun-msgpack
17
48
  ```
18
49
 
19
- Four exports — one encode/decode pair for generic values, one for `Float32Array`:
50
+ One runtime dependency **in the emitted bundle**
51
+ ([`@ygoe/msgpack`](https://www.npmjs.com/package/@ygoe/msgpack)), no peer dependencies. ESM only
52
+ (`"type": "module"`). That is not the same as the install cost: `@statewalker/webrun-rpc` is a
53
+ declared `dependency`, so `npm install` also pulls it and, transitively, `@statewalker/webrun-streams`
54
+ into `node_modules` — even for a consumer who uses only the stream codec.
55
+
56
+ `@statewalker/webrun-rpc` is declared as a dependency but is **type-only**: `msgpackCodec` imports
57
+ the `PortCodec` interface from it and no runtime code, so nothing of `webrun-rpc` is in the built
58
+ bundle (`dist/index.js` imports `@ygoe/msgpack` and nothing else) and `webrun-rpc` gains no msgpack
59
+ dependency in either direction.
60
+
61
+ ## How to use
62
+
63
+ ### Exports
20
64
 
21
65
  | Export | Direction | Use case |
22
66
  | --- | --- | --- |
@@ -24,6 +68,7 @@ Four exports — one encode/decode pair for generic values, one for `Float32Arra
24
68
  | `decodeMsgpack<T>(src: AsyncIterable<Uint8Array>)` | bytes → values | inverse of `encodeMsgpack` |
25
69
  | `encodeFloat32Arrays(src: AsyncIterable<Float32Array>)` | arrays → bytes | zero-copy float streaming |
26
70
  | `decodeFloat32Arrays(src: AsyncIterable<Uint8Array>)` | bytes → arrays | inverse of `encodeFloat32Arrays` |
71
+ | `msgpackCodec: PortCodec` | envelope ⇄ one framed message | `multiplexPort` over a byte transport |
27
72
 
28
73
  ## Examples
29
74
 
@@ -82,6 +127,149 @@ for await (const v of decodeMsgpack<{ a: number; b: string }>(byOne())) {
82
127
  }
83
128
  ```
84
129
 
130
+ ### An RPC stream over a byte transport
131
+
132
+ `msgpackCodec` on both ends of a transport that carries `Uint8Array`s, one virtual port, one
133
+ `duplexOverPort` round trip. The pipe below stands in for the real thing — replace it with a
134
+ WebSocket pair, an `RTCDataChannel`, or a LiveKit packet stream and nothing else changes.
135
+
136
+ ```js
137
+ import { duplexOverPort, multiplexPort, serveDuplexOverPort } from "@statewalker/webrun-rpc";
138
+ import { msgpackCodec } from "@statewalker/webrun-msgpack";
139
+
140
+ // A byte transport: two ends that carry `Uint8Array`s and nothing else.
141
+ function bytePipePair() {
142
+ const listeners = [new Set(), new Set()];
143
+ const make = (self) => ({
144
+ postMessage(bytes) {
145
+ const copy = bytes.slice(); // a real transport does not share the sender's buffer
146
+ setTimeout(() => {
147
+ for (const listener of [...listeners[1 - self]]) listener({ data: copy });
148
+ }, 0);
149
+ },
150
+ addEventListener: (_type, listener) => listeners[self].add(listener),
151
+ removeEventListener: (_type, listener) => listeners[self].delete(listener),
152
+ });
153
+ return { a: make(0), b: make(1) };
154
+ }
155
+
156
+ const pipe = bytePipePair();
157
+
158
+ // The responder: every virtual port the peer opens gets an echo handler.
159
+ const server = multiplexPort(pipe.b, {
160
+ codec: msgpackCodec,
161
+ side: "responder",
162
+ onPort: (port) => {
163
+ serveDuplexOverPort(port, async function* (input) {
164
+ for await (const chunk of input) yield chunk;
165
+ });
166
+ },
167
+ });
168
+
169
+ // The initiator: one virtual port, one duplex round trip over it.
170
+ const client = multiplexPort(pipe.a, { codec: msgpackCodec, side: "initiator" });
171
+ const port = await client.openPort({ kind: "stream" });
172
+ const call = duplexOverPort(port, { maxMessageSize: client.maxMessageSize });
173
+
174
+ async function* body() {
175
+ yield new TextEncoder().encode("hello ");
176
+ yield new TextEncoder().encode("bytes");
177
+ }
178
+
179
+ const decoder = new TextDecoder();
180
+ let echoed = "";
181
+ for await (const chunk of call(body())) echoed += decoder.decode(chunk);
182
+ console.log(echoed); // "hello bytes"
183
+
184
+ await client.close();
185
+ await server.close();
186
+ ```
187
+
188
+ The same stack passes the unmodified `webrun-streams-conformance` L0–L6 suite over a byte pipe, in
189
+ both framing regimes — unlimited, and with frames capped at 64 KiB
190
+ (`tests/conformance-bytes.test.ts`).
191
+
192
+ **Read that green narrowly: an in-process pipe is not a transport.** The pipe hands `Uint8Array`s
193
+ straight from one object to another inside one process, so what the suite covers is this codec's own
194
+ contract end to end, including under chunking. It covers *none* of what a real byte transport brings:
195
+ framing, message-size ceilings and what a transport does when you exceed one, backpressure,
196
+ reconnection, close codes and error semantics. A WebSocket, an `RTCDataChannel` and a LiveKit data
197
+ track each need their own run before anything here is claimed of them.
198
+
199
+ ## `maxMessageSize` bounds the payload, not the frame
200
+
201
+ **Leave a margin of at least 256 bytes below your transport's hard limit.** This is the one thing
202
+ that will bite you when wiring `msgpackCodec` to a capped transport, and the frame sizes below are
203
+ measured rather than cautious:
204
+
205
+ `duplexOverPort` applies `toChunks(maxMessageSize)` to the *payload*. The envelope framing —
206
+ `WireChunk`, `callPort`'s `{type, channelName, callId, params}`, the mux's `{type, id, payload}`,
207
+ then this codec — is added **on top, afterwards**. Over `msgpackCodec` that overhead is
208
+ `87 + len(callId)` bytes and it is **not constant**: `callId` is
209
+ `` `call-${Date.now()}-${String(Math.random()).substring(2)}` ``, whose length varies **31–40**
210
+ characters *per chunk* because `Math.random()` drops trailing zeros; the port id's integer width
211
+ adds 0–4; the channel name adds 1 for `"out"` over `"in"`; and a chunk at or above 64 KiB adds 2 as
212
+ the payload's `bin` header widens.
213
+
214
+ Two numbers, and the difference between them matters: adding those terms up gives a **modelled
215
+ ceiling of 134 bytes**, while the overheads *actually observed* span **123–128 bytes**. The 134 is
216
+ arithmetic; the 123–128 is measurement.
217
+
218
+ The largest, 128, comes from a **64 KiB** cap — a 65,664-byte frame in the capped conformance run
219
+ over a 10 MiB body — which is the regime where the `bin`-header term applies. A separate sweep, eight
220
+ runs at a **16 KiB** cap with a 1 MiB body (several thousand chunks, so several thousand `callId`s),
221
+ never exceeded **126** at that cap; the table below is one run per cap and is not that sweep. A
222
+ 256-byte margin covers all of it, which is why the advice is a round number rather than a tight one.
223
+
224
+ Measured, with a 512 KiB body through the stack above:
225
+
226
+ | `maxMessageSize` | intent | largest frame actually posted | overhead |
227
+ | --- | --- | --- | --- |
228
+ | `16 * 1024` | an `RTCDataChannel`'s conservative ceiling | **16,508 bytes** | 124 |
229
+ | `12 * 1024` | LiveKit's safe packet size | **12,413 bytes** | 125 |
230
+ | `64 * 1024` | | **65,662 bytes** | 126 |
231
+
232
+ So setting `maxMessageSize` to the transport's hard limit **overruns it on the first full-size
233
+ chunk** — and a transport that silently drops an oversized message (LiveKit does; the body arrives
234
+ as zero bytes with no error on either side) gives you no signal at all. Set it to
235
+ `limit - 256` and the arithmetic stops mattering.
236
+
237
+ This is spec D10's correction, recorded in
238
+ `docs/superpowers/specs/2026-09-05-port-multiplexer-design.md`.
239
+
240
+ ## Two things `msgpackCodec` does that `structuredCodec` does not
241
+
242
+ **The transfer list is ignored.** `PortCodec.post` receives an optional `Transferable[]`;
243
+ `msgpackCodec` drops it. After encoding, the payload is *inside* the bytes — there is no live
244
+ `ArrayBuffer` left on the far side of the call to hand over, and passing the caller's original
245
+ buffers as transferables would detach buffers the caller still owns. `structuredCodec` forwards the
246
+ list, because there the objects themselves cross.
247
+
248
+ **msgpack drops object keys whose value is explicitly `undefined`**; structured clone keeps them. So
249
+ `{ result: undefined }` arrives as `{}` over this codec and as `{ result: undefined }` over
250
+ `structuredCodec`. Nothing `webrun-rpc`'s layer 2 sends depends on the difference — every wire shape
251
+ it produces is pinned against both codecs in `tests/codec-equivalence.test.ts` — and per **spec
252
+ D16** it must not come to. If you build a payload where `"key" in obj` means something different
253
+ from `obj.key === undefined`, it will not survive this codec.
254
+
255
+ D16's reach has one open edge, worth knowing before you put arbitrary application errors on a byte
256
+ transport: `serializeError` copies **every own enumerable property** off a thrown `Error` onto the
257
+ wire. Whether the resulting `error` payload is msgpack-expressible therefore depends on what your
258
+ code throws — a `cause` holding a `Map` or a class instance collapses to `{}`, and a circular
259
+ reference makes `serialize` throw.
260
+
261
+ ## A `@ygoe/msgpack` wart, so nobody debugs it twice
262
+
263
+ On some truncated input `@ygoe/msgpack`'s `deserialize` calls **`console.debug("msgpack array:", …)`
264
+ with the whole offending buffer** before it throws. `msgpackCodec` catches the throw and drops the
265
+ frame, but it cannot suppress the log — the call is inside the library.
266
+
267
+ Precisely: the log fires when the decode runs off the end of the buffer *where a byte code is
268
+ expected* (`Invalid byte value 'undefined' at index N`). A truncation that lands mid-string throws
269
+ `Cannot read properties of undefined (reading 'toString')` with no log, and `0xc1` garbage or empty
270
+ input throws silently. So the noise is real but intermittent — a peer that half-writes a frame will
271
+ print a buffer dump on your console and nothing will be wrong with your code.
272
+
85
273
  ## Internals
86
274
 
87
275
  ### Frame layout
@@ -95,6 +283,8 @@ for await (const v of decodeMsgpack<{ a: number; b: string }>(byOne())) {
95
283
 
96
284
  - Big-endian 32-bit length prefix — same convention as Java `DataOutputStream` and most wire protocols.
97
285
  - Max payload per frame: 2³²−1 bytes. No fragmentation within a frame (a single call to `serialize` produces the whole payload up-front); very large values will allocate proportionally.
286
+ - **This layout is the stream codec's only.** `msgpackCodec` writes a bare msgpack document per
287
+ message and prefixes nothing.
98
288
 
99
289
  ### Decoder state machine
100
290
 
@@ -108,30 +298,63 @@ The decoder keeps a rolling `Uint8Array` buffer. Each incoming chunk is appended
108
298
 
109
299
  Zero-length chunks are tolerated and simply no-op through the loop. Truncated trailing frames are silently dropped — the buffer retains them but the consuming `for await` ends without yielding a partial value.
110
300
 
301
+ ### What `msgpackCodec.read` accepts, and what it refuses
302
+
303
+ It accepts whatever byte shape a transport pump hands over — a `Uint8Array`, a bare `ArrayBuffer`,
304
+ or any `ArrayBufferView` (a `DataView` included, offset and length honoured). Everything else is
305
+ refused by returning `undefined`: a non-byte value, empty bytes, malformed msgpack, and well-formed
306
+ msgpack that decodes to something that is not a `PortEnvelope` (the `id` must be a non-negative
307
+ integer and the `type` one of `open` / `message` / `close`). A shared transport carries traffic that
308
+ is not ours, and layer 1 must not mistake it for an envelope.
309
+
310
+ Refusal is always a dropped message, never a throw: a throw here would escape inside the raw port's
311
+ own listener, outside any consumer's reach, and would let one hostile frame take the multiplexer
312
+ down.
313
+
314
+ There are three inputs for which `read` *can* still throw, all inside the byte-shape check that runs
315
+ before the `try`: a detached `ArrayBuffer`, a `DataView` over a detached buffer, and a `Proxy` with
316
+ a throwing `getPrototypeOf`. None is producible by a remote peer sending bytes — each needs
317
+ same-process JavaScript already holding the backing memory — so the guarantee is "cannot throw for
318
+ anything that arrives over a wire", not "cannot throw for any JavaScript value".
319
+
111
320
  ### Float32Array zero-copy
112
321
 
113
322
  `encodeFloat32Arrays` constructs a `Uint8Array` view over the `Float32Array`'s underlying buffer and serialises it as a msgpack `bin` payload — no float-by-float conversion. `decodeFloat32Arrays` reinterprets the decoded `Uint8Array` as a `Float32Array`. When the decoded buffer's `byteOffset` is not 4-byte aligned (can happen if `@ygoe/msgpack` returns a view into a larger buffer), we copy into a fresh aligned `Uint8Array` before constructing the `Float32Array`; otherwise the operation is view-only.
114
323
 
324
+ ### A trap when writing a transport pump
325
+
326
+ `serialize` returns a **view** over an internal buffer that is not trimmed: a 10 MiB envelope
327
+ measures ~10,485,800 bytes over a 16,777,216-byte `ArrayBuffer` (~1.6× slack, `byteOffset` 0). Send
328
+ the view. A pump that reaches for `frame.buffer` instead would put ~6 MiB of trailing zeros on the
329
+ wire per message.
330
+
115
331
  ### Dependencies
116
332
 
117
333
  - [`@ygoe/msgpack`](https://github.com/ygoe/msgpack.js) — single-file msgpack implementation (≈7 kB gzipped), no transitive deps.
334
+ - [`@statewalker/webrun-rpc`](../webrun-rpc) — **types only** (`PortCodec`, `PortEnvelope`); no runtime import is emitted.
118
335
 
119
- Dev: TypeScript, vitest, tsdown, rimraf (catalog versions from the monorepo root).
336
+ Dev: TypeScript, vitest, rolldown, rimraf (catalog versions from the monorepo root).
337
+ `@statewalker/webrun-streams` and `@statewalker/webrun-streams-conformance` are dev-only, for the
338
+ conformance run.
120
339
 
121
340
  ### Constraints
122
341
 
123
342
  - Big-endian length prefix only — no little-endian variant.
124
343
  - `decodeMsgpack` allocates one `Uint8Array` per incoming chunk for the `concat`; long streams with many tiny chunks may benefit from a batched source upstream.
125
344
  - `Float32Array` codec is strictly `Float32` — no element-size negotiation.
345
+ - `msgpackCodec` carries no length prefix, so it is **unusable** on a transport without message
346
+ boundaries. Use the stream codec there.
126
347
 
127
348
  ## Scripts
128
349
 
129
350
  ```sh
130
- pnpm test # vitest run (25 tests)
131
- pnpm run build # tsdown
132
- pnpm lint # biome check
351
+ pnpm test # vitest run (84 tests / 4 files)
352
+ pnpm run build # rolldown + tsc --emitDeclarationOnly
353
+ pnpm lint # biome check src tests
354
+ pnpm typecheck # tsc --noEmit (src)
355
+ pnpm typecheck:tests # tsc -p tsconfig.tests.json — needs the sibling packages built
133
356
  ```
134
357
 
135
358
  ## License
136
359
 
137
- MIT © statewalker
360
+ MIT © statewalker — see [LICENSE](../../LICENSE).
@@ -0,0 +1,3 @@
1
+ export { decodeFloat32Arrays, decodeMsgpack, encodeFloat32Arrays, encodeMsgpack, } from "./msgpack.js";
2
+ export { msgpackCodec } from "./port-codec.js";
3
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,mBAAmB,EACnB,aAAa,EACb,mBAAmB,EACnB,aAAa,GACd,MAAM,cAAc,CAAC;AACtB,OAAO,EAAE,YAAY,EAAE,MAAM,iBAAiB,CAAC"}
package/dist/index.js ADDED
@@ -0,0 +1,133 @@
1
+ import msgpack from "@ygoe/msgpack";
2
+ //#region src/msgpack.ts
3
+ const { serialize: serialize$1, deserialize: deserialize$1 } = msgpack;
4
+ /**
5
+ * Encode each value as a length-prefixed msgpack frame.
6
+ * Frame format: [4-byte big-endian length][msgpack bytes]
7
+ */
8
+ async function* encodeMsgpack(input) {
9
+ for await (const item of input) {
10
+ const payload = serialize$1(item);
11
+ const frame = new Uint8Array(4 + payload.length);
12
+ new DataView(frame.buffer).setUint32(0, payload.length, false);
13
+ frame.set(payload, 4);
14
+ yield frame;
15
+ }
16
+ }
17
+ /**
18
+ * Decode length-prefixed msgpack frames, reassembling across chunk boundaries.
19
+ */
20
+ async function* decodeMsgpack(input) {
21
+ let buffer = /* @__PURE__ */ new Uint8Array(0);
22
+ for await (const chunk of input) {
23
+ buffer = concat(buffer, chunk);
24
+ while (buffer.length >= 4) {
25
+ const frameLen = new DataView(buffer.buffer, buffer.byteOffset, buffer.byteLength).getUint32(0, false);
26
+ if (buffer.length < 4 + frameLen) break;
27
+ const payload = buffer.subarray(4, 4 + frameLen);
28
+ buffer = buffer.subarray(4 + frameLen);
29
+ yield deserialize$1(Uint8Array.from(payload));
30
+ }
31
+ }
32
+ }
33
+ /**
34
+ * Encode each Float32Array as a msgpack frame.
35
+ * The Float32Array is converted to a Uint8Array view (zero-copy) before encoding as msgpack bin.
36
+ */
37
+ async function* encodeFloat32Arrays(input) {
38
+ for await (const arr of input) {
39
+ const bytes = new Uint8Array(arr.buffer, arr.byteOffset, arr.byteLength);
40
+ const payload = serialize$1(bytes);
41
+ const frame = new Uint8Array(4 + payload.length);
42
+ new DataView(frame.buffer).setUint32(0, payload.length, false);
43
+ frame.set(payload, 4);
44
+ yield frame;
45
+ }
46
+ }
47
+ /**
48
+ * Decode msgpack frames back to Float32Array.
49
+ * Each frame contains a msgpack bin value (Uint8Array), reinterpreted as Float32Array.
50
+ */
51
+ async function* decodeFloat32Arrays(input) {
52
+ for await (const item of decodeMsgpack(wrapIterable(input))) {
53
+ const aligned = alignBuffer(item);
54
+ yield new Float32Array(aligned.buffer, aligned.byteOffset, aligned.byteLength / 4);
55
+ }
56
+ }
57
+ function alignBuffer(bytes) {
58
+ if (bytes.byteOffset % 4 === 0) return bytes;
59
+ const aligned = new Uint8Array(bytes.length);
60
+ aligned.set(bytes);
61
+ return aligned;
62
+ }
63
+ function concat(a, b) {
64
+ if (a.length === 0) return b;
65
+ const result = new Uint8Array(a.length + b.length);
66
+ result.set(a, 0);
67
+ result.set(b, a.length);
68
+ return result;
69
+ }
70
+ async function* wrapIterable(input) {
71
+ yield* input;
72
+ }
73
+ //#endregion
74
+ //#region src/port-codec.ts
75
+ const { serialize, deserialize } = msgpack;
76
+ /**
77
+ * Same shape check as `structuredCodec`'s, applied after decoding. A shared
78
+ * transport carries traffic that is not ours, and layer 1 must not mistake it
79
+ * for an envelope.
80
+ */
81
+ function isEnvelope(value) {
82
+ if (typeof value !== "object" || value === null) return false;
83
+ const candidate = value;
84
+ if (typeof candidate.id !== "number") return false;
85
+ if (!Number.isInteger(candidate.id) || candidate.id < 0) return false;
86
+ return candidate.type === "open" || candidate.type === "message" || candidate.type === "close";
87
+ }
88
+ /** Accept whatever byte shape a transport pump hands over. */
89
+ function toBytes(data) {
90
+ if (data instanceof Uint8Array) return data;
91
+ if (data instanceof ArrayBuffer) return new Uint8Array(data);
92
+ if (ArrayBuffer.isView(data)) {
93
+ const view = data;
94
+ return new Uint8Array(view.buffer, view.byteOffset, view.byteLength);
95
+ }
96
+ }
97
+ /**
98
+ * For ports whose messages are bytes — a WebSocket, a WebRTC data channel, a
99
+ * LiveKit data packet.
100
+ *
101
+ * One envelope becomes one msgpack frame and one `postMessage`. There is no
102
+ * length prefix, because every transport this codec targets preserves message
103
+ * boundaries; adding one would be redundant framing on a transport that
104
+ * already frames. (`encodeMsgpack`/`decodeMsgpack` in this package *do* carry
105
+ * a length prefix — they are a stream codec for a transport with no
106
+ * boundaries, and are a different thing.)
107
+ *
108
+ * The transfer list is deliberately ignored: after encoding, the payload is
109
+ * inside the bytes, so there is nothing left to hand over.
110
+ *
111
+ * **Not interchangeable with `structuredCodec` in one respect:** msgpack drops
112
+ * object keys whose value is `undefined`, where structured clone preserves
113
+ * them. Nothing layer 2 sends depends on that distinction today, and it must
114
+ * not come to — see spec D16.
115
+ */
116
+ const msgpackCodec = {
117
+ post(port, envelope) {
118
+ port.postMessage(serialize(envelope));
119
+ },
120
+ read(event) {
121
+ const bytes = toBytes(event.data);
122
+ if (!bytes || bytes.byteLength === 0) return void 0;
123
+ let decoded;
124
+ try {
125
+ decoded = deserialize(bytes);
126
+ } catch {
127
+ return;
128
+ }
129
+ return isEnvelope(decoded) ? decoded : void 0;
130
+ }
131
+ };
132
+ //#endregion
133
+ export { decodeFloat32Arrays, decodeMsgpack, encodeFloat32Arrays, encodeMsgpack, msgpackCodec };
@@ -0,0 +1,20 @@
1
+ /**
2
+ * Encode each value as a length-prefixed msgpack frame.
3
+ * Frame format: [4-byte big-endian length][msgpack bytes]
4
+ */
5
+ export declare function encodeMsgpack<T>(input: AsyncIterable<T>): AsyncGenerator<Uint8Array>;
6
+ /**
7
+ * Decode length-prefixed msgpack frames, reassembling across chunk boundaries.
8
+ */
9
+ export declare function decodeMsgpack<T>(input: AsyncIterable<Uint8Array>): AsyncGenerator<T>;
10
+ /**
11
+ * Encode each Float32Array as a msgpack frame.
12
+ * The Float32Array is converted to a Uint8Array view (zero-copy) before encoding as msgpack bin.
13
+ */
14
+ export declare function encodeFloat32Arrays(input: AsyncIterable<Float32Array>): AsyncGenerator<Uint8Array>;
15
+ /**
16
+ * Decode msgpack frames back to Float32Array.
17
+ * Each frame contains a msgpack bin value (Uint8Array), reinterpreted as Float32Array.
18
+ */
19
+ export declare function decodeFloat32Arrays(input: AsyncIterable<Uint8Array>): AsyncGenerator<Float32Array>;
20
+ //# sourceMappingURL=msgpack.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"msgpack.d.ts","sourceRoot":"","sources":["../src/msgpack.ts"],"names":[],"mappings":"AAIA;;;GAGG;AACH,wBAAuB,aAAa,CAAC,CAAC,EAAE,KAAK,EAAE,aAAa,CAAC,CAAC,CAAC,GAAG,cAAc,CAAC,UAAU,CAAC,CAS3F;AAED;;GAEG;AACH,wBAAuB,aAAa,CAAC,CAAC,EAAE,KAAK,EAAE,aAAa,CAAC,UAAU,CAAC,GAAG,cAAc,CAAC,CAAC,CAAC,CAgB3F;AAED;;;GAGG;AACH,wBAAuB,mBAAmB,CACxC,KAAK,EAAE,aAAa,CAAC,YAAY,CAAC,GACjC,cAAc,CAAC,UAAU,CAAC,CAU5B;AAED;;;GAGG;AACH,wBAAuB,mBAAmB,CACxC,KAAK,EAAE,aAAa,CAAC,UAAU,CAAC,GAC/B,cAAc,CAAC,YAAY,CAAC,CAK9B"}
@@ -0,0 +1,22 @@
1
+ import type { PortCodec } from "@statewalker/webrun-rpc";
2
+ /**
3
+ * For ports whose messages are bytes — a WebSocket, a WebRTC data channel, a
4
+ * LiveKit data packet.
5
+ *
6
+ * One envelope becomes one msgpack frame and one `postMessage`. There is no
7
+ * length prefix, because every transport this codec targets preserves message
8
+ * boundaries; adding one would be redundant framing on a transport that
9
+ * already frames. (`encodeMsgpack`/`decodeMsgpack` in this package *do* carry
10
+ * a length prefix — they are a stream codec for a transport with no
11
+ * boundaries, and are a different thing.)
12
+ *
13
+ * The transfer list is deliberately ignored: after encoding, the payload is
14
+ * inside the bytes, so there is nothing left to hand over.
15
+ *
16
+ * **Not interchangeable with `structuredCodec` in one respect:** msgpack drops
17
+ * object keys whose value is `undefined`, where structured clone preserves
18
+ * them. Nothing layer 2 sends depends on that distinction today, and it must
19
+ * not come to — see spec D16.
20
+ */
21
+ export declare const msgpackCodec: PortCodec;
22
+ //# sourceMappingURL=port-codec.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"port-codec.d.ts","sourceRoot":"","sources":["../src/port-codec.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAgB,MAAM,yBAAyB,CAAC;AA6BvE;;;;;;;;;;;;;;;;;;GAkBG;AACH,eAAO,MAAM,YAAY,EAAE,SAmB1B,CAAC"}
package/package.json CHANGED
@@ -1,9 +1,9 @@
1
1
  {
2
2
  "name": "@statewalker/webrun-msgpack",
3
- "version": "0.1.1",
3
+ "version": "0.2.2",
4
4
  "private": false,
5
5
  "type": "module",
6
- "description": "Length-prefixed MessagePack frame codec for async iterables. Streams-safe encode/decode for values and Float32Arrays.",
6
+ "description": "MessagePack codecs for the wire: a length-prefixed stream codec for async iterables (values and Float32Arrays), and msgpackCodec, a PortCodec that carries webrun-rpc port envelopes over a byte transport.",
7
7
  "homepage": "https://github.com/statewalker/webrun-wire",
8
8
  "author": {
9
9
  "name": "Mikhail Kotelnikov",
@@ -16,33 +16,41 @@
16
16
  "directory": "packages/webrun-msgpack"
17
17
  },
18
18
  "exports": {
19
- ".": "./src/index.ts"
19
+ ".": {
20
+ "types": "./dist/index.d.ts",
21
+ "import": "./dist/index.js"
22
+ }
20
23
  },
21
24
  "files": [
22
25
  "dist",
23
26
  "src"
24
27
  ],
25
28
  "dependencies": {
26
- "@ygoe/msgpack": "^1.0.3"
29
+ "@ygoe/msgpack": "^1.0.3",
30
+ "@statewalker/webrun-rpc": "0.4.0"
27
31
  },
28
32
  "devDependencies": {
29
33
  "rimraf": "^6.1.3",
30
- "tsdown": "^0.22.14",
34
+ "rolldown": "^1.2.4",
31
35
  "typescript": "^7.0.2",
32
- "vitest": "^4.1.10"
36
+ "vitest": "^4.1.10",
37
+ "@statewalker/webrun-streams": "0.2.0",
38
+ "@statewalker/webrun-streams-conformance": "0.2.0"
33
39
  },
34
40
  "sideEffects": false,
35
41
  "publishConfig": {
36
42
  "access": "public"
37
43
  },
44
+ "types": "./dist/index.d.ts",
38
45
  "scripts": {
39
- "build": "tsdown",
46
+ "build": "rimraf dist && rolldown -c && tsc --emitDeclarationOnly --declaration",
40
47
  "dev": "tsdown --watch",
41
48
  "test": "vitest run",
42
49
  "test:watch": "vitest",
43
50
  "typecheck": "tsc --noEmit",
51
+ "typecheck:tests": "tsc -p tsconfig.tests.json",
44
52
  "clean": "rimraf dist",
45
- "lint": "biome check --write .",
53
+ "lint": "biome check src tests",
46
54
  "format": "biome format --write ."
47
55
  }
48
56
  }
package/src/index.ts CHANGED
@@ -4,3 +4,4 @@ export {
4
4
  encodeFloat32Arrays,
5
5
  encodeMsgpack,
6
6
  } from "./msgpack.js";
7
+ export { msgpackCodec } from "./port-codec.js";
@@ -0,0 +1,68 @@
1
+ import type { PortCodec, PortEnvelope } from "@statewalker/webrun-rpc";
2
+ import msgpack from "@ygoe/msgpack";
3
+
4
+ const { serialize, deserialize } = msgpack;
5
+
6
+ /**
7
+ * Same shape check as `structuredCodec`'s, applied after decoding. A shared
8
+ * transport carries traffic that is not ours, and layer 1 must not mistake it
9
+ * for an envelope.
10
+ */
11
+ function isEnvelope(value: unknown): value is PortEnvelope {
12
+ if (typeof value !== "object" || value === null) return false;
13
+ const candidate = value as { type?: unknown; id?: unknown };
14
+ if (typeof candidate.id !== "number") return false;
15
+ if (!Number.isInteger(candidate.id) || candidate.id < 0) return false;
16
+ return candidate.type === "open" || candidate.type === "message" || candidate.type === "close";
17
+ }
18
+
19
+ /** Accept whatever byte shape a transport pump hands over. */
20
+ function toBytes(data: unknown): Uint8Array | undefined {
21
+ if (data instanceof Uint8Array) return data;
22
+ if (data instanceof ArrayBuffer) return new Uint8Array(data);
23
+ if (ArrayBuffer.isView(data)) {
24
+ const view = data as ArrayBufferView;
25
+ return new Uint8Array(view.buffer, view.byteOffset, view.byteLength);
26
+ }
27
+ return undefined;
28
+ }
29
+
30
+ /**
31
+ * For ports whose messages are bytes — a WebSocket, a WebRTC data channel, a
32
+ * LiveKit data packet.
33
+ *
34
+ * One envelope becomes one msgpack frame and one `postMessage`. There is no
35
+ * length prefix, because every transport this codec targets preserves message
36
+ * boundaries; adding one would be redundant framing on a transport that
37
+ * already frames. (`encodeMsgpack`/`decodeMsgpack` in this package *do* carry
38
+ * a length prefix — they are a stream codec for a transport with no
39
+ * boundaries, and are a different thing.)
40
+ *
41
+ * The transfer list is deliberately ignored: after encoding, the payload is
42
+ * inside the bytes, so there is nothing left to hand over.
43
+ *
44
+ * **Not interchangeable with `structuredCodec` in one respect:** msgpack drops
45
+ * object keys whose value is `undefined`, where structured clone preserves
46
+ * them. Nothing layer 2 sends depends on that distinction today, and it must
47
+ * not come to — see spec D16.
48
+ */
49
+ export const msgpackCodec: PortCodec = {
50
+ post(port, envelope) {
51
+ port.postMessage(serialize(envelope));
52
+ },
53
+
54
+ read(event) {
55
+ const bytes = toBytes(event.data);
56
+ if (!bytes || bytes.byteLength === 0) return undefined;
57
+ let decoded: unknown;
58
+ try {
59
+ decoded = deserialize(bytes);
60
+ } catch {
61
+ // A malformed frame is a peer bug or hostile traffic. Dropping it keeps
62
+ // layer 1's drop-never-queue posture; throwing here would escape into
63
+ // the raw port's own listener, outside any consumer's reach.
64
+ return undefined;
65
+ }
66
+ return isEnvelope(decoded) ? decoded : undefined;
67
+ },
68
+ };
package/LICENSE DELETED
@@ -1,21 +0,0 @@
1
- MIT License
2
-
3
- Copyright (c) 2022-2026 statewalker
4
-
5
- Permission is hereby granted, free of charge, to any person obtaining a copy
6
- of this software and associated documentation files (the "Software"), to deal
7
- in the Software without restriction, including without limitation the rights
8
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
- copies of the Software, and to permit persons to whom the Software is
10
- furnished to do so, subject to the following conditions:
11
-
12
- The above copyright notice and this permission notice shall be included in all
13
- copies or substantial portions of the Software.
14
-
15
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
- SOFTWARE.
package/dist/index.d.mts DELETED
@@ -1,23 +0,0 @@
1
- //#region src/msgpack.d.ts
2
- /**
3
- * Encode each value as a length-prefixed msgpack frame.
4
- * Frame format: [4-byte big-endian length][msgpack bytes]
5
- */
6
- declare function encodeMsgpack<T>(input: AsyncIterable<T>): AsyncGenerator<Uint8Array>;
7
- /**
8
- * Decode length-prefixed msgpack frames, reassembling across chunk boundaries.
9
- */
10
- declare function decodeMsgpack<T>(input: AsyncIterable<Uint8Array>): AsyncGenerator<T>;
11
- /**
12
- * Encode each Float32Array as a msgpack frame.
13
- * The Float32Array is converted to a Uint8Array view (zero-copy) before encoding as msgpack bin.
14
- */
15
- declare function encodeFloat32Arrays(input: AsyncIterable<Float32Array>): AsyncGenerator<Uint8Array>;
16
- /**
17
- * Decode msgpack frames back to Float32Array.
18
- * Each frame contains a msgpack bin value (Uint8Array), reinterpreted as Float32Array.
19
- */
20
- declare function decodeFloat32Arrays(input: AsyncIterable<Uint8Array>): AsyncGenerator<Float32Array>;
21
- //#endregion
22
- export { decodeFloat32Arrays, decodeMsgpack, encodeFloat32Arrays, encodeMsgpack };
23
- //# sourceMappingURL=index.d.mts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"index.d.mts","names":[],"sources":["../src/msgpack.ts"],"mappings":";;;;;iBAQuB,cAAc,GAAG,OAAO,cAAc,KAAK,eAAe;;;;iBAc1D,cAAc,GAAG,OAAO,cAAc,cAAc,eAAe;;;;;iBAsBnE,oBACrB,OAAO,cAAc,gBACpB,eAAe;;;;;iBAgBK,oBACrB,OAAO,cAAc,cACpB,eAAe"}
package/dist/index.mjs DELETED
@@ -1,76 +0,0 @@
1
- import msgpack from "@ygoe/msgpack";
2
- //#region src/msgpack.ts
3
- const { serialize, deserialize } = msgpack;
4
- /**
5
- * Encode each value as a length-prefixed msgpack frame.
6
- * Frame format: [4-byte big-endian length][msgpack bytes]
7
- */
8
- async function* encodeMsgpack(input) {
9
- for await (const item of input) {
10
- const payload = serialize(item);
11
- const frame = new Uint8Array(4 + payload.length);
12
- new DataView(frame.buffer).setUint32(0, payload.length, false);
13
- frame.set(payload, 4);
14
- yield frame;
15
- }
16
- }
17
- /**
18
- * Decode length-prefixed msgpack frames, reassembling across chunk boundaries.
19
- */
20
- async function* decodeMsgpack(input) {
21
- let buffer = /* @__PURE__ */ new Uint8Array(0);
22
- for await (const chunk of input) {
23
- buffer = concat(buffer, chunk);
24
- while (buffer.length >= 4) {
25
- const frameLen = new DataView(buffer.buffer, buffer.byteOffset, buffer.byteLength).getUint32(0, false);
26
- if (buffer.length < 4 + frameLen) break;
27
- const payload = buffer.subarray(4, 4 + frameLen);
28
- buffer = buffer.subarray(4 + frameLen);
29
- yield deserialize(Uint8Array.from(payload));
30
- }
31
- }
32
- }
33
- /**
34
- * Encode each Float32Array as a msgpack frame.
35
- * The Float32Array is converted to a Uint8Array view (zero-copy) before encoding as msgpack bin.
36
- */
37
- async function* encodeFloat32Arrays(input) {
38
- for await (const arr of input) {
39
- const bytes = new Uint8Array(arr.buffer, arr.byteOffset, arr.byteLength);
40
- const payload = serialize(bytes);
41
- const frame = new Uint8Array(4 + payload.length);
42
- new DataView(frame.buffer).setUint32(0, payload.length, false);
43
- frame.set(payload, 4);
44
- yield frame;
45
- }
46
- }
47
- /**
48
- * Decode msgpack frames back to Float32Array.
49
- * Each frame contains a msgpack bin value (Uint8Array), reinterpreted as Float32Array.
50
- */
51
- async function* decodeFloat32Arrays(input) {
52
- for await (const item of decodeMsgpack(wrapIterable(input))) {
53
- const aligned = alignBuffer(item);
54
- yield new Float32Array(aligned.buffer, aligned.byteOffset, aligned.byteLength / 4);
55
- }
56
- }
57
- function alignBuffer(bytes) {
58
- if (bytes.byteOffset % 4 === 0) return bytes;
59
- const aligned = new Uint8Array(bytes.length);
60
- aligned.set(bytes);
61
- return aligned;
62
- }
63
- function concat(a, b) {
64
- if (a.length === 0) return b;
65
- const result = new Uint8Array(a.length + b.length);
66
- result.set(a, 0);
67
- result.set(b, a.length);
68
- return result;
69
- }
70
- async function* wrapIterable(input) {
71
- yield* input;
72
- }
73
- //#endregion
74
- export { decodeFloat32Arrays, decodeMsgpack, encodeFloat32Arrays, encodeMsgpack };
75
-
76
- //# sourceMappingURL=index.mjs.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"index.mjs","names":[],"sources":["../src/msgpack.ts"],"sourcesContent":["import msgpack from \"@ygoe/msgpack\";\n\nconst { serialize, deserialize } = msgpack;\n\n/**\n * Encode each value as a length-prefixed msgpack frame.\n * Frame format: [4-byte big-endian length][msgpack bytes]\n */\nexport async function* encodeMsgpack<T>(input: AsyncIterable<T>): AsyncGenerator<Uint8Array> {\n for await (const item of input) {\n const payload = serialize(item);\n const frame = new Uint8Array(4 + payload.length);\n const view = new DataView(frame.buffer);\n view.setUint32(0, payload.length, false);\n frame.set(payload, 4);\n yield frame;\n }\n}\n\n/**\n * Decode length-prefixed msgpack frames, reassembling across chunk boundaries.\n */\nexport async function* decodeMsgpack<T>(input: AsyncIterable<Uint8Array>): AsyncGenerator<T> {\n let buffer: Uint8Array = new Uint8Array(0);\n\n for await (const chunk of input) {\n buffer = concat(buffer, chunk);\n\n while (buffer.length >= 4) {\n const view = new DataView(buffer.buffer, buffer.byteOffset, buffer.byteLength);\n const frameLen = view.getUint32(0, false);\n if (buffer.length < 4 + frameLen) break;\n\n const payload = buffer.subarray(4, 4 + frameLen);\n buffer = buffer.subarray(4 + frameLen);\n yield deserialize(Uint8Array.from(payload)) as T;\n }\n }\n}\n\n/**\n * Encode each Float32Array as a msgpack frame.\n * The Float32Array is converted to a Uint8Array view (zero-copy) before encoding as msgpack bin.\n */\nexport async function* encodeFloat32Arrays(\n input: AsyncIterable<Float32Array>,\n): AsyncGenerator<Uint8Array> {\n for await (const arr of input) {\n const bytes = new Uint8Array(arr.buffer, arr.byteOffset, arr.byteLength);\n const payload = serialize(bytes);\n const frame = new Uint8Array(4 + payload.length);\n const view = new DataView(frame.buffer);\n view.setUint32(0, payload.length, false);\n frame.set(payload, 4);\n yield frame;\n }\n}\n\n/**\n * Decode msgpack frames back to Float32Array.\n * Each frame contains a msgpack bin value (Uint8Array), reinterpreted as Float32Array.\n */\nexport async function* decodeFloat32Arrays(\n input: AsyncIterable<Uint8Array>,\n): AsyncGenerator<Float32Array> {\n for await (const item of decodeMsgpack<Uint8Array>(wrapIterable(input))) {\n const aligned = alignBuffer(item);\n yield new Float32Array(aligned.buffer, aligned.byteOffset, aligned.byteLength / 4);\n }\n}\n\nfunction alignBuffer(bytes: Uint8Array): Uint8Array {\n if (bytes.byteOffset % 4 === 0) return bytes;\n const aligned = new Uint8Array(bytes.length);\n aligned.set(bytes);\n return aligned;\n}\n\nfunction concat(a: Uint8Array, b: Uint8Array): Uint8Array {\n if (a.length === 0) return b;\n const result = new Uint8Array(a.length + b.length);\n result.set(a, 0);\n result.set(b, a.length);\n return result;\n}\n\nasync function* wrapIterable<T>(input: AsyncIterable<T>): AsyncGenerator<T> {\n yield* input;\n}\n"],"mappings":";;AAEA,MAAM,EAAE,WAAW,gBAAgB;;;;;AAMnC,gBAAuB,cAAiB,OAAqD;CAC3F,WAAW,MAAM,QAAQ,OAAO;EAC9B,MAAM,UAAU,UAAU,IAAI;EAC9B,MAAM,QAAQ,IAAI,WAAW,IAAI,QAAQ,MAAM;EAE/C,IADiB,SAAS,MAAM,MAC7B,CAAC,CAAC,UAAU,GAAG,QAAQ,QAAQ,KAAK;EACvC,MAAM,IAAI,SAAS,CAAC;EACpB,MAAM;CACR;AACF;;;;AAKA,gBAAuB,cAAiB,OAAqD;CAC3F,IAAI,yBAAqB,IAAI,WAAW,CAAC;CAEzC,WAAW,MAAM,SAAS,OAAO;EAC/B,SAAS,OAAO,QAAQ,KAAK;EAE7B,OAAO,OAAO,UAAU,GAAG;GAEzB,MAAM,WAAW,IADA,SAAS,OAAO,QAAQ,OAAO,YAAY,OAAO,UAC/C,CAAC,CAAC,UAAU,GAAG,KAAK;GACxC,IAAI,OAAO,SAAS,IAAI,UAAU;GAElC,MAAM,UAAU,OAAO,SAAS,GAAG,IAAI,QAAQ;GAC/C,SAAS,OAAO,SAAS,IAAI,QAAQ;GACrC,MAAM,YAAY,WAAW,KAAK,OAAO,CAAC;EAC5C;CACF;AACF;;;;;AAMA,gBAAuB,oBACrB,OAC4B;CAC5B,WAAW,MAAM,OAAO,OAAO;EAC7B,MAAM,QAAQ,IAAI,WAAW,IAAI,QAAQ,IAAI,YAAY,IAAI,UAAU;EACvE,MAAM,UAAU,UAAU,KAAK;EAC/B,MAAM,QAAQ,IAAI,WAAW,IAAI,QAAQ,MAAM;EAE/C,IADiB,SAAS,MAAM,MAC7B,CAAC,CAAC,UAAU,GAAG,QAAQ,QAAQ,KAAK;EACvC,MAAM,IAAI,SAAS,CAAC;EACpB,MAAM;CACR;AACF;;;;;AAMA,gBAAuB,oBACrB,OAC8B;CAC9B,WAAW,MAAM,QAAQ,cAA0B,aAAa,KAAK,CAAC,GAAG;EACvE,MAAM,UAAU,YAAY,IAAI;EAChC,MAAM,IAAI,aAAa,QAAQ,QAAQ,QAAQ,YAAY,QAAQ,aAAa,CAAC;CACnF;AACF;AAEA,SAAS,YAAY,OAA+B;CAClD,IAAI,MAAM,aAAa,MAAM,GAAG,OAAO;CACvC,MAAM,UAAU,IAAI,WAAW,MAAM,MAAM;CAC3C,QAAQ,IAAI,KAAK;CACjB,OAAO;AACT;AAEA,SAAS,OAAO,GAAe,GAA2B;CACxD,IAAI,EAAE,WAAW,GAAG,OAAO;CAC3B,MAAM,SAAS,IAAI,WAAW,EAAE,SAAS,EAAE,MAAM;CACjD,OAAO,IAAI,GAAG,CAAC;CACf,OAAO,IAAI,GAAG,EAAE,MAAM;CACtB,OAAO;AACT;AAEA,gBAAgB,aAAgB,OAA4C;CAC1E,OAAO;AACT"}