@norskvideo/moq-net 0.1.8 → 0.2.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.
Files changed (286) hide show
  1. package/README.md +2 -2
  2. package/announce.d.ts +7 -0
  3. package/announce.d.ts.map +1 -0
  4. package/announce.js +8 -0
  5. package/announce.js.map +1 -0
  6. package/announced.d.ts +49 -91
  7. package/announced.d.ts.map +1 -1
  8. package/announced.js +21 -156
  9. package/announced.js.map +1 -1
  10. package/bandwidth.d.ts +163 -0
  11. package/bandwidth.d.ts.map +1 -0
  12. package/bandwidth.js +304 -0
  13. package/bandwidth.js.map +1 -0
  14. package/bandwidth_api.d.ts +7 -0
  15. package/bandwidth_api.d.ts.map +1 -0
  16. package/bandwidth_api.js +8 -0
  17. package/bandwidth_api.js.map +1 -0
  18. package/broadcast.d.ts +44 -35
  19. package/broadcast.d.ts.map +1 -1
  20. package/broadcast.js +104 -60
  21. package/broadcast.js.map +1 -1
  22. package/connection/accept.d.ts +16 -1
  23. package/connection/accept.d.ts.map +1 -1
  24. package/connection/accept.js +52 -28
  25. package/connection/accept.js.map +1 -1
  26. package/connection/browser.d.ts.map +1 -1
  27. package/connection/browser.js +9 -7
  28. package/connection/browser.js.map +1 -1
  29. package/connection/connect.d.ts +30 -6
  30. package/connection/connect.d.ts.map +1 -1
  31. package/connection/connect.js +110 -54
  32. package/connection/connect.js.map +1 -1
  33. package/connection/established.d.ts +17 -21
  34. package/connection/established.d.ts.map +1 -1
  35. package/connection/established.js.map +1 -1
  36. package/connection/forward.d.ts +2 -0
  37. package/connection/forward.d.ts.map +1 -0
  38. package/connection/forward.js +173 -0
  39. package/connection/forward.js.map +1 -0
  40. package/connection/handshake.d.ts +1 -0
  41. package/connection/handshake.d.ts.map +1 -1
  42. package/connection/handshake.js +5 -2
  43. package/connection/handshake.js.map +1 -1
  44. package/connection/index.d.ts +5 -5
  45. package/connection/index.d.ts.map +1 -1
  46. package/connection/index.js +4 -5
  47. package/connection/index.js.map +1 -1
  48. package/connection/pool.d.ts +186 -0
  49. package/connection/pool.d.ts.map +1 -0
  50. package/connection/pool.js +361 -0
  51. package/connection/pool.js.map +1 -0
  52. package/connection/reload.d.ts +14 -93
  53. package/connection/reload.d.ts.map +1 -1
  54. package/connection/reload.js +217 -81
  55. package/connection/reload.js.map +1 -1
  56. package/connection/stats.d.ts +3 -26
  57. package/connection/stats.d.ts.map +1 -1
  58. package/connection/stats.js.map +1 -1
  59. package/connection/transport.d.ts +0 -7
  60. package/connection/transport.d.ts.map +1 -1
  61. package/consume.d.ts +1 -43
  62. package/consume.d.ts.map +1 -1
  63. package/consume.js +1 -1
  64. package/consume.js.map +1 -1
  65. package/error.d.ts +180 -29
  66. package/error.d.ts.map +1 -1
  67. package/error.js +331 -16
  68. package/error.js.map +1 -1
  69. package/errors.d.ts +7 -0
  70. package/errors.d.ts.map +1 -0
  71. package/errors.js +8 -0
  72. package/errors.js.map +1 -0
  73. package/group.d.ts +9 -43
  74. package/group.d.ts.map +1 -1
  75. package/group.js +284 -69
  76. package/group.js.map +1 -1
  77. package/hop.d.ts +115 -0
  78. package/hop.d.ts.map +1 -0
  79. package/hop.js +119 -0
  80. package/hop.js.map +1 -0
  81. package/ietf/adapter.d.ts +5 -1
  82. package/ietf/adapter.d.ts.map +1 -1
  83. package/ietf/adapter.js +105 -60
  84. package/ietf/adapter.js.map +1 -1
  85. package/ietf/aliases.d.ts +1 -78
  86. package/ietf/aliases.d.ts.map +1 -1
  87. package/ietf/cluster.d.ts +9 -123
  88. package/ietf/cluster.d.ts.map +1 -1
  89. package/ietf/cluster.js +84 -44
  90. package/ietf/cluster.js.map +1 -1
  91. package/ietf/connection.d.ts +6 -72
  92. package/ietf/connection.d.ts.map +1 -1
  93. package/ietf/connection.js +62 -56
  94. package/ietf/connection.js.map +1 -1
  95. package/ietf/error.d.ts +11 -0
  96. package/ietf/error.d.ts.map +1 -0
  97. package/ietf/error.js +193 -0
  98. package/ietf/error.js.map +1 -0
  99. package/ietf/fetch.d.ts +7 -20
  100. package/ietf/fetch.d.ts.map +1 -1
  101. package/ietf/fetch.js +52 -22
  102. package/ietf/fetch.js.map +1 -1
  103. package/ietf/filter.d.ts +2 -0
  104. package/ietf/filter.d.ts.map +1 -1
  105. package/ietf/filter.js +10 -1
  106. package/ietf/filter.js.map +1 -1
  107. package/ietf/goaway.d.ts.map +1 -1
  108. package/ietf/goaway.js +22 -5
  109. package/ietf/goaway.js.map +1 -1
  110. package/ietf/hidden.d.ts +2 -0
  111. package/ietf/hidden.d.ts.map +1 -0
  112. package/ietf/hidden.js +30 -0
  113. package/ietf/hidden.js.map +1 -0
  114. package/ietf/index.d.ts +2 -0
  115. package/ietf/index.d.ts.map +1 -1
  116. package/ietf/index.js +2 -0
  117. package/ietf/index.js.map +1 -1
  118. package/ietf/object.d.ts +15 -8
  119. package/ietf/object.d.ts.map +1 -1
  120. package/ietf/object.js +51 -36
  121. package/ietf/object.js.map +1 -1
  122. package/ietf/parameters.d.ts +14 -2
  123. package/ietf/parameters.d.ts.map +1 -1
  124. package/ietf/parameters.js +97 -29
  125. package/ietf/parameters.js.map +1 -1
  126. package/ietf/properties.d.ts +1 -0
  127. package/ietf/properties.d.ts.map +1 -1
  128. package/ietf/properties.js +14 -0
  129. package/ietf/properties.js.map +1 -1
  130. package/ietf/publish.d.ts +19 -2
  131. package/ietf/publish.d.ts.map +1 -1
  132. package/ietf/publish.js +40 -6
  133. package/ietf/publish.js.map +1 -1
  134. package/ietf/publish_namespace.d.ts +25 -0
  135. package/ietf/publish_namespace.d.ts.map +1 -1
  136. package/ietf/publish_namespace.js +63 -0
  137. package/ietf/publish_namespace.js.map +1 -1
  138. package/ietf/publisher.d.ts +1 -82
  139. package/ietf/publisher.d.ts.map +1 -1
  140. package/ietf/publisher.js +483 -235
  141. package/ietf/publisher.js.map +1 -1
  142. package/ietf/solicit.d.ts +1 -40
  143. package/ietf/solicit.d.ts.map +1 -1
  144. package/ietf/subscribe.d.ts +8 -6
  145. package/ietf/subscribe.d.ts.map +1 -1
  146. package/ietf/subscribe.js +33 -27
  147. package/ietf/subscribe.js.map +1 -1
  148. package/ietf/subscribe_namespace.d.ts +8 -2
  149. package/ietf/subscribe_namespace.d.ts.map +1 -1
  150. package/ietf/subscribe_namespace.js +18 -8
  151. package/ietf/subscribe_namespace.js.map +1 -1
  152. package/ietf/subscriber.d.ts +1 -65
  153. package/ietf/subscriber.d.ts.map +1 -1
  154. package/ietf/subscriber.js +337 -123
  155. package/ietf/subscriber.js.map +1 -1
  156. package/ietf/token.d.ts +2 -0
  157. package/ietf/token.d.ts.map +1 -0
  158. package/ietf/token.js +99 -0
  159. package/ietf/token.js.map +1 -0
  160. package/ietf/track.d.ts +4 -0
  161. package/ietf/track.d.ts.map +1 -1
  162. package/ietf/track.js +6 -20
  163. package/ietf/track.js.map +1 -1
  164. package/ietf/version.d.ts +12 -1
  165. package/ietf/version.d.ts.map +1 -1
  166. package/ietf/version.js +13 -0
  167. package/ietf/version.js.map +1 -1
  168. package/index.d.ts +12 -7
  169. package/index.d.ts.map +1 -1
  170. package/index.js +10 -5
  171. package/index.js.map +1 -1
  172. package/internal.d.ts +115 -1
  173. package/internal.d.ts.map +1 -1
  174. package/internal.js +108 -0
  175. package/internal.js.map +1 -1
  176. package/lite/announce.d.ts +60 -10
  177. package/lite/announce.d.ts.map +1 -1
  178. package/lite/announce.js +176 -31
  179. package/lite/announce.js.map +1 -1
  180. package/lite/connection.d.ts +9 -59
  181. package/lite/connection.d.ts.map +1 -1
  182. package/lite/connection.js +34 -35
  183. package/lite/connection.js.map +1 -1
  184. package/lite/datagram.d.ts +3 -2
  185. package/lite/datagram.d.ts.map +1 -1
  186. package/lite/datagram.js +7 -8
  187. package/lite/datagram.js.map +1 -1
  188. package/lite/fetch.d.ts +15 -1
  189. package/lite/fetch.d.ts.map +1 -1
  190. package/lite/fetch.js +39 -7
  191. package/lite/fetch.js.map +1 -1
  192. package/lite/goaway.d.ts.map +1 -1
  193. package/lite/goaway.js +7 -1
  194. package/lite/goaway.js.map +1 -1
  195. package/lite/group.d.ts +30 -12
  196. package/lite/group.d.ts.map +1 -1
  197. package/lite/group.js +68 -26
  198. package/lite/group.js.map +1 -1
  199. package/lite/message.d.ts +2 -2
  200. package/lite/message.d.ts.map +1 -1
  201. package/lite/message.js +14 -5
  202. package/lite/message.js.map +1 -1
  203. package/lite/priority.d.ts +1 -61
  204. package/lite/priority.d.ts.map +1 -1
  205. package/lite/priority.js +4 -5
  206. package/lite/priority.js.map +1 -1
  207. package/lite/publisher.d.ts +1 -69
  208. package/lite/publisher.d.ts.map +1 -1
  209. package/lite/publisher.js +630 -262
  210. package/lite/publisher.js.map +1 -1
  211. package/lite/setup.d.ts +8 -8
  212. package/lite/setup.d.ts.map +1 -1
  213. package/lite/setup.js +33 -30
  214. package/lite/setup.js.map +1 -1
  215. package/lite/subscribe.d.ts +71 -17
  216. package/lite/subscribe.d.ts.map +1 -1
  217. package/lite/subscribe.js +205 -53
  218. package/lite/subscribe.js.map +1 -1
  219. package/lite/subscriber.d.ts +15 -56
  220. package/lite/subscriber.d.ts.map +1 -1
  221. package/lite/subscriber.js +451 -247
  222. package/lite/subscriber.js.map +1 -1
  223. package/lite/track.d.ts +4 -10
  224. package/lite/track.d.ts.map +1 -1
  225. package/lite/track.js +34 -29
  226. package/lite/track.js.map +1 -1
  227. package/lite/version.d.ts +42 -6
  228. package/lite/version.d.ts.map +1 -1
  229. package/lite/version.js +126 -10
  230. package/lite/version.js.map +1 -1
  231. package/origin.d.ts +256 -29
  232. package/origin.d.ts.map +1 -1
  233. package/origin.js +1427 -37
  234. package/origin.js.map +1 -1
  235. package/package.json +8 -3
  236. package/path.d.ts +25 -7
  237. package/path.d.ts.map +1 -1
  238. package/path.js +5 -3
  239. package/path.js.map +1 -1
  240. package/stream.d.ts +73 -14
  241. package/stream.d.ts.map +1 -1
  242. package/stream.js +372 -141
  243. package/stream.js.map +1 -1
  244. package/tail.d.ts +18 -0
  245. package/tail.d.ts.map +1 -0
  246. package/tail.js +167 -0
  247. package/tail.js.map +1 -0
  248. package/time.d.ts +15 -2
  249. package/time.d.ts.map +1 -1
  250. package/time.js +28 -9
  251. package/time.js.map +1 -1
  252. package/track.d.ts +211 -83
  253. package/track.d.ts.map +1 -1
  254. package/track.js +816 -205
  255. package/track.js.map +1 -1
  256. package/util/abort.d.ts +2 -0
  257. package/util/abort.d.ts.map +1 -0
  258. package/util/abort.js +20 -0
  259. package/util/abort.js.map +1 -0
  260. package/util/log.d.ts +5 -0
  261. package/util/log.d.ts.map +1 -0
  262. package/util/log.js +17 -0
  263. package/util/log.js.map +1 -0
  264. package/util/u64.d.ts +39 -0
  265. package/util/u64.d.ts.map +1 -0
  266. package/util/u64.js +83 -0
  267. package/util/u64.js.map +1 -0
  268. package/util/varint.d.ts +29 -0
  269. package/util/varint.d.ts.map +1 -0
  270. package/util/varint.js +198 -0
  271. package/util/varint.js.map +1 -0
  272. package/varint.d.ts +10 -6
  273. package/varint.d.ts.map +1 -1
  274. package/varint.js +40 -237
  275. package/varint.js.map +1 -1
  276. package/wire.d.ts +80 -0
  277. package/wire.d.ts.map +1 -0
  278. package/wire.js +32 -0
  279. package/wire.js.map +1 -0
  280. package/zod.d.ts +1 -1
  281. package/zod.d.ts.map +1 -1
  282. package/zod.js.map +1 -1
  283. package/mock.d.ts +0 -66
  284. package/mock.d.ts.map +0 -1
  285. package/mock.js +0 -243
  286. package/mock.js.map +0 -1
package/stream.js CHANGED
@@ -1,9 +1,20 @@
1
1
  /* @ts-self-types="./stream.d.ts" */
2
- import { fromTransport } from "./error.js";
2
+ import { race } from "@norskvideo/moq-signals";
3
+ import { fromTransport, StreamCode, StreamError, toStreamCode, toTransport } from "./error.js";
3
4
  import { Version } from "./ietf/version.js";
5
+ import { Version as Lite } from "./lite/version.js";
4
6
  import { TimeoutError, withTimeout } from "./util/timeout.js";
7
+ import { POW32, toBigInt, toNumber, U64 } from "./util/u64.js";
5
8
  import { decodeUtf8 } from "./util/utf8.js";
6
- import * as Varint from "./varint.js";
9
+ import { lengthLeadingOnes, lengthQuic, parts, peekLeadingOnes, peekQuic, readLeadingOnes, readQuic, split, writeLeadingOnes, writeQuic, } from "./util/varint.js";
10
+ // Decode raw transport errors before mapping so they cannot bypass the negotiated
11
+ // registry. Ordinary errors already send 0 and retain their local identity.
12
+ function withCode(reason, stream) {
13
+ const version = asIetf(stream);
14
+ const decoded = fromTransport(reason, { version });
15
+ const code = toStreamCode(decoded, { version });
16
+ return code === StreamCode.Internal && decoded === reason ? reason : toTransport(code, decoded.message);
17
+ }
7
18
  const MAX_U31 = 2 ** 31 - 1;
8
19
  const MAX_READ_SIZE = 1024 * 1024 * 64; // don't allocate more than 64MB for a message
9
20
  /**
@@ -35,18 +46,56 @@ async function openWithin(opening, timeout, discard) {
35
46
  throw err;
36
47
  }
37
48
  }
49
+ const LITE = new Set(Object.values(Lite));
50
+ function isLite(version) {
51
+ return LITE.has(version);
52
+ }
53
+ /** The moq-transport draft a stream follows, or undefined on moq-lite, whose stream codes are the same on every draft. */
54
+ export function asIetf(version) {
55
+ return isLite(version) ? undefined : version;
56
+ }
57
+ // Every draft newer than these counts leading ones, so a new version falls forward.
38
58
  function isLeadingOnes(version) {
39
- return (version !== undefined &&
40
- version !== Version.DRAFT_14 &&
41
- version !== Version.DRAFT_15 &&
42
- version !== Version.DRAFT_16);
59
+ switch (version) {
60
+ case Version.DRAFT_14:
61
+ case Version.DRAFT_15:
62
+ case Version.DRAFT_16:
63
+ case Lite.DRAFT_01:
64
+ case Lite.DRAFT_02:
65
+ case Lite.DRAFT_03:
66
+ case Lite.DRAFT_04:
67
+ case Lite.DRAFT_05:
68
+ case Lite.DRAFT_06:
69
+ return false;
70
+ default:
71
+ return true;
72
+ }
73
+ }
74
+ // Encode `hi`/`lo` into `dst` in the varint form `version` uses: QUIC up to 2^62-1, leading-ones up to 2^64-1.
75
+ function encodeTo(dst, hi, lo, version) {
76
+ let buf;
77
+ if (isLeadingOnes(version)) {
78
+ buf = new Uint8Array(dst, 0, lengthLeadingOnes(hi, lo));
79
+ writeLeadingOnes(buf, hi, lo, buf.length);
80
+ }
81
+ else {
82
+ buf = new Uint8Array(dst, 0, lengthQuic(hi, lo));
83
+ writeQuic(buf, hi, lo, buf.length);
84
+ }
85
+ return buf;
86
+ }
87
+ /** Encode one varint in the form `version` uses, for a body written outside a {@link Writer}. */
88
+ export function encodeVarint(v, version) {
89
+ const lo = split(v);
90
+ return encodeTo(new ArrayBuffer(9), parts.hi, lo, version);
43
91
  }
44
92
  export class Stream {
45
93
  reader;
46
94
  writer;
47
95
  constructor(props) {
48
- const writer = props.writer ?? (props.writable && new Writer(props.writable, props.version));
49
- const reader = props.reader ?? (props.readable && new Reader(props.readable, undefined, props.version));
96
+ const version = props.version;
97
+ const writer = props.writer ?? (props.writable && version !== undefined && new Writer(props.writable, version));
98
+ const reader = props.reader ?? (props.readable && version !== undefined && new Reader(props.readable, undefined, version));
50
99
  if (!writer || !reader)
51
100
  throw new Error("stream needs both halves");
52
101
  this.writer = writer;
@@ -74,11 +123,14 @@ export class Stream {
74
123
  void stream.writable.abort().catch(() => void 0);
75
124
  void stream.readable.cancel().catch(() => void 0);
76
125
  });
77
- return new Stream({ readable, writable, version: options?.version });
126
+ return new Stream({ readable, writable, version: options.version });
78
127
  }
79
128
  close() {
80
129
  this.writer.close();
81
- this.reader.stop(new Error("cancel"));
130
+ // A routine unsubscribe, so send CANCELLED. A bare Error would put 0 on the wire,
131
+ // which the stream registry reads as INTERNAL_ERROR: the peer would log a failure
132
+ // for every subscription we walk away from.
133
+ this.reader.stop(new StreamError(StreamCode.Cancel, { message: "cancel" }));
82
134
  }
83
135
  abort(reason) {
84
136
  this.writer.reset(reason);
@@ -88,10 +140,16 @@ export class Stream {
88
140
  // Reader wraps a stream and provides convience methods for reading pieces from a stream
89
141
  // Unfortunately we can't use a BYOB reader because it's not supported with WebTransport+WebWorkers yet.
90
142
  export class Reader {
143
+ // Contiguous unread bytes, followed by chunks not yet joined onto it. Joining only once a
144
+ // read needs the bytes keeps a frame arriving in N chunks linear rather than quadratic.
91
145
  #buffer;
146
+ #chunks = [];
147
+ #chunked = 0; // bytes across #chunks
92
148
  #stream; // if undefined, the buffer is consumed then EOF
93
149
  #reader;
94
150
  #closed;
151
+ // The decode that last ran short and how far, so a retry can wait for those bytes.
152
+ #short;
95
153
  version;
96
154
  constructor(stream, buffer, version) {
97
155
  this.#buffer = buffer ?? new Uint8Array();
@@ -107,7 +165,7 @@ export class Reader {
107
165
  // Every read of this stream funnels through here, so decoding the peer's reset code
108
166
  // once is enough to keep the raw transport error out of every caller (and every app).
109
167
  const result = await this.#reader.read().catch((err) => {
110
- throw fromTransport(err);
168
+ throw fromTransport(err, { version: asIetf(this.version) });
111
169
  });
112
170
  if (result.done) {
113
171
  return false;
@@ -115,16 +173,8 @@ export class Reader {
115
173
  if (result.value.byteLength === 0) {
116
174
  throw new Error("unexpected empty chunk");
117
175
  }
118
- const buffer = new Uint8Array(result.value);
119
- if (this.#buffer.byteLength === 0) {
120
- this.#buffer = buffer;
121
- }
122
- else {
123
- const temp = new Uint8Array(this.#buffer.byteLength + buffer.byteLength);
124
- temp.set(this.#buffer);
125
- temp.set(buffer, this.#buffer.byteLength);
126
- this.#buffer = temp;
127
- }
176
+ this.#chunks.push(result.value);
177
+ this.#chunked += result.value.byteLength;
128
178
  return true;
129
179
  }
130
180
  // Add more data to the buffer until it's at least size bytes.
@@ -132,11 +182,34 @@ export class Reader {
132
182
  if (size > MAX_READ_SIZE) {
133
183
  throw new Error(`read size ${size} exceeds max size ${MAX_READ_SIZE}`);
134
184
  }
135
- while (this.#buffer.byteLength < size) {
185
+ if (this.#buffer.byteLength >= size)
186
+ return;
187
+ while (this.#buffer.byteLength + this.#chunked < size) {
136
188
  if (!(await this.#fill())) {
137
189
  throw new Error("unexpected end of stream");
138
190
  }
139
191
  }
192
+ this.#join();
193
+ }
194
+ // Move every pending chunk into the buffer, copying only when there's more than one piece.
195
+ #join() {
196
+ if (this.#chunks.length === 0)
197
+ return;
198
+ if (this.#buffer.byteLength === 0 && this.#chunks.length === 1) {
199
+ this.#buffer = this.#chunks[0];
200
+ }
201
+ else {
202
+ const joined = new Uint8Array(this.#buffer.byteLength + this.#chunked);
203
+ joined.set(this.#buffer);
204
+ let offset = this.#buffer.byteLength;
205
+ for (const chunk of this.#chunks) {
206
+ joined.set(chunk, offset);
207
+ offset += chunk.byteLength;
208
+ }
209
+ this.#buffer = joined;
210
+ }
211
+ this.#chunks = [];
212
+ this.#chunked = 0;
140
213
  }
141
214
  // Consumes the first size bytes of the buffer.
142
215
  #slice(size) {
@@ -144,135 +217,293 @@ export class Reader {
144
217
  this.#buffer = new Uint8Array(this.#buffer.buffer, this.#buffer.byteOffset + size, this.#buffer.byteLength - size);
145
218
  return result;
146
219
  }
220
+ /**
221
+ * Run a synchronous decode over the buffered bytes and consume what it read.
222
+ *
223
+ * Returns undefined and consumes nothing when the decode ran past the buffered bytes, so a
224
+ * caller can drain every complete message already here without waiting on the stream.
225
+ */
226
+ tryDecode(decode) {
227
+ const result = this.#try(decode);
228
+ return result instanceof Short ? undefined : result;
229
+ }
230
+ /** Run a synchronous decode, filling from the stream until it has the bytes it needs. */
231
+ async decode(decode) {
232
+ for (;;) {
233
+ const result = this.#try(decode);
234
+ if (!(result instanceof Short))
235
+ return result;
236
+ await this.#fillTo(result.need);
237
+ }
238
+ }
239
+ /** Like {@link decode}, but returns undefined if the stream ends cleanly first. */
240
+ async decodeMaybe(decode) {
241
+ if (await this.done())
242
+ return undefined;
243
+ return this.decode(decode);
244
+ }
245
+ #try(decode) {
246
+ // A retry of the decode that last ran short, before the bytes it needs have arrived,
247
+ // would only throw again. Every decode reads at least a byte, so none can succeed on
248
+ // an empty buffer either.
249
+ const available = this.#buffer.byteLength + this.#chunked;
250
+ if (available === 0)
251
+ return EMPTY;
252
+ if (decode === this.#short?.decode && available < this.#short.err.need)
253
+ return this.#short.err;
254
+ this.#join();
255
+ const cursor = new Cursor(this.#buffer, this.version);
256
+ try {
257
+ const result = decode(cursor);
258
+ this.#slice(cursor.offset);
259
+ this.#short = undefined;
260
+ return result;
261
+ }
262
+ catch (err) {
263
+ if (!(err instanceof Short))
264
+ throw err;
265
+ // Filling could never satisfy it, so retrying would spin.
266
+ if (err.need <= this.#buffer.byteLength)
267
+ throw new Error("decode ran short of bytes it already had");
268
+ this.#short = { decode, err };
269
+ return err;
270
+ }
271
+ }
147
272
  async read(size) {
148
273
  if (size === 0)
149
274
  return new Uint8Array();
150
- await this.#fillTo(size);
151
- return this.#slice(size);
275
+ return this.decode((c) => c.read(size));
152
276
  }
153
277
  async readAll() {
154
278
  while (await this.#fill()) {
155
279
  // keep going
156
280
  }
281
+ this.#join();
157
282
  return this.#slice(this.#buffer.byteLength);
158
283
  }
284
+ // Reads to the end of the stream, dropping every byte instead of buffering it.
285
+ async discard() {
286
+ this.#buffer = new Uint8Array();
287
+ do {
288
+ this.#chunks = [];
289
+ this.#chunked = 0;
290
+ } while (await this.#fill());
291
+ }
159
292
  async string() {
160
- const length = await this.u53();
161
- const buffer = await this.read(length);
162
- return decodeUtf8(buffer);
293
+ return this.decode(STRING);
163
294
  }
164
295
  async bool() {
165
- const v = await this.u8();
166
- if (v === 0)
167
- return false;
168
- if (v === 1)
169
- return true;
170
- throw new Error("invalid bool value");
296
+ return this.decode(BOOL);
171
297
  }
172
298
  async u8() {
173
- await this.#fillTo(1);
174
- return this.#slice(1)[0];
299
+ return this.decode(U8);
175
300
  }
176
301
  async u16() {
177
- await this.#fillTo(2);
178
- const view = new DataView(this.#buffer.buffer, this.#buffer.byteOffset, 2);
179
- const result = view.getUint16(0);
180
- this.#slice(2);
181
- return result;
302
+ return this.decode(U16);
182
303
  }
183
304
  // Returns a Number using 53-bits, the max Javascript can use for integer math.
184
305
  async u53() {
185
- const v = await this.u62();
186
- if (v > Varint.MAX_U53) {
187
- throw new Error(`value larger than 53-bits: ${v.toString()}`);
188
- }
189
- return Number(v);
306
+ return this.decode(U53);
190
307
  }
191
308
  // NOTE: Returns a bigint instead of a number since it may be larger than 53-bits
192
309
  async u62() {
193
- if (isLeadingOnes(this.version)) {
194
- return this.#readLeadingOnes();
195
- }
196
- return this.#readQuicVarint();
197
- }
198
- async #readQuicVarint() {
199
- await this.#fillTo(1);
200
- const size = (this.#buffer[0] & 0xc0) >> 6;
201
- if (size === 0) {
202
- const first = this.#slice(1)[0];
203
- return BigInt(first) & 0x3fn;
204
- }
205
- if (size === 1) {
206
- await this.#fillTo(2);
207
- const slice = this.#slice(2);
208
- const view = new DataView(slice.buffer, slice.byteOffset, slice.byteLength);
209
- return BigInt(view.getUint16(0)) & 0x3fffn;
210
- }
211
- if (size === 2) {
212
- await this.#fillTo(4);
213
- const slice = this.#slice(4);
214
- const view = new DataView(slice.buffer, slice.byteOffset, slice.byteLength);
215
- return BigInt(view.getUint32(0)) & 0x3fffffffn;
216
- }
217
- await this.#fillTo(8);
218
- const slice = this.#slice(8);
219
- const view = new DataView(slice.buffer, slice.byteOffset, slice.byteLength);
220
- return view.getBigUint64(0) & 0x3fffffffffffffffn;
221
- }
222
- async #readLeadingOnes() {
223
- await this.#fillTo(1);
224
- const b = this.#buffer[0];
225
- // Count leading 1-bits
226
- let ones = 0;
227
- for (let bit = 7; bit >= 0; bit--) {
228
- if (b & (1 << bit))
229
- ones++;
230
- else
231
- break;
232
- }
233
- // 1111110x is a 7-byte form. Draft-17 rejects it; draft-18+ allows it per #1595.
234
- if (ones === 6 && this.version === Version.DRAFT_17) {
235
- throw new Error("invalid leading-ones varint: 1111110x prefix is reserved on draft-17");
236
- }
237
- let totalSize;
238
- if (ones <= 5)
239
- totalSize = ones + 1;
240
- else if (ones === 6)
241
- totalSize = 7;
242
- else if (ones === 7)
243
- totalSize = 8;
244
- else
245
- totalSize = 9; // ones === 8
246
- await this.#fillTo(totalSize);
247
- const slice = this.#slice(totalSize);
248
- const [value] = Varint.decodeLeadingOnes(slice);
249
- return value;
310
+ return this.decode(U62);
311
+ }
312
+ async varint() {
313
+ return this.decode(VARINT);
250
314
  }
251
315
  // Returns false if there is more data to read, blocking if it hasn't been received yet.
252
316
  async done() {
253
- if (this.#buffer.byteLength > 0)
317
+ if (this.#buffer.byteLength > 0 || this.#chunked > 0)
254
318
  return false;
255
319
  return !(await this.#fill());
256
320
  }
257
321
  stop(reason) {
258
- this.#reader?.cancel(reason).catch(() => void 0);
322
+ this.#reader?.cancel(withCode(reason, this.version)).catch(() => void 0);
259
323
  }
260
324
  // Decoded like #fill: a caller racing this against a read must not get a different error
261
325
  // shape depending on which one won. Derived once, so racing it per frame doesn't allocate.
262
326
  get closed() {
263
327
  this.#closed ??= (this.#reader?.closed ?? Promise.resolve()).catch((err) => {
264
- throw fromTransport(err);
328
+ throw fromTransport(err, { version: asIetf(this.version) });
265
329
  });
266
330
  return this.#closed;
267
331
  }
268
332
  }
333
+ // Thrown by a Cursor read that runs past the buffered bytes, carrying how many bytes from the
334
+ // start of the buffer the decode needs. Not an Error: it ends every chunk, so it must not
335
+ // capture a stack.
336
+ class Short {
337
+ need;
338
+ constructor(need) {
339
+ this.need = need;
340
+ }
341
+ }
342
+ const EMPTY = new Short(1);
343
+ /**
344
+ * A synchronous view over a {@link Reader}'s buffered bytes, handed to {@link Reader.decode}.
345
+ *
346
+ * A read past the buffered bytes throws an internal signal that the Reader catches: it consumes
347
+ * nothing, fills, and runs the decode again from the start. A decode must therefore not mutate
348
+ * anything before its last read, must not swallow what it throws, and must read at least a byte.
349
+ */
350
+ export class Cursor {
351
+ version;
352
+ #buffer;
353
+ #offset = 0;
354
+ // Resolved once, since every varint read branches on it.
355
+ #leadingOnes;
356
+ // First bytes below this are a whole 1-byte varint, and below this + 0x40 a 2-byte one whose
357
+ // value is the low 6 bits and the next byte. Both formats share that shape; only the bound moves.
358
+ #short;
359
+ // First bytes below this are a varint of at most 4 bytes: 0xc0 for QUIC, 0xf0 for leading-ones.
360
+ #word;
361
+ constructor(buffer, version) {
362
+ this.#buffer = buffer;
363
+ this.version = version;
364
+ this.#leadingOnes = isLeadingOnes(version);
365
+ this.#short = this.#leadingOnes ? 0x80 : 0x40;
366
+ this.#word = this.#leadingOnes ? 0xf0 : 0xc0;
367
+ }
368
+ /** How many bytes have been read. */
369
+ get offset() {
370
+ return this.#offset;
371
+ }
372
+ /** How many buffered bytes are left to read. */
373
+ get remaining() {
374
+ return this.#buffer.byteLength - this.#offset;
375
+ }
376
+ /**
377
+ * Decode the next `size` bytes on their own. Running past them, or leaving any unread, is
378
+ * malformed rather than a reason to wait for more.
379
+ */
380
+ exact(size, decode) {
381
+ const inner = new Cursor(this.read(size), this.version);
382
+ let result;
383
+ try {
384
+ result = decode(inner);
385
+ }
386
+ catch (err) {
387
+ if (err instanceof Short)
388
+ throw new Error(`message is shorter than its fields: ${size} bytes`);
389
+ throw err;
390
+ }
391
+ if (inner.remaining > 0)
392
+ throw new Error(`message has ${inner.remaining} unread bytes`);
393
+ return result;
394
+ }
395
+ #ensure(size) {
396
+ const need = this.#offset + size;
397
+ // Checked here too, and on the whole decode like the fill, since bytes that are already
398
+ // buffered never reach the fill.
399
+ if (need > MAX_READ_SIZE)
400
+ throw new Error(`read size ${need} exceeds max size ${MAX_READ_SIZE}`);
401
+ if (need > this.#buffer.byteLength)
402
+ throw new Short(need);
403
+ }
404
+ /** Read `size` bytes, as a view onto the buffer rather than a copy. */
405
+ read(size) {
406
+ this.#ensure(size);
407
+ const start = this.#offset;
408
+ this.#offset += size;
409
+ return this.#buffer.subarray(start, this.#offset);
410
+ }
411
+ string() {
412
+ return decodeUtf8(this.read(this.u53()));
413
+ }
414
+ bool() {
415
+ const v = this.u8();
416
+ if (v === 0)
417
+ return false;
418
+ if (v === 1)
419
+ return true;
420
+ throw new Error("invalid bool value");
421
+ }
422
+ u8() {
423
+ this.#ensure(1);
424
+ return this.#buffer[this.#offset++];
425
+ }
426
+ u16() {
427
+ this.#ensure(2);
428
+ const b = this.#buffer;
429
+ const o = this.#offset;
430
+ this.#offset += 2;
431
+ return (b[o] << 8) | b[o + 1];
432
+ }
433
+ /** Read a varint as a `number`, throwing if it is above `Number.MAX_SAFE_INTEGER`. */
434
+ u53() {
435
+ // Most varints are 1 or 2 bytes, which skip the general decode.
436
+ this.#ensure(1);
437
+ const b = this.#buffer;
438
+ const o = this.#offset;
439
+ const first = b[o];
440
+ if (first < this.#short) {
441
+ this.#offset = o + 1;
442
+ return first;
443
+ }
444
+ if (first < this.#short + 0x40) {
445
+ this.#ensure(2);
446
+ this.#offset = o + 2;
447
+ return ((first & 0x3f) << 8) | b[o + 1];
448
+ }
449
+ // Up to 4 bytes still fits 28 (leading-ones) or 30 (QUIC) bits, with no upper half.
450
+ if (first < this.#word) {
451
+ const size = this.#leadingOnes ? peekLeadingOnes(first) : 4;
452
+ this.#ensure(size);
453
+ this.#offset = o + size;
454
+ if (size === 3)
455
+ return ((first & 0x1f) << 16) | (b[o + 1] << 8) | b[o + 2];
456
+ return ((first & (this.#leadingOnes ? 0x0f : 0x3f)) << 24) | (b[o + 1] << 16) | (b[o + 2] << 8) | b[o + 3];
457
+ }
458
+ const lo = this.#varint();
459
+ return toNumber(parts.hi, lo);
460
+ }
461
+ /** Read a varint as a bigint. A leading-ones varint may exceed 62 bits. */
462
+ u62() {
463
+ const lo = this.#varint();
464
+ return toBigInt(parts.hi, lo);
465
+ }
466
+ /** Read a varint. */
467
+ varint() {
468
+ const lo = this.#varint();
469
+ return new U64(parts.hi, lo);
470
+ }
471
+ // Decode the next varint in the version's format, returning its lower half and leaving the upper in `parts`.
472
+ #varint() {
473
+ this.#ensure(1);
474
+ const b = this.#buffer;
475
+ const o = this.#offset;
476
+ let size;
477
+ if (this.#leadingOnes) {
478
+ size = peekLeadingOnes(b[o]);
479
+ // 1111110x is a 7-byte form. Draft-17 rejects it; draft-18+ allows it per #1595.
480
+ if (size === 7 && this.version === Version.DRAFT_17) {
481
+ throw new Error("invalid leading-ones varint: 1111110x prefix is reserved on draft-17");
482
+ }
483
+ this.#ensure(size);
484
+ this.#offset += size;
485
+ return readLeadingOnes(b, o, size);
486
+ }
487
+ size = peekQuic(b[o]);
488
+ this.#ensure(size);
489
+ this.#offset += size;
490
+ return readQuic(b, o, size);
491
+ }
492
+ }
493
+ // Shared decodes for the Reader's async primitives, so a read allocates no closure.
494
+ const STRING = (c) => c.string();
495
+ const BOOL = (c) => c.bool();
496
+ const U8 = (c) => c.u8();
497
+ const U16 = (c) => c.u16();
498
+ const U53 = (c) => c.u53();
499
+ const U62 = (c) => c.u62();
500
+ const VARINT = (c) => c.varint();
269
501
  // Writer wraps a stream and writes chunks of data
270
502
  export class Writer {
271
503
  #writer;
272
504
  #stream;
273
505
  #closed;
274
- // Scratch buffer for writing varints.
275
- // Fixed at 9 bytes (leading-ones max).
506
+ // Scratch buffer for each primitive write, sized for the longest (a 9-byte leading-ones varint).
276
507
  #scratch;
277
508
  version;
278
509
  constructor(stream, version) {
@@ -298,6 +529,9 @@ export class Writer {
298
529
  await this.write(setUint8(this.#scratch, v ? 1 : 0));
299
530
  }
300
531
  async u8(v) {
532
+ if (!Number.isInteger(v) || v < 0 || v > 255) {
533
+ throw new RangeError(`invalid u8: ${v}`);
534
+ }
301
535
  await this.write(setUint8(this.#scratch, v));
302
536
  }
303
537
  async u16(v) {
@@ -307,38 +541,31 @@ export class Writer {
307
541
  if (Math.abs(v) > MAX_U31) {
308
542
  throw new Error(`overflow, value larger than 32-bits: ${v.toString()}`);
309
543
  }
310
- // We don't use a VarInt, so it always takes 4 bytes.
544
+ // We don't use a varint, so it always takes 4 bytes.
311
545
  // This could be improved but nothing is standardized yet.
312
546
  await this.write(setInt32(this.#scratch, v));
313
547
  }
314
548
  async u53(v) {
315
- if (v > Varint.MAX_U53) {
316
- // Number values above 2^53-1 have already lost precision before reaching
317
- // the wire, but downgrade overflow to warn so an upstream miscount
318
- // doesn't tear down the whole stream. The encoded varint will reflect
319
- // the truncated Number value.
320
- console.warn(`value larger than 53-bits; use u62 instead (precision lost): ${v.toString()}`);
321
- }
322
- if (isLeadingOnes(this.version)) {
323
- await this.write(Varint.encodeLeadingOnesTo(this.#scratch, v));
324
- }
325
- else {
326
- await this.write(Varint.encodeTo(this.#scratch, v));
549
+ if (!Number.isSafeInteger(v) || v < 0) {
550
+ throw new RangeError(`invalid u53: ${v}`);
327
551
  }
552
+ await this.#varint(Math.floor(v / POW32), v >>> 0);
328
553
  }
329
554
  async u62(v) {
330
- if (isLeadingOnes(this.version)) {
331
- await this.write(Varint.encodeLeadingOnesTo(this.#scratch, v));
332
- }
333
- else {
334
- await this.write(Varint.encodeTo(this.#scratch, v));
335
- }
555
+ const lo = split(v);
556
+ await this.#varint(parts.hi, lo);
557
+ }
558
+ async varint(v) {
559
+ await this.#varint(v.hi, v.lo);
560
+ }
561
+ #varint(hi, lo) {
562
+ return this.write(encodeTo(this.#scratch, hi, lo, this.version));
336
563
  }
337
564
  async write(v) {
338
565
  // Mirrors Reader.#fill: every write funnels through here, so a STOP_SENDING from the
339
566
  // peer surfaces as a typed code rather than the transport's own error shape.
340
567
  await this.#writer.write(v).catch((err) => {
341
- throw fromTransport(err);
568
+ throw fromTransport(err, { version: asIetf(this.version) });
342
569
  });
343
570
  }
344
571
  async string(str) {
@@ -353,12 +580,12 @@ export class Writer {
353
580
  // typed code it would get from a write.
354
581
  get closed() {
355
582
  this.#closed ??= this.#writer.closed.catch((err) => {
356
- throw fromTransport(err);
583
+ throw fromTransport(err, { version: asIetf(this.version) });
357
584
  });
358
585
  return this.#closed;
359
586
  }
360
587
  reset(reason) {
361
- this.#writer.abort(reason).catch(() => void 0);
588
+ this.#writer.abort(withCode(reason, this.version)).catch(() => void 0);
362
589
  }
363
590
  /**
364
591
  * Open an outgoing unidirectional stream.
@@ -368,7 +595,7 @@ export class Writer {
368
595
  */
369
596
  static async open(quic, options) {
370
597
  const writable = await openWithin(quic.createUnidirectionalStream(sendOptions(options)), options?.timeout ?? OPEN_TIMEOUT_MS, (stream) => void stream.abort().catch(() => void 0));
371
- return new Writer(writable, options?.version);
598
+ return new Writer(writable, options.version);
372
599
  }
373
600
  /**
374
601
  * Like {@link Writer.open}, but gives up when `cancel` settles or `timeout` elapses,
@@ -379,25 +606,29 @@ export class Writer {
379
606
  * an over-limit open instead of rejecting it.
380
607
  */
381
608
  static async tryOpen(quic, options) {
382
- // A rejected `cancel` (STOP_SENDING) means the peer is gone too, so both settle
383
- // paths mean "give up" and neither is left unhandled. Built before the open, and
384
- // raced ahead of it, so an already-cancelled caller wins even against a slot that
385
- // is free right now.
386
- const cancelled = options.cancel.then(() => undefined, () => undefined);
609
+ // Raced ahead of the open, so an already-cancelled caller wins even against a slot
610
+ // that is free right now. `race` rather than `Promise.race`: the caller shares one
611
+ // `cancel` across every group of a subscription, which must not gain a reaction per call.
387
612
  const open = Writer.open(quic, options);
613
+ // Resets a stream that opens after we gave up; a no-op if the open itself failed.
614
+ const abandon = () => {
615
+ const abandoned = new Error("abandoned waiting for a stream slot");
616
+ open.then((w) => w.reset(abandoned)).catch(() => void 0);
617
+ };
388
618
  try {
389
- const stream = await Promise.race([cancelled, open]);
619
+ const stream = await race([options.cancel, open]);
390
620
  if (stream)
391
621
  return stream;
392
622
  }
393
623
  catch (err) {
394
624
  // open already discarded the late stream on its way out.
395
- if (!(err instanceof TimeoutError))
396
- throw err;
397
- return undefined;
625
+ if (err instanceof TimeoutError)
626
+ return undefined;
627
+ // A rejected `cancel` still leaves the open pending.
628
+ abandon();
629
+ throw err;
398
630
  }
399
- const abandoned = new Error("abandoned waiting for a stream slot");
400
- open.then((w) => w.reset(abandoned)).catch(() => void 0);
631
+ abandon();
401
632
  return undefined;
402
633
  }
403
634
  }