@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 +232 -9
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +133 -0
- package/dist/msgpack.d.ts +20 -0
- package/dist/msgpack.d.ts.map +1 -0
- package/dist/port-codec.d.ts +22 -0
- package/dist/port-codec.d.ts.map +1 -0
- package/package.json +16 -8
- package/src/index.ts +1 -0
- package/src/port-codec.ts +68 -0
- package/LICENSE +0 -21
- package/dist/index.d.mts +0 -23
- package/dist/index.d.mts.map +0 -1
- package/dist/index.mjs +0 -76
- package/dist/index.mjs.map +0 -1
package/README.md
CHANGED
|
@@ -1,8 +1,31 @@
|
|
|
1
1
|
# @statewalker/webrun-msgpack
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
MessagePack on the wire, in **two distinct shapes**:
|
|
4
4
|
|
|
5
|
-
|
|
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
|
-
##
|
|
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
|
-
|
|
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,
|
|
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
|
|
131
|
-
pnpm run build
|
|
132
|
-
pnpm lint
|
|
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).
|
package/dist/index.d.ts
ADDED
|
@@ -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.
|
|
3
|
+
"version": "0.2.2",
|
|
4
4
|
"private": false,
|
|
5
5
|
"type": "module",
|
|
6
|
-
"description": "
|
|
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
|
-
".":
|
|
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
|
-
"
|
|
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": "
|
|
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
|
|
53
|
+
"lint": "biome check src tests",
|
|
46
54
|
"format": "biome format --write ."
|
|
47
55
|
}
|
|
48
56
|
}
|
package/src/index.ts
CHANGED
|
@@ -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
|
package/dist/index.d.mts.map
DELETED
|
@@ -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
|
package/dist/index.mjs.map
DELETED
|
@@ -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"}
|