@norskvideo/moq-net 0.1.8 → 0.2.1

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
@@ -1,104 +1,25 @@
1
- import { type Getter, Signal } from "@norskvideo/moq-signals";
2
- import * as Announce from "../announced";
3
- import type * as Path from "../path";
4
- import { type ConnectProps, type WebSocketOptions, type WebTransportProps } from "./connect";
5
- import type { Established } from "./established";
6
- import type { Probe, Stats } from "./stats";
1
+ import * as Time from "../time";
7
2
  /**
8
- * Exponential backoff settings for {@link Reload}'s reconnect loop.
3
+ * Exponential backoff settings for the reconnect loop.
9
4
  *
10
5
  * The delays carry jitter, so a fleet of tabs knocked offline together doesn't reconnect in
11
- * lockstep. Every failure is retried; {@link ReloadDelay.timeout} is what stops the loop.
6
+ * lockstep. Every failure is retried; {@link ReloadDelay.timeout} is what stops the current
7
+ * URL. A new URL or a disable/re-enable starts another sequence.
12
8
  */
13
9
  export type ReloadDelay = {
14
- /** The delay in milliseconds before reconnecting (default: 1000). */
15
- initial: DOMHighResTimeStamp;
10
+ /** The delay before reconnecting (default: 1000ms). */
11
+ initial?: Time.Milli;
16
12
  /** The multiplier for the delay (default: 2). */
17
- multiplier: number;
18
- /** The maximum delay in milliseconds (default: 5000). */
19
- max: DOMHighResTimeStamp;
13
+ multiplier?: number;
14
+ /** The maximum delay (default: 5000ms). */
15
+ max?: Time.Milli;
20
16
  /**
21
- * Maximum total time in milliseconds to spend retrying before giving up (default:
22
- * 10000). Resets after each successful connection. Set to 0 for unlimited retries.
17
+ * Maximum total time to spend retrying the current URL before giving up
18
+ * (default: 10000ms). Resets after each successful connection, a URL change, or a
19
+ * disable/re-enable. Set to 0 for unlimited retries.
23
20
  */
24
- timeout?: DOMHighResTimeStamp;
21
+ timeout?: Time.Milli;
25
22
  };
26
- /** Connection and retry options for {@link Reload}. */
27
- export type ReloadProps = Omit<ConnectProps, "signal"> & {
28
- /** Whether to reload the connection when it disconnects (default: true). */
29
- enabled?: boolean | Signal<boolean>;
30
- /** The URL of the relay server. */
31
- url?: URL | Signal<URL | undefined>;
32
- /** Backoff settings for the reconnect loop. */
33
- delay?: ReloadDelay;
34
- };
35
- /** Current state of a {@link Reload} connection. */
23
+ /** Current state of a reconnecting connection. */
36
24
  export type ReloadStatus = "connecting" | "connected" | "disconnected";
37
- /** Maintains a MoQ connection, reconnecting with exponential backoff when it drops. */
38
- export declare class Reload {
39
- #private;
40
- /** Relay URL to connect to; updating it triggers a reconnect. */
41
- url: Signal<URL | undefined>;
42
- /** Whether reconnecting is active. */
43
- enabled: Signal<boolean>;
44
- /** Current connection status. */
45
- status: Signal<ReloadStatus>;
46
- /** The currently established session, or undefined while disconnected. */
47
- established: Signal<Established | undefined>;
48
- /**
49
- * The current connection's PROBE estimates, spanning reconnects.
50
- *
51
- * Undefined while disconnected: the estimates belong to a single connection.
52
- * See {@link Established.probe}.
53
- */
54
- readonly probe: Getter<Probe | undefined>;
55
- /** WebTransport options applied to each connection attempt (not reactive). */
56
- webtransport?: WebTransportProps;
57
- /** WebSocket fallback options applied to each connection attempt (not reactive). */
58
- websocket: WebSocketOptions | undefined;
59
- /**
60
- * Whether the relay supports broadcast discovery, applied to each connection attempt (not
61
- * reactive). Undefined defers to the default for the URL. See {@link Established.discovery}.
62
- */
63
- discovery?: boolean;
64
- /** Backoff settings for the reconnect loop. */
65
- delay: ReloadDelay;
66
- /**
67
- * Resolves when the reconnect loop stops via {@link Reload.close}.
68
- *
69
- * Rejects when the loop gives up instead, carrying the failure that was in flight when the
70
- * retry window expired.
71
- */
72
- closed: Promise<void>;
73
- constructor(props?: ReloadProps);
74
- /**
75
- * Subscribe to broadcast announcements under an optional prefix, spanning reconnects.
76
- *
77
- * The same {@link Announce.Consumer} stream as {@link Established.announced}, but everything active
78
- * is retracted (an `active: false` update) whenever the connection drops and re-announced on
79
- * reconnect, so a consumer draining `next()` never clings to a dead route across a reconnect.
80
- *
81
- * Stays empty while the relay lacks {@link Established.discovery}.
82
- */
83
- announced(prefix?: Path.Valid): Announce.Consumer;
84
- /**
85
- * A reactive handle to one broadcast, spanning reconnects.
86
- *
87
- * The same {@link Announce.Broadcast} as {@link Established.announcedBroadcast}, but it
88
- * follows the reconnect loop: the broadcast drops to `undefined` when the connection dies
89
- * and resolves again once the new connection announces the path. Use it instead of
90
- * consuming off {@link Reload.established} whenever the broadcast may come online after you
91
- * do, which is exactly the case a blind `consume` loses.
92
- *
93
- * Close the handle when done; {@link Reload.close} only drops it to `undefined`.
94
- */
95
- announcedBroadcast(path: Path.Valid): Announce.Broadcast;
96
- /**
97
- * Snapshot the live connection's transport counters, or undefined while disconnected.
98
- * See {@link Established.stats}.
99
- */
100
- stats(): Promise<Stats | undefined>;
101
- /** Stop reconnecting, close the current connection, and resolve {@link Reload.closed}. */
102
- close(): void;
103
- }
104
25
  //# sourceMappingURL=reload.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"reload.d.ts","sourceRoot":"","sources":["../../src/connection/reload.ts"],"names":[],"mappings":"AAAA,OAAO,EAAU,KAAK,MAAM,EAAE,MAAM,EAAE,MAAM,cAAc,CAAC;AAC3D,OAAO,KAAK,QAAQ,MAAM,iBAAiB,CAAC;AAE5C,OAAO,KAAK,KAAK,IAAI,MAAM,YAAY,CAAC;AAExC,OAAO,EAAE,KAAK,YAAY,EAAW,KAAK,gBAAgB,EAAE,KAAK,iBAAiB,EAAE,MAAM,cAAc,CAAC;AACzG,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,kBAAkB,CAAC;AACpD,OAAO,KAAK,EAAE,KAAK,EAAE,KAAK,EAAE,MAAM,YAAY,CAAC;AAE/C;;;;;GAKG;AACH,MAAM,MAAM,WAAW,GAAG;IACzB,qEAAqE;IACrE,OAAO,EAAE,mBAAmB,CAAC;IAE7B,iDAAiD;IACjD,UAAU,EAAE,MAAM,CAAC;IAEnB,yDAAyD;IACzD,GAAG,EAAE,mBAAmB,CAAC;IAEzB;;;OAGG;IACH,OAAO,CAAC,EAAE,mBAAmB,CAAC;CAC9B,CAAC;AAEF,uDAAuD;AACvD,MAAM,MAAM,WAAW,GAAG,IAAI,CAAC,YAAY,EAAE,QAAQ,CAAC,GAAG;IACxD,4EAA4E;IAC5E,OAAO,CAAC,EAAE,OAAO,GAAG,MAAM,CAAC,OAAO,CAAC,CAAC;IAEpC,mCAAmC;IACnC,GAAG,CAAC,EAAE,GAAG,GAAG,MAAM,CAAC,GAAG,GAAG,SAAS,CAAC,CAAC;IAEpC,+CAA+C;IAC/C,KAAK,CAAC,EAAE,WAAW,CAAC;CACpB,CAAC;AAUF,oDAAoD;AACpD,MAAM,MAAM,YAAY,GAAG,YAAY,GAAG,WAAW,GAAG,cAAc,CAAC;AAEvE,uFAAuF;AACvF,qBAAa,MAAM;;IAClB,iEAAiE;IACjE,GAAG,EAAE,MAAM,CAAC,GAAG,GAAG,SAAS,CAAC,CAAC;IAE7B,sCAAsC;IACtC,OAAO,EAAE,MAAM,CAAC,OAAO,CAAC,CAAC;IAEzB,iCAAiC;IACjC,MAAM,uBAA4C;IAElD,0EAA0E;IAC1E,WAAW,kCAAkD;IAE7D;;;;;OAKG;IACH,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC,KAAK,GAAG,SAAS,CAAC,CAAC;IAE1C,8EAA8E;IAC9E,YAAY,CAAC,EAAE,iBAAiB,CAAC;IAEjC,oFAAoF;IACpF,SAAS,EAAE,gBAAgB,GAAG,SAAS,CAAC;IAExC;;;OAGG;IACH,SAAS,CAAC,EAAE,OAAO,CAAC;IAEpB,+CAA+C;IAC/C,KAAK,EAAE,WAAW,CAAC;IAKnB;;;;;OAKG;IACH,MAAM,EAAE,OAAO,CAAC,IAAI,CAAC,CAAC;IAkBtB,YAAY,KAAK,CAAC,EAAE,WAAW,EAmC9B;IAmGD;;;;;;;;OAQG;IACH,SAAS,CAAC,MAAM,GAAE,IAAI,CAAC,KAAmB,GAAG,QAAQ,CAAC,QAAQ,CAoD7D;IAED;;;;;;;;;;OAUG;IACH,kBAAkB,CAAC,IAAI,EAAE,IAAI,CAAC,KAAK,GAAG,QAAQ,CAAC,SAAS,CAEvD;IAED;;;OAGG;IACG,KAAK,IAAI,OAAO,CAAC,KAAK,GAAG,SAAS,CAAC,CAExC;IAED,0FAA0F;IAC1F,KAAK,SAGJ;CACD"}
1
+ {"version":3,"file":"reload.d.ts","sourceRoot":"","sources":["../../src/connection/reload.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,IAAI,MAAM,YAAY,CAAC;AAMnC;;;;;;GAMG;AACH,MAAM,MAAM,WAAW,GAAG;IACzB,uDAAuD;IACvD,OAAO,CAAC,EAAE,IAAI,CAAC,KAAK,CAAC;IAErB,iDAAiD;IACjD,UAAU,CAAC,EAAE,MAAM,CAAC;IAEpB,2CAA2C;IAC3C,GAAG,CAAC,EAAE,IAAI,CAAC,KAAK,CAAC;IAEjB;;;;OAIG;IACH,OAAO,CAAC,EAAE,IAAI,CAAC,KAAK,CAAC;CACrB,CAAC;AA+CF,kDAAkD;AAClD,MAAM,MAAM,YAAY,GAAG,YAAY,GAAG,WAAW,GAAG,cAAc,CAAC"}
@@ -1,17 +1,35 @@
1
1
  /* @ts-self-types="./reload.d.ts" */
2
- import { Effect, Signal } from "@norskvideo/moq-signals";
2
+ import { Effect, Once, Signal } from "@norskvideo/moq-signals";
3
3
  import * as Announce from "../announced.js";
4
- import { error } from "../error.js";
5
- import { empty as emptyPath } from "../path.js";
4
+ import { Allocator } from "../bandwidth.js";
5
+ import { error, SessionCode, SessionError } from "../error.js";
6
+ import * as Path from "../path.js";
7
+ import * as Time from "../time.js";
8
+ import { wireOf } from "../wire.js";
6
9
  import { connect } from "./connect.js";
7
10
  /**
8
- * How long to keep retrying before giving up, when {@link ReloadDelay.timeout} is unset.
11
+ * The backoff applied to whichever {@link ReloadDelay} fields a caller leaves out.
9
12
  *
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.
13
+ * The timeout is short on purpose: a failure that clears within it was transient, and one that
14
+ * doesn't should surface on {@link Reload.error} rather than leave the page silently
15
+ * reconnecting for minutes. A loop nobody watches wants `timeout: 0` instead, since there is
16
+ * no one to react. Giving up does not dispose the loop: a new URL or a disable/re-enable
17
+ * starts another sequence.
18
+ */
19
+ const DEFAULT_DELAY = {
20
+ initial: Time.Milli(1000),
21
+ multiplier: 2,
22
+ max: Time.Milli(5000),
23
+ timeout: Time.Milli(10000),
24
+ };
25
+ /** How often the send-rate estimate is sampled from the live transport. */
26
+ const BANDWIDTH_POLL = 100;
27
+ /**
28
+ * The reconnect loop behind a Connection handle: connects, waits for the session to end, then
29
+ * redials with exponential backoff.
30
+ *
31
+ * @internal
12
32
  */
13
- const DEFAULT_TIMEOUT = 10000;
14
- /** Maintains a MoQ connection, reconnecting with exponential backoff when it drops. */
15
33
  export class Reload {
16
34
  /** Relay URL to connect to; updating it triggers a reconnect. */
17
35
  url;
@@ -19,6 +37,14 @@ export class Reload {
19
37
  enabled;
20
38
  /** Current connection status. */
21
39
  status = new Signal("disconnected");
40
+ /**
41
+ * The failure that stopped retrying the current URL, or undefined while the loop is live.
42
+ *
43
+ * Set on an auth rejection or when the retry window expires. Cleared when a new URL or a
44
+ * disable/re-enable starts another sequence, not on a page hide/show. Transient drops
45
+ * that are still being retried leave this empty.
46
+ */
47
+ error;
22
48
  /** The currently established session, or undefined while disconnected. */
23
49
  established = new Signal(undefined);
24
50
  /**
@@ -28,6 +54,14 @@ export class Reload {
28
54
  * See {@link Established.probe}.
29
55
  */
30
56
  probe;
57
+ /**
58
+ * Divides this connection's send-rate estimate among the tracks sharing it.
59
+ *
60
+ * The same instance across reconnects, so reservations survive a drop. The
61
+ * estimate is `undefined` while disconnected, which tells a sender to hold
62
+ * its rate rather than encode at zero.
63
+ */
64
+ bandwidth;
31
65
  /** WebTransport options applied to each connection attempt (not reactive). */
32
66
  webtransport;
33
67
  /** WebSocket fallback options applied to each connection attempt (not reactive). */
@@ -37,23 +71,50 @@ export class Reload {
37
71
  * reactive). Undefined defers to the default for the URL. See {@link Established.discovery}.
38
72
  */
39
73
  discovery;
40
- /** Backoff settings for the reconnect loop. */
74
+ /**
75
+ * The origin whose broadcasts are served, spanning reconnects (not reactive).
76
+ *
77
+ * Each session announces the origin's table when it attaches, so a broadcast published
78
+ * while offline surfaces on the next connection and a reconnect re-announces everything
79
+ * still published. See the `publish` connect option.
80
+ */
81
+ publish;
82
+ /**
83
+ * The origin fed with the peer's announced broadcasts, spanning reconnects (not
84
+ * reactive).
85
+ *
86
+ * The entries a session fed retract when it dies, and the next session re-populates the
87
+ * table, so a consumer watching the origin sees offline/online transitions across a
88
+ * reconnect. See the `consume` connect option.
89
+ */
90
+ consume;
91
+ /** Backoff settings for the reconnect loop; an unset field uses its default. */
41
92
  delay;
42
93
  /** The reactive effect scope driving the connect loop; closed by {@link Reload.close}. */
43
94
  #signals = new Effect();
95
+ // Sampled from the live session; undefined while disconnected or the transport has none.
96
+ #estimate = new Signal(undefined);
44
97
  /**
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.
98
+ * Settles once this loop is disposed via {@link Reload.close}: `null` on a clean close,
99
+ * or the abort {@link Error}. Peek it synchronously (`undefined` while open), observe it
100
+ * reactively, or `await` it. Attempt failures live on {@link Reload.error} instead.
49
101
  */
50
- closed;
51
- #closedResolve;
52
- #closedReject;
102
+ get closed() {
103
+ return this.#closed;
104
+ }
105
+ #closed = new Once();
106
+ #error = new Signal(undefined);
53
107
  // The current wait between attempts, doubling per failure, and when the retry window expires.
54
108
  // Both are undefined between sequences, so a later edit to `delay` applies to the next one.
55
109
  #delay;
56
110
  #deadline;
111
+ // The URL the current retry sequence is for. Cleared when disabled, URL-less, or given
112
+ // up, so the next attempt starts a fresh backoff window.
113
+ #sequenceHref;
114
+ // The href that exhausted its retry sequence. Survives a page hide/show so resume does
115
+ // not redial credentials the peer already refused. Cleared when the URL changes or the
116
+ // loop is disabled.
117
+ #givenUpHref;
57
118
  // Increased by 1 each time to trigger a reload.
58
119
  #tick = new Signal(0);
59
120
  // True after the browser freezes or hides the page until it visibly resumes.
@@ -63,25 +124,30 @@ export class Reload {
63
124
  #url;
64
125
  constructor(props) {
65
126
  this.url = Signal.from(props?.url);
66
- this.enabled = Signal.from(props?.enabled ?? false);
67
- this.delay = props?.delay ?? { initial: 1000, multiplier: 2, max: 5000 };
127
+ this.enabled = Signal.from(props?.enabled ?? true);
128
+ this.delay = props?.delay ?? {};
68
129
  this.webtransport = props?.webtransport;
69
130
  this.websocket = props?.websocket;
70
131
  this.discovery = props?.discovery;
71
- this.closed = new Promise((resolve, reject) => {
72
- this.#closedResolve = resolve;
73
- this.#closedReject = reject;
74
- });
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)
132
+ this.publish = props?.publish;
133
+ this.consume = props?.consume;
134
+ // Requests on the consume origin stay pending across a reconnect, and before the
135
+ // first session establishes, rather than reading as unroutable the moment no session
136
+ // is attached. Released only when this loop is disposed: giving up the current URL
137
+ // is recoverable (a new URL or a disable/re-enable starts another sequence), so a
138
+ // request must keep waiting rather than go unroutable in the gap.
139
+ if (this.consume) {
140
+ this.#signals.cleanup(wireOf(this.consume).expect());
141
+ }
142
+ this.error = this.#error;
143
+ const win = globalThis.window;
144
+ const doc = globalThis.document;
145
+ if (typeof win !== "undefined" && typeof doc !== "undefined") {
146
+ this.#signals.event(win, "pagehide", () => this.#suspended.set(true));
147
+ this.#signals.event(win, "pageshow", () => this.#suspended.set(false));
148
+ this.#signals.event(win, "unload", () => this.#suspended.set(true));
149
+ this.#signals.event(doc, "visibilitychange", () => {
150
+ if (!doc.hidden)
85
151
  this.#suspended.set(false);
86
152
  });
87
153
  }
@@ -89,6 +155,32 @@ export class Reload {
89
155
  const connection = effect.get(this.established);
90
156
  return connection && effect.get(connection.probe);
91
157
  });
158
+ this.bandwidth = new Allocator(this.#estimate);
159
+ this.#signals.cleanup(() => this.bandwidth.close());
160
+ // The transport has no event for the send-rate estimate, so sample on our
161
+ // own schedule and skip a tick while the previous snapshot is outstanding.
162
+ this.#signals.run((effect) => {
163
+ effect.set(this.#estimate, undefined);
164
+ const connection = effect.get(this.established);
165
+ if (!connection)
166
+ return;
167
+ let pending = false;
168
+ const sample = async () => {
169
+ if (pending)
170
+ return;
171
+ pending = true;
172
+ try {
173
+ const stats = await effect.race(connection.stats());
174
+ if (stats)
175
+ this.#estimate.set(stats.estimatedSendRate);
176
+ }
177
+ finally {
178
+ pending = false;
179
+ }
180
+ };
181
+ void sample();
182
+ effect.interval(() => void sample(), BANDWIDTH_POLL);
183
+ });
92
184
  this.#url = this.#signals.computed((effect) => effect.get(this.url)?.href);
93
185
  // Create a reactive root so cleanup is easier.
94
186
  this.#signals.run(this.#connect.bind(this));
@@ -96,14 +188,34 @@ export class Reload {
96
188
  #connect(effect) {
97
189
  // Will retry when the tick changes.
98
190
  effect.get(this.#tick);
99
- const suspended = effect.get(this.#suspended);
100
191
  const enabled = effect.get(this.enabled);
101
- if (!enabled || suspended)
192
+ if (!enabled) {
193
+ this.#givenUpHref = undefined;
194
+ this.#resetSequence();
195
+ return;
196
+ }
197
+ // Hide/freeze pauses an in-flight sequence without recovering from give-up.
198
+ const suspended = effect.get(this.#suspended);
199
+ if (suspended) {
200
+ this.#resetSequence();
102
201
  return;
202
+ }
103
203
  const href = effect.get(this.#url);
104
- if (!href)
204
+ if (!href) {
205
+ this.#givenUpHref = undefined;
206
+ this.#resetSequence();
105
207
  return;
208
+ }
106
209
  const url = new URL(href);
210
+ if (this.#givenUpHref === href) {
211
+ return;
212
+ }
213
+ if (this.#sequenceHref !== href) {
214
+ this.#resetSequence();
215
+ this.#sequenceHref = href;
216
+ this.#error.set(undefined);
217
+ this.#givenUpHref = undefined;
218
+ }
107
219
  effect.set(this.status, "connecting", "disconnected");
108
220
  // This run's teardown, handed to connect() so a rerun cancels the attempt in flight.
109
221
  const signal = effect.abort;
@@ -112,10 +224,13 @@ export class Reload {
112
224
  // later dropped from a connect failure or a peer that flaps immediately.
113
225
  let connected;
114
226
  try {
115
- const connection = await connect(url, {
227
+ const connection = await connect({
228
+ url,
116
229
  websocket: this.websocket,
117
230
  webtransport: this.webtransport,
118
231
  discovery: this.discovery,
232
+ publish: this.publish,
233
+ consume: this.consume,
119
234
  signal,
120
235
  });
121
236
  // Hand the connection to the effect, which closes it now if this run is already over.
@@ -126,12 +241,13 @@ export class Reload {
126
241
  effect.set(this.status, "connected", "disconnected");
127
242
  connected = performance.now();
128
243
  // 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)
244
+ // closing (null for clean, an Error otherwise) apart from this run being
245
+ // torn down.
246
+ const closed = await effect.race(connection.closed);
247
+ if (closed === undefined)
132
248
  return;
133
249
  console.warn("connection closed, reconnecting");
134
- this.#retry(effect, connected);
250
+ this.#retry(effect, connected, closed ?? undefined);
135
251
  }
136
252
  catch (err) {
137
253
  // Treat teardown as cancellation, not a connection failure.
@@ -148,6 +264,16 @@ export class Reload {
148
264
  * `cause` the error that killed it, if it died with one.
149
265
  */
150
266
  #retry(effect, connected, cause) {
267
+ // Resolved per sequence rather than at construction, so an edit to `delay` (including
268
+ // one that drops a field back to its default) applies to the next retry. Field by
269
+ // field rather than by spread: a caller building `{ initial: maybeInitial }` from an
270
+ // optional value passes an explicit undefined, which a spread would take as the
271
+ // answer, turning the backoff into NaN or the window into forever.
272
+ const delay = this.delay ?? {};
273
+ const initial = delay.initial ?? DEFAULT_DELAY.initial;
274
+ const multiplier = delay.multiplier ?? DEFAULT_DELAY.multiplier;
275
+ const max = delay.max ?? DEFAULT_DELAY.max;
276
+ const timeout = delay.timeout ?? DEFAULT_DELAY.timeout;
151
277
  // Any session is dead now: report disconnected during the backoff rather than
152
278
  // when the retry reruns the effect.
153
279
  this.established.set(undefined);
@@ -156,44 +282,66 @@ export class Reload {
156
282
  // start a fresh retry window: a one-off drop should reconnect promptly. Anything
157
283
  // shorter is a peer that accepts and immediately severs, which has to keep
158
284
  // escalating or we hammer it forever at the initial delay.
159
- if (connected !== undefined && performance.now() - connected >= this.delay.initial) {
285
+ if (connected !== undefined && performance.now() - connected >= initial) {
160
286
  this.#delay = undefined;
161
287
  this.#deadline = undefined;
162
288
  }
289
+ // An auth rejection stops this URL however long the session lived. UNAUTHORIZED is a
290
+ // specified code rather than one we guessed at, so this is the peer saying these
291
+ // credentials will never work; retrying them just burns the window. Matches
292
+ // moq-tokio's reconnect loop, which stops on the same close. A new URL or a
293
+ // disable/re-enable starts another sequence; the handle itself is not disposed.
294
+ //
295
+ // Only a session close says that. The stream registry gives 2 to DELIVERY_TIMEOUT,
296
+ // so a stream reset during the SETUP exchange would otherwise suppress reconnect
297
+ // for good.
298
+ if (cause instanceof SessionError && cause.code === SessionCode.Unauthorized) {
299
+ console.warn("session rejected as unauthorized, not retrying");
300
+ this.#giveUp(cause);
301
+ return;
302
+ }
163
303
  const now = performance.now();
164
- const timeout = this.delay.timeout ?? DEFAULT_TIMEOUT;
165
- this.#delay ??= this.delay.initial;
304
+ this.#delay ??= initial;
166
305
  this.#deadline ??= timeout > 0 ? now + timeout : Number.POSITIVE_INFINITY;
167
306
  if (now >= this.#deadline) {
168
307
  console.warn("reconnect timed out");
169
308
  // A graceful close has no error, so report the timeout itself.
170
- this.#closedReject(cause === undefined ? new Error("reconnect timed out") : error(cause));
309
+ this.#giveUp(cause === undefined ? new Error("reconnect timed out") : error(cause));
171
310
  return;
172
311
  }
173
312
  // Equal jitter, so a fleet of tabs knocked offline together doesn't reconnect on the same
174
313
  // tick, and never past the deadline the retry window promised.
175
314
  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);
315
+ this.#delay = Math.min(this.#delay * multiplier, max);
177
316
  const tick = this.#tick.peek() + 1;
178
317
  effect.timer(() => this.#tick.update((prev) => Math.max(prev, tick)), wait);
179
318
  }
319
+ #giveUp(cause) {
320
+ this.#error.set(cause);
321
+ this.#givenUpHref = this.#sequenceHref;
322
+ this.#resetSequence();
323
+ }
324
+ #resetSequence() {
325
+ this.#delay = undefined;
326
+ this.#deadline = undefined;
327
+ this.#sequenceHref = undefined;
328
+ }
180
329
  /**
181
- * Subscribe to broadcast announcements under an optional prefix, spanning reconnects.
330
+ * Subscribe to broadcast announcements matching `scope`, spanning reconnects.
182
331
  *
183
332
  * 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
333
+ * is retracted (a `retracted` update) whenever the connection drops and re-announced on
185
334
  * reconnect, so a consumer draining `next()` never clings to a dead route across a reconnect.
186
335
  *
187
336
  * Stays empty while the relay lacks {@link Established.discovery}.
188
337
  */
189
- announced(prefix = emptyPath()) {
190
- const producer = new Announce.Producer(prefix);
338
+ announced(scope = Path.Pattern.all(), options) {
339
+ // With a consume origin the table already spans reconnects (the forwarder retracts
340
+ // a dead session's entries), so its stream is the same thing with less machinery.
341
+ if (this.consume)
342
+ return this.consume.announced(scope, options);
343
+ const producer = new Announce.Producer();
191
344
  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
345
  const pump = new Effect();
198
346
  pump.run((effect) => {
199
347
  const conn = effect.get(this.established);
@@ -203,20 +351,21 @@ export class Reload {
203
351
  // consumer empty rather than opening a subscription that can't be answered.
204
352
  if (!conn.discovery)
205
353
  return;
206
- const upstream = conn.announced(prefix);
354
+ const upstream = conn.announced(scope, options);
207
355
  effect.cleanup(() => upstream.close());
208
- // Track what this connection announced so we can retract it if the connection drops.
209
- const active = new Set();
356
+ // Track what this connection announced so we can retract it if the connection
357
+ // drops; the last event rides along for the retraction.
358
+ const active = new Map();
210
359
  effect.spawn(async () => {
211
360
  try {
212
361
  for (;;) {
213
- const entry = await Promise.race([effect.cancel, upstream.next()]);
362
+ const entry = await effect.race(upstream.next());
214
363
  if (!entry)
215
364
  break;
216
- if (entry.active)
217
- active.add(entry.path);
365
+ if (Announce.isActive(entry.kind))
366
+ active.set(entry.prefix, entry);
218
367
  else
219
- active.delete(entry.path);
368
+ active.delete(entry.prefix);
220
369
  producer.append(entry);
221
370
  }
222
371
  }
@@ -226,9 +375,9 @@ export class Reload {
226
375
  finally {
227
376
  // Retract everything from the connection that just went away, so a per-broadcast
228
377
  // 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 });
378
+ if (consumer.closed.peek() === undefined) {
379
+ for (const entry of active.values()) {
380
+ producer.append({ ...entry, kind: "retracted" });
232
381
  }
233
382
  }
234
383
  }
@@ -238,20 +387,6 @@ export class Reload {
238
387
  void consumer.closed.then(() => pump.close());
239
388
  return consumer;
240
389
  }
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
390
  /**
256
391
  * Snapshot the live connection's transport counters, or undefined while disconnected.
257
392
  * See {@link Established.stats}.
@@ -259,10 +394,11 @@ export class Reload {
259
394
  async stats() {
260
395
  return this.established.peek()?.stats();
261
396
  }
262
- /** Stop reconnecting, close the current connection, and resolve {@link Reload.closed}. */
263
- close() {
397
+ /** Stop reconnecting, close the current connection, and settle {@link Reload.closed}. Idempotent. */
398
+ close(abort) {
264
399
  this.#signals.close();
265
- this.#closedResolve();
400
+ if (this.#closed.peek() === undefined)
401
+ this.#closed.set(abort ?? null);
266
402
  }
267
403
  }
268
404
  //# sourceMappingURL=reload.js.map