@statewalker/webrun-msgpack 0.3.2 → 0.4.0
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 +242 -307
- package/package.json +9 -6
package/README.md
CHANGED
|
@@ -1,89 +1,77 @@
|
|
|
1
1
|
# @statewalker/webrun-msgpack
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
## What it is
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
5
|
+
MessagePack for the wire, in two shapes. A **stream codec** (`encodeMsgpack` / `decodeMsgpack`,
|
|
6
|
+
plus `Float32Array` variants) turns an iterable of values into length-prefixed frames and back, for
|
|
7
|
+
a transport with no message boundaries. A **message codec** (`msgpackCodec`) is a `PortCodec` for
|
|
8
|
+
`@statewalker/webrun-rpc`'s `multiplexPort`: one envelope becomes one bare MessagePack document and
|
|
9
|
+
one `postMessage`, for a transport that already frames. Both sit on the package's own MessagePack
|
|
10
|
+
implementation, `serialize` / `deserialize`, a TypeScript port of Yves Goergen's
|
|
11
|
+
[msgpack.js](https://github.com/ygoe/msgpack.js), which is exported too.
|
|
9
12
|
|
|
10
|
-
|
|
11
|
-
exported too. It is a TypeScript port of Yves Goergen's
|
|
12
|
-
[msgpack.js](https://github.com/ygoe/msgpack.js) with a handful of fixes — see
|
|
13
|
-
[Provenance and credits](#provenance-and-credits). The package has no runtime dependencies.
|
|
13
|
+
## Why it exists
|
|
14
14
|
|
|
15
|
-
|
|
16
|
-
the
|
|
15
|
+
A raw MessagePack stream has no frame boundaries. A decoder fed a byte stream only succeeds if the
|
|
16
|
+
chunk boundaries happen to line up with the document boundaries. Code that pipes values across a
|
|
17
|
+
transport — a scanner writing chunks to a store, a pipeline streaming embeddings — needs to
|
|
18
|
+
serialise a stream of objects into bytes and get exactly those objects back on the other side,
|
|
19
|
+
without truncation surprises. The stream codec does that with a length prefix.
|
|
17
20
|
|
|
18
|
-
|
|
21
|
+
`multiplexPort` runs many virtual ports over one transport and needs a codec to put its envelopes
|
|
22
|
+
on the wire. `structuredCodec` (in `webrun-rpc`) passes envelopes through unencoded, which only
|
|
23
|
+
works where messages are structured values: a `MessagePort`, a worker, an iframe. `msgpackCodec`
|
|
24
|
+
is the codec for transports whose messages are bytes.
|
|
19
25
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
| Shape | `AsyncIterable<T>` ⇄ `AsyncIterable<Uint8Array>` | one `PortEnvelope` ⇄ one `postMessage` |
|
|
24
|
-
| Framing | **4-byte big-endian length prefix**, added by this package | **none** — the transport's own message boundaries are the framing |
|
|
25
|
-
| 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 |
|
|
26
|
-
| 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 |
|
|
27
|
-
| Depends on | nothing | **type-only** `@statewalker/webrun-rpc` |
|
|
28
|
-
|
|
29
|
-
Adding a length prefix on a transport that already frames is redundant framing; relying on message
|
|
30
|
-
boundaries where there are none is the truncation bug the stream codec exists to prevent. Pick by
|
|
31
|
-
the transport, not by taste.
|
|
32
|
-
|
|
33
|
-
## Why the stream codec exists
|
|
34
|
-
|
|
35
|
-
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.
|
|
36
|
-
|
|
37
|
-
A raw MessagePack stream has no frame boundaries: a decoder can only succeed if the chunk boundaries happen to line up with the payload boundaries. Length-prefix framing fixes this — the decoder buffers incoming bytes and only yields when a complete `[length][payload]` pair is available. Partial trailing frames are NEVER emitted, so callers can detect truncation by comparing observed count to expected.
|
|
38
|
-
|
|
39
|
-
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) MessagePack lives in exactly one place.
|
|
40
|
-
|
|
41
|
-
## Why the port codec exists
|
|
26
|
+
The package carries its own MessagePack implementation so that its bundle imports nothing at
|
|
27
|
+
runtime, and so that its behaviour on truncated or hostile input is defined by tests in this
|
|
28
|
+
package (see [Internals](#internals)).
|
|
42
29
|
|
|
43
|
-
|
|
44
|
-
a codec to put its envelopes on the wire. `structuredCodec` (in `webrun-rpc`) passes them through
|
|
45
|
-
unencoded, which works only where messages are *structured values* — a `MessagePort`, a worker, an
|
|
46
|
-
iframe. `msgpackCodec` is the byte-transport sibling: one envelope becomes one msgpack frame and one
|
|
47
|
-
`postMessage`.
|
|
48
|
-
|
|
49
|
-
## Install
|
|
30
|
+
## How to use
|
|
50
31
|
|
|
51
32
|
```sh
|
|
52
|
-
|
|
33
|
+
pnpm add @statewalker/webrun-msgpack
|
|
53
34
|
```
|
|
54
35
|
|
|
55
|
-
No
|
|
56
|
-
|
|
57
|
-
`@statewalker/webrun-rpc` is a declared `dependency`, so `npm install` also pulls it and,
|
|
58
|
-
transitively, `@statewalker/webrun-streams` into `node_modules` — even for a consumer who uses only
|
|
59
|
-
the stream codec or `serialize` / `deserialize`.
|
|
36
|
+
No peer dependencies. ESM only. One entry point, `.`, which works in browsers, Node, Deno, Bun and
|
|
37
|
+
workers; the runtime needs `TextDecoder`.
|
|
60
38
|
|
|
61
|
-
`@statewalker/webrun-rpc` is declared
|
|
62
|
-
|
|
63
|
-
|
|
39
|
+
`@statewalker/webrun-rpc` is a declared dependency but is used for **types only** (`PortCodec`,
|
|
40
|
+
`PortEnvelope`): `dist/index.js` imports nothing. The install still pulls `webrun-rpc` and,
|
|
41
|
+
through it, `@statewalker/webrun-streams` into `node_modules`, even if you use only the stream
|
|
42
|
+
codec or `serialize` / `deserialize`.
|
|
64
43
|
|
|
65
|
-
|
|
44
|
+
### Pick the codec by the transport
|
|
66
45
|
|
|
67
|
-
|
|
46
|
+
| | `encodeMsgpack` / `decodeMsgpack` | `msgpackCodec` |
|
|
47
|
+
| --- | --- | --- |
|
|
48
|
+
| Kind | stream codec | message codec (`PortCodec`) |
|
|
49
|
+
| Shape | `Iterable<T> \| AsyncIterable<T>` ⇄ `AsyncGenerator<Uint8Array>` | one `PortEnvelope` ⇄ one `postMessage` |
|
|
50
|
+
| Framing | 4-byte big-endian length prefix, added here | none: the transport's message boundaries are the framing |
|
|
51
|
+
| Use it when | the transport is a byte stream: a TCP-like socket, a `ReadableStream`, a file | the transport preserves message boundaries: a WebSocket, an `RTCDataChannel`, a LiveKit data packet |
|
|
52
|
+
| Malformed input | a truncated trailing frame is never emitted; the iteration just ends | dropped, never thrown |
|
|
53
|
+
|
|
54
|
+
A length prefix on a transport that already frames is redundant. Relying on message boundaries
|
|
55
|
+
where there are none is the truncation bug the stream codec exists to prevent.
|
|
68
56
|
|
|
69
57
|
### Exports
|
|
70
58
|
|
|
71
|
-
| Export | Direction | Use
|
|
59
|
+
| Export | Direction | Use |
|
|
72
60
|
| --- | --- | --- |
|
|
73
|
-
| `encodeMsgpack<T>(
|
|
74
|
-
| `decodeMsgpack<T>(
|
|
75
|
-
| `encodeFloat32Arrays(
|
|
76
|
-
| `decodeFloat32Arrays(
|
|
77
|
-
| `msgpackCodec: PortCodec` | envelope ⇄ one
|
|
61
|
+
| `encodeMsgpack<T>(input: Iterable<T> \| AsyncIterable<T>)` | values → frames | generic JSON-like values |
|
|
62
|
+
| `decodeMsgpack<T>(input: Iterable<Uint8Array> \| AsyncIterable<Uint8Array>)` | frames → values | inverse of `encodeMsgpack` |
|
|
63
|
+
| `encodeFloat32Arrays(input: Iterable<Float32Array> \| AsyncIterable<Float32Array>)` | arrays → frames | float vectors, no per-element conversion |
|
|
64
|
+
| `decodeFloat32Arrays(input: Iterable<Uint8Array> \| AsyncIterable<Uint8Array>)` | frames → arrays | inverse of `encodeFloat32Arrays` |
|
|
65
|
+
| `msgpackCodec: PortCodec` | envelope ⇄ one message | `multiplexPort` over a byte transport |
|
|
78
66
|
| `serialize(value, options?)` | one value → one MessagePack document | the format itself, no framing |
|
|
79
67
|
| `deserialize(bytes, options?)` | one document → one value | inverse of `serialize` |
|
|
80
68
|
|
|
81
|
-
Types: `SerializeOptions
|
|
82
|
-
`
|
|
83
|
-
|
|
69
|
+
Types: `SerializeOptions` (`multiple?: boolean`, `invalidTypeReplacement?: unknown`),
|
|
70
|
+
`DeserializeOptions` (`multiple?: boolean`), `MsgpackInput` (`Uint8Array | ArrayBuffer |
|
|
71
|
+
ArrayLike<number>`) and `MsgpackExtension` (`{ type: number; data: Uint8Array }`, an extension
|
|
72
|
+
value other than a timestamp).
|
|
84
73
|
|
|
85
|
-
|
|
86
|
-
list needs no async wrapper.
|
|
74
|
+
The four stream functions accept synchronous iterables too, so an array needs no async wrapper.
|
|
87
75
|
|
|
88
76
|
## Examples
|
|
89
77
|
|
|
@@ -98,12 +86,15 @@ const value = deserialize(bytes); // same shape; `at` is a Date, `raw` a Uint8Ar
|
|
|
98
86
|
// Several documents back to back:
|
|
99
87
|
const three = serialize([1, "two", { three: 3 }], { multiple: true });
|
|
100
88
|
deserialize(three, { multiple: true }); // [1, "two", { three: 3 }]
|
|
89
|
+
|
|
90
|
+
// A value that cannot be encoded, replaced instead of throwing:
|
|
91
|
+
serialize({ f: () => 1 }, { invalidTypeReplacement: "unsupported" }); // encodes { f: "unsupported" }
|
|
101
92
|
```
|
|
102
93
|
|
|
103
|
-
###
|
|
94
|
+
### A stream of values
|
|
104
95
|
|
|
105
96
|
```ts
|
|
106
|
-
import {
|
|
97
|
+
import { decodeMsgpack, encodeMsgpack } from "@statewalker/webrun-msgpack";
|
|
107
98
|
|
|
108
99
|
async function* events() {
|
|
109
100
|
yield { type: "start" };
|
|
@@ -111,55 +102,49 @@ async function* events() {
|
|
|
111
102
|
yield { type: "done" };
|
|
112
103
|
}
|
|
113
104
|
|
|
114
|
-
// encode
|
|
115
105
|
const bytes: AsyncIterable<Uint8Array> = encodeMsgpack(events());
|
|
116
106
|
|
|
117
|
-
//
|
|
107
|
+
// The decoder handles arbitrary chunk boundaries.
|
|
118
108
|
for await (const msg of decodeMsgpack<{ type: string; text?: string }>(bytes)) {
|
|
119
109
|
console.log(msg);
|
|
120
110
|
}
|
|
121
111
|
```
|
|
122
112
|
|
|
123
|
-
###
|
|
113
|
+
### Re-framing across arbitrary chunks
|
|
124
114
|
|
|
125
115
|
```ts
|
|
126
|
-
import {
|
|
127
|
-
decodeFloat32Arrays,
|
|
128
|
-
encodeFloat32Arrays,
|
|
129
|
-
} from "@statewalker/webrun-msgpack";
|
|
116
|
+
import { decodeMsgpack, encodeMsgpack } from "@statewalker/webrun-msgpack";
|
|
130
117
|
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
yield new Float32Array([0.5, 0.6, 0.7, 0.8]);
|
|
134
|
-
}
|
|
118
|
+
const frames: Uint8Array[] = [];
|
|
119
|
+
for await (const f of encodeMsgpack([{ a: 1, b: "hi" }])) frames.push(f);
|
|
135
120
|
|
|
136
|
-
//
|
|
137
|
-
const
|
|
138
|
-
for await (const
|
|
121
|
+
// Feed the decoder one byte at a time; it buffers until a frame is complete.
|
|
122
|
+
const oneByteAtATime = frames.flatMap((f) => [...f].map((byte) => new Uint8Array([byte])));
|
|
123
|
+
for await (const v of decodeMsgpack<{ a: number; b: string }>(oneByteAtATime)) {
|
|
124
|
+
console.log(v); // { a: 1, b: "hi" }
|
|
125
|
+
}
|
|
139
126
|
```
|
|
140
127
|
|
|
141
|
-
###
|
|
128
|
+
### Float32 vectors
|
|
142
129
|
|
|
143
130
|
```ts
|
|
144
|
-
import {
|
|
131
|
+
import { decodeFloat32Arrays, encodeFloat32Arrays } from "@statewalker/webrun-msgpack";
|
|
145
132
|
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
// Hand the decoder arbitrarily small slices — it buffers until complete:
|
|
150
|
-
async function* byOne() {
|
|
151
|
-
for (const b of bytes) for (const byte of b) yield new Uint8Array([byte]);
|
|
133
|
+
async function* chunks() {
|
|
134
|
+
yield new Float32Array([0.1, 0.2, 0.3, 0.4]);
|
|
135
|
+
yield new Float32Array([0.5, 0.6, 0.7, 0.8]);
|
|
152
136
|
}
|
|
153
|
-
|
|
154
|
-
|
|
137
|
+
|
|
138
|
+
// Each array travels as a msgpack `bin`, reinterpreted byte for byte as Float32 on arrival.
|
|
139
|
+
for await (const arr of decodeFloat32Arrays(encodeFloat32Arrays(chunks()))) {
|
|
140
|
+
console.log(arr.length); // 4, 4
|
|
155
141
|
}
|
|
156
142
|
```
|
|
157
143
|
|
|
158
144
|
### An RPC stream over a byte transport
|
|
159
145
|
|
|
160
|
-
`msgpackCodec` on both ends of a transport that carries `Uint8Array`s
|
|
161
|
-
|
|
162
|
-
WebSocket pair, an `RTCDataChannel`, or a LiveKit packet stream and nothing else changes.
|
|
146
|
+
`msgpackCodec` on both ends of a transport that carries `Uint8Array`s. The in-process pipe below
|
|
147
|
+
stands in for a WebSocket pair, an `RTCDataChannel` or a LiveKit packet stream.
|
|
163
148
|
|
|
164
149
|
```js
|
|
165
150
|
import { duplexOverPort, multiplexPort, serveDuplexOverPort } from "@statewalker/webrun-rpc";
|
|
@@ -213,124 +198,72 @@ await client.close();
|
|
|
213
198
|
await server.close();
|
|
214
199
|
```
|
|
215
200
|
|
|
216
|
-
The same stack passes the unmodified
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
then this codec — is added **on top, afterwards**. Over `msgpackCodec` that overhead is
|
|
236
|
-
`87 + len(callId)` bytes and it is **not constant**: `callId` is
|
|
237
|
-
`` `call-${Date.now()}-${String(Math.random()).substring(2)}` ``, whose length varies **31–40**
|
|
238
|
-
characters *per chunk* because `Math.random()` drops trailing zeros; the port id's integer width
|
|
239
|
-
adds 0–4; the channel name adds 1 for `"out"` over `"in"`; and a chunk at or above 64 KiB adds 2 as
|
|
240
|
-
the payload's `bin` header widens.
|
|
241
|
-
|
|
242
|
-
Two numbers, and the difference between them matters: adding those terms up gives a **modelled
|
|
243
|
-
ceiling of 134 bytes**, while the overheads *actually observed* span **123–128 bytes**. The 134 is
|
|
244
|
-
arithmetic; the 123–128 is measurement.
|
|
245
|
-
|
|
246
|
-
The largest, 128, comes from a **64 KiB** cap — a 65,664-byte frame in the capped conformance run
|
|
247
|
-
over a 10 MiB body — which is the regime where the `bin`-header term applies. A separate sweep, eight
|
|
248
|
-
runs at a **16 KiB** cap with a 1 MiB body (several thousand chunks, so several thousand `callId`s),
|
|
249
|
-
never exceeded **126** at that cap; the table below is one run per cap and is not that sweep. A
|
|
250
|
-
256-byte margin covers all of it, which is why the advice is a round number rather than a tight one.
|
|
251
|
-
|
|
252
|
-
Measured, with a 512 KiB body through the stack above:
|
|
253
|
-
|
|
254
|
-
| `maxMessageSize` | intent | largest frame actually posted | overhead |
|
|
255
|
-
| --- | --- | --- | --- |
|
|
256
|
-
| `16 * 1024` | an `RTCDataChannel`'s conservative ceiling | **16,508 bytes** | 124 |
|
|
257
|
-
| `12 * 1024` | LiveKit's safe packet size | **12,413 bytes** | 125 |
|
|
258
|
-
| `64 * 1024` | | **65,662 bytes** | 126 |
|
|
201
|
+
The same stack passes the unmodified `@statewalker/webrun-streams-conformance` L0–L6 suite over a
|
|
202
|
+
byte pipe, both unlimited and with frames capped at 64 KiB (`tests/conformance-bytes.test.ts`).
|
|
203
|
+
Read that narrowly: an in-process pipe covers this codec's own contract, including chunking. It
|
|
204
|
+
covers none of what a real transport adds — message-size ceilings and what happens past them,
|
|
205
|
+
backpressure, reconnection, close codes. Each real transport needs its own run.
|
|
206
|
+
|
|
207
|
+
## Internals
|
|
208
|
+
|
|
209
|
+
### `maxMessageSize` bounds the payload, not the frame
|
|
210
|
+
|
|
211
|
+
**Set `maxMessageSize` at least 256 bytes below the transport's hard limit.** `duplexOverPort`
|
|
212
|
+
splits the *payload* to `maxMessageSize`. The framing — the chunk wrapper, `callPort`'s
|
|
213
|
+
`{type, channelName, callId, params}`, the mux's `{type, id, payload}`, then this codec — is added
|
|
214
|
+
on top afterwards. Over `msgpackCodec` that overhead is `87 + len(callId)` bytes and it is not
|
|
215
|
+
constant: `callId` is `` `call-${Date.now()}-${String(Math.random()).substring(2)}` ``, 31–40
|
|
216
|
+
characters per chunk because `Math.random()` drops trailing zeros; the port id's integer width adds
|
|
217
|
+
0–4; the channel name `"out"` adds 1 over `"in"`; a chunk of 64 KiB or more adds 2 as the `bin`
|
|
218
|
+
header widens. Summing the terms gives a modelled ceiling of 134 bytes. The overheads actually
|
|
219
|
+
observed span 123–128 bytes.
|
|
259
220
|
|
|
260
|
-
|
|
261
|
-
chunk** — and a transport that silently drops an oversized message (LiveKit does; the body arrives
|
|
262
|
-
as zero bytes with no error on either side) gives you no signal at all. Set it to
|
|
263
|
-
`limit - 256` and the arithmetic stops mattering.
|
|
221
|
+
Measured with a 512 KiB body through the stack in the example above:
|
|
264
222
|
|
|
265
|
-
|
|
266
|
-
|
|
223
|
+
| `maxMessageSize` | intent | largest frame posted | overhead |
|
|
224
|
+
| --- | --- | --- | --- |
|
|
225
|
+
| `16 * 1024` | an `RTCDataChannel`'s conservative ceiling | 16,508 bytes | 124 |
|
|
226
|
+
| `12 * 1024` | LiveKit's safe packet size | 12,413 bytes | 125 |
|
|
227
|
+
| `64 * 1024` | | 65,662 bytes | 126 |
|
|
228
|
+
|
|
229
|
+
The 128-byte maximum came from a 64 KiB cap in the capped conformance run (a 65,664-byte frame over
|
|
230
|
+
a 10 MiB body). Setting `maxMessageSize` to the transport's hard limit overruns it on the first
|
|
231
|
+
full-size chunk, and a transport that silently drops oversized messages gives no signal. On LiveKit
|
|
232
|
+
the body arrives as zero bytes with no error on either side. `limit - 256` covers every measured
|
|
233
|
+
case with room to spare.
|
|
267
234
|
|
|
268
|
-
|
|
235
|
+
### Two ways `msgpackCodec` differs from `structuredCodec`
|
|
269
236
|
|
|
270
237
|
**The transfer list is ignored.** `PortCodec.post` receives an optional `Transferable[]`;
|
|
271
|
-
`msgpackCodec` drops it. After encoding, the payload is
|
|
272
|
-
|
|
273
|
-
buffers as transferables would detach buffers the caller still owns. `structuredCodec` forwards the
|
|
274
|
-
list, because there the objects themselves cross.
|
|
275
|
-
|
|
276
|
-
**msgpack drops object keys whose value is explicitly `undefined`**; structured clone keeps them. So
|
|
277
|
-
`{ result: undefined }` arrives as `{}` over this codec and as `{ result: undefined }` over
|
|
278
|
-
`structuredCodec`. Nothing `webrun-rpc`'s layer 2 sends depends on the difference — every wire shape
|
|
279
|
-
it produces is pinned against both codecs in `tests/codec-equivalence.test.ts` — and per **spec
|
|
280
|
-
D16** it must not come to. If you build a payload where `"key" in obj` means something different
|
|
281
|
-
from `obj.key === undefined`, it will not survive this codec.
|
|
282
|
-
|
|
283
|
-
D16's reach has one open edge, worth knowing before you put arbitrary application errors on a byte
|
|
284
|
-
transport: `serializeError` copies **every own enumerable property** off a thrown `Error` onto the
|
|
285
|
-
wire. Whether the resulting `error` payload is msgpack-expressible therefore depends on what your
|
|
286
|
-
code throws — a `cause` holding a `Map` or a class instance collapses to `{}`, and a circular
|
|
287
|
-
reference makes `serialize` throw.
|
|
288
|
-
|
|
289
|
-
## What `deserialize` refuses, and how
|
|
290
|
-
|
|
291
|
-
`deserialize` throws on input that is not MessagePack; it never logs. Specifically:
|
|
292
|
-
|
|
293
|
-
- **Truncated input** — any read that would run past the end — throws a `RangeError`
|
|
294
|
-
(`Insufficient data: …`). Every proper prefix of a valid encoding is refused this way, so a cut
|
|
295
|
-
integer can no longer come back as `NaN`, nor a cut `bin` as a shorter array.
|
|
296
|
-
- **The never-used type byte `0xc1`**, an empty input, a timestamp extension of an unknown size,
|
|
297
|
-
and a non-byte argument throw an `Error`.
|
|
298
|
-
- **Malformed UTF-8 inside a string is not an error**: each malformed sequence decodes to U+FFFD,
|
|
299
|
-
exactly as `TextDecoder` does, and the values after the string are read normally. Overlong
|
|
300
|
-
forms are malformed — `C0 AF` does not decode to `/`.
|
|
301
|
-
- **Bytes after the first document are ignored** unless `{ multiple: true }` asks for all of them.
|
|
302
|
-
|
|
303
|
-
`msgpackCodec` turns every one of these throws into a dropped message. (Before this package
|
|
304
|
-
carried its own implementation, `@ygoe/msgpack` printed the whole buffer with `console.debug` on
|
|
305
|
-
some truncated input before throwing. That is gone.)
|
|
306
|
-
|
|
307
|
-
## The value model
|
|
308
|
-
|
|
309
|
-
What each JavaScript value becomes on the wire, and what comes back:
|
|
238
|
+
`msgpackCodec` drops it. After encoding, the payload is inside the bytes, so there is nothing left
|
|
239
|
+
to hand over, and transferring the caller's buffers would detach buffers the caller still owns.
|
|
310
240
|
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
| any other number (fraction, beyond ±2⁵³, `NaN`, `±Infinity`) | float 64 | `number` |
|
|
317
|
-
| `string` | str, UTF-8; a lone surrogate is written as U+FFFD, as `TextEncoder` does | `string` |
|
|
318
|
-
| `Uint8Array`, `Uint8ClampedArray` | bin | `Uint8Array` — a **view into the input**, not a copy |
|
|
319
|
-
| other typed arrays (`Float32Array`, `Int16Array`, …) | array of numbers | `number[]` |
|
|
320
|
-
| `Array` | array | `unknown[]` |
|
|
321
|
-
| `Date` | timestamp extension (type -1), 32/64/96-bit as the instant needs | `Date`, floored to the millisecond; an invalid `Date` throws |
|
|
322
|
-
| any other object | map of its **own** enumerable string keys | plain object; every key, `__proto__` included, is an own property |
|
|
323
|
-
| `bigint`, `function`, `symbol` | — throws, unless `invalidTypeReplacement` supplies a stand-in | |
|
|
241
|
+
**Object keys whose value is `undefined` are dropped**; structured clone keeps them.
|
|
242
|
+
`{ result: undefined }` arrives as `{}`. Nothing `webrun-rpc`'s stream and RPC layers send depends
|
|
243
|
+
on the difference: every wire shape they produce is pinned against both codecs in
|
|
244
|
+
`tests/codec-equivalence.test.ts`. A payload where `"key" in obj` means something different from
|
|
245
|
+
`obj.key === undefined` will not survive this codec.
|
|
324
246
|
|
|
325
|
-
|
|
326
|
-
`
|
|
327
|
-
|
|
328
|
-
`{ type, data }` with `type` as the unsigned byte (so type `-2` reads as `254`). A `Map` or `Set`
|
|
329
|
-
has no own enumerable keys and is written as an empty map.
|
|
247
|
+
One edge: `webrun-streams`' `serializeError` copies every own enumerable property of a thrown
|
|
248
|
+
`Error` onto the wire. A `cause` holding a `Map` or a class instance collapses to `{}`, and a
|
|
249
|
+
circular reference makes `serialize` throw.
|
|
330
250
|
|
|
331
|
-
|
|
251
|
+
### What `msgpackCodec.read` accepts, and why it never throws
|
|
332
252
|
|
|
333
|
-
|
|
253
|
+
It accepts a `Uint8Array`, an `ArrayBuffer`, or any `ArrayBufferView` (a `DataView` included,
|
|
254
|
+
offset and length honoured). Everything else returns `undefined`, which the multiplexer treats as
|
|
255
|
+
"not ours": a non-byte value, empty bytes, malformed MessagePack, and valid MessagePack that is not
|
|
256
|
+
a `PortEnvelope` (`id` must be a non-negative integer, `type` one of `open` / `message` / `close`).
|
|
257
|
+
A shared transport may carry other traffic, and the multiplexer must not mistake it for an
|
|
258
|
+
envelope.
|
|
259
|
+
|
|
260
|
+
Refusal is a dropped message, never a throw. A throw here would escape inside the raw port's own
|
|
261
|
+
listener, outside any consumer's reach, and one hostile frame could take the multiplexer down.
|
|
262
|
+
`read` can still throw for three inputs, all in the byte-shape check before the `try`: a detached
|
|
263
|
+
`ArrayBuffer`, a `DataView` over a detached buffer, and a `Proxy` with a throwing
|
|
264
|
+
`getPrototypeOf`. None can arrive over a wire.
|
|
265
|
+
|
|
266
|
+
### Stream frame layout and decoder
|
|
334
267
|
|
|
335
268
|
```
|
|
336
269
|
┌──────────────┬────────────────────────────┐
|
|
@@ -339,141 +272,143 @@ has no own enumerable keys and is written as an empty map.
|
|
|
339
272
|
└──────────────┴────────────────────────────┘
|
|
340
273
|
```
|
|
341
274
|
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
message and prefixes nothing.
|
|
346
|
-
|
|
347
|
-
### Decoder state machine
|
|
348
|
-
|
|
349
|
-
The decoder keeps a rolling `Uint8Array` buffer. Each incoming chunk is appended (single allocation per chunk); then:
|
|
350
|
-
|
|
351
|
-
1. If buffer is shorter than 4 bytes — wait for more.
|
|
352
|
-
2. Read the 32-bit BE length.
|
|
353
|
-
3. If buffer doesn't hold `4 + length` bytes — wait for more.
|
|
354
|
-
4. Copy the payload out, `deserialize` it, yield.
|
|
355
|
-
5. Advance the buffer past this frame; repeat step 1.
|
|
356
|
-
|
|
357
|
-
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.
|
|
358
|
-
|
|
359
|
-
### What `msgpackCodec.read` accepts, and what it refuses
|
|
360
|
-
|
|
361
|
-
It accepts whatever byte shape a transport pump hands over — a `Uint8Array`, a bare `ArrayBuffer`,
|
|
362
|
-
or any `ArrayBufferView` (a `DataView` included, offset and length honoured). Everything else is
|
|
363
|
-
refused by returning `undefined`: a non-byte value, empty bytes, malformed msgpack, and well-formed
|
|
364
|
-
msgpack that decodes to something that is not a `PortEnvelope` (the `id` must be a non-negative
|
|
365
|
-
integer and the `type` one of `open` / `message` / `close`). A shared transport carries traffic that
|
|
366
|
-
is not ours, and layer 1 must not mistake it for an envelope.
|
|
367
|
-
|
|
368
|
-
Refusal is always a dropped message, never a throw: a throw here would escape inside the raw port's
|
|
369
|
-
own listener, outside any consumer's reach, and would let one hostile frame take the multiplexer
|
|
370
|
-
down.
|
|
275
|
+
Big-endian 32-bit length, so at most 2³²−1 bytes per frame. A frame is built whole: one
|
|
276
|
+
`serialize` call per value, no fragmentation. This layout belongs to the stream codec only;
|
|
277
|
+
`msgpackCodec` prefixes nothing.
|
|
371
278
|
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
279
|
+
The decoder keeps a rolling buffer. Each chunk is appended (one allocation per chunk), then: if
|
|
280
|
+
fewer than 4 bytes are buffered, wait; read the length; if `4 + length` bytes are not buffered,
|
|
281
|
+
wait; copy the payload out, `deserialize` it, yield; advance and repeat. Zero-length chunks are
|
|
282
|
+
harmless. A truncated trailing frame stays in the buffer and the iteration ends without yielding
|
|
283
|
+
it, so a caller detects truncation by comparing the count received with the count expected.
|
|
377
284
|
|
|
378
|
-
###
|
|
285
|
+
### Float32: no per-element conversion, but not zero-copy
|
|
379
286
|
|
|
380
|
-
`encodeFloat32Arrays`
|
|
287
|
+
`encodeFloat32Arrays` wraps the array's bytes in a `Uint8Array` view and serialises it as a `bin`.
|
|
288
|
+
`decodeFloat32Arrays` reinterprets the decoded bytes as a `Float32Array`, copying first when the
|
|
289
|
+
`byteOffset` is not 4-byte aligned. In practice the copy always happens: the decoded `bin` is a
|
|
290
|
+
view starting after a 2-, 3- or 5-byte header, never a multiple of 4. `serialize` and frame
|
|
291
|
+
assembly also copy on the way out. Element type is strictly Float32; there is no negotiation.
|
|
381
292
|
|
|
382
|
-
|
|
383
|
-
`serialize` copies the view into its output, and the frame is assembled by another copy. The
|
|
384
|
-
decoded `bin` is a view into the payload, starting just after its 2-, 3- or 5-byte header, and
|
|
385
|
-
none of those offsets is a multiple of 4. So "no per-element conversion" is the whole claim, not
|
|
386
|
-
"zero-copy".
|
|
293
|
+
### Send the view `serialize` returns, not its buffer
|
|
387
294
|
|
|
388
|
-
|
|
295
|
+
`serialize` returns a view over an internal buffer that is not trimmed: a 10 MiB envelope measures
|
|
296
|
+
about 10,485,800 bytes over a 16,777,216-byte `ArrayBuffer`. A transport pump that sends
|
|
297
|
+
`frame.buffer` instead of `frame` puts about 6 MiB of trailing zeros on the wire per message.
|
|
389
298
|
|
|
390
|
-
`
|
|
391
|
-
measures ~10,485,800 bytes over a 16,777,216-byte `ArrayBuffer` (~1.6× slack, `byteOffset` 0). Send
|
|
392
|
-
the view. A pump that reaches for `frame.buffer` instead would put ~6 MiB of trailing zeros on the
|
|
393
|
-
wire per message.
|
|
299
|
+
### What `deserialize` refuses, and how
|
|
394
300
|
|
|
395
|
-
|
|
301
|
+
`deserialize` throws on input that is not MessagePack, and never logs:
|
|
396
302
|
|
|
397
|
-
-
|
|
398
|
-
|
|
303
|
+
- Truncated input — any read past the end — throws a `RangeError`:
|
|
304
|
+
`Insufficient data: N byte(s) needed at index I in the MessagePack binary data (length L).`
|
|
305
|
+
Every proper prefix of a valid encoding is refused this way.
|
|
306
|
+
- `Invalid byte code 0xc1 found.` for the never-used type byte.
|
|
307
|
+
- `Invalid argument: The byte array to deserialize is empty.` for empty input.
|
|
308
|
+
- `Invalid argument type: Expected a byte array (Array or Uint8Array) to deserialize.` for a
|
|
309
|
+
non-byte argument.
|
|
310
|
+
- `Invalid data length for a date value.` for a timestamp extension of unknown size.
|
|
311
|
+
- Malformed UTF-8 inside a string is not an error: each bad sequence decodes to U+FFFD, as
|
|
312
|
+
`TextDecoder` does. Overlong forms are malformed, so `C0 AF` does not decode to `/`.
|
|
313
|
+
- Bytes after the first document are ignored unless `{ multiple: true }` is passed.
|
|
399
314
|
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
315
|
+
`serialize` throws `Invalid argument type: The type '<type>' cannot be serialized.` for a
|
|
316
|
+
`bigint`, function or symbol when `invalidTypeReplacement` is not set. A falsy replacement (`null`,
|
|
317
|
+
`0`, `false`, `""`) counts as not set and still throws; to write nil, pass a function such as
|
|
318
|
+
`() => null`. It also throws `Invalid argument: an
|
|
319
|
+
invalid Date cannot be serialized.` for an invalid `Date`, and `Invalid argument type: Expected an
|
|
320
|
+
Array to serialize multiple values.` when `multiple` is set on a non-array.
|
|
403
321
|
|
|
404
|
-
|
|
322
|
+
`msgpackCodec` turns every `deserialize` throw into a dropped message.
|
|
405
323
|
|
|
406
|
-
|
|
407
|
-
- `decodeMsgpack` allocates one `Uint8Array` per incoming chunk for the `concat`; long streams with many tiny chunks may benefit from a batched source upstream.
|
|
408
|
-
- `Float32Array` codec is strictly `Float32` — no element-size negotiation.
|
|
409
|
-
- `msgpackCodec` carries no length prefix, so it is **unusable** on a transport without message
|
|
410
|
-
boundaries. Use the stream codec there.
|
|
324
|
+
### The value model
|
|
411
325
|
|
|
412
|
-
|
|
326
|
+
| Written | As | Read back as |
|
|
327
|
+
| --- | --- | --- |
|
|
328
|
+
| `null`, `undefined` | nil | `null`; an object key whose value is `undefined` is dropped |
|
|
329
|
+
| `boolean` | bool | `boolean` |
|
|
330
|
+
| safe integer | narrowest int / uint | `number`; `-0` is written as `0` |
|
|
331
|
+
| any other number (fraction, beyond ±2⁵³, `NaN`, `±Infinity`) | float 64 | `number` |
|
|
332
|
+
| `string` | str, UTF-8; a lone surrogate is written as U+FFFD | `string` |
|
|
333
|
+
| `Uint8Array`, `Uint8ClampedArray` | bin | `Uint8Array`, a view into the input, not a copy |
|
|
334
|
+
| other typed arrays | array of numbers | `number[]` |
|
|
335
|
+
| `Array` | array | `unknown[]` |
|
|
336
|
+
| `Date` | timestamp extension (type -1), 32/64/96-bit as needed | `Date`, floored to the millisecond |
|
|
337
|
+
| any other object | map of its own enumerable string keys | plain object; every key, `__proto__` included, is an own property |
|
|
338
|
+
| `bigint`, `function`, `symbol` | throws, unless `invalidTypeReplacement` supplies a stand-in | |
|
|
413
339
|
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
pnpm typecheck:tests # tsc -p tsconfig.tests.json — needs the sibling packages built
|
|
420
|
-
```
|
|
340
|
+
Reading also accepts what other encoders write: float 32, int 64 / uint 64 (as the nearest
|
|
341
|
+
`number`, losing precision beyond 2⁵³), maps with non-string keys (converted with `String()`), and
|
|
342
|
+
extension types other than timestamps, returned as `{ type, data }` with `type` as the unsigned
|
|
343
|
+
byte (type `-2` reads as `254`). A `Map` or `Set` has no own enumerable keys and is written as an
|
|
344
|
+
empty map.
|
|
421
345
|
|
|
422
|
-
|
|
346
|
+
### The MessagePack implementation is a port of msgpack.js
|
|
423
347
|
|
|
424
348
|
`src/msgpack-core.ts` is a TypeScript port of **[msgpack.js](https://github.com/ygoe/msgpack.js)**
|
|
425
|
-
by **Yves Goergen**, © 2019, MIT license
|
|
426
|
-
[`@ygoe/msgpack`](https://www.npmjs.com/package/@ygoe/msgpack), until 0.3.0. Thank you.
|
|
349
|
+
by **Yves Goergen**, © 2019, MIT license. Thank you.
|
|
427
350
|
|
|
428
351
|
It is ported from commit
|
|
429
352
|
[`05733cf`](https://github.com/ygoe/msgpack.js/tree/05733cfb43a2974cf669f0eb8693f43b548bdcd4)
|
|
430
|
-
(2024-04-16) on `master`, not from the npm release 1.0.3
|
|
431
|
-
|
|
353
|
+
(2024-04-16) on `master`, not from the npm release 1.0.3, because `master` carries three fixes
|
|
354
|
+
that change what goes on the wire:
|
|
432
355
|
|
|
433
356
|
- [#34](https://github.com/ygoe/msgpack.js/pull/34): an integer beyond the safe range is written
|
|
434
|
-
as a float 64
|
|
435
|
-
- [#32](https://github.com/ygoe/msgpack.js/issues/32): a positive integer above uint 32
|
|
436
|
-
|
|
357
|
+
as a float 64, not as the maximal uint 64.
|
|
358
|
+
- [#32](https://github.com/ygoe/msgpack.js/issues/32): a positive integer above uint 32 uses the
|
|
359
|
+
uint 64 prefix `0xcf`, not int 64's `0xd3`.
|
|
437
360
|
- [#33](https://github.com/ygoe/msgpack.js/issues/33): a 16–255-byte `bin` gets the one-byte bin 8
|
|
438
|
-
header
|
|
361
|
+
header.
|
|
439
362
|
|
|
440
363
|
The port keeps upstream's structure, comments and error messages. Before any change it was checked
|
|
441
|
-
against upstream
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
replaced the decoded object's prototype — and only own enumerable keys are written, where
|
|
458
|
-
upstream's `for…in` also wrote inherited ones.
|
|
364
|
+
against upstream itself: byte-identical output for 3,000 randomised values, and the same value or
|
|
365
|
+
the same error message for 20,000 random garbage inputs. Each change below is marked
|
|
366
|
+
`Modified from upstream` in the source and was made where a test adopted from another
|
|
367
|
+
implementation failed:
|
|
368
|
+
|
|
369
|
+
1. Truncated input throws a `RangeError` instead of decoding to `NaN` or a short `bin`, and
|
|
370
|
+
nothing is logged (upstream called `console.debug` with the whole input).
|
|
371
|
+
2. Timestamps are floored to the millisecond both ways. Upstream rounded `…:07.999999999Z` up to
|
|
372
|
+
the next second, read pre-1970 instants toward zero, and wrote `new Date(-1002)` as second -1.
|
|
373
|
+
An invalid `Date` throws.
|
|
374
|
+
3. Strings follow the WHATWG Encoding standard: decoding goes through `TextDecoder`, and encoding
|
|
375
|
+
writes a lone surrogate as U+FFFD, where upstream threw on a high one and wrote a low one as
|
|
376
|
+
invalid UTF-8.
|
|
377
|
+
4. A `__proto__` map key is decoded as an own property (upstream's assignment replaced the
|
|
378
|
+
prototype), and only own enumerable keys are written (upstream's `for…in` also wrote inherited
|
|
379
|
+
ones).
|
|
459
380
|
5. TypeScript types, `unknown` in place of `any`, and the bounds `0xffffffffffffffff` /
|
|
460
381
|
`0x7fffffffffffffff` spelled `2 ** 64` / `2 ** 63` (the same doubles).
|
|
461
382
|
|
|
462
|
-
|
|
383
|
+
Besides this package's own tests, the suite runs upstream's test page ported to vitest
|
|
384
|
+
(`tests/msgpack-core.upstream.test.ts`);
|
|
385
|
+
[kawanet/msgpack-test-suite](https://github.com/kawanet/msgpack-test-suite) (MIT, © Yusuke
|
|
386
|
+
Kawasaki), vendored (`tests/msgpack-core.test-suite.test.ts`); and cases adopted from
|
|
387
|
+
[msgpack/msgpack-javascript](https://github.com/msgpack/msgpack-javascript) (ISC, © The
|
|
388
|
+
MessagePack Community) and [kriszyp/msgpackr](https://github.com/kriszyp/msgpackr) (MIT, © Kris
|
|
389
|
+
Zyp), with msgpackr's sample documents vendored (`tests/msgpack-core.adopted.test.ts`). Sources,
|
|
390
|
+
commits and licenses of the vendored fixtures are in
|
|
391
|
+
[`tests/fixtures/README.md`](./tests/fixtures/README.md). Outside the suite, the implementation was
|
|
392
|
+
checked for interoperability with `@msgpack/msgpack` 3.1.3 and `msgpackr` 2.1.0: 2,000 random
|
|
393
|
+
values encoded by each side decode identically on the other, in both directions.
|
|
394
|
+
|
|
395
|
+
### Dependencies
|
|
396
|
+
|
|
397
|
+
- Runtime: none. `dist/index.js` imports nothing.
|
|
398
|
+
- `@statewalker/webrun-rpc` (`dependency`, type-only): `PortCodec`, `PortEnvelope`.
|
|
399
|
+
- Dev only: `@statewalker/webrun-streams` and `@statewalker/webrun-streams-conformance` for the
|
|
400
|
+
conformance run; TypeScript, vitest, rolldown, rimraf, Biome.
|
|
463
401
|
|
|
464
|
-
|
|
465
|
-
- [kawanet/msgpack-test-suite](https://github.com/kawanet/msgpack-test-suite) (MIT, © Yusuke
|
|
466
|
-
Kawasaki), vendored — `tests/msgpack-core.test-suite.test.ts`;
|
|
467
|
-
- cases adopted from [msgpack/msgpack-javascript](https://github.com/msgpack/msgpack-javascript)
|
|
468
|
-
(ISC, © The MessagePack Community) and [kriszyp/msgpackr](https://github.com/kriszyp/msgpackr)
|
|
469
|
-
(MIT, © Kris Zyp), with msgpackr's sample documents vendored —
|
|
470
|
-
`tests/msgpack-core.adopted.test.ts`.
|
|
402
|
+
### Scripts
|
|
471
403
|
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
404
|
+
```sh
|
|
405
|
+
pnpm test # vitest run
|
|
406
|
+
pnpm run build # rolldown + tsc --emitDeclarationOnly
|
|
407
|
+
pnpm lint # biome check src tests
|
|
408
|
+
pnpm typecheck # tsc --noEmit
|
|
409
|
+
pnpm typecheck:tests # tsc -p tsconfig.tests.json; needs the workspace packages built
|
|
410
|
+
```
|
|
476
411
|
|
|
477
412
|
## License
|
|
478
413
|
|
|
479
|
-
MIT © statewalker
|
|
414
|
+
MIT © statewalker. See [LICENSE](./LICENSE), which also carries msgpack.js's MIT notice.
|
package/package.json
CHANGED
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@statewalker/webrun-msgpack",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"private": false,
|
|
5
5
|
"type": "module",
|
|
6
6
|
"description": "MessagePack for the wire, with no runtime dependencies: serialize/deserialize (a TypeScript port of msgpack.js), a length-prefixed stream codec for iterables (values and Float32Arrays), and msgpackCodec, a PortCodec that carries webrun-rpc port envelopes over a byte transport.",
|
|
7
|
-
"
|
|
7
|
+
"bugs": {
|
|
8
|
+
"url": "https://github.com/statewalker/webrun-wire/issues"
|
|
9
|
+
},
|
|
10
|
+
"homepage": "https://github.com/statewalker/webrun-wire/tree/main/packages/webrun-msgpack#readme",
|
|
8
11
|
"author": {
|
|
9
12
|
"name": "Mikhail Kotelnikov",
|
|
10
13
|
"email": "mikhail.kotelnikov@gmail.com"
|
|
@@ -12,7 +15,7 @@
|
|
|
12
15
|
"license": "MIT",
|
|
13
16
|
"repository": {
|
|
14
17
|
"type": "git",
|
|
15
|
-
"url": "git
|
|
18
|
+
"url": "git+https://github.com/statewalker/webrun-wire.git",
|
|
16
19
|
"directory": "packages/webrun-msgpack"
|
|
17
20
|
},
|
|
18
21
|
"exports": {
|
|
@@ -27,7 +30,7 @@
|
|
|
27
30
|
"src"
|
|
28
31
|
],
|
|
29
32
|
"dependencies": {
|
|
30
|
-
"@statewalker/webrun-rpc": "^0.
|
|
33
|
+
"@statewalker/webrun-rpc": "^0.5.0"
|
|
31
34
|
},
|
|
32
35
|
"devDependencies": {
|
|
33
36
|
"@biomejs/biome": "^2.5.15",
|
|
@@ -35,8 +38,8 @@
|
|
|
35
38
|
"rolldown": "^1.2.12",
|
|
36
39
|
"typescript": "^7.0.2",
|
|
37
40
|
"vitest": "^5.0.3",
|
|
38
|
-
"@statewalker/webrun-streams": "^0.2
|
|
39
|
-
"@statewalker/webrun-streams
|
|
41
|
+
"@statewalker/webrun-streams-conformance": "^0.3.2",
|
|
42
|
+
"@statewalker/webrun-streams": "^0.2.2"
|
|
40
43
|
},
|
|
41
44
|
"sideEffects": false,
|
|
42
45
|
"publishConfig": {
|