@lesomnus/grpc-dgram 0.0.1 → 0.1.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/CHANGELOG.md ADDED
@@ -0,0 +1,43 @@
1
+ # Changelog
2
+
3
+ `@lesomnus/grpc-dgram` is the TypeScript port of gRPC-dgram. The wire
4
+ protocol it speaks is unversioned by design (`docs/PROTOCOL.md`, §10.6); a
5
+ package version is a cut of that text at one date, and a minor bump while the
6
+ major is 0 means the wire broke. Both ends of a channel must be deployed
7
+ together.
8
+
9
+ ## 0.1.0 — 2026-09-12
10
+
11
+ **Wire, breaking against 0.0.1** — a peer on 0.0.1 cannot talk to one on
12
+ 0.1.0:
13
+
14
+ - `Metadata` is an ordered list of entries, `repeated Entry { key, values }`,
15
+ emitted in ascending key order; a receiver merges a repeated key (§11).
16
+ - `Frame.conn_window` (field 18) advertises the connection window: the client
17
+ on every OPEN, the server on every H and T. The assumed-then-raised window
18
+ is gone, with everything that existed only for it (§4.2.1).
19
+ - The wire unit is spelled `Envelope`; the adapter seam is `EnvelopeSender`
20
+ (`sendFrames`) beside `FrameHandler`, and batching is a seam, not a
21
+ component (§4.1).
22
+ - Field 6 is unassigned and held for `ack`; a breaking generation of the wire
23
+ sets flag bit 64 on the first frame of every call (§10.6).
24
+ - A proto `string` field that is not valid UTF-8 makes the envelope
25
+ undecodable and dropped whole, as in the Go core (§5, §11).
26
+
27
+ **Behaviour**
28
+
29
+ - The per-peer connection window (`limits.maxPeerWindow`, §4.2.1): one ledger
30
+ per peer, `sid = 0` grants, overrun fails only the offending call.
31
+ - A call RESET before the `Conn` has locked to a server incarnation refunds
32
+ the connection credit its frames took (§4.2.1 *Sending*).
33
+ - Interceptors: `interceptors: [...]` on `ConnOptions` and `ServerOptions`,
34
+ unary and stream, chained in gRPC's order.
35
+ - `ProtocolStats` and `Counters` (§14), so a browser client reports gaps.
36
+ - WebTransport datagrams: `@lesomnus/grpc-dgram/transport/webtransport`.
37
+ - The wasm bridge: `dial({ entryPoint })` for a second server in one
38
+ instance, a per-dial `readyTimeoutMs`, and the timers a Go program leaves
39
+ pending are cancelled when it exits.
40
+
41
+ ## 0.0.1 — 2026-08-08
42
+
43
+ First publish. Deprecated on 2026-09-12: it speaks a wire that 0.1.0 broke.
package/README.md CHANGED
@@ -1,12 +1,12 @@
1
1
  # @lesomnus/grpc-dgram
2
2
 
3
3
  TypeScript port of [gRPC-dgram](../): gRPC-style RPC over unreliable datagram
4
- channels, implementing the **dRPC wire protocol v1.1** (`../docs/PROTOCOL.md`).
4
+ channels, implementing the **dRPC wire protocol** (`../docs/PROTOCOL.md`).
5
5
  Wire-compatible with the Go implementation — the §5 golden byte vectors are
6
6
  shared between the two test suites — so a TS client interoperates with a Go
7
7
  server and vice versa.
8
8
 
9
- - **Zero runtime dependencies.** The three wire messages (`Frame`, `Envelop`,
9
+ - **Zero runtime dependencies.** The three wire messages (`Frame`, `Envelope`,
10
10
  `Metadata`) are hand-encoded; user payloads go through pluggable
11
11
  per-method marshallers (protobuf-es, JSON, anything that produces bytes).
12
12
  A **protobuf-es binding** (`@lesomnus/grpc-dgram/transport/protobuf-es`, optional peer
@@ -17,22 +17,37 @@ server and vice versa.
17
17
  mode machinery: seq windows and dedup, epoch/`peer_epoch` incarnation
18
18
  isolation, control-frame retransmission, tombstones + aged watermark,
19
19
  PING/probe liveness, and the §15 resource caps. On a reliable transport all
20
- timers are off, sequencing is strict fail-loud (§10.6), and **per-stream
21
- flow control** (§4.2.1) keeps one slow consumer from stalling the other
22
- calls on the channel — the browser case where a blocked receive path would
23
- otherwise wedge the whole event loop.
24
- - **v1.1 surface.** Binary metadata (`-bin` keys carry arbitrary octets;
20
+ timers are off, sequencing is strict fail-loud (§10.6), and **flow
21
+ control** (§4.2.1) keeps one slow consumer from stalling the other calls on
22
+ the channel — the browser case where a blocked receive path would otherwise
23
+ wedge the whole event loop: a credit window per stream, and beside it a
24
+ connection window per peer (`limits.maxPeerWindow`, 1024 messages by
25
+ default, advertised on every OPEN by a `Conn` and on every H and T by a
26
+ `Server`) that bounds what one peer can pin across *all* of its calls, as
27
+ HTTP/2's does.
28
+ - **gRPC-fidelity surface.** Binary metadata (`-bin` keys carry arbitrary octets;
25
29
  base64 at the TS API, raw bytes on the wire), rich status details on the
26
30
  terminal frame, per-message compression (`gzip` via the platform's
27
31
  `CompressionStream`, never expanding a payload), per-call recv/send size
28
32
  caps, and the shape/modifier flag split (§7.1) — an unimplemented flag bit
29
33
  fails the call instead of silently corrupting or gapping it.
30
34
  - **WebSocket adapter** (`@lesomnus/grpc-dgram/transport/websocket`), the TS
31
- twin of the Go `transport/gorilla` adapter: reliable mode, one Envelop per
35
+ twin of the Go `transport/gorilla` adapter: reliable mode, one Envelope per
32
36
  message, and the §4.5 teardown duty carried by `onclose`/`onerror` plus a
33
37
  keepalive — browser-safe (WhatWG `WebSocket`, `binaryType='arraybuffer'`).
34
38
  `dialWebSocket(url)` is a `Conn` in one line; `new WebSocketTransport(ws)` is
35
39
  the path for a socket you brought yourself.
40
+ - **WebTransport datagram adapter** (`@lesomnus/grpc-dgram/transport/webtransport`),
41
+ the TS twin of the Go `transport/webtransport` adapter: unreliable mode over
42
+ the session's datagrams, one Envelope per datagram — the browser's datagram
43
+ channel with no signaling, a URL and nothing else. `dialWebTransport(url)` is
44
+ a `Conn` in one line (`https://` only; `serverCertificateHashes` pins a
45
+ development certificate), `new WebTransportDatagramTransport(wt)` the path
46
+ for a session you brought. The size ceiling follows the session's own
47
+ `datagrams.maxDatagramSize` (1200 B when it reports none), and the §4.5
48
+ teardown comes from the session — `closed` settling, `ready` rejecting — not
49
+ the read loop. Client only: Node has no `WebTransport`, so the server side is
50
+ the Go module.
36
51
  - **Message-port adapter** (`@lesomnus/grpc-dgram/transport/port`), the TS twin
37
52
  of the Go `transport/jsport` adapter: `PortTransport`/`PortGateway` over
38
53
  anything with `postMessage` and a `message` event — a `MessagePort`, a
@@ -72,7 +87,8 @@ server and vice versa.
72
87
 
73
88
  **Getting a connection.** Four cases, and the verb says which:
74
89
  `new Conn(new XTransport(ch), opts)` when you already hold the channel;
75
- `dialUdp` / `dialWebSocket` / `dialWorker` when the library should make it —
90
+ `dialUdp` / `dialWebSocket` / `dialWebTransport` / `dialWorker` when the
91
+ library should make it —
76
92
  each hands back a `Conn`, and takes one options bag, since `ConnOptions` and an
77
93
  adapter's own options share no key; `open(app)` when there is no peer yet,
78
94
  which returns a `Sock` whose `dial()` is the connection; and, on the serving
@@ -204,34 +220,58 @@ else maps to `UNKNOWN`).
204
220
  | `drpc.ErrMessageTooLarge` | `MessageTooLargeError` (adapters throw it or set it as `cause`) |
205
221
  | `NewPeerContext` / `NewReliableContext` | `FrameContext { peer, reliable, signal }` argument to `handle` |
206
222
  | mutexes + atomics | none needed: state transitions are synchronous between `await` points |
207
-
208
- Deliberately not ported (yet): client/server interceptors, the observability
209
- surface (Go's `stats.Handler` + `ProtocolStats`), and `Envelop` batching
210
- (`Coalescer` deferred to M8 in Go; every envelop carries one frame, as the
211
- shipped Go adapters do).
223
+ | `WithProtocolStats(obs)` (repeatable) | `protocolStats: obs` or `protocolStats: [obs, …]` on `ConnOptions` / `ServerOptions`; `Counters.observe` is the ready-made observer |
224
+ | `ProtocolEventKind` (`EventSkipped` … `EventFlowStall`, `EventFlowResume`, `EventPeerFlowStall`, `EventPeerFlowResume`) | `ProtocolEventKind`, the strings Go's `String()` fixes (`'skipped'` … `'flow-stall'`, `'flow-resume'`, `'peer-flow-stall'`, `'peer-flow-resume'`); `Counters` keeps `flowStall` / `flowResume` and `peerFlowStall` / `peerFlowResume` apart, as Go does |
225
+ | `WithLimits(Limits{MaxPeerWindow: n})` (the §4.2.1 connection window) | `limits: { maxPeerWindow: n }` on `ConnOptions` / `ServerOptions` — advertised as `Frame.connWindow` on every OPEN (`Conn`) and on every H and T (`Server`), the peer honouring the first it hears; floored at `W_CONN` (1024, exported beside `W_INIT`) because a client streams on that assumption until the server's first H or T; capped at 2³² − 1 — the wire's uint32 — so `Infinity` means that much, not off |
226
+ | `WithChainUnaryInterceptor(…)` / `ChainUnaryInterceptor(…)` and the stream twins | `unaryInterceptors: […]` / `streamInterceptors: […]` on `ConnOptions` / `ServerOptions` — same order (element 0 outermost), `(req, call, next)` shape; see `docs/typescript.md` |
227
+
228
+ Deliberately not ported (yet): the `stats.Handler` bridge (a grpc-go type; the
229
+ drpc half of the observability surface,
230
+ `ProtocolStats`/`Counters`, is ported — `protocolStats` on `ConnOptions` and
231
+ `ServerOptions`, `docs/observability.md`).
232
+
233
+ Batching is the application's policy in both languages and neither ships a
234
+ batcher (`docs/TODO.md` §1); both ship the seam to write one against. Here it
235
+ is `EnvelopeSender`, implemented by every exported adapter class as
236
+ `sendFrames`, with `handle` delegating to it so an override reaches the same
237
+ code the core would. It
238
+ mirrors that class's own `handle`: `sendFrames(frames)` where `handle` takes
239
+ a frame alone, `sendFrames(frames, ctx)` where `handle` takes a
240
+ `FrameContext` — on the gateways the ctx *is* the address, so a batch may
241
+ hold only frames whose contexts name one peer. Subclass the adapter and override `handle`: the core finds
242
+ `reliable()`, `attachConn()` and `close()` structurally, so the prototype
243
+ keeps all three. `src/transport/node-udp/batcher.test.ts` is the worked
244
+ example, per-peer gateway batching included.
212
245
 
213
246
  Receive-path note for browsers: an `RTCDataChannel` cannot pause delivery, so
214
- adapter-level buffering is unavoidable — but since v1.1 the *protocol* paces
247
+ adapter-level buffering is unavoidable — but since per-stream flow control the *protocol* paces
215
248
  the sender instead of the receiver (§4.2.1 flow control), so a slow consumer
216
249
  no longer needs the receive path to block at all, and never stalls the other
217
250
  calls sharing the channel.
218
251
 
219
252
  ## Tests
220
253
 
221
- `pnpm test` — 353 tests mirroring the Go suites: the §5 golden wire vectors
222
- byte-for-byte (including the v1.1 vectors generated from the Go
254
+ `pnpm test` — 575 tests mirroring the Go suites: the §5 golden wire vectors
255
+ byte-for-byte (including the 2026-07-25 vectors generated from the Go
223
256
  implementation), e2e for all four RPC types, the §10 timeout system under
224
257
  deterministic fake-timer loss (blackhole, lost terminals/acks/half-closes,
225
258
  probes, liveness), the §6.5 restart walkthroughs, §15 caps and §4.2 drop
226
- policies, §4.2.1 flow control (advertisement, parking, grants, `T_stall`,
227
- overrun), compression, size caps and binary metadata, each adapter
228
- (WebRTC/WebSocket/Port/UDP/protobuf-es/Connect) next to its source, `open()`
259
+ policies, §4.2.1 flow control — per stream (advertisement, parking, grants,
260
+ `T_stall`, overrun) and per peer (the advertisement on every OPEN / H / T and
261
+ its once-only adoption, the `W_conn` assumption until it arrives, absent =
262
+ off, `sid = 0` grants, the starvation clause, credit returned for every
263
+ non-buffered frame, one stall budget across both windows, the evicted
264
+ sender's stash) — compression, size caps and binary metadata, each adapter
265
+ (WebRTC/WebSocket/WebTransport/Port/UDP/protobuf-es/Connect) next to its
266
+ source, `open()`
229
267
  and the worker it ships against a fake `Go` (`src/wasm/`), and two
230
268
  cross-language conformance tests driving a real Go `drpc.Server`: one over
231
269
  loopback UDP, one a `js/wasm` build of the server loaded into the test process
232
270
  and talked to across a `MessageChannel` — the second being where reliable mode
233
271
  is genuinely reliable rather than asserted, so the §4.2.1 windows are exercised
234
- between the two implementations.
272
+ between the two implementations — per stream, and on `sid = 0` past `W_conn`
273
+ each way across three streams, which completes only because both ends grant
274
+ on the connection window.
235
275
 
236
276
  **Layout.** Unit and per-adapter tests are **co-located** next to their source
237
277
  (`src/wire.test.ts`, `src/transport/webrtc/index.test.ts`, …); cross-cutting