@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/hop.d.ts ADDED
@@ -0,0 +1,115 @@
1
+ /**
2
+ * Endpoint identity within a hop chain, shared by both wire protocols.
3
+ *
4
+ * moq-lite carries these natively on every announcement; moq-transport carries them
5
+ * via the MoQ Cluster extension (see `ietf/cluster.ts`). Mirrors `Hop` in
6
+ * `rs/moq-net`.
7
+ *
8
+ * @module
9
+ */
10
+ import * as z from "zod/mini";
11
+ /**
12
+ * One relay's identity in a broadcast's hop chain, encoded as a 62-bit varint on the wire.
13
+ *
14
+ * Names a *hop*, not an {@link !Origin | Origin} routing table: this is the id a relay
15
+ * stamps into a chain as an announcement passes through, so a receiver can spot its own
16
+ * id and reject a loop. The SETUP parameter that carries it session-wide is `Hop` too.
17
+ *
18
+ * The {@link HopSchema} validates any incoming value and brands it so the type system
19
+ * enforces "only validated ids flow into hop chains." Internal code that synthesizes one
20
+ * (e.g. {@link randomHop}) uses `HopSchema.parse(...)` to brand a raw bigint.
21
+ */
22
+ export declare const HopSchema: z.core.$ZodBranded<z.ZodMiniBigInt<bigint>, "Hop", "out">;
23
+ export type Hop = z.infer<typeof HopSchema>;
24
+ /**
25
+ * The reserved id 0, meaning "no identity".
26
+ *
27
+ * It stands in for an endpoint that never declared one, and any number of endpoints can
28
+ * be 0, so it identifies nothing: it is never a loop, never a publisher two chains have
29
+ * in common, and never excluded from an advertisement. A chain that holds a 0 anywhere
30
+ * is anonymous for route selection.
31
+ */
32
+ export declare const UNKNOWN_HOP: Hop;
33
+ /**
34
+ * Maximum length of a hop chain. Must match `MAX_HOPS` in Rust's `model/origin.rs`.
35
+ *
36
+ * Broadcasts with longer chains are rejected, which bounds loop detection and rejects
37
+ * pathological announcements across clusters with unbounded forwarding.
38
+ */
39
+ export declare const MAX_HOPS = 32;
40
+ /**
41
+ * Generate a fresh hop with a random non-zero id.
42
+ *
43
+ * `crypto.getRandomValues` is overkill for best-effort loop detection, but
44
+ * used for slightly better distribution than `Math.random` at negligible cost.
45
+ *
46
+ * TEMPORARY: the wire format allows 62 bits, but older `@moq/lite` JS clients
47
+ * decode `AnnounceInterest.exclude_hop` as a u53 (number) and throw on anything
48
+ * > 2^53-1. To keep those clients alive against fresh peers, we cap the random
49
+ * id at 53 bits. Restore to 62 bits once the u62 fix has propagated to deployed
50
+ * bundles. Mirrors `Hop::random` in rs/moq-net.
51
+ */
52
+ export declare function randomHop(): Hop;
53
+ /**
54
+ * Name an unknown original publisher: put `stamp`, the receiving connection's own random id,
55
+ * in front of a chain that starts with 0, and turn an empty chain into `[stamp, 0]`. A
56
+ * publisher that reconnects then reads downstream as a new first hop, while the 0 after the
57
+ * stamp keeps the route ranked as anonymous. `undefined` if the chain is full or already holds
58
+ * `stamp`. Mirrors `Hops::stamp` in rs/moq-net.
59
+ */
60
+ export declare function stampHops(hops: readonly Hop[], stamp: Hop): Hop[] | undefined;
61
+ /**
62
+ * What pulling content via a route costs, in two magnitudes accumulated together
63
+ * and compared in that order: lower {@link Cost.warm} wins, and {@link Cost.cold}
64
+ * breaks the tie.
65
+ *
66
+ * Both price the same path against different cache states. `warm` is what one more
67
+ * subscription would cost the mesh right now, so it collapses to zero at any relay
68
+ * already carrying the broadcast. `cold` prices the identical path as if nothing were
69
+ * cached, so it keeps flowing through a warm relay unchanged and still says which of
70
+ * two warm relays sits closer to the publisher.
71
+ */
72
+ export interface Cost {
73
+ /** The cost as the mesh stands today, discounted to zero at every carrying relay. */
74
+ warm: bigint;
75
+ /** The same path with every warm discount removed. */
76
+ cold: bigint;
77
+ }
78
+ /** Constructors for {@link Cost}. */
79
+ export declare const Cost: {
80
+ /** A free path in both magnitudes: what a live publisher seeds. */
81
+ zero: Cost;
82
+ };
83
+ /**
84
+ * The path a route took through the mesh and what using it costs.
85
+ *
86
+ * The metadata half of an advertisement: an origin `dynamic()` pairs it with the
87
+ * pattern it covers, a broadcast `announce()` with the broadcast's exact path, and
88
+ * an announce event carries it so consumers can read it back.
89
+ */
90
+ export interface Route {
91
+ /** The chain of hops the route has traversed, oldest first. */
92
+ hops: Hop[];
93
+ /** What pulling content via this route costs; lower wins. */
94
+ cost: Cost;
95
+ }
96
+ /** Constructors and helpers for {@link Route}. */
97
+ export declare const Route: {
98
+ /** An empty hop chain at zero cost: what a publisher seeds for a live broadcast. */
99
+ default: Route;
100
+ /** Normalize a partial route, treating a bare bigint cost as both magnitudes alike. */
101
+ normalize(route?: Route | {
102
+ hops?: readonly Hop[];
103
+ cost?: Cost | bigint;
104
+ }): Route;
105
+ };
106
+ /**
107
+ * Whether this route passed through an anonymous hop (id 0) at any depth.
108
+ *
109
+ * An empty chain is a local announcement, not the anonymous mark. Ingress fills a
110
+ * received empty list with {@link UNKNOWN_HOP} before yielding it.
111
+ */
112
+ export declare function isAnonymous(route: Route): boolean;
113
+ /** Whether two routes name the same hop chain and cost. */
114
+ export declare function routesEqual(a: Route | undefined, b: Route | undefined): boolean;
115
+ //# sourceMappingURL=hop.d.ts.map
package/hop.d.ts.map ADDED
@@ -0,0 +1 @@
1
+ {"version":3,"file":"hop.d.ts","sourceRoot":"","sources":["../src/hop.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AACH,OAAO,KAAK,CAAC,MAAM,UAAU,CAAC;AAE9B;;;;;;;;;;GAUG;AACH,eAAO,MAAM,SAAS,2DAGR,CAAC;AAEf,MAAM,MAAM,GAAG,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,SAAS,CAAC,CAAC;AAE5C;;;;;;;GAOG;AACH,eAAO,MAAM,WAAW,EAAE,GAAyB,CAAC;AAEpD;;;;;GAKG;AACH,eAAO,MAAM,QAAQ,KAAK,CAAC;AAE3B;;;;;;;;;;;GAWG;AACH,wBAAgB,SAAS,IAAI,GAAG,CAO/B;AAED;;;;;;GAMG;AACH,wBAAgB,SAAS,CAAC,IAAI,EAAE,SAAS,GAAG,EAAE,EAAE,KAAK,EAAE,GAAG,GAAG,GAAG,EAAE,GAAG,SAAS,CAK7E;AAED;;;;;;;;;;GAUG;AACH,MAAM,WAAW,IAAI;IACpB,qFAAqF;IACrF,IAAI,EAAE,MAAM,CAAC;IACb,sDAAsD;IACtD,IAAI,EAAE,MAAM,CAAC;CACb;AAED,qCAAqC;AACrC,eAAO,MAAM,IAAI;IAChB,mEAAmE;IACnE,IAAI,EAA4B,IAAI;CACpC,CAAC;AAEF;;;;;;GAMG;AACH,MAAM,WAAW,KAAK;IACrB,+DAA+D;IAC/D,IAAI,EAAE,GAAG,EAAE,CAAC;IACZ,6DAA6D;IAC7D,IAAI,EAAE,IAAI,CAAC;CACX;AAED,kDAAkD;AAClD,eAAO,MAAM,KAAK;IACjB,oFAAoF;IACpF,OAAO,EAAmC,KAAK;IAE/C,uFAAuF;IACvF,SAAS,SAAQ,KAAK,GAAG;QAAE,IAAI,CAAC,EAAE,SAAS,GAAG,EAAE,CAAC;QAAC,IAAI,CAAC,EAAE,IAAI,GAAG,MAAM,CAAA;KAAE,GAAQ,KAAK;CAOrF,CAAC;AAEF;;;;;GAKG;AACH,wBAAgB,WAAW,CAAC,KAAK,EAAE,KAAK,GAAG,OAAO,CAEjD;AAED,2DAA2D;AAC3D,wBAAgB,WAAW,CAAC,CAAC,EAAE,KAAK,GAAG,SAAS,EAAE,CAAC,EAAE,KAAK,GAAG,SAAS,GAAG,OAAO,CAS/E"}
package/hop.js ADDED
@@ -0,0 +1,119 @@
1
+ /* @ts-self-types="./hop.d.ts" */
2
+ /**
3
+ * Endpoint identity within a hop chain, shared by both wire protocols.
4
+ *
5
+ * moq-lite carries these natively on every announcement; moq-transport carries them
6
+ * via the MoQ Cluster extension (see `ietf/cluster.ts`). Mirrors `Hop` in
7
+ * `rs/moq-net`.
8
+ *
9
+ * @module
10
+ */
11
+ import * as z from "zod/mini";
12
+ /**
13
+ * One relay's identity in a broadcast's hop chain, encoded as a 62-bit varint on the wire.
14
+ *
15
+ * Names a *hop*, not an {@link !Origin | Origin} routing table: this is the id a relay
16
+ * stamps into a chain as an announcement passes through, so a receiver can spot its own
17
+ * id and reject a loop. The SETUP parameter that carries it session-wide is `Hop` too.
18
+ *
19
+ * The {@link HopSchema} validates any incoming value and brands it so the type system
20
+ * enforces "only validated ids flow into hop chains." Internal code that synthesizes one
21
+ * (e.g. {@link randomHop}) uses `HopSchema.parse(...)` to brand a raw bigint.
22
+ */
23
+ export const HopSchema = z
24
+ .bigint()
25
+ .check(z.refine((value) => value >= 0n && value < 1n << 62n, "Hop must be a non-negative 62-bit integer"))
26
+ .brand("Hop");
27
+ /**
28
+ * The reserved id 0, meaning "no identity".
29
+ *
30
+ * It stands in for an endpoint that never declared one, and any number of endpoints can
31
+ * be 0, so it identifies nothing: it is never a loop, never a publisher two chains have
32
+ * in common, and never excluded from an advertisement. A chain that holds a 0 anywhere
33
+ * is anonymous for route selection.
34
+ */
35
+ export const UNKNOWN_HOP = HopSchema.parse(0n);
36
+ /**
37
+ * Maximum length of a hop chain. Must match `MAX_HOPS` in Rust's `model/origin.rs`.
38
+ *
39
+ * Broadcasts with longer chains are rejected, which bounds loop detection and rejects
40
+ * pathological announcements across clusters with unbounded forwarding.
41
+ */
42
+ export const MAX_HOPS = 32;
43
+ /**
44
+ * Generate a fresh hop with a random non-zero id.
45
+ *
46
+ * `crypto.getRandomValues` is overkill for best-effort loop detection, but
47
+ * used for slightly better distribution than `Math.random` at negligible cost.
48
+ *
49
+ * TEMPORARY: the wire format allows 62 bits, but older `@moq/lite` JS clients
50
+ * decode `AnnounceInterest.exclude_hop` as a u53 (number) and throw on anything
51
+ * > 2^53-1. To keep those clients alive against fresh peers, we cap the random
52
+ * id at 53 bits. Restore to 62 bits once the u62 fix has propagated to deployed
53
+ * bundles. Mirrors `Hop::random` in rs/moq-net.
54
+ */
55
+ export function randomHop() {
56
+ const buf = new BigUint64Array(1);
57
+ crypto.getRandomValues(buf);
58
+ // Mask to 53 bits.
59
+ const raw = buf[0] & 0x1fffffffffffffn;
60
+ // Guard against the (astronomically unlikely) zero draw.
61
+ return HopSchema.parse(raw === 0n ? 1n : raw);
62
+ }
63
+ /**
64
+ * Name an unknown original publisher: put `stamp`, the receiving connection's own random id,
65
+ * in front of a chain that starts with 0, and turn an empty chain into `[stamp, 0]`. A
66
+ * publisher that reconnects then reads downstream as a new first hop, while the 0 after the
67
+ * stamp keeps the route ranked as anonymous. `undefined` if the chain is full or already holds
68
+ * `stamp`. Mirrors `Hops::stamp` in rs/moq-net.
69
+ */
70
+ export function stampHops(hops, stamp) {
71
+ if (hops.length === 0)
72
+ return [stamp, UNKNOWN_HOP];
73
+ if (hops[0] !== UNKNOWN_HOP)
74
+ return [...hops];
75
+ if (hops.length >= MAX_HOPS || hops.includes(stamp))
76
+ return undefined;
77
+ return [stamp, ...hops];
78
+ }
79
+ /** Constructors for {@link Cost}. */
80
+ export const Cost = {
81
+ /** A free path in both magnitudes: what a live publisher seeds. */
82
+ zero: { warm: 0n, cold: 0n },
83
+ };
84
+ /** Constructors and helpers for {@link Route}. */
85
+ export const Route = {
86
+ /** An empty hop chain at zero cost: what a publisher seeds for a live broadcast. */
87
+ default: { hops: [], cost: Cost.zero },
88
+ /** Normalize a partial route, treating a bare bigint cost as both magnitudes alike. */
89
+ normalize(route = {}) {
90
+ const hops = route.hops ? [...route.hops] : [];
91
+ const cost = route.cost;
92
+ if (cost === undefined)
93
+ return { hops, cost: Cost.zero };
94
+ if (typeof cost === "bigint")
95
+ return { hops, cost: { warm: cost, cold: cost } };
96
+ return { hops, cost: { warm: cost.warm, cold: cost.cold } };
97
+ },
98
+ };
99
+ /**
100
+ * Whether this route passed through an anonymous hop (id 0) at any depth.
101
+ *
102
+ * An empty chain is a local announcement, not the anonymous mark. Ingress fills a
103
+ * received empty list with {@link UNKNOWN_HOP} before yielding it.
104
+ */
105
+ export function isAnonymous(route) {
106
+ return route.hops.includes(UNKNOWN_HOP);
107
+ }
108
+ /** Whether two routes name the same hop chain and cost. */
109
+ export function routesEqual(a, b) {
110
+ if (a === b)
111
+ return true;
112
+ if (!a || !b)
113
+ return false;
114
+ return (a.cost.warm === b.cost.warm &&
115
+ a.cost.cold === b.cost.cold &&
116
+ a.hops.length === b.hops.length &&
117
+ a.hops.every((hop, i) => hop === b.hops[i]));
118
+ }
119
+ //# sourceMappingURL=hop.js.map
package/hop.js.map ADDED
@@ -0,0 +1 @@
1
+ {"version":3,"file":"hop.js","sourceRoot":"","sources":["../src/hop.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Endpoint identity within a hop chain, shared by both wire protocols.\n *\n * moq-lite carries these natively on every announcement; moq-transport carries them\n * via the MoQ Cluster extension (see `ietf/cluster.ts`). Mirrors `Hop` in\n * `rs/moq-net`.\n *\n * @module\n */\nimport * as z from \"zod/mini\";\n\n/**\n * One relay's identity in a broadcast's hop chain, encoded as a 62-bit varint on the wire.\n *\n * Names a *hop*, not an {@link !Origin | Origin} routing table: this is the id a relay\n * stamps into a chain as an announcement passes through, so a receiver can spot its own\n * id and reject a loop. The SETUP parameter that carries it session-wide is `Hop` too.\n *\n * The {@link HopSchema} validates any incoming value and brands it so the type system\n * enforces \"only validated ids flow into hop chains.\" Internal code that synthesizes one\n * (e.g. {@link randomHop}) uses `HopSchema.parse(...)` to brand a raw bigint.\n */\nexport const HopSchema = z\n\t.bigint()\n\t.check(z.refine((value) => value >= 0n && value < 1n << 62n, \"Hop must be a non-negative 62-bit integer\"))\n\t.brand(\"Hop\");\n\nexport type Hop = z.infer<typeof HopSchema>;\n\n/**\n * The reserved id 0, meaning \"no identity\".\n *\n * It stands in for an endpoint that never declared one, and any number of endpoints can\n * be 0, so it identifies nothing: it is never a loop, never a publisher two chains have\n * in common, and never excluded from an advertisement. A chain that holds a 0 anywhere\n * is anonymous for route selection.\n */\nexport const UNKNOWN_HOP: Hop = HopSchema.parse(0n);\n\n/**\n * Maximum length of a hop chain. Must match `MAX_HOPS` in Rust's `model/origin.rs`.\n *\n * Broadcasts with longer chains are rejected, which bounds loop detection and rejects\n * pathological announcements across clusters with unbounded forwarding.\n */\nexport const MAX_HOPS = 32;\n\n/**\n * Generate a fresh hop with a random non-zero id.\n *\n * `crypto.getRandomValues` is overkill for best-effort loop detection, but\n * used for slightly better distribution than `Math.random` at negligible cost.\n *\n * TEMPORARY: the wire format allows 62 bits, but older `@moq/lite` JS clients\n * decode `AnnounceInterest.exclude_hop` as a u53 (number) and throw on anything\n * > 2^53-1. To keep those clients alive against fresh peers, we cap the random\n * id at 53 bits. Restore to 62 bits once the u62 fix has propagated to deployed\n * bundles. Mirrors `Hop::random` in rs/moq-net.\n */\nexport function randomHop(): Hop {\n\tconst buf = new BigUint64Array(1);\n\tcrypto.getRandomValues(buf);\n\t// Mask to 53 bits.\n\tconst raw = buf[0] & 0x1f_ffff_ffff_ffffn;\n\t// Guard against the (astronomically unlikely) zero draw.\n\treturn HopSchema.parse(raw === 0n ? 1n : raw);\n}\n\n/**\n * Name an unknown original publisher: put `stamp`, the receiving connection's own random id,\n * in front of a chain that starts with 0, and turn an empty chain into `[stamp, 0]`. A\n * publisher that reconnects then reads downstream as a new first hop, while the 0 after the\n * stamp keeps the route ranked as anonymous. `undefined` if the chain is full or already holds\n * `stamp`. Mirrors `Hops::stamp` in rs/moq-net.\n */\nexport function stampHops(hops: readonly Hop[], stamp: Hop): Hop[] | undefined {\n\tif (hops.length === 0) return [stamp, UNKNOWN_HOP];\n\tif (hops[0] !== UNKNOWN_HOP) return [...hops];\n\tif (hops.length >= MAX_HOPS || hops.includes(stamp)) return undefined;\n\treturn [stamp, ...hops];\n}\n\n/**\n * What pulling content via a route costs, in two magnitudes accumulated together\n * and compared in that order: lower {@link Cost.warm} wins, and {@link Cost.cold}\n * breaks the tie.\n *\n * Both price the same path against different cache states. `warm` is what one more\n * subscription would cost the mesh right now, so it collapses to zero at any relay\n * already carrying the broadcast. `cold` prices the identical path as if nothing were\n * cached, so it keeps flowing through a warm relay unchanged and still says which of\n * two warm relays sits closer to the publisher.\n */\nexport interface Cost {\n\t/** The cost as the mesh stands today, discounted to zero at every carrying relay. */\n\twarm: bigint;\n\t/** The same path with every warm discount removed. */\n\tcold: bigint;\n}\n\n/** Constructors for {@link Cost}. */\nexport const Cost = {\n\t/** A free path in both magnitudes: what a live publisher seeds. */\n\tzero: { warm: 0n, cold: 0n } as Cost,\n};\n\n/**\n * The path a route took through the mesh and what using it costs.\n *\n * The metadata half of an advertisement: an origin `dynamic()` pairs it with the\n * pattern it covers, a broadcast `announce()` with the broadcast's exact path, and\n * an announce event carries it so consumers can read it back.\n */\nexport interface Route {\n\t/** The chain of hops the route has traversed, oldest first. */\n\thops: Hop[];\n\t/** What pulling content via this route costs; lower wins. */\n\tcost: Cost;\n}\n\n/** Constructors and helpers for {@link Route}. */\nexport const Route = {\n\t/** An empty hop chain at zero cost: what a publisher seeds for a live broadcast. */\n\tdefault: { hops: [], cost: Cost.zero } as Route,\n\n\t/** Normalize a partial route, treating a bare bigint cost as both magnitudes alike. */\n\tnormalize(route: Route | { hops?: readonly Hop[]; cost?: Cost | bigint } = {}): Route {\n\t\tconst hops = route.hops ? [...route.hops] : [];\n\t\tconst cost = route.cost;\n\t\tif (cost === undefined) return { hops, cost: Cost.zero };\n\t\tif (typeof cost === \"bigint\") return { hops, cost: { warm: cost, cold: cost } };\n\t\treturn { hops, cost: { warm: cost.warm, cold: cost.cold } };\n\t},\n};\n\n/**\n * Whether this route passed through an anonymous hop (id 0) at any depth.\n *\n * An empty chain is a local announcement, not the anonymous mark. Ingress fills a\n * received empty list with {@link UNKNOWN_HOP} before yielding it.\n */\nexport function isAnonymous(route: Route): boolean {\n\treturn route.hops.includes(UNKNOWN_HOP);\n}\n\n/** Whether two routes name the same hop chain and cost. */\nexport function routesEqual(a: Route | undefined, b: Route | undefined): boolean {\n\tif (a === b) return true;\n\tif (!a || !b) return false;\n\treturn (\n\t\ta.cost.warm === b.cost.warm &&\n\t\ta.cost.cold === b.cost.cold &&\n\t\ta.hops.length === b.hops.length &&\n\t\ta.hops.every((hop, i) => hop === b.hops[i])\n\t);\n}\n"]}
package/ietf/adapter.d.ts CHANGED
@@ -62,6 +62,10 @@ export declare class ControlStreamAdapter implements Session {
62
62
  * Must be called after construction. Runs until the control stream closes.
63
63
  */
64
64
  run(): Promise<void>;
65
- close(): void;
65
+ /**
66
+ * Ends every virtual stream: cleanly for a deliberate close, or with `err` when the
67
+ * control stream died under them, since every request riding it was cut off.
68
+ */
69
+ close(err?: Error): void;
66
70
  }
67
71
  //# sourceMappingURL=adapter.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"adapter.d.ts","sourceRoot":"","sources":["../../src/ietf/adapter.ts"],"names":[],"mappings":"AACA,OAAO,EAAU,MAAM,EAAe,MAAM,cAAc,CAAC;AAI3D,OAAO,EAAE,KAAK,WAAW,EAAW,MAAM,cAAc,CAAC;AAEzD;;;GAGG;AACH,MAAM,WAAW,OAAO;IACvB,MAAM,IAAI,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC;IACnC,YAAY,CAAC,IAAI,OAAO,CAAC,MAAM,CAAC,CAAC;IACjC,QAAQ,IAAI,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC,CAAC;IACxC,aAAa,IAAI,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC,CAAC;IAC7C,KAAK,IAAI,IAAI,CAAC;IACd,QAAQ,CAAC,OAAO,EAAE,WAAW,CAAC;CAC9B;AAED;;;GAGG;AACH,qBAAa,aAAc,YAAW,OAAO;;IAK5C,QAAQ,CAAC,OAAO,EAAE,WAAW,CAAC;IAE9B,YAAY,IAAI,EAAE,YAAY,EAAE,OAAO,EAAE,WAAW,EAAE,MAAM,EAAE,OAAO,EAIpE;IAEK,MAAM,IAAI,OAAO,CAAC,MAAM,CAAC,CAE9B;IAEK,QAAQ,IAAI,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC,CAE5C;IAEK,aAAa,IAAI,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC,CAIjD;IAED,kDAAkD;IAClD,KAAK,SAEJ;CACD;AAmBD;;;;;;GAMG;AACH,qBAAa,oBAAqB,YAAW,OAAO;;IAQnD,QAAQ,CAAC,OAAO,EAAE,WAAW,CAAC;IA+B9B,YACC,IAAI,EAAE,YAAY,EAClB,aAAa,EAAE,MAAM,EACrB,OAAO,EAAE,WAAW,EACpB,YAAY,EAAE,MAAM,EACpB,MAAM,EAAE,OAAO,EAUf;IAED;;;OAGG;IACG,QAAQ,IAAI,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC,CAS5C;IAED;;;;;OAKG;IACH,MAAM,IAAI,MAAM,CAsDf;IAED;;OAEG;IACG,YAAY,IAAI,OAAO,CAAC,MAAM,CAAC,CAEpC;IAED;;OAEG;IACG,aAAa,IAAI,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC,CAYjD;IAED;;;OAGG;IACG,GAAG,IAAI,OAAO,CAAC,IAAI,CAAC,CAmDzB;IAuYD,KAAK,SA6BJ;CACD"}
1
+ {"version":3,"file":"adapter.d.ts","sourceRoot":"","sources":["../../src/ietf/adapter.ts"],"names":[],"mappings":"AAEA,OAAO,EAAU,MAAM,EAAe,MAAM,cAAc,CAAC;AAG3D,OAAO,EAAE,KAAK,WAAW,EAAW,MAAM,cAAc,CAAC;AAEzD;;;GAGG;AACH,MAAM,WAAW,OAAO;IACvB,MAAM,IAAI,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC;IACnC,YAAY,CAAC,IAAI,OAAO,CAAC,MAAM,CAAC,CAAC;IACjC,QAAQ,IAAI,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC,CAAC;IACxC,aAAa,IAAI,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC,CAAC;IAC7C,KAAK,IAAI,IAAI,CAAC;IACd,QAAQ,CAAC,OAAO,EAAE,WAAW,CAAC;CAC9B;AAED;;;GAGG;AACH,qBAAa,aAAc,YAAW,OAAO;;IAK5C,QAAQ,CAAC,OAAO,EAAE,WAAW,CAAC;IAE9B,YAAY,IAAI,EAAE,YAAY,EAAE,OAAO,EAAE,WAAW,EAAE,MAAM,EAAE,OAAO,EAIpE;IAEK,MAAM,IAAI,OAAO,CAAC,MAAM,CAAC,CAE9B;IAEK,QAAQ,IAAI,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC,CAE5C;IAEK,aAAa,IAAI,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC,CAIjD;IAED,kDAAkD;IAClD,KAAK,SAEJ;CACD;AAqBD;;;;;;GAMG;AACH,qBAAa,oBAAqB,YAAW,OAAO;;IAQnD,QAAQ,CAAC,OAAO,EAAE,WAAW,CAAC;IAmC9B,YACC,IAAI,EAAE,YAAY,EAClB,aAAa,EAAE,MAAM,EACrB,OAAO,EAAE,WAAW,EACpB,YAAY,EAAE,MAAM,EACpB,MAAM,EAAE,OAAO,EAUf;IAED;;;OAGG;IACG,QAAQ,IAAI,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC,CAS5C;IAED;;;;;OAKG;IACH,MAAM,IAAI,MAAM,CAoDf;IAED;;OAEG;IACG,YAAY,IAAI,OAAO,CAAC,MAAM,CAAC,CAEpC;IAED;;OAEG;IACG,aAAa,IAAI,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC,CAYjD;IAED;;;OAGG;IACG,GAAG,IAAI,OAAO,CAAC,IAAI,CAAC,CA2DzB;IAqaD;;;OAGG;IACH,KAAK,CAAC,GAAG,CAAC,EAAE,KAAK,QA8BhB;CACD"}
package/ietf/adapter.js CHANGED
@@ -1,7 +1,7 @@
1
1
  /* @ts-self-types="./adapter.d.ts" */
2
2
  import { Mutex } from "async-mutex";
3
+ import { error, ProtocolViolation } from "../error.js";
3
4
  import { Reader, Stream } from "../stream.js";
4
- import { decodeUtf8 } from "../util/utf8.js";
5
5
  import * as Varint from "../varint.js";
6
6
  import * as Namespace from "./namespace.js";
7
7
  import { Version } from "./version.js";
@@ -40,7 +40,7 @@ export class NativeSession {
40
40
  const Route = {
41
41
  NewRequest: 0, // Create virtual bidi stream, push initial message
42
42
  Response: 1, // Push message to existing stream (keep open)
43
- ErrorResponse: 2, // Push message to existing stream, then close
43
+ ErrorResponse: 2, // Push a final message to existing stream, then close
44
44
  CloseStream: 3, // Close stream recv (no bytes pushed)
45
45
  FollowUp: 4, // Push follow-up message to existing stream
46
46
  MaxRequestId: 5, // Update flow control
@@ -64,9 +64,13 @@ export class ControlStreamAdapter {
64
64
  version;
65
65
  // Virtual streams keyed by requestId
66
66
  #streams = new Map();
67
- // Namespace → requestId reverse lookup (v14/v15 namespace-keyed messages)
68
- #namespaces = new Map();
69
- // requestId → namespace reverse lookup (for cleanup in #closeStream)
67
+ // Namespace → requestId reverse lookups for v14/v15 namespace-keyed messages.
68
+ // DONE names an incoming announcement and CANCEL names an outgoing one, so a relay
69
+ // may legitimately hold the same namespace in both maps at once.
70
+ #incomingNamespaces = new Map();
71
+ #outgoingNamespaces = new Map();
72
+ // requestId → namespace reverse lookup, one entry per request including a refused
73
+ // duplicate, so #forget can clean up whichever request goes away.
70
74
  #namespacesByRequestId = new Map();
71
75
  // SubscribeNamespace requestIds — for routing 0x08/0x0E entries that lack requestId (v14/v15)
72
76
  #subscribeNamespaces = new Set();
@@ -123,7 +127,7 @@ export class ControlStreamAdapter {
123
127
  },
124
128
  cancel: () => {
125
129
  if (registeredRequestId !== undefined) {
126
- this.#streams.delete(registeredRequestId);
130
+ this.#forget(registeredRequestId);
127
131
  }
128
132
  },
129
133
  });
@@ -146,7 +150,7 @@ export class ControlStreamAdapter {
146
150
  buffer = buffer.subarray(boundary);
147
151
  if (!registered) {
148
152
  // First message: extract requestId and register before flushing
149
- const parsed = this.#tryParseOutgoing(toFlush);
153
+ const parsed = await this.#tryParseOutgoing(toFlush);
150
154
  if (parsed) {
151
155
  registeredRequestId = parsed.requestId;
152
156
  this.#streams.set(parsed.requestId, { controller });
@@ -157,9 +161,7 @@ export class ControlStreamAdapter {
157
161
  }
158
162
  },
159
163
  });
160
- const stream = new Stream({ readable, writable: sendWritable });
161
- stream.reader.version = this.version;
162
- stream.writer.version = this.version;
164
+ const stream = new Stream({ readable, writable: sendWritable, version: this.version });
163
165
  return stream;
164
166
  }
165
167
  /**
@@ -190,6 +192,8 @@ export class ControlStreamAdapter {
190
192
  * Must be called after construction. Runs until the control stream closes.
191
193
  */
192
194
  async run() {
195
+ // Why the virtual streams end: undefined only for a GOAWAY, which is not a failure.
196
+ let cause;
193
197
  try {
194
198
  // v16: also accept real bidi streams (for SubscribeNamespace)
195
199
  if (this.version === Version.DRAFT_16) {
@@ -197,8 +201,10 @@ export class ControlStreamAdapter {
197
201
  }
198
202
  for (;;) {
199
203
  const done = await this.#reader.done();
200
- if (done)
204
+ if (done) {
205
+ cause = new ProtocolViolation("control stream closed");
201
206
  break;
207
+ }
202
208
  const typeId = await this.#reader.u53();
203
209
  const size = await this.#reader.u16();
204
210
  const body = await this.#reader.read(size);
@@ -234,8 +240,12 @@ export class ControlStreamAdapter {
234
240
  }
235
241
  }
236
242
  }
243
+ catch (err) {
244
+ cause = error(err);
245
+ throw err;
246
+ }
237
247
  finally {
238
- this.close();
248
+ this.close(cause);
239
249
  }
240
250
  }
241
251
  /** Accept real WebTransport bidi streams and queue them for acceptBi (v16). */
@@ -265,13 +275,11 @@ export class ControlStreamAdapter {
265
275
  controller = c;
266
276
  },
267
277
  cancel: () => {
268
- this.#streams.delete(requestId);
278
+ this.#forget(requestId);
269
279
  },
270
280
  });
271
281
  const sendWritable = this.#createSendWritable();
272
- const stream = new Stream({ readable, writable: sendWritable });
273
- stream.reader.version = this.version;
274
- stream.writer.version = this.version;
282
+ const stream = new Stream({ readable, writable: sendWritable, version: this.version });
275
283
  this.#streams.set(requestId, { controller });
276
284
  // Push initial message bytes so the dispatcher can read typeId + decode
277
285
  controller.enqueue(this.#encodeRaw(typeId, size, body));
@@ -299,16 +307,10 @@ export class ControlStreamAdapter {
299
307
  }
300
308
  #closeStream(requestId) {
301
309
  const entry = this.#streams.get(requestId);
310
+ this.#forget(requestId);
302
311
  if (!entry)
303
312
  return;
304
313
  console.debug(`adapter: closing stream requestId=${requestId}`);
305
- this.#streams.delete(requestId);
306
- this.#subscribeNamespaces.delete(requestId);
307
- const namespace = this.#namespacesByRequestId.get(requestId);
308
- if (namespace !== undefined) {
309
- this.#namespaces.delete(namespace);
310
- this.#namespacesByRequestId.delete(requestId);
311
- }
312
314
  try {
313
315
  entry.controller.close();
314
316
  }
@@ -316,6 +318,26 @@ export class ControlStreamAdapter {
316
318
  // Already closed
317
319
  }
318
320
  }
321
+ /**
322
+ * Drop every routing entry a request owns, keeping the two namespace maps in step.
323
+ *
324
+ * Runs even when the virtual stream is already gone, so a request that was torn down
325
+ * locally still gives its namespace back.
326
+ */
327
+ #forget(requestId) {
328
+ this.#streams.delete(requestId);
329
+ this.#subscribeNamespaces.delete(requestId);
330
+ const registered = this.#namespacesByRequestId.get(requestId);
331
+ if (registered === undefined)
332
+ return;
333
+ this.#namespacesByRequestId.delete(requestId);
334
+ const namespaces = this.#namespaceRequests(registered.direction);
335
+ // Only the request holding the live announcement may retract it. A refused duplicate
336
+ // carries the same name, and letting its close withdraw the name would strand the
337
+ // original: its own DONE would then resolve to nothing.
338
+ if (namespaces.get(registered.namespace) === requestId)
339
+ namespaces.delete(registered.namespace);
340
+ }
319
341
  /**
320
342
  * Returns the total byte size of the first complete message in buffer,
321
343
  * or undefined if the buffer doesn't contain a complete message yet.
@@ -340,7 +362,7 @@ export class ControlStreamAdapter {
340
362
  * Try to parse the first outgoing message from accumulated bytes.
341
363
  * Returns the requestId if enough data is available, undefined otherwise.
342
364
  */
343
- #tryParseOutgoing(buffer) {
365
+ async #tryParseOutgoing(buffer) {
344
366
  if (buffer.length === 0)
345
367
  return undefined;
346
368
  // Check typeId varint size before decoding
@@ -363,8 +385,7 @@ export class ControlStreamAdapter {
363
385
  // PublishNamespace (0x06): also parse namespace for v14/v15 reverse lookup
364
386
  if (typeId === 0x06) {
365
387
  try {
366
- const [, afterReqId] = Varint.decode(body);
367
- this.#parseAndRegisterNamespace(afterReqId, requestId);
388
+ await this.#registerNamespace(body, "outgoing");
368
389
  }
369
390
  catch {
370
391
  // Non-critical: only needed for v14/v15 PublishNamespaceDone/Cancel
@@ -377,20 +398,28 @@ export class ControlStreamAdapter {
377
398
  return { requestId };
378
399
  }
379
400
  /**
380
- * Parse a namespace from raw bytes and register it for reverse lookup.
401
+ * Decode a PublishNamespace body and register its namespace for the v14/v15
402
+ * namespace-keyed withdrawals. Returns the request ID it carried.
403
+ *
404
+ * The one decode site for both directions: outgoing announcements go through
405
+ * #tryParseOutgoing and incoming ones through #classify. The direction selects the
406
+ * map that the peer's DONE or CANCEL will later consult.
381
407
  */
382
- #parseAndRegisterNamespace(buf, requestId) {
383
- const [partCount, afterCount] = Varint.decode(buf);
384
- let cursor = afterCount;
385
- const parts = [];
386
- for (let i = 0; i < partCount; i++) {
387
- const [len, afterLen] = Varint.decode(cursor);
388
- parts.push(decodeUtf8(afterLen.subarray(0, len)));
389
- cursor = afterLen.subarray(len);
390
- }
391
- const namespace = Namespace.fromTuple(parts);
392
- this.#namespaces.set(namespace, requestId);
393
- this.#namespacesByRequestId.set(requestId, namespace);
408
+ async #registerNamespace(body, direction) {
409
+ const r = new Reader(undefined, body, this.version);
410
+ const requestId = await r.u62();
411
+ const namespace = await Namespace.decode(r);
412
+ const namespaces = this.#namespaceRequests(direction);
413
+ this.#namespacesByRequestId.set(requestId, { namespace, direction });
414
+ // First announcement in this direction wins. A duplicate is refused with 409, and
415
+ // overwriting here would point the first request's withdrawal at the refused one,
416
+ // which has no stream left, leaving the namespace announced forever.
417
+ if (!namespaces.has(namespace))
418
+ namespaces.set(namespace, requestId);
419
+ return requestId;
420
+ }
421
+ #namespaceRequests(direction) {
422
+ return direction === "incoming" ? this.#incomingNamespaces : this.#outgoingNamespaces;
394
423
  }
395
424
  /** Create a WritableStream that buffers and writes complete messages to the control stream under mutex. */
396
425
  #createSendWritable() {
@@ -431,13 +460,17 @@ export class ControlStreamAdapter {
431
460
  const r = new Reader(undefined, body, this.version);
432
461
  return await r.u62();
433
462
  };
434
- const readNamespaceRequestId = async () => {
463
+ // v14/v15 name their withdrawals instead of numbering them. A name we have no
464
+ // announcement for is dropped, not fatal: it belongs to a duplicate we already
465
+ // refused, or to a request that is already gone, so there is nothing left to close.
466
+ // The mapping itself is released by #forget when the stream goes.
467
+ const readNamespaceRequestId = async (direction) => {
435
468
  const r = new Reader(undefined, body, this.version);
436
469
  const namespace = await Namespace.decode(r);
437
- const requestId = this.#namespaces.get(namespace);
470
+ const namespaces = this.#namespaceRequests(direction);
471
+ const requestId = namespaces.get(namespace);
438
472
  if (requestId === undefined)
439
- throw new Error(`unknown namespace: ${namespace}`);
440
- this.#namespaces.delete(namespace);
473
+ console.warn(`adapter: no announcement for namespace: ${namespace}`);
441
474
  return requestId;
442
475
  };
443
476
  switch (typeId) {
@@ -457,12 +490,8 @@ export class ControlStreamAdapter {
457
490
  return { route: Route.NewRequest, requestId };
458
491
  }
459
492
  case 0x06: {
460
- // PublishNamespace — also store namespace for v14/v15 reverse lookup
461
- const r = new Reader(undefined, body, this.version);
462
- const requestId = await r.u62();
463
- const namespace = await Namespace.decode(r);
464
- this.#namespaces.set(namespace, requestId);
465
- this.#namespacesByRequestId.set(requestId, namespace);
493
+ // PublishNamespace: also store the namespace for v14/v15 reverse lookup
494
+ const requestId = await this.#registerNamespace(body, "incoming");
466
495
  return { route: Route.NewRequest, requestId };
467
496
  }
468
497
  case 0x11: {
@@ -534,7 +563,10 @@ export class ControlStreamAdapter {
534
563
  return { route: Route.FollowUp, requestId: subNs08 };
535
564
  }
536
565
  case 0x0e: {
537
- // v15: NamespaceDone entry (no requestId) — route to SubscribeNamespace stream
566
+ if (this.version === Version.DRAFT_14 || this.version === Version.DRAFT_15) {
567
+ throw new Error("unexpected message 0x0e");
568
+ }
569
+ // v16+: NamespaceDone entry (no requestId) — route to SubscribeNamespace stream
538
570
  const subNs0e = this.#subscribeNamespaces.values().next().value;
539
571
  if (subNs0e === undefined)
540
572
  throw new Error("unexpected message 0x0e: no SubscribeNamespace stream");
@@ -554,9 +586,9 @@ export class ControlStreamAdapter {
554
586
  return { route: Route.CloseStream, requestId };
555
587
  }
556
588
  case 0x0b: {
557
- // PublishDone
589
+ // PublishDone: the subscriber reads its status and stream count before the end.
558
590
  const requestId = await readRequestId();
559
- return { route: Route.CloseStream, requestId };
591
+ return { route: Route.ErrorResponse, requestId };
560
592
  }
561
593
  case 0x17: {
562
594
  // FetchCancel
@@ -564,21 +596,27 @@ export class ControlStreamAdapter {
564
596
  return { route: Route.CloseStream, requestId };
565
597
  }
566
598
  case 0x09: {
567
- // PublishNamespaceDone: v16 uses requestId, v14/v15 uses namespace
599
+ // PublishNamespaceDone: v16 uses requestId, v14/v15 names the incoming
600
+ // announcement its sender is withdrawing.
568
601
  if (this.version === Version.DRAFT_16) {
569
602
  const requestId = await readRequestId();
570
603
  return { route: Route.CloseStream, requestId };
571
604
  }
572
- const requestId = await readNamespaceRequestId();
605
+ const requestId = await readNamespaceRequestId("incoming");
606
+ if (requestId === undefined)
607
+ return { route: Route.Ignore, requestId: 0n };
573
608
  return { route: Route.CloseStream, requestId };
574
609
  }
575
610
  case 0x0c: {
576
- // PublishNamespaceCancel: v16 uses requestId, v14/v15 uses namespace
611
+ // PublishNamespaceCancel: v16 uses requestId, v14/v15 names the outgoing
612
+ // announcement its sender is rejecting.
577
613
  if (this.version === Version.DRAFT_16) {
578
614
  const requestId = await readRequestId();
579
615
  return { route: Route.CloseStream, requestId };
580
616
  }
581
- const requestId = await readNamespaceRequestId();
617
+ const requestId = await readNamespaceRequestId("outgoing");
618
+ if (requestId === undefined)
619
+ return { route: Route.Ignore, requestId: 0n };
582
620
  return { route: Route.CloseStream, requestId };
583
621
  }
584
622
  case 0x14: {
@@ -607,15 +645,21 @@ export class ControlStreamAdapter {
607
645
  throw new Error(`unknown control message type: 0x${typeId.toString(16)}`);
608
646
  }
609
647
  }
610
- close() {
648
+ /**
649
+ * Ends every virtual stream: cleanly for a deliberate close, or with `err` when the
650
+ * control stream died under them, since every request riding it was cut off.
651
+ */
652
+ close(err) {
611
653
  if (this.#closed)
612
654
  return;
613
655
  this.#closed = true;
614
656
  console.debug("adapter: close() called");
615
- // Close all virtual streams
616
657
  for (const entry of this.#streams.values()) {
617
658
  try {
618
- entry.controller.close();
659
+ if (err)
660
+ entry.controller.error(err);
661
+ else
662
+ entry.controller.close();
619
663
  }
620
664
  catch {
621
665
  // Already closed
@@ -628,7 +672,8 @@ export class ControlStreamAdapter {
628
672
  }
629
673
  this.#incomingWaiters = [];
630
674
  // Clear namespace mappings
631
- this.#namespaces.clear();
675
+ this.#incomingNamespaces.clear();
676
+ this.#outgoingNamespaces.clear();
632
677
  this.#namespacesByRequestId.clear();
633
678
  this.#subscribeNamespaces.clear();
634
679
  // Unblock maxRequestId waiters