@norskvideo/moq-net 0.1.3 → 0.1.4

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 (310) hide show
  1. package/README.md +1 -0
  2. package/announced.d.ts +111 -21
  3. package/announced.d.ts.map +1 -1
  4. package/announced.js +213 -40
  5. package/announced.js.map +1 -1
  6. package/broadcast.d.ts +80 -25
  7. package/broadcast.d.ts.map +1 -1
  8. package/broadcast.js +270 -49
  9. package/broadcast.js.map +1 -1
  10. package/connection/accept.d.ts +6 -0
  11. package/connection/accept.d.ts.map +1 -1
  12. package/connection/accept.js +42 -13
  13. package/connection/accept.js.map +1 -1
  14. package/connection/browser.d.ts +5 -0
  15. package/connection/browser.d.ts.map +1 -0
  16. package/connection/browser.js +31 -0
  17. package/connection/browser.js.map +1 -0
  18. package/connection/connect.d.ts +43 -1
  19. package/connection/connect.d.ts.map +1 -1
  20. package/connection/connect.js +118 -123
  21. package/connection/connect.js.map +1 -1
  22. package/connection/established.d.ts +43 -16
  23. package/connection/established.d.ts.map +1 -1
  24. package/connection/established.js.map +1 -1
  25. package/connection/handshake.d.ts +12 -1
  26. package/connection/handshake.d.ts.map +1 -1
  27. package/connection/handshake.js +30 -14
  28. package/connection/handshake.js.map +1 -1
  29. package/connection/index.d.ts +8 -0
  30. package/connection/index.d.ts.map +1 -1
  31. package/connection/index.js +6 -0
  32. package/connection/index.js.map +1 -1
  33. package/connection/reload.d.ts +66 -9
  34. package/connection/reload.d.ts.map +1 -1
  35. package/connection/reload.js +192 -66
  36. package/connection/reload.js.map +1 -1
  37. package/connection/stats.d.ts +92 -0
  38. package/connection/stats.d.ts.map +1 -0
  39. package/connection/stats.js +35 -0
  40. package/connection/stats.js.map +1 -0
  41. package/connection/transport.d.ts +10 -0
  42. package/connection/transport.d.ts.map +1 -0
  43. package/connection/transport.js +12 -0
  44. package/connection/transport.js.map +1 -0
  45. package/consume.d.ts +44 -0
  46. package/consume.d.ts.map +1 -0
  47. package/consume.js +60 -0
  48. package/consume.js.map +1 -0
  49. package/datagram.d.ts +23 -0
  50. package/datagram.d.ts.map +1 -0
  51. package/datagram.js +3 -0
  52. package/datagram.js.map +1 -0
  53. package/error.d.ts +76 -0
  54. package/error.d.ts.map +1 -0
  55. package/error.js +107 -0
  56. package/error.js.map +1 -0
  57. package/group.d.ts +143 -38
  58. package/group.d.ts.map +1 -1
  59. package/group.js +270 -68
  60. package/group.js.map +1 -1
  61. package/ietf/adapter.d.ts +2 -2
  62. package/ietf/adapter.d.ts.map +1 -1
  63. package/ietf/adapter.js +20 -10
  64. package/ietf/adapter.js.map +1 -1
  65. package/ietf/aliases.d.ts +73 -5
  66. package/ietf/aliases.d.ts.map +1 -1
  67. package/ietf/aliases.js +121 -14
  68. package/ietf/aliases.js.map +1 -1
  69. package/ietf/cluster.d.ts +141 -0
  70. package/ietf/cluster.d.ts.map +1 -0
  71. package/ietf/cluster.js +204 -0
  72. package/ietf/cluster.js.map +1 -0
  73. package/ietf/connection.d.ts +39 -6
  74. package/ietf/connection.d.ts.map +1 -1
  75. package/ietf/connection.js +73 -12
  76. package/ietf/connection.js.map +1 -1
  77. package/ietf/fetch.d.ts +17 -0
  78. package/ietf/fetch.d.ts.map +1 -1
  79. package/ietf/fetch.js +21 -0
  80. package/ietf/fetch.js.map +1 -1
  81. package/ietf/filter.d.ts +87 -0
  82. package/ietf/filter.d.ts.map +1 -0
  83. package/ietf/filter.js +292 -0
  84. package/ietf/filter.js.map +1 -0
  85. package/ietf/index.d.ts +2 -0
  86. package/ietf/index.d.ts.map +1 -1
  87. package/ietf/index.js +3 -0
  88. package/ietf/index.js.map +1 -1
  89. package/ietf/namespace.d.ts +6 -0
  90. package/ietf/namespace.d.ts.map +1 -1
  91. package/ietf/namespace.js +27 -2
  92. package/ietf/namespace.js.map +1 -1
  93. package/ietf/object.d.ts +54 -6
  94. package/ietf/object.d.ts.map +1 -1
  95. package/ietf/object.js +178 -25
  96. package/ietf/object.js.map +1 -1
  97. package/ietf/parameters.d.ts +35 -10
  98. package/ietf/parameters.d.ts.map +1 -1
  99. package/ietf/parameters.js +236 -45
  100. package/ietf/parameters.js.map +1 -1
  101. package/ietf/priority.d.ts +5 -0
  102. package/ietf/priority.d.ts.map +1 -0
  103. package/ietf/priority.js +10 -0
  104. package/ietf/priority.js.map +1 -0
  105. package/ietf/properties.d.ts +13 -2
  106. package/ietf/properties.d.ts.map +1 -1
  107. package/ietf/properties.js +65 -8
  108. package/ietf/properties.js.map +1 -1
  109. package/ietf/publish.d.ts.map +1 -1
  110. package/ietf/publish.js +13 -3
  111. package/ietf/publish.js.map +1 -1
  112. package/ietf/publish_namespace.d.ts +16 -2
  113. package/ietf/publish_namespace.d.ts.map +1 -1
  114. package/ietf/publish_namespace.js +25 -6
  115. package/ietf/publish_namespace.js.map +1 -1
  116. package/ietf/publisher.d.ts +40 -7
  117. package/ietf/publisher.d.ts.map +1 -1
  118. package/ietf/publisher.js +751 -134
  119. package/ietf/publisher.js.map +1 -1
  120. package/ietf/request.js +1 -1
  121. package/ietf/request.js.map +1 -1
  122. package/ietf/solicit.d.ts +41 -0
  123. package/ietf/solicit.d.ts.map +1 -0
  124. package/ietf/solicit.js +47 -0
  125. package/ietf/solicit.js.map +1 -0
  126. package/ietf/subscribe.d.ts +41 -2
  127. package/ietf/subscribe.d.ts.map +1 -1
  128. package/ietf/subscribe.js +103 -22
  129. package/ietf/subscribe.js.map +1 -1
  130. package/ietf/subscribe_namespace.d.ts +21 -7
  131. package/ietf/subscribe_namespace.d.ts.map +1 -1
  132. package/ietf/subscribe_namespace.js +31 -14
  133. package/ietf/subscribe_namespace.js.map +1 -1
  134. package/ietf/subscriber.d.ts +20 -7
  135. package/ietf/subscriber.d.ts.map +1 -1
  136. package/ietf/subscriber.js +512 -142
  137. package/ietf/subscriber.js.map +1 -1
  138. package/ietf/version.d.ts +7 -1
  139. package/ietf/version.d.ts.map +1 -1
  140. package/ietf/version.js +7 -0
  141. package/ietf/version.js.map +1 -1
  142. package/index.d.ts +10 -5
  143. package/index.d.ts.map +1 -1
  144. package/index.js +10 -5
  145. package/index.js.map +1 -1
  146. package/internal.d.ts +29 -0
  147. package/internal.d.ts.map +1 -0
  148. package/internal.js +8 -0
  149. package/internal.js.map +1 -0
  150. package/lite/announce.d.ts +62 -15
  151. package/lite/announce.d.ts.map +1 -1
  152. package/lite/announce.js +230 -94
  153. package/lite/announce.js.map +1 -1
  154. package/lite/connection.d.ts +69 -20
  155. package/lite/connection.d.ts.map +1 -1
  156. package/lite/connection.js +128 -81
  157. package/lite/connection.js.map +1 -1
  158. package/lite/datagram.d.ts +23 -0
  159. package/lite/datagram.d.ts.map +1 -0
  160. package/lite/datagram.js +57 -0
  161. package/lite/datagram.js.map +1 -0
  162. package/lite/datagram_stream.d.ts +8 -0
  163. package/lite/datagram_stream.d.ts.map +1 -0
  164. package/lite/datagram_stream.js +44 -0
  165. package/lite/datagram_stream.js.map +1 -0
  166. package/lite/fetch.d.ts.map +1 -1
  167. package/lite/fetch.js +2 -2
  168. package/lite/fetch.js.map +1 -1
  169. package/lite/index.d.ts +3 -0
  170. package/lite/index.d.ts.map +1 -1
  171. package/lite/index.js +3 -0
  172. package/lite/index.js.map +1 -1
  173. package/lite/priority.d.ts +67 -0
  174. package/lite/priority.d.ts.map +1 -0
  175. package/lite/priority.js +114 -0
  176. package/lite/priority.js.map +1 -0
  177. package/lite/probe.d.ts +18 -2
  178. package/lite/probe.d.ts.map +1 -1
  179. package/lite/probe.js +24 -22
  180. package/lite/probe.js.map +1 -1
  181. package/lite/publisher.d.ts +14 -7
  182. package/lite/publisher.d.ts.map +1 -1
  183. package/lite/publisher.js +406 -84
  184. package/lite/publisher.js.map +1 -1
  185. package/lite/setup.d.ts +83 -6
  186. package/lite/setup.d.ts.map +1 -1
  187. package/lite/setup.js +203 -31
  188. package/lite/setup.js.map +1 -1
  189. package/lite/stream.d.ts +12 -3
  190. package/lite/stream.d.ts.map +1 -1
  191. package/lite/stream.js +7 -4
  192. package/lite/stream.js.map +1 -1
  193. package/lite/subscribe.d.ts +37 -23
  194. package/lite/subscribe.d.ts.map +1 -1
  195. package/lite/subscribe.js +70 -55
  196. package/lite/subscribe.js.map +1 -1
  197. package/lite/subscriber.d.ts +29 -23
  198. package/lite/subscriber.d.ts.map +1 -1
  199. package/lite/subscriber.js +689 -112
  200. package/lite/subscriber.js.map +1 -1
  201. package/lite/track.d.ts +25 -13
  202. package/lite/track.d.ts.map +1 -1
  203. package/lite/track.js +60 -35
  204. package/lite/track.js.map +1 -1
  205. package/lite/version.d.ts +39 -4
  206. package/lite/version.d.ts.map +1 -1
  207. package/lite/version.js +139 -17
  208. package/lite/version.js.map +1 -1
  209. package/mock.d.ts +53 -3
  210. package/mock.d.ts.map +1 -1
  211. package/mock.js +110 -21
  212. package/mock.js.map +1 -1
  213. package/{lite/origin.d.ts → origin.d.ts} +25 -1
  214. package/origin.d.ts.map +1 -0
  215. package/{lite/origin.js → origin.js} +25 -1
  216. package/origin.js.map +1 -0
  217. package/package.json +6 -4
  218. package/path.d.ts +67 -0
  219. package/path.d.ts.map +1 -1
  220. package/path.js +128 -0
  221. package/path.js.map +1 -1
  222. package/stream.d.ts +94 -4
  223. package/stream.d.ts.map +1 -1
  224. package/stream.js +128 -17
  225. package/stream.js.map +1 -1
  226. package/time.d.ts +109 -86
  227. package/time.d.ts.map +1 -1
  228. package/time.js +80 -30
  229. package/time.js.map +1 -1
  230. package/track.d.ts +298 -47
  231. package/track.d.ts.map +1 -1
  232. package/track.js +721 -109
  233. package/track.js.map +1 -1
  234. package/util/timeout.d.ts +5 -0
  235. package/util/timeout.d.ts.map +1 -0
  236. package/util/timeout.js +18 -0
  237. package/util/timeout.js.map +1 -0
  238. package/util/utf8.d.ts +3 -0
  239. package/util/utf8.d.ts.map +1 -0
  240. package/util/utf8.js +7 -0
  241. package/util/utf8.js.map +1 -0
  242. package/varint.d.ts +14 -3
  243. package/varint.d.ts.map +1 -1
  244. package/varint.js +23 -12
  245. package/varint.js.map +1 -1
  246. package/zod.d.ts +5 -5
  247. package/zod.d.ts.map +1 -1
  248. package/zod.js.map +1 -1
  249. package/bandwidth.d.ts +0 -11
  250. package/bandwidth.d.ts.map +0 -1
  251. package/bandwidth.js +0 -7
  252. package/bandwidth.js.map +0 -1
  253. package/connection/cert.test.d.ts +0 -2
  254. package/connection/cert.test.d.ts.map +0 -1
  255. package/connection/cert.test.js +0 -26
  256. package/connection/cert.test.js.map +0 -1
  257. package/connection/reload.test.d.ts +0 -2
  258. package/connection/reload.test.d.ts.map +0 -1
  259. package/connection/reload.test.js +0 -39
  260. package/connection/reload.test.js.map +0 -1
  261. package/group.test.d.ts +0 -2
  262. package/group.test.d.ts.map +0 -1
  263. package/group.test.js +0 -66
  264. package/group.test.js.map +0 -1
  265. package/ietf/aliases.test.d.ts +0 -2
  266. package/ietf/aliases.test.d.ts.map +0 -1
  267. package/ietf/aliases.test.js +0 -30
  268. package/ietf/aliases.test.js.map +0 -1
  269. package/ietf/ietf.test.d.ts +0 -2
  270. package/ietf/ietf.test.d.ts.map +0 -1
  271. package/ietf/ietf.test.js +0 -873
  272. package/ietf/ietf.test.js.map +0 -1
  273. package/integration.test.d.ts +0 -2
  274. package/integration.test.d.ts.map +0 -1
  275. package/integration.test.js +0 -90
  276. package/integration.test.js.map +0 -1
  277. package/lite/origin.d.ts.map +0 -1
  278. package/lite/origin.js.map +0 -1
  279. package/lite/subscriber.test.d.ts +0 -2
  280. package/lite/subscriber.test.d.ts.map +0 -1
  281. package/lite/subscriber.test.js +0 -30
  282. package/lite/subscriber.test.js.map +0 -1
  283. package/lite/track.test.d.ts +0 -2
  284. package/lite/track.test.d.ts.map +0 -1
  285. package/lite/track.test.js +0 -53
  286. package/lite/track.test.js.map +0 -1
  287. package/path.test.d.ts +0 -2
  288. package/path.test.d.ts.map +0 -1
  289. package/path.test.js +0 -169
  290. package/path.test.js.map +0 -1
  291. package/stream.test.d.ts +0 -2
  292. package/stream.test.d.ts.map +0 -1
  293. package/stream.test.js +0 -210
  294. package/stream.test.js.map +0 -1
  295. package/track.test.d.ts +0 -2
  296. package/track.test.d.ts.map +0 -1
  297. package/track.test.js +0 -56
  298. package/track.test.js.map +0 -1
  299. package/util/error.d.ts +0 -3
  300. package/util/error.d.ts.map +0 -1
  301. package/util/error.js +0 -9
  302. package/util/error.js.map +0 -1
  303. package/util/index.d.ts +0 -2
  304. package/util/index.d.ts.map +0 -1
  305. package/util/index.js +0 -3
  306. package/util/index.js.map +0 -1
  307. package/varint.test.d.ts +0 -2
  308. package/varint.test.d.ts.map +0 -1
  309. package/varint.test.js +0 -106
  310. package/varint.test.js.map +0 -1
@@ -1,7 +1,16 @@
1
1
  /* @ts-self-types="./reload.d.ts" */
2
2
  import { Effect, Signal } from "@norskvideo/moq-signals";
3
+ import * as Announce from "../announced.js";
4
+ import { error } from "../error.js";
3
5
  import { empty as emptyPath } from "../path.js";
4
6
  import { connect } from "./connect.js";
7
+ /**
8
+ * How long to keep retrying before giving up, when {@link ReloadDelay.timeout} is unset.
9
+ *
10
+ * Short on purpose: a failure that clears within it was transient, and one that doesn't should
11
+ * surface as an error rather than leave the page silently reconnecting for minutes.
12
+ */
13
+ const DEFAULT_TIMEOUT = 10000;
5
14
  /** Maintains a MoQ connection, reconnecting with exponential backoff when it drops. */
6
15
  export class Reload {
7
16
  /** Relay URL to connect to; updating it triggers a reconnect. */
@@ -12,130 +21,247 @@ export class Reload {
12
21
  status = new Signal("disconnected");
13
22
  /** The currently established session, or undefined while disconnected. */
14
23
  established = new Signal(undefined);
15
- // All actively announced broadcast paths, updated reactively.
16
- #announced = new Signal(new Set());
17
- /** The set of broadcast paths currently announced by the server, updated reactively. */
18
- announced = this.#announced;
24
+ /**
25
+ * The current connection's PROBE estimates, spanning reconnects.
26
+ *
27
+ * Undefined while disconnected: the estimates belong to a single connection.
28
+ * See {@link Established.probe}.
29
+ */
30
+ probe;
19
31
  /** WebTransport options applied to each connection attempt (not reactive). */
20
32
  webtransport;
21
33
  /** WebSocket fallback options applied to each connection attempt (not reactive). */
22
34
  websocket;
35
+ /**
36
+ * Whether the relay supports broadcast discovery, applied to each connection attempt (not
37
+ * reactive). Undefined defers to the default for the URL. See {@link Established.discovery}.
38
+ */
39
+ discovery;
23
40
  /** Backoff settings for the reconnect loop. */
24
41
  delay;
25
42
  /** The reactive effect scope driving the connect loop; closed by {@link Reload.close}. */
26
- signals = new Effect();
27
- /** Resolves when the reconnect loop stops via {@link Reload.close} or the retry timeout. */
43
+ #signals = new Effect();
44
+ /**
45
+ * Resolves when the reconnect loop stops via {@link Reload.close}.
46
+ *
47
+ * Rejects when the loop gives up instead, carrying the failure that was in flight when the
48
+ * retry window expired.
49
+ */
28
50
  closed;
29
51
  #closedResolve;
30
52
  #closedReject;
53
+ // The current wait between attempts, doubling per failure, and when the retry window expires.
54
+ // Both are undefined between sequences, so a later edit to `delay` applies to the next one.
31
55
  #delay;
32
- // Timestamp when the current retry sequence started (for timeout).
33
- #retryStart;
56
+ #deadline;
34
57
  // Increased by 1 each time to trigger a reload.
35
58
  #tick = new Signal(0);
59
+ // True after the browser freezes or hides the page until it visibly resumes.
60
+ #suspended = new Signal(false);
36
61
  // Use the serialized URL as the reactive connection key. URL objects use identity
37
62
  // equality, but replacing one with an equivalent instance should not reconnect.
38
63
  #url;
39
64
  constructor(props) {
40
65
  this.url = Signal.from(props?.url);
41
66
  this.enabled = Signal.from(props?.enabled ?? false);
42
- this.delay = props?.delay ?? { initial: 1000, multiplier: 2, max: 30000 };
67
+ this.delay = props?.delay ?? { initial: 1000, multiplier: 2, max: 5000 };
43
68
  this.webtransport = props?.webtransport;
44
69
  this.websocket = props?.websocket;
45
- this.#delay = this.delay.initial;
70
+ this.discovery = props?.discovery;
46
71
  this.closed = new Promise((resolve, reject) => {
47
72
  this.#closedResolve = resolve;
48
73
  this.#closedReject = reject;
49
74
  });
50
- this.#url = this.signals.computed((effect) => effect.get(this.url)?.href);
75
+ // A caller is free to never await `closed`, and giving up rejects it unprompted. Marking the
76
+ // rejection handled here keeps that from surfacing as an `unhandledrejection`; a consumer
77
+ // awaiting the same promise still receives it.
78
+ this.closed.catch(() => { });
79
+ if (typeof window !== "undefined" && typeof document !== "undefined") {
80
+ this.#signals.event(window, "pagehide", () => this.#suspended.set(true));
81
+ this.#signals.event(window, "pageshow", () => this.#suspended.set(false));
82
+ this.#signals.event(window, "unload", () => this.#suspended.set(true));
83
+ this.#signals.event(document, "visibilitychange", () => {
84
+ if (!document.hidden)
85
+ this.#suspended.set(false);
86
+ });
87
+ }
88
+ this.probe = this.#signals.computed((effect) => {
89
+ const connection = effect.get(this.established);
90
+ return connection && effect.get(connection.probe);
91
+ });
92
+ this.#url = this.#signals.computed((effect) => effect.get(this.url)?.href);
51
93
  // Create a reactive root so cleanup is easier.
52
- this.signals.run(this.#connect.bind(this));
53
- this.signals.run(this.#runAnnounced.bind(this));
94
+ this.#signals.run(this.#connect.bind(this));
54
95
  }
55
96
  #connect(effect) {
56
97
  // Will retry when the tick changes.
57
98
  effect.get(this.#tick);
99
+ const suspended = effect.get(this.#suspended);
58
100
  const enabled = effect.get(this.enabled);
59
- if (!enabled)
101
+ if (!enabled || suspended)
60
102
  return;
61
103
  const href = effect.get(this.#url);
62
104
  if (!href)
63
105
  return;
64
106
  const url = new URL(href);
65
107
  effect.set(this.status, "connecting", "disconnected");
108
+ // This run's teardown, handed to connect() so a rerun cancels the attempt in flight.
109
+ const signal = effect.abort;
66
110
  effect.spawn(async () => {
111
+ // Set once the session is live, so #retry can tell a healthy session that
112
+ // later dropped from a connect failure or a peer that flaps immediately.
113
+ let connected;
67
114
  try {
68
- const pending = connect(url, { websocket: this.websocket, webtransport: this.webtransport });
69
- const connection = await Promise.race([effect.cancel, pending]);
70
- if (!connection) {
71
- pending.then((conn) => conn.close()).catch(() => { });
115
+ const connection = await connect(url, {
116
+ websocket: this.websocket,
117
+ webtransport: this.webtransport,
118
+ discovery: this.discovery,
119
+ signal,
120
+ });
121
+ // Hand the connection to the effect, which closes it now if this run is already over.
122
+ effect.cleanup(() => connection.close());
123
+ if (signal.aborted)
72
124
  return;
73
- }
74
125
  effect.set(this.established, connection);
75
- effect.cleanup(() => connection.close());
76
126
  effect.set(this.status, "connected", "disconnected");
77
- // Reset the exponential backoff and timeout on success.
78
- this.#delay = this.delay.initial;
79
- this.#retryStart = undefined;
80
- await Promise.race([effect.cancel, connection.closed]);
127
+ connected = performance.now();
128
+ // A cancelled effect resolves undefined, so the sentinel tells the session
129
+ // closing apart from this run being torn down.
130
+ const closed = await Promise.race([effect.cancel, connection.closed.then(() => true)]);
131
+ if (!closed)
132
+ return;
133
+ console.warn("connection closed, reconnecting");
134
+ this.#retry(effect, connected);
81
135
  }
82
136
  catch (err) {
137
+ // Treat teardown as cancellation, not a connection failure.
138
+ if (signal.aborted)
139
+ return;
83
140
  console.warn("connection error:", err);
84
- // Track retry start for timeout.
85
- this.#retryStart ??= performance.now();
86
- const timeout = this.delay.timeout ?? 300000;
87
- if (timeout > 0) {
88
- const elapsed = performance.now() - this.#retryStart;
89
- if (elapsed >= timeout) {
90
- console.warn("reconnect timed out");
91
- this.#closedReject(err instanceof Error ? err : new Error(String(err)));
92
- return;
93
- }
94
- }
95
- const tick = this.#tick.peek() + 1;
96
- effect.timer(() => this.#tick.update((prev) => Math.max(prev, tick)), this.#delay);
97
- this.#delay = Math.min(this.#delay * this.delay.multiplier, this.delay.max);
141
+ this.#retry(effect, connected, err);
98
142
  }
99
143
  });
100
144
  }
101
- #runAnnounced(effect) {
102
- this.#announced.set(new Set());
103
- const conn = effect.get(this.established);
104
- if (!conn)
105
- return;
106
- effect.cleanup(() => this.#announced.set(new Set()));
107
- // Cloudflare's relay does not yet support SUBSCRIBE_NAMESPACE, so
108
- // skip announce subscriptions entirely for those hosts.
109
- if (conn.url.hostname.endsWith("mediaoverquic.com")) {
145
+ /**
146
+ * Schedule the next connect attempt after the current backoff, or stop once the retry window
147
+ * has expired. `connected` is when the dead session was established, if it ever was, and
148
+ * `cause` the error that killed it, if it died with one.
149
+ */
150
+ #retry(effect, connected, cause) {
151
+ // Any session is dead now: report disconnected during the backoff rather than
152
+ // when the retry reruns the effect.
153
+ this.established.set(undefined);
154
+ this.status.set("disconnected");
155
+ // A session that outlived the initial delay was healthy, so clear the backoff and
156
+ // start a fresh retry window: a one-off drop should reconnect promptly. Anything
157
+ // shorter is a peer that accepts and immediately severs, which has to keep
158
+ // escalating or we hammer it forever at the initial delay.
159
+ if (connected !== undefined && performance.now() - connected >= this.delay.initial) {
160
+ this.#delay = undefined;
161
+ this.#deadline = undefined;
162
+ }
163
+ const now = performance.now();
164
+ const timeout = this.delay.timeout ?? DEFAULT_TIMEOUT;
165
+ this.#delay ??= this.delay.initial;
166
+ this.#deadline ??= timeout > 0 ? now + timeout : Number.POSITIVE_INFINITY;
167
+ if (now >= this.#deadline) {
168
+ console.warn("reconnect timed out");
169
+ // A graceful close has no error, so report the timeout itself.
170
+ this.#closedReject(cause === undefined ? new Error("reconnect timed out") : error(cause));
110
171
  return;
111
172
  }
112
- const announced = conn.announced(emptyPath());
113
- effect.cleanup(() => announced.close());
114
- effect.spawn(async () => {
115
- try {
116
- for (;;) {
117
- const entry = await Promise.race([effect.cancel, announced.next()]);
118
- if (!entry)
119
- break;
120
- this.#announced.mutate((active) => {
121
- if (entry.active) {
173
+ // Equal jitter, so a fleet of tabs knocked offline together doesn't reconnect on the same
174
+ // tick, and never past the deadline the retry window promised.
175
+ const wait = Math.min(this.#delay * (0.5 + Math.random() / 2), this.#deadline - now);
176
+ this.#delay = Math.min(this.#delay * this.delay.multiplier, this.delay.max);
177
+ const tick = this.#tick.peek() + 1;
178
+ effect.timer(() => this.#tick.update((prev) => Math.max(prev, tick)), wait);
179
+ }
180
+ /**
181
+ * Subscribe to broadcast announcements under an optional prefix, spanning reconnects.
182
+ *
183
+ * The same {@link Announce.Consumer} stream as {@link Established.announced}, but everything active
184
+ * is retracted (an `active: false` update) whenever the connection drops and re-announced on
185
+ * reconnect, so a consumer draining `next()` never clings to a dead route across a reconnect.
186
+ *
187
+ * Stays empty while the relay lacks {@link Established.discovery}.
188
+ */
189
+ announced(prefix = emptyPath()) {
190
+ const producer = new Announce.Producer(prefix);
191
+ const consumer = producer.consume();
192
+ // Closing the consumer closes the shared state, so stop appending after that.
193
+ let closed = false;
194
+ void consumer.closed.then(() => {
195
+ closed = true;
196
+ });
197
+ const pump = new Effect();
198
+ pump.run((effect) => {
199
+ const conn = effect.get(this.established);
200
+ if (!conn)
201
+ return;
202
+ // Without discovery the upstream announce stream never yields, so leave the
203
+ // consumer empty rather than opening a subscription that can't be answered.
204
+ if (!conn.discovery)
205
+ return;
206
+ const upstream = conn.announced(prefix);
207
+ effect.cleanup(() => upstream.close());
208
+ // Track what this connection announced so we can retract it if the connection drops.
209
+ const active = new Set();
210
+ effect.spawn(async () => {
211
+ try {
212
+ for (;;) {
213
+ const entry = await Promise.race([effect.cancel, upstream.next()]);
214
+ if (!entry)
215
+ break;
216
+ if (entry.active)
122
217
  active.add(entry.path);
123
- }
124
- else {
218
+ else
125
219
  active.delete(entry.path);
220
+ producer.append(entry);
221
+ }
222
+ }
223
+ catch {
224
+ // A dropped connection resets the announce stream; the retractions below cover it.
225
+ }
226
+ finally {
227
+ // Retract everything from the connection that just went away, so a per-broadcast
228
+ // watcher tears down instead of clinging to the dead route.
229
+ if (!closed) {
230
+ for (const path of active) {
231
+ producer.append({ path, active: false });
126
232
  }
127
- });
233
+ }
128
234
  }
129
- }
130
- catch (err) {
131
- this.#announced.set(new Set());
132
- throw err;
133
- }
235
+ });
134
236
  });
237
+ this.#signals.cleanup(() => pump.close());
238
+ void consumer.closed.then(() => pump.close());
239
+ return consumer;
240
+ }
241
+ /**
242
+ * A reactive handle to one broadcast, spanning reconnects.
243
+ *
244
+ * The same {@link Announce.Broadcast} as {@link Established.announcedBroadcast}, but it
245
+ * follows the reconnect loop: the broadcast drops to `undefined` when the connection dies
246
+ * and resolves again once the new connection announces the path. Use it instead of
247
+ * consuming off {@link Reload.established} whenever the broadcast may come online after you
248
+ * do, which is exactly the case a blind `consume` loses.
249
+ *
250
+ * Close the handle when done; {@link Reload.close} only drops it to `undefined`.
251
+ */
252
+ announcedBroadcast(path) {
253
+ return new Announce.Broadcast({ connection: this.established, path });
254
+ }
255
+ /**
256
+ * Snapshot the live connection's transport counters, or undefined while disconnected.
257
+ * See {@link Established.stats}.
258
+ */
259
+ async stats() {
260
+ return this.established.peek()?.stats();
135
261
  }
136
262
  /** Stop reconnecting, close the current connection, and resolve {@link Reload.closed}. */
137
263
  close() {
138
- this.signals.close();
264
+ this.#signals.close();
139
265
  this.#closedResolve();
140
266
  }
141
267
  }
@@ -1 +1 @@
1
- {"version":3,"file":"reload.js","sourceRoot":"","sources":["../../src/connection/reload.ts"],"names":[],"mappings":"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","sourcesContent":["import { Effect, type Getter, Signal } from \"@moq/signals\";\nimport type * as Path from \"../path.ts\";\nimport { empty as emptyPath } from \"../path.ts\";\nimport { type ConnectProps, connect, type WebSocketOptions, type WebTransportProps } from \"./connect.ts\";\nimport type { Established } from \"./established.ts\";\n\n/** Exponential backoff settings for {@link Reload}'s reconnect loop. */\nexport type ReloadDelay = {\n\t// The delay in milliseconds before reconnecting.\n\t// default: 1000\n\tinitial: DOMHighResTimeStamp;\n\n\t// The multiplier for the delay.\n\t// default: 2\n\tmultiplier: number;\n\n\t// The maximum delay in milliseconds.\n\t// default: 30000\n\tmax: DOMHighResTimeStamp;\n\n\t// Maximum total time in milliseconds to spend retrying before giving up.\n\t// Resets after each successful connection. Set to 0 for unlimited retries.\n\t// default: 300000 (5 minutes)\n\ttimeout?: DOMHighResTimeStamp;\n};\n\n/** Options for {@link Reload}: connect options plus reactive URL/enabled signals and backoff tuning. */\nexport type ReloadProps = ConnectProps & {\n\t// Whether to reload the connection when it disconnects.\n\t// default: true\n\tenabled?: boolean | Signal<boolean>;\n\n\t// The URL of the relay server.\n\turl?: URL | Signal<URL | undefined>;\n\n\t// The delay for the reload.\n\tdelay?: ReloadDelay;\n};\n\n/** Current state of a {@link Reload} connection. */\nexport type ReloadStatus = \"connecting\" | \"connected\" | \"disconnected\";\n\n/** Maintains a MoQ connection, reconnecting with exponential backoff when it drops. */\nexport class Reload {\n\t/** Relay URL to connect to; updating it triggers a reconnect. */\n\turl: Signal<URL | undefined>;\n\n\t/** Whether reconnecting is active. */\n\tenabled: Signal<boolean>;\n\n\t/** Current connection status. */\n\tstatus = new Signal<ReloadStatus>(\"disconnected\");\n\n\t/** The currently established session, or undefined while disconnected. */\n\testablished = new Signal<Established | undefined>(undefined);\n\n\t// All actively announced broadcast paths, updated reactively.\n\t#announced = new Signal<Set<Path.Valid>>(new Set());\n\n\t/** The set of broadcast paths currently announced by the server, updated reactively. */\n\treadonly announced: Getter<Set<Path.Valid>> = this.#announced;\n\n\t/** WebTransport options applied to each connection attempt (not reactive). */\n\twebtransport?: WebTransportProps;\n\n\t/** WebSocket fallback options applied to each connection attempt (not reactive). */\n\twebsocket: WebSocketOptions | undefined;\n\n\t/** Backoff settings for the reconnect loop. */\n\tdelay: ReloadDelay;\n\n\t/** The reactive effect scope driving the connect loop; closed by {@link Reload.close}. */\n\tsignals = new Effect();\n\n\t/** Resolves when the reconnect loop stops via {@link Reload.close} or the retry timeout. */\n\tclosed: Promise<void>;\n\t#closedResolve!: () => void;\n\t#closedReject!: (err: Error) => void;\n\n\t#delay: DOMHighResTimeStamp;\n\n\t// Timestamp when the current retry sequence started (for timeout).\n\t#retryStart: DOMHighResTimeStamp | undefined;\n\n\t// Increased by 1 each time to trigger a reload.\n\t#tick = new Signal(0);\n\n\t// Use the serialized URL as the reactive connection key. URL objects use identity\n\t// equality, but replacing one with an equivalent instance should not reconnect.\n\t#url: Getter<string | undefined>;\n\n\tconstructor(props?: ReloadProps) {\n\t\tthis.url = Signal.from(props?.url);\n\t\tthis.enabled = Signal.from(props?.enabled ?? false);\n\t\tthis.delay = props?.delay ?? { initial: 1000, multiplier: 2, max: 30000 };\n\t\tthis.webtransport = props?.webtransport;\n\t\tthis.websocket = props?.websocket;\n\n\t\tthis.#delay = this.delay.initial;\n\n\t\tthis.closed = new Promise((resolve, reject) => {\n\t\t\tthis.#closedResolve = resolve;\n\t\t\tthis.#closedReject = reject;\n\t\t});\n\n\t\tthis.#url = this.signals.computed((effect) => effect.get(this.url)?.href);\n\n\t\t// Create a reactive root so cleanup is easier.\n\t\tthis.signals.run(this.#connect.bind(this));\n\t\tthis.signals.run(this.#runAnnounced.bind(this));\n\t}\n\n\t#connect(effect: Effect): void {\n\t\t// Will retry when the tick changes.\n\t\teffect.get(this.#tick);\n\n\t\tconst enabled = effect.get(this.enabled);\n\t\tif (!enabled) return;\n\n\t\tconst href = effect.get(this.#url);\n\t\tif (!href) return;\n\t\tconst url = new URL(href);\n\n\t\teffect.set(this.status, \"connecting\", \"disconnected\");\n\n\t\teffect.spawn(async () => {\n\t\t\ttry {\n\t\t\t\tconst pending = connect(url, { websocket: this.websocket, webtransport: this.webtransport });\n\n\t\t\t\tconst connection = await Promise.race([effect.cancel, pending]);\n\t\t\t\tif (!connection) {\n\t\t\t\t\tpending.then((conn) => conn.close()).catch(() => {});\n\t\t\t\t\treturn;\n\t\t\t\t}\n\n\t\t\t\teffect.set(this.established, connection);\n\t\t\t\teffect.cleanup(() => connection.close());\n\n\t\t\t\teffect.set(this.status, \"connected\", \"disconnected\");\n\n\t\t\t\t// Reset the exponential backoff and timeout on success.\n\t\t\t\tthis.#delay = this.delay.initial;\n\t\t\t\tthis.#retryStart = undefined;\n\n\t\t\t\tawait Promise.race([effect.cancel, connection.closed]);\n\t\t\t} catch (err) {\n\t\t\t\tconsole.warn(\"connection error:\", err);\n\n\t\t\t\t// Track retry start for timeout.\n\t\t\t\tthis.#retryStart ??= performance.now();\n\n\t\t\t\tconst timeout = this.delay.timeout ?? 300000;\n\t\t\t\tif (timeout > 0) {\n\t\t\t\t\tconst elapsed = performance.now() - this.#retryStart;\n\t\t\t\t\tif (elapsed >= timeout) {\n\t\t\t\t\t\tconsole.warn(\"reconnect timed out\");\n\t\t\t\t\t\tthis.#closedReject(err instanceof Error ? err : new Error(String(err)));\n\t\t\t\t\t\treturn;\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\tconst tick = this.#tick.peek() + 1;\n\t\t\t\teffect.timer(() => this.#tick.update((prev) => Math.max(prev, tick)), this.#delay);\n\n\t\t\t\tthis.#delay = Math.min(this.#delay * this.delay.multiplier, this.delay.max);\n\t\t\t}\n\t\t});\n\t}\n\n\t#runAnnounced(effect: Effect): void {\n\t\tthis.#announced.set(new Set());\n\n\t\tconst conn = effect.get(this.established);\n\t\tif (!conn) return;\n\n\t\teffect.cleanup(() => this.#announced.set(new Set()));\n\n\t\t// Cloudflare's relay does not yet support SUBSCRIBE_NAMESPACE, so\n\t\t// skip announce subscriptions entirely for those hosts.\n\t\tif (conn.url.hostname.endsWith(\"mediaoverquic.com\")) {\n\t\t\treturn;\n\t\t}\n\n\t\tconst announced = conn.announced(emptyPath());\n\t\teffect.cleanup(() => announced.close());\n\n\t\teffect.spawn(async () => {\n\t\t\ttry {\n\t\t\t\tfor (;;) {\n\t\t\t\t\tconst entry = await Promise.race([effect.cancel, announced.next()]);\n\t\t\t\t\tif (!entry) break;\n\n\t\t\t\t\tthis.#announced.mutate((active) => {\n\t\t\t\t\t\tif (entry.active) {\n\t\t\t\t\t\t\tactive.add(entry.path);\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\tactive.delete(entry.path);\n\t\t\t\t\t\t}\n\t\t\t\t\t});\n\t\t\t\t}\n\t\t\t} catch (err) {\n\t\t\t\tthis.#announced.set(new Set());\n\t\t\t\tthrow err;\n\t\t\t}\n\t\t});\n\t}\n\n\t/** Stop reconnecting, close the current connection, and resolve {@link Reload.closed}. */\n\tclose() {\n\t\tthis.signals.close();\n\t\tthis.#closedResolve();\n\t}\n}\n"]}
1
+ {"version":3,"file":"reload.js","sourceRoot":"","sources":["../../src/connection/reload.ts"],"names":[],"mappings":"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","sourcesContent":["import { Effect, type Getter, Signal } from \"@moq/signals\";\nimport * as Announce from \"../announced.ts\";\nimport { error } from \"../error.ts\";\nimport type * as Path from \"../path.ts\";\nimport { empty as emptyPath } from \"../path.ts\";\nimport { type ConnectProps, connect, type WebSocketOptions, type WebTransportProps } from \"./connect.ts\";\nimport type { Established } from \"./established.ts\";\nimport type { Probe, Stats } from \"./stats.ts\";\n\n/**\n * Exponential backoff settings for {@link Reload}'s reconnect loop.\n *\n * The delays carry jitter, so a fleet of tabs knocked offline together doesn't reconnect in\n * lockstep. Every failure is retried; {@link ReloadDelay.timeout} is what stops the loop.\n */\nexport type ReloadDelay = {\n\t/** The delay in milliseconds before reconnecting (default: 1000). */\n\tinitial: DOMHighResTimeStamp;\n\n\t/** The multiplier for the delay (default: 2). */\n\tmultiplier: number;\n\n\t/** The maximum delay in milliseconds (default: 5000). */\n\tmax: DOMHighResTimeStamp;\n\n\t/**\n\t * Maximum total time in milliseconds to spend retrying before giving up (default:\n\t * 10000). Resets after each successful connection. Set to 0 for unlimited retries.\n\t */\n\ttimeout?: DOMHighResTimeStamp;\n};\n\n/** Connection and retry options for {@link Reload}. */\nexport type ReloadProps = Omit<ConnectProps, \"signal\"> & {\n\t/** Whether to reload the connection when it disconnects (default: true). */\n\tenabled?: boolean | Signal<boolean>;\n\n\t/** The URL of the relay server. */\n\turl?: URL | Signal<URL | undefined>;\n\n\t/** Backoff settings for the reconnect loop. */\n\tdelay?: ReloadDelay;\n};\n\n/**\n * How long to keep retrying before giving up, when {@link ReloadDelay.timeout} is unset.\n *\n * Short on purpose: a failure that clears within it was transient, and one that doesn't should\n * surface as an error rather than leave the page silently reconnecting for minutes.\n */\nconst DEFAULT_TIMEOUT = 10000;\n\n/** Current state of a {@link Reload} connection. */\nexport type ReloadStatus = \"connecting\" | \"connected\" | \"disconnected\";\n\n/** Maintains a MoQ connection, reconnecting with exponential backoff when it drops. */\nexport class Reload {\n\t/** Relay URL to connect to; updating it triggers a reconnect. */\n\turl: Signal<URL | undefined>;\n\n\t/** Whether reconnecting is active. */\n\tenabled: Signal<boolean>;\n\n\t/** Current connection status. */\n\tstatus = new Signal<ReloadStatus>(\"disconnected\");\n\n\t/** The currently established session, or undefined while disconnected. */\n\testablished = new Signal<Established | undefined>(undefined);\n\n\t/**\n\t * The current connection's PROBE estimates, spanning reconnects.\n\t *\n\t * Undefined while disconnected: the estimates belong to a single connection.\n\t * See {@link Established.probe}.\n\t */\n\treadonly probe: Getter<Probe | undefined>;\n\n\t/** WebTransport options applied to each connection attempt (not reactive). */\n\twebtransport?: WebTransportProps;\n\n\t/** WebSocket fallback options applied to each connection attempt (not reactive). */\n\twebsocket: WebSocketOptions | undefined;\n\n\t/**\n\t * Whether the relay supports broadcast discovery, applied to each connection attempt (not\n\t * reactive). Undefined defers to the default for the URL. See {@link Established.discovery}.\n\t */\n\tdiscovery?: boolean;\n\n\t/** Backoff settings for the reconnect loop. */\n\tdelay: ReloadDelay;\n\n\t/** The reactive effect scope driving the connect loop; closed by {@link Reload.close}. */\n\t#signals = new Effect();\n\n\t/**\n\t * Resolves when the reconnect loop stops via {@link Reload.close}.\n\t *\n\t * Rejects when the loop gives up instead, carrying the failure that was in flight when the\n\t * retry window expired.\n\t */\n\tclosed: Promise<void>;\n\t#closedResolve!: () => void;\n\t#closedReject!: (err: Error) => void;\n\n\t// The current wait between attempts, doubling per failure, and when the retry window expires.\n\t// Both are undefined between sequences, so a later edit to `delay` applies to the next one.\n\t#delay: DOMHighResTimeStamp | undefined;\n\t#deadline: DOMHighResTimeStamp | undefined;\n\n\t// Increased by 1 each time to trigger a reload.\n\t#tick = new Signal(0);\n\n\t// True after the browser freezes or hides the page until it visibly resumes.\n\t#suspended = new Signal(false);\n\n\t// Use the serialized URL as the reactive connection key. URL objects use identity\n\t// equality, but replacing one with an equivalent instance should not reconnect.\n\t#url: Getter<string | undefined>;\n\tconstructor(props?: ReloadProps) {\n\t\tthis.url = Signal.from(props?.url);\n\t\tthis.enabled = Signal.from(props?.enabled ?? false);\n\t\tthis.delay = props?.delay ?? { initial: 1000, multiplier: 2, max: 5000 };\n\t\tthis.webtransport = props?.webtransport;\n\t\tthis.websocket = props?.websocket;\n\t\tthis.discovery = props?.discovery;\n\n\t\tthis.closed = new Promise((resolve, reject) => {\n\t\t\tthis.#closedResolve = resolve;\n\t\t\tthis.#closedReject = reject;\n\t\t});\n\n\t\t// A caller is free to never await `closed`, and giving up rejects it unprompted. Marking the\n\t\t// rejection handled here keeps that from surfacing as an `unhandledrejection`; a consumer\n\t\t// awaiting the same promise still receives it.\n\t\tthis.closed.catch(() => {});\n\n\t\tif (typeof window !== \"undefined\" && typeof document !== \"undefined\") {\n\t\t\tthis.#signals.event(window, \"pagehide\", () => this.#suspended.set(true));\n\t\t\tthis.#signals.event(window, \"pageshow\", () => this.#suspended.set(false));\n\t\t\tthis.#signals.event(window, \"unload\", () => this.#suspended.set(true));\n\t\t\tthis.#signals.event(document, \"visibilitychange\", () => {\n\t\t\t\tif (!document.hidden) this.#suspended.set(false);\n\t\t\t});\n\t\t}\n\n\t\tthis.probe = this.#signals.computed((effect) => {\n\t\t\tconst connection = effect.get(this.established);\n\t\t\treturn connection && effect.get(connection.probe);\n\t\t});\n\n\t\tthis.#url = this.#signals.computed((effect) => effect.get(this.url)?.href);\n\t\t// Create a reactive root so cleanup is easier.\n\t\tthis.#signals.run(this.#connect.bind(this));\n\t}\n\n\t#connect(effect: Effect): void {\n\t\t// Will retry when the tick changes.\n\t\teffect.get(this.#tick);\n\n\t\tconst suspended = effect.get(this.#suspended);\n\t\tconst enabled = effect.get(this.enabled);\n\t\tif (!enabled || suspended) return;\n\n\t\tconst href = effect.get(this.#url);\n\t\tif (!href) return;\n\t\tconst url = new URL(href);\n\n\t\teffect.set(this.status, \"connecting\", \"disconnected\");\n\n\t\t// This run's teardown, handed to connect() so a rerun cancels the attempt in flight.\n\t\tconst signal = effect.abort;\n\n\t\teffect.spawn(async () => {\n\t\t\t// Set once the session is live, so #retry can tell a healthy session that\n\t\t\t// later dropped from a connect failure or a peer that flaps immediately.\n\t\t\tlet connected: DOMHighResTimeStamp | undefined;\n\n\t\t\ttry {\n\t\t\t\tconst connection = await connect(url, {\n\t\t\t\t\twebsocket: this.websocket,\n\t\t\t\t\twebtransport: this.webtransport,\n\t\t\t\t\tdiscovery: this.discovery,\n\t\t\t\t\tsignal,\n\t\t\t\t});\n\n\t\t\t\t// Hand the connection to the effect, which closes it now if this run is already over.\n\t\t\t\teffect.cleanup(() => connection.close());\n\t\t\t\tif (signal.aborted) return;\n\n\t\t\t\teffect.set(this.established, connection);\n\t\t\t\teffect.set(this.status, \"connected\", \"disconnected\");\n\n\t\t\t\tconnected = performance.now();\n\n\t\t\t\t// A cancelled effect resolves undefined, so the sentinel tells the session\n\t\t\t\t// closing apart from this run being torn down.\n\t\t\t\tconst closed = await Promise.race([effect.cancel, connection.closed.then(() => true)]);\n\t\t\t\tif (!closed) return;\n\n\t\t\t\tconsole.warn(\"connection closed, reconnecting\");\n\t\t\t\tthis.#retry(effect, connected);\n\t\t\t} catch (err) {\n\t\t\t\t// Treat teardown as cancellation, not a connection failure.\n\t\t\t\tif (signal.aborted) return;\n\n\t\t\t\tconsole.warn(\"connection error:\", err);\n\t\t\t\tthis.#retry(effect, connected, err);\n\t\t\t}\n\t\t});\n\t}\n\n\t/**\n\t * Schedule the next connect attempt after the current backoff, or stop once the retry window\n\t * has expired. `connected` is when the dead session was established, if it ever was, and\n\t * `cause` the error that killed it, if it died with one.\n\t */\n\t#retry(effect: Effect, connected: DOMHighResTimeStamp | undefined, cause?: unknown): void {\n\t\t// Any session is dead now: report disconnected during the backoff rather than\n\t\t// when the retry reruns the effect.\n\t\tthis.established.set(undefined);\n\t\tthis.status.set(\"disconnected\");\n\n\t\t// A session that outlived the initial delay was healthy, so clear the backoff and\n\t\t// start a fresh retry window: a one-off drop should reconnect promptly. Anything\n\t\t// shorter is a peer that accepts and immediately severs, which has to keep\n\t\t// escalating or we hammer it forever at the initial delay.\n\t\tif (connected !== undefined && performance.now() - connected >= this.delay.initial) {\n\t\t\tthis.#delay = undefined;\n\t\t\tthis.#deadline = undefined;\n\t\t}\n\n\t\tconst now = performance.now();\n\t\tconst timeout = this.delay.timeout ?? DEFAULT_TIMEOUT;\n\t\tthis.#delay ??= this.delay.initial;\n\t\tthis.#deadline ??= timeout > 0 ? now + timeout : Number.POSITIVE_INFINITY;\n\n\t\tif (now >= this.#deadline) {\n\t\t\tconsole.warn(\"reconnect timed out\");\n\t\t\t// A graceful close has no error, so report the timeout itself.\n\t\t\tthis.#closedReject(cause === undefined ? new Error(\"reconnect timed out\") : error(cause));\n\t\t\treturn;\n\t\t}\n\n\t\t// Equal jitter, so a fleet of tabs knocked offline together doesn't reconnect on the same\n\t\t// tick, and never past the deadline the retry window promised.\n\t\tconst wait = Math.min(this.#delay * (0.5 + Math.random() / 2), this.#deadline - now);\n\t\tthis.#delay = Math.min(this.#delay * this.delay.multiplier, this.delay.max);\n\n\t\tconst tick = this.#tick.peek() + 1;\n\t\teffect.timer(() => this.#tick.update((prev) => Math.max(prev, tick)), wait);\n\t}\n\n\t/**\n\t * Subscribe to broadcast announcements under an optional prefix, spanning reconnects.\n\t *\n\t * The same {@link Announce.Consumer} stream as {@link Established.announced}, but everything active\n\t * is retracted (an `active: false` update) whenever the connection drops and re-announced on\n\t * reconnect, so a consumer draining `next()` never clings to a dead route across a reconnect.\n\t *\n\t * Stays empty while the relay lacks {@link Established.discovery}.\n\t */\n\tannounced(prefix: Path.Valid = emptyPath()): Announce.Consumer {\n\t\tconst producer = new Announce.Producer(prefix);\n\t\tconst consumer = producer.consume();\n\n\t\t// Closing the consumer closes the shared state, so stop appending after that.\n\t\tlet closed = false;\n\t\tvoid consumer.closed.then(() => {\n\t\t\tclosed = true;\n\t\t});\n\n\t\tconst pump = new Effect();\n\t\tpump.run((effect) => {\n\t\t\tconst conn = effect.get(this.established);\n\t\t\tif (!conn) return;\n\n\t\t\t// Without discovery the upstream announce stream never yields, so leave the\n\t\t\t// consumer empty rather than opening a subscription that can't be answered.\n\t\t\tif (!conn.discovery) return;\n\n\t\t\tconst upstream = conn.announced(prefix);\n\t\t\teffect.cleanup(() => upstream.close());\n\n\t\t\t// Track what this connection announced so we can retract it if the connection drops.\n\t\t\tconst active = new Set<Path.Valid>();\n\n\t\t\teffect.spawn(async () => {\n\t\t\t\ttry {\n\t\t\t\t\tfor (;;) {\n\t\t\t\t\t\tconst entry = await Promise.race([effect.cancel, upstream.next()]);\n\t\t\t\t\t\tif (!entry) break;\n\t\t\t\t\t\tif (entry.active) active.add(entry.path);\n\t\t\t\t\t\telse active.delete(entry.path);\n\t\t\t\t\t\tproducer.append(entry);\n\t\t\t\t\t}\n\t\t\t\t} catch {\n\t\t\t\t\t// A dropped connection resets the announce stream; the retractions below cover it.\n\t\t\t\t} finally {\n\t\t\t\t\t// Retract everything from the connection that just went away, so a per-broadcast\n\t\t\t\t\t// watcher tears down instead of clinging to the dead route.\n\t\t\t\t\tif (!closed) {\n\t\t\t\t\t\tfor (const path of active) {\n\t\t\t\t\t\t\tproducer.append({ path, active: false });\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t});\n\t\t});\n\n\t\tthis.#signals.cleanup(() => pump.close());\n\t\tvoid consumer.closed.then(() => pump.close());\n\n\t\treturn consumer;\n\t}\n\n\t/**\n\t * A reactive handle to one broadcast, spanning reconnects.\n\t *\n\t * The same {@link Announce.Broadcast} as {@link Established.announcedBroadcast}, but it\n\t * follows the reconnect loop: the broadcast drops to `undefined` when the connection dies\n\t * and resolves again once the new connection announces the path. Use it instead of\n\t * consuming off {@link Reload.established} whenever the broadcast may come online after you\n\t * do, which is exactly the case a blind `consume` loses.\n\t *\n\t * Close the handle when done; {@link Reload.close} only drops it to `undefined`.\n\t */\n\tannouncedBroadcast(path: Path.Valid): Announce.Broadcast {\n\t\treturn new Announce.Broadcast({ connection: this.established, path });\n\t}\n\n\t/**\n\t * Snapshot the live connection's transport counters, or undefined while disconnected.\n\t * See {@link Established.stats}.\n\t */\n\tasync stats(): Promise<Stats | undefined> {\n\t\treturn this.established.peek()?.stats();\n\t}\n\n\t/** Stop reconnecting, close the current connection, and resolve {@link Reload.closed}. */\n\tclose() {\n\t\tthis.#signals.close();\n\t\tthis.#closedResolve();\n\t}\n}\n"]}
@@ -0,0 +1,92 @@
1
+ /**
2
+ * Connection statistics: what the local transport counts, and what the peer reports.
3
+ *
4
+ * @module
5
+ */
6
+ import * as Time from "../time";
7
+ /**
8
+ * A point-in-time snapshot of the transport's counters, from `WebTransport.getStats()`.
9
+ *
10
+ * Pulled on demand rather than pushed: the transport has no event to subscribe to, and
11
+ * a caller computing a rate wants to choose its own sampling instants. Snapshot twice
12
+ * and divide by the interval you measured.
13
+ *
14
+ * Every field is optional. The qmux/WebSocket fallback implements no `getStats()` at
15
+ * all and reports nothing; see {@link Probe} for what the peer measures.
16
+ *
17
+ * The field names match Rust's `moq_net::ConnectionStats` but the counters keep each
18
+ * stack's own semantics rather than being normalized. Notably W3C excludes
19
+ * retransmissions and QUIC overhead from the byte counts where quinn includes them,
20
+ * and counts packets where quinn counts datagrams.
21
+ *
22
+ * @public
23
+ */
24
+ export interface Stats {
25
+ /**
26
+ * Smoothed round-trip time estimate in milliseconds.
27
+ *
28
+ * Usually absent: browsers don't report an RTT yet even though the W3C dictionary
29
+ * has the field. Use {@link Probe.rtt}, which the peer measures.
30
+ */
31
+ rtt?: Time.Milli;
32
+ /** Estimated send bandwidth from the congestion controller, in bits per second. */
33
+ estimatedSendRate?: number;
34
+ /** Total bytes sent on streams and datagrams, excluding retransmissions and QUIC overhead. */
35
+ bytesSent?: number;
36
+ /** Total bytes received on streams and datagrams, including duplicates, excluding QUIC overhead. */
37
+ bytesReceived?: number;
38
+ /** Bytes currently declared lost. The estimate can decrease when a loss proves spurious. */
39
+ bytesLost?: number;
40
+ /** Total packets sent, including retransmissions. */
41
+ packetsSent?: number;
42
+ /** Total packets received, including duplicates. */
43
+ packetsReceived?: number;
44
+ /** Packets currently declared lost. The estimate can decrease when a loss proves spurious. */
45
+ packetsLost?: number;
46
+ }
47
+ /**
48
+ * Estimates measured by the peer and delivered over the MoQ PROBE stream.
49
+ *
50
+ * Pushed rather than pulled: these arrive when the peer sends them, so they're read
51
+ * from a signal. Empty until the first PROBE message, on versions without PROBE
52
+ * (moq-lite-01/02, moq-transport), and again once the stream ends, so a consumer
53
+ * never holds a stale estimate from a dead stream.
54
+ *
55
+ * @public
56
+ */
57
+ export interface Probe {
58
+ /**
59
+ * Round-trip time in milliseconds.
60
+ *
61
+ * The only RTT available today, since browsers don't populate the transport's.
62
+ * Measured through the peer's application layer, so it reads higher than a
63
+ * transport-level RTT would.
64
+ */
65
+ rtt?: Time.Milli;
66
+ /** The peer's estimate of the bitrate it is receiving from us, in bits per second. */
67
+ estimatedRecvRate?: number;
68
+ }
69
+ /**
70
+ * The subset of the W3C `WebTransportConnectionStats` dictionary we consume.
71
+ * `getStats()` is not in TypeScript's DOM lib yet, so it is declared here.
72
+ *
73
+ * @internal
74
+ */
75
+ export interface TransportStats {
76
+ smoothedRtt?: number;
77
+ estimatedSendRate?: number | null;
78
+ bytesSent?: number;
79
+ bytesReceived?: number;
80
+ bytesLost?: number;
81
+ packetsSent?: number;
82
+ packetsReceived?: number;
83
+ packetsLost?: number;
84
+ }
85
+ /**
86
+ * Snapshot `quic` via `getStats()`, or an empty snapshot when the transport doesn't
87
+ * implement it (the qmux/WebSocket fallback, older browsers) or the call fails.
88
+ *
89
+ * @internal
90
+ */
91
+ export declare function transportStats(quic: WebTransport): Promise<Stats>;
92
+ //# sourceMappingURL=stats.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"stats.d.ts","sourceRoot":"","sources":["../../src/connection/stats.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AACH,OAAO,KAAK,IAAI,MAAM,YAAY,CAAC;AAEnC;;;;;;;;;;;;;;;;GAgBG;AACH,MAAM,WAAW,KAAK;IACrB;;;;;OAKG;IACH,GAAG,CAAC,EAAE,IAAI,CAAC,KAAK,CAAC;IAEjB,mFAAmF;IACnF,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAE3B,8FAA8F;IAC9F,SAAS,CAAC,EAAE,MAAM,CAAC;IAEnB,oGAAoG;IACpG,aAAa,CAAC,EAAE,MAAM,CAAC;IAEvB,4FAA4F;IAC5F,SAAS,CAAC,EAAE,MAAM,CAAC;IAEnB,qDAAqD;IACrD,WAAW,CAAC,EAAE,MAAM,CAAC;IAErB,oDAAoD;IACpD,eAAe,CAAC,EAAE,MAAM,CAAC;IAEzB,8FAA8F;IAC9F,WAAW,CAAC,EAAE,MAAM,CAAC;CACrB;AAED;;;;;;;;;GASG;AACH,MAAM,WAAW,KAAK;IACrB;;;;;;OAMG;IACH,GAAG,CAAC,EAAE,IAAI,CAAC,KAAK,CAAC;IAEjB,sFAAsF;IACtF,iBAAiB,CAAC,EAAE,MAAM,CAAC;CAC3B;AAED;;;;;GAKG;AACH,MAAM,WAAW,cAAc;IAC9B,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,iBAAiB,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAClC,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,WAAW,CAAC,EAAE,MAAM,CAAC;CACrB;AAED;;;;;GAKG;AACH,wBAAsB,cAAc,CAAC,IAAI,EAAE,YAAY,GAAG,OAAO,CAAC,KAAK,CAAC,CAkBvE"}
@@ -0,0 +1,35 @@
1
+ /* @ts-self-types="./stats.d.ts" */
2
+ /**
3
+ * Connection statistics: what the local transport counts, and what the peer reports.
4
+ *
5
+ * @module
6
+ */
7
+ import * as Time from "../time.js";
8
+ /**
9
+ * Snapshot `quic` via `getStats()`, or an empty snapshot when the transport doesn't
10
+ * implement it (the qmux/WebSocket fallback, older browsers) or the call fails.
11
+ *
12
+ * @internal
13
+ */
14
+ export async function transportStats(quic) {
15
+ const getStats = quic.getStats;
16
+ if (typeof getStats !== "function")
17
+ return {};
18
+ try {
19
+ const stats = await getStats.call(quic);
20
+ return {
21
+ rtt: stats.smoothedRtt !== undefined ? Time.Milli(stats.smoothedRtt) : undefined,
22
+ estimatedSendRate: stats.estimatedSendRate ?? undefined,
23
+ bytesSent: stats.bytesSent,
24
+ bytesReceived: stats.bytesReceived,
25
+ bytesLost: stats.bytesLost,
26
+ packetsSent: stats.packetsSent,
27
+ packetsReceived: stats.packetsReceived,
28
+ packetsLost: stats.packetsLost,
29
+ };
30
+ }
31
+ catch {
32
+ return {};
33
+ }
34
+ }
35
+ //# sourceMappingURL=stats.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"stats.js","sourceRoot":"","sources":["../../src/connection/stats.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AACH,OAAO,KAAK,IAAI,MAAM,YAAY,CAAC;AA2FnC;;;;;GAKG;AACH,MAAM,CAAC,KAAK,UAAU,cAAc,CAAC,IAAkB;IACtD,MAAM,QAAQ,GAAI,IAAqD,CAAC,QAAQ,CAAC;IACjF,IAAI,OAAO,QAAQ,KAAK,UAAU;QAAE,OAAO,EAAE,CAAC;IAC9C,IAAI,CAAC;QACJ,MAAM,KAAK,GAAG,MAAM,QAAQ,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QACxC,OAAO;YACN,GAAG,EAAE,KAAK,CAAC,WAAW,KAAK,SAAS,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,KAAK,CAAC,WAAW,CAAC,CAAC,CAAC,CAAC,SAAS;YAChF,iBAAiB,EAAE,KAAK,CAAC,iBAAiB,IAAI,SAAS;YACvD,SAAS,EAAE,KAAK,CAAC,SAAS;YAC1B,aAAa,EAAE,KAAK,CAAC,aAAa;YAClC,SAAS,EAAE,KAAK,CAAC,SAAS;YAC1B,WAAW,EAAE,KAAK,CAAC,WAAW;YAC9B,eAAe,EAAE,KAAK,CAAC,eAAe;YACtC,WAAW,EAAE,KAAK,CAAC,WAAW;SAC9B,CAAC;IACH,CAAC;IAAC,MAAM,CAAC;QACR,OAAO,EAAE,CAAC;IACX,CAAC;AACF,CAAC","sourcesContent":["/**\n * Connection statistics: what the local transport counts, and what the peer reports.\n *\n * @module\n */\nimport * as Time from \"../time.ts\";\n\n/**\n * A point-in-time snapshot of the transport's counters, from `WebTransport.getStats()`.\n *\n * Pulled on demand rather than pushed: the transport has no event to subscribe to, and\n * a caller computing a rate wants to choose its own sampling instants. Snapshot twice\n * and divide by the interval you measured.\n *\n * Every field is optional. The qmux/WebSocket fallback implements no `getStats()` at\n * all and reports nothing; see {@link Probe} for what the peer measures.\n *\n * The field names match Rust's `moq_net::ConnectionStats` but the counters keep each\n * stack's own semantics rather than being normalized. Notably W3C excludes\n * retransmissions and QUIC overhead from the byte counts where quinn includes them,\n * and counts packets where quinn counts datagrams.\n *\n * @public\n */\nexport interface Stats {\n\t/**\n\t * Smoothed round-trip time estimate in milliseconds.\n\t *\n\t * Usually absent: browsers don't report an RTT yet even though the W3C dictionary\n\t * has the field. Use {@link Probe.rtt}, which the peer measures.\n\t */\n\trtt?: Time.Milli;\n\n\t/** Estimated send bandwidth from the congestion controller, in bits per second. */\n\testimatedSendRate?: number;\n\n\t/** Total bytes sent on streams and datagrams, excluding retransmissions and QUIC overhead. */\n\tbytesSent?: number;\n\n\t/** Total bytes received on streams and datagrams, including duplicates, excluding QUIC overhead. */\n\tbytesReceived?: number;\n\n\t/** Bytes currently declared lost. The estimate can decrease when a loss proves spurious. */\n\tbytesLost?: number;\n\n\t/** Total packets sent, including retransmissions. */\n\tpacketsSent?: number;\n\n\t/** Total packets received, including duplicates. */\n\tpacketsReceived?: number;\n\n\t/** Packets currently declared lost. The estimate can decrease when a loss proves spurious. */\n\tpacketsLost?: number;\n}\n\n/**\n * Estimates measured by the peer and delivered over the MoQ PROBE stream.\n *\n * Pushed rather than pulled: these arrive when the peer sends them, so they're read\n * from a signal. Empty until the first PROBE message, on versions without PROBE\n * (moq-lite-01/02, moq-transport), and again once the stream ends, so a consumer\n * never holds a stale estimate from a dead stream.\n *\n * @public\n */\nexport interface Probe {\n\t/**\n\t * Round-trip time in milliseconds.\n\t *\n\t * The only RTT available today, since browsers don't populate the transport's.\n\t * Measured through the peer's application layer, so it reads higher than a\n\t * transport-level RTT would.\n\t */\n\trtt?: Time.Milli;\n\n\t/** The peer's estimate of the bitrate it is receiving from us, in bits per second. */\n\testimatedRecvRate?: number;\n}\n\n/**\n * The subset of the W3C `WebTransportConnectionStats` dictionary we consume.\n * `getStats()` is not in TypeScript's DOM lib yet, so it is declared here.\n *\n * @internal\n */\nexport interface TransportStats {\n\tsmoothedRtt?: number;\n\testimatedSendRate?: number | null;\n\tbytesSent?: number;\n\tbytesReceived?: number;\n\tbytesLost?: number;\n\tpacketsSent?: number;\n\tpacketsReceived?: number;\n\tpacketsLost?: number;\n}\n\n/**\n * Snapshot `quic` via `getStats()`, or an empty snapshot when the transport doesn't\n * implement it (the qmux/WebSocket fallback, older browsers) or the call fails.\n *\n * @internal\n */\nexport async function transportStats(quic: WebTransport): Promise<Stats> {\n\tconst getStats = (quic as { getStats?: () => Promise<TransportStats> }).getStats;\n\tif (typeof getStats !== \"function\") return {};\n\ttry {\n\t\tconst stats = await getStats.call(quic);\n\t\treturn {\n\t\t\trtt: stats.smoothedRtt !== undefined ? Time.Milli(stats.smoothedRtt) : undefined,\n\t\t\testimatedSendRate: stats.estimatedSendRate ?? undefined,\n\t\t\tbytesSent: stats.bytesSent,\n\t\t\tbytesReceived: stats.bytesReceived,\n\t\t\tbytesLost: stats.bytesLost,\n\t\t\tpacketsSent: stats.packetsSent,\n\t\t\tpacketsReceived: stats.packetsReceived,\n\t\t\tpacketsLost: stats.packetsLost,\n\t\t};\n\t} catch {\n\t\treturn {};\n\t}\n}\n"]}
@@ -0,0 +1,10 @@
1
+ /** The wire transport a session runs over. */
2
+ export type Transport = "webtransport" | "websocket";
3
+ /**
4
+ * The transport a live session is actually running over. qmux implements the `WebTransport`
5
+ * interface on top of a WebSocket, so the two are indistinguishable by shape.
6
+ *
7
+ * @internal Not re-exported from the package entrypoint.
8
+ */
9
+ export declare function transportOf(quic: WebTransport): Transport;
10
+ //# sourceMappingURL=transport.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"transport.d.ts","sourceRoot":"","sources":["../../src/connection/transport.ts"],"names":[],"mappings":"AAEA,8CAA8C;AAC9C,MAAM,MAAM,SAAS,GAAG,cAAc,GAAG,WAAW,CAAC;AAErD;;;;;GAKG;AACH,wBAAgB,WAAW,CAAC,IAAI,EAAE,YAAY,GAAG,SAAS,CAEzD"}
@@ -0,0 +1,12 @@
1
+ /* @ts-self-types="./transport.d.ts" */
2
+ import Session from "@moq/qmux";
3
+ /**
4
+ * The transport a live session is actually running over. qmux implements the `WebTransport`
5
+ * interface on top of a WebSocket, so the two are indistinguishable by shape.
6
+ *
7
+ * @internal Not re-exported from the package entrypoint.
8
+ */
9
+ export function transportOf(quic) {
10
+ return quic instanceof Session ? "websocket" : "webtransport";
11
+ }
12
+ //# sourceMappingURL=transport.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"transport.js","sourceRoot":"","sources":["../../src/connection/transport.ts"],"names":[],"mappings":"AAAA,OAAO,OAAO,MAAM,WAAW,CAAC;AAKhC;;;;;GAKG;AACH,MAAM,UAAU,WAAW,CAAC,IAAkB;IAC7C,OAAO,IAAI,YAAY,OAAO,CAAC,CAAC,CAAC,WAAW,CAAC,CAAC,CAAC,cAAc,CAAC;AAC/D,CAAC","sourcesContent":["import Session from \"@moq/qmux\";\n\n/** The wire transport a session runs over. */\nexport type Transport = \"webtransport\" | \"websocket\";\n\n/**\n * The transport a live session is actually running over. qmux implements the `WebTransport`\n * interface on top of a WebSocket, so the two are indistinguishable by shape.\n *\n * @internal Not re-exported from the package entrypoint.\n */\nexport function transportOf(quic: WebTransport): Transport {\n\treturn quic instanceof Session ? \"websocket\" : \"webtransport\";\n}\n"]}
package/consume.d.ts ADDED
@@ -0,0 +1,44 @@
1
+ import type * as broadcast from "./broadcast";
2
+ import type * as Path from "./path";
3
+ /**
4
+ * Per-path dedup cache for consumed broadcasts, shared by the moq-lite and moq-ietf
5
+ * subscribers.
6
+ *
7
+ * `Connection.consume(path)` must not mint a fresh subscription per call: repeat requests
8
+ * for the same path (e.g. several renditions referencing one `broadcast: "./source"`) should
9
+ * share a single upstream subscription. This mirrors the Rust `origin::Consumer` weak-cache:
10
+ * a still-live path resolves to a shared {@link broadcast.Consumer.clone}, a closed one is
11
+ * re-consumed on the next request. Each handle is reference-counted, so the shared broadcast
12
+ * closes once every caller has closed its handle.
13
+ *
14
+ * @internal
15
+ */
16
+ export declare class BroadcastCache {
17
+ #private;
18
+ /** A shared handle to the live broadcast cached for `path`, or `undefined` on a miss. */
19
+ get(path: Path.Valid): broadcast.Consumer | undefined;
20
+ /**
21
+ * Cache `consumer` as the base handle for `path` (evicting it once it closes) and return it.
22
+ * Call on a {@link get} miss, after wiring up the fresh consumer's subscribe loop.
23
+ */
24
+ insert(path: Path.Valid, consumer: broadcast.Consumer): broadcast.Consumer;
25
+ /**
26
+ * Stop sharing the broadcast cached for `path`, so the next request subscribes fresh.
27
+ *
28
+ * Call when the path's advertisement goes away. A handle only leaves the cache on its own
29
+ * once *every* holder has closed it, so one holder outliving the publisher (a second
30
+ * watcher, or a caller consuming the path directly) would otherwise keep the dead
31
+ * generation's cached tracks alive and hand them to whoever consumes the path next.
32
+ * Existing handles are left alone: they belong to their holders, and the wire resets
33
+ * whatever they still have open.
34
+ *
35
+ * Eviction is unconditional, which costs a dedup miss when two announcement streams watch
36
+ * one path: the second stream's retraction can arrive after the first has already seen the
37
+ * replacement, dropping the fresh entry so the next request subscribes again instead of
38
+ * sharing. Telling that stale retraction from a live one needs a generation id on the
39
+ * advertisement (moq-lite's `Epoch`, not yet on the wire), so until then this errs toward a
40
+ * duplicate subscription rather than risk handing out a dead one.
41
+ */
42
+ evict(path: Path.Valid): void;
43
+ }
44
+ //# sourceMappingURL=consume.d.ts.map