@norskvideo/moq-net 0.1.2 → 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
package/track.js CHANGED
@@ -1,69 +1,491 @@
1
1
  /* @ts-self-types="./track.d.ts" */
2
- import { Signal } from "@norskvideo/moq-signals";
3
- import { Group } from "./group.js";
4
- /** Reactive backing state for a {@link Track}: buffered groups, a closed flag, and the subscription priority. */
5
- export class TrackState {
2
+ /**
3
+ * Track role handles: a live stream of groups (and best-effort datagrams) within a broadcast.
4
+ *
5
+ * @module
6
+ */
7
+ import { Once, Signal } from "@norskvideo/moq-signals";
8
+ import { Producer as GroupProducer, Lagged } from "./group.js";
9
+ import { hooks } from "./internal.js";
10
+ import { Timescale } from "./time.js";
11
+ /** Default {@link Info.latencyMax} window (milliseconds) when the publisher does not set one. */
12
+ export const DEFAULT_LATENCY_MAX_MS = 5000;
13
+ /**
14
+ * How long (milliseconds) a datagram stays in the per-subscriber buffer before it is dropped.
15
+ *
16
+ * Datagrams are a best-effort send buffer, not a replay cache (unlike groups): only the last few
17
+ * tens of milliseconds are kept, so a consumer that stalls loses stale datagrams instead of
18
+ * replaying them. Mirrors the Rust `MAX_DATAGRAM_AGE`.
19
+ */
20
+ const MAX_DATAGRAM_AGE_MS = 50;
21
+ /**
22
+ * Sanity cap on a datagram payload: the QUIC DATAGRAM frame ceiling. The real limit is
23
+ * per-hop (the negotiated transport datagram size minus a small header) and oversize
24
+ * datagrams are dropped there; a payload above this cap could never fit anywhere.
25
+ */
26
+ const MAX_DATAGRAM_BYTES = 65535;
27
+ /** Fill in any unset {@link Info} fields with their defaults. */
28
+ export function infoDefaults(info = {}) {
29
+ return {
30
+ timescale: info.timescale ?? Timescale.MILLI,
31
+ latencyMax: info.latencyMax ?? DEFAULT_LATENCY_MAX_MS,
32
+ priority: info.priority ?? 0,
33
+ ordered: info.ordered ?? false,
34
+ };
35
+ }
36
+ // Materialize the defaults at the model boundary so every layer observes a complete
37
+ // subscription rather than interpreting an omitted field differently.
38
+ function subscriptionDefaults(subscription = {}) {
39
+ return {
40
+ priority: subscription.priority ?? 0,
41
+ ordered: subscription.ordered ?? false,
42
+ latencyMax: subscription.latencyMax ?? 0,
43
+ startGroup: subscription.startGroup,
44
+ endGroup: subscription.endGroup,
45
+ };
46
+ }
47
+ // Aggregate the preferences of every live subscriber, matching Rust's Subscription::poll_combined.
48
+ function combineSubscriptions(states) {
49
+ let combined;
50
+ for (const state of states) {
51
+ const subscription = state.update.peek();
52
+ if (!subscription)
53
+ continue;
54
+ if (!combined) {
55
+ combined = { ...subscription };
56
+ continue;
57
+ }
58
+ combined.priority = Math.max(combined.priority ?? 0, subscription.priority ?? 0);
59
+ combined.ordered = (combined.ordered ?? false) && (subscription.ordered ?? false);
60
+ combined.latencyMax = Math.max(combined.latencyMax ?? 0, subscription.latencyMax ?? 0);
61
+ if (subscription.startGroup !== undefined) {
62
+ combined.startGroup =
63
+ combined.startGroup === undefined
64
+ ? subscription.startGroup
65
+ : Math.min(combined.startGroup, subscription.startGroup);
66
+ }
67
+ if (combined.endGroup === undefined || subscription.endGroup === undefined) {
68
+ combined.endGroup = undefined;
69
+ }
70
+ else {
71
+ combined.endGroup = Math.max(combined.endGroup, subscription.endGroup);
72
+ }
73
+ }
74
+ return combined;
75
+ }
76
+ /**
77
+ * A request for a track the peer wants, yielded by `Broadcast.Producer.requested`.
78
+ *
79
+ * Created internally by the broadcast when a subscription (or info lookup) needs a track
80
+ * served; answer it with {@link accept} or {@link reject}.
81
+ *
82
+ * @public
83
+ */
84
+ export class Request {
85
+ /** The requested track name. */
86
+ name;
87
+ #producer;
88
+ #sequences;
89
+ constructor(options) {
90
+ this.name = options.name;
91
+ this.#producer = options.producer;
92
+ this.#sequences = options.sequences;
93
+ }
94
+ static {
95
+ hooks.makeRequest = (options) => new Request(options);
96
+ }
97
+ /** The aggregate subscription requested for this track. */
98
+ get subscription() {
99
+ return this.#producer.subscription.peek() ?? subscriptionDefaults();
100
+ }
101
+ /** The subscriber's priority for this track. */
102
+ get priority() {
103
+ return this.subscription.priority ?? 0;
104
+ }
105
+ /** Accept the request, committing the track's immutable {@link Info}. */
106
+ accept(info = {}) {
107
+ bindProducer(this.name, this.#producer, this.#sequences);
108
+ return this.#producer.accept(info);
109
+ }
110
+ /** Reject the request, closing the track optionally with an error. */
111
+ reject(err) {
112
+ this.#producer.close(err);
113
+ }
114
+ }
115
+ /**
116
+ * A lazy handle to a track on a consumed broadcast.
117
+ *
118
+ * @public
119
+ */
120
+ export class Consumer {
121
+ /** The track name. */
122
+ name;
123
+ #broadcast;
124
+ constructor(name, broadcast) {
125
+ this.name = name;
126
+ this.#broadcast = broadcast;
127
+ }
128
+ /** Open a live subscription to the track. */
129
+ subscribe(options) {
130
+ return this.#broadcast.subscribe(this.name, options);
131
+ }
132
+ /** Fetch the track's immutable publisher properties without subscribing. */
133
+ info() {
134
+ return this.#broadcast.resolveTrackInfo(this.name);
135
+ }
136
+ /** Fetch a single group by sequence without holding a live subscription. */
137
+ fetchGroup(sequence, options) {
138
+ return this.#broadcast.fetchGroup(this.name, sequence, options);
139
+ }
140
+ }
141
+ // The shared state behind a Producer / Subscriber pair. Package-internal
142
+ // wiring, unexported so it never appears in the published type declarations.
143
+ class TrackState {
144
+ /** The producer fanning into this sink, so a subscriber can mint a sibling of itself. */
145
+ producer;
6
146
  groups = new Signal([]);
7
- closed = new Signal(false);
8
- priority = new Signal(undefined);
147
+ /** Best-effort datagram channel, parallel to {@link groups}; an age-evicted send buffer per subscriber. */
148
+ datagrams = new Signal([]);
149
+ latest;
150
+ closed = new Once();
151
+ update;
152
+ /** Resolved once the producer commits the immutable properties. */
153
+ info = new Signal(undefined);
154
+ constructor(subscription) {
155
+ this.update = new Signal(subscription === undefined ? undefined : subscriptionDefaults(subscription));
156
+ }
157
+ }
158
+ // Settle a track state's closed Once. Idempotent: Once.set throws on a second settle, and a
159
+ // Producer closing its sinks races the Subscriber closing itself (the sink is only removed from
160
+ // #sinks a microtask later, via the closed subscription below).
161
+ function closeTrackState(state, abort) {
162
+ if (state.closed.peek() !== undefined)
163
+ return false;
164
+ state.closed.set(abort ?? null);
165
+ return true;
166
+ }
167
+ // Resolve the track's immutable publisher properties, or reject if it closes first.
168
+ // On a producer this resolves once info is committed (at accept time); on a consumer
169
+ // once the wire layer commits the TRACK_INFO it received (lite-05+) or defaults (older
170
+ // drafts), so awaiting it never yields a placeholder.
171
+ async function resolveInfo(state) {
172
+ for (;;) {
173
+ const info = state.info.peek();
174
+ if (info)
175
+ return info;
176
+ const closed = state.closed.peek();
177
+ if (closed instanceof Error)
178
+ throw closed;
179
+ if (closed !== undefined)
180
+ throw new Error("track closed before info was known");
181
+ await Signal.race(state.info, state.closed);
182
+ }
183
+ }
184
+ function bindProducer(name, producer, sequences) {
185
+ let shared = sequences.get(name);
186
+ if (!shared) {
187
+ shared = { next: 0 };
188
+ sequences.set(name, shared);
189
+ }
190
+ bindProducerSequence(producer, shared);
9
191
  }
10
- /** A live stream of groups within a broadcast, identified by name. */
11
- export class Track {
12
- /** Name of this track within its broadcast. */
192
+ let bindProducerSequence;
193
+ // Constructs a Subscriber from within this module without exposing a public
194
+ // constructor that would leak the unexported TrackState. Assigned in the class's
195
+ // static block.
196
+ let makeSubscriber;
197
+ /**
198
+ * The write side of a track, mirroring the Rust `Producer`.
199
+ *
200
+ * A producer is a fan-out source: every {@link subscribe} (including each wire
201
+ * subscription the publisher serves from it) gets an independent
202
+ * {@link Subscriber} that receives a full copy of the groups, each with its own
203
+ * read cursor. Groups are mirrored into every live subscriber and retained for the
204
+ * track's `latencyMax` window so a late subscriber replays the recent groups.
205
+ *
206
+ * Obtained from {@link Request.accept} (the wire asks the application for a track to
207
+ * serve) or constructed directly for an in-process track.
208
+ */
209
+ export class Producer {
210
+ /** The track name. */
13
211
  name;
14
- /** Reactive backing state. */
15
- state = new TrackState();
16
- #next;
17
- #nextSequence = 0;
18
- /** Resolves with the abort error (or undefined) once closed. */
19
- closed;
212
+ // The producer's own state is the source of truth (info/closed); subscribers
213
+ // read mirrored sinks, never this state directly.
214
+ #state = new TrackState();
215
+ #sequence = { next: 0 };
216
+ // Recently written source groups, retained for replay to late subscribers and
217
+ // pruned once closed and older than the cache window. Each entry tracks the mirror
218
+ // it handed to every sink so eviction can drop them too: otherwise a slow consumer
219
+ // that never reads would pin old groups (and their frame bytes) forever.
220
+ #cache = [];
221
+ // One independent downstream state per live subscriber.
222
+ #sinks = new Set();
223
+ // Whether any subscriber is currently attached. Exposed as {@link used}; the consumer wire
224
+ // watches it to tear down an idle upstream, and a publisher can watch it for on-demand capture.
225
+ #used = new Signal(false);
20
226
  constructor(name) {
21
227
  this.name = name;
22
- this.closed = new Promise((resolve) => {
23
- const dispose = this.state.closed.subscribe((closed) => {
24
- if (!closed)
228
+ }
229
+ static {
230
+ bindProducerSequence = (producer, sequence) => {
231
+ producer.#sequence = sequence;
232
+ };
233
+ }
234
+ /**
235
+ * Resolve this track's immutable publisher properties, committed at accept time.
236
+ * Rejects if the track is closed before the properties are known.
237
+ */
238
+ info() {
239
+ return resolveInfo(this.#state);
240
+ }
241
+ /**
242
+ * Settles once the track closes: `null` on a clean close, or the abort {@link Error}.
243
+ * Peek it synchronously (`undefined` while open), observe it reactively, or `await` it.
244
+ */
245
+ get closed() {
246
+ return this.#state.closed;
247
+ }
248
+ /**
249
+ * The aggregate subscription across live subscribers, or `undefined` when there are none.
250
+ * The wire layer watches this to emit SUBSCRIBE_UPDATE.
251
+ */
252
+ get subscription() {
253
+ return this.#state.update;
254
+ }
255
+ /** Commit the immutable publisher properties, resolving {@link info}. Returns `this`. */
256
+ accept(info = {}) {
257
+ const resolved = infoDefaults(info);
258
+ this.#state.info.set(resolved);
259
+ // Propagate to any sink handed out before accept (the on-demand path).
260
+ for (const sink of this.#sinks)
261
+ sink.info.set(resolved);
262
+ return this;
263
+ }
264
+ /** An independent {@link Subscriber} receiving a full copy of this track's groups. */
265
+ subscribe(options = {}) {
266
+ const sink = new TrackState(options);
267
+ this.#addSink(sink);
268
+ return makeSubscriber(this.name, sink);
269
+ }
270
+ /**
271
+ * Whether the track currently has any subscribers.
272
+ *
273
+ * Watch it (`effect.get` / `.peek()`) to drive on-demand work: a publisher can start and stop
274
+ * capture with demand, and the consumer wire watches it to tear an idle upstream subscription
275
+ * down instead of downloading to nobody. Pairs with {@link unused}. Mirrors the Rust `Demand`.
276
+ */
277
+ get used() {
278
+ return this.#used;
279
+ }
280
+ /** Resolves once the track has no subscribers (or has closed). Await it to react to demand ending. */
281
+ async unused() {
282
+ while (this.#used.peek() && this.#state.closed.peek() === undefined) {
283
+ await Signal.race(this.#used, this.#state.closed);
284
+ }
285
+ }
286
+ // Register a downstream sink: seed its info, replay the retained window, and (while
287
+ // the track is open) mirror future groups into it. A late subscriber to a closed
288
+ // track still drains the buffered groups before seeing the end.
289
+ #addSink(sink) {
290
+ sink.producer = this;
291
+ const info = this.#state.info.peek();
292
+ if (info)
293
+ sink.info.set(info);
294
+ const closed = this.#state.closed.peek();
295
+ if (closed === undefined) {
296
+ this.#sinks.add(sink);
297
+ this.#used.set(true);
298
+ // Forward subscription updates from the sink's Subscriber to the producer's own
299
+ // state, which the wire layer (or the serving application) watches.
300
+ const forward = sink.update.subscribe(() => this.#updateSubscription());
301
+ this.#updateSubscription();
302
+ // Drop the sink once its consumer goes away, closing its mirrors so source
303
+ // groups stop teeing into them, so a long-lived producer doesn't leak. This
304
+ // covers mirrors already handed out via recvGroup (no longer in sink.groups)
305
+ // by closing them through the cache's per-sink tracking.
306
+ const dispose = sink.closed.subscribe((c) => {
307
+ if (c === undefined)
25
308
  return;
26
- resolve(closed instanceof Error ? closed : undefined);
309
+ const abort = c instanceof Error ? c : undefined;
310
+ forward();
311
+ this.#sinks.delete(sink);
312
+ this.#updateSubscription();
313
+ for (const entry of this.#cache) {
314
+ const mirror = entry.mirrors.get(sink);
315
+ if (mirror) {
316
+ mirror.close(abort);
317
+ entry.mirrors.delete(sink);
318
+ }
319
+ }
320
+ for (const group of sink.groups.peek())
321
+ group.close(abort);
27
322
  dispose();
323
+ // Update demand: once the last subscriber leaves, the consumer wire (watching
324
+ // {@link unused}) tears the upstream down instead of downloading to nobody.
325
+ this.#used.set(this.#sinks.size > 0);
28
326
  });
327
+ }
328
+ this.#prune();
329
+ for (const entry of this.#cache)
330
+ this.#mirror(entry, sink);
331
+ if (closed !== undefined)
332
+ closeTrackState(sink, closed instanceof Error ? closed : undefined);
333
+ }
334
+ // Recompute from every live sink because an update or close can narrow as well as widen
335
+ // the aggregate. The wire layer observes this signal and emits SUBSCRIBE_UPDATE.
336
+ #updateSubscription() {
337
+ this.#state.update.set(combineSubscriptions(this.#sinks));
338
+ }
339
+ // Mirror a cached source group into a sink. The mirror fills synchronously as the
340
+ // source is written and keeps its own read cursor; frame bytes are shared by
341
+ // reference. Tracked on the entry so eviction can drop it from the sink.
342
+ #mirror(entry, sink) {
343
+ const dst = entry.group.mirror();
344
+ entry.mirrors.set(sink, dst);
345
+ sink.latest = Math.max(sink.latest ?? 0, dst.sequence);
346
+ sink.groups.mutate((groups) => {
347
+ groups.push(dst);
348
+ groups.sort((a, b) => a.sequence - b.sequence);
29
349
  });
30
350
  }
31
- /**
32
- * Appends a new group to the track.
33
- * @returns A GroupProducer for the new group
34
- */
351
+ // Drop a cached group's mirror from every sink so no consumer can pin it.
352
+ #evict(entry) {
353
+ for (const [sink, mirror] of entry.mirrors) {
354
+ sink.groups.mutate((groups) => {
355
+ const i = groups.indexOf(mirror);
356
+ if (i >= 0)
357
+ groups.splice(i, 1);
358
+ });
359
+ mirror.close();
360
+ }
361
+ entry.mirrors.clear();
362
+ }
363
+ // Evict cached groups that are closed and older than the cache window.
364
+ #prune() {
365
+ const latencyMaxMs = this.#state.info.peek()?.latencyMax ?? DEFAULT_LATENCY_MAX_MS;
366
+ const cutoff = Date.now() - latencyMaxMs;
367
+ const retained = [];
368
+ for (const entry of this.#cache) {
369
+ if (entry.time > cutoff || entry.group.closed.peek() === undefined) {
370
+ retained.push(entry);
371
+ continue;
372
+ }
373
+ this.#evict(entry);
374
+ }
375
+ this.#cache = retained;
376
+ }
377
+ // Retain a source group and fan it out to every live sink.
378
+ #publish(group) {
379
+ const entry = { group, time: Date.now(), mirrors: new Map() };
380
+ this.#cache.push(entry);
381
+ this.#prune();
382
+ for (const sink of this.#sinks)
383
+ this.#mirror(entry, sink);
384
+ }
385
+ /** Append a new group with the next sequence number. */
35
386
  appendGroup() {
36
- if (this.state.closed.peek())
387
+ if (this.#state.closed.peek() !== undefined)
37
388
  throw new Error("track is closed");
38
- const group = new Group(this.#next ?? 0);
39
- this.#next = group.sequence + 1;
40
- this.state.groups.mutate((groups) => {
41
- groups.push(group);
42
- groups.sort((a, b) => a.sequence - b.sequence);
43
- });
389
+ const sequence = this.#sequence;
390
+ const group = new GroupProducer(sequence.next);
391
+ sequence.next = group.sequence + 1;
392
+ this.#publish(group);
44
393
  return group;
45
394
  }
46
395
  /**
47
- * Inserts an existing group into the track.
48
- * @param group - The group to insert
396
+ * Insert an existing group into the track.
397
+ *
398
+ * Throws on a sequence that is still cached: a live duplicate would fan out to every
399
+ * subscriber twice. An aborted incarnation is evicted so a fresh group can serve the
400
+ * sequence again. Best effort (mirrors Rust): nothing remembers a sequence whose cache
401
+ * entry is already gone, so a long-evicted sequence is accepted as new.
49
402
  */
50
403
  writeGroup(group) {
51
- if (this.state.closed.peek())
404
+ if (this.#state.closed.peek() !== undefined)
52
405
  throw new Error("track is closed");
53
- // Only advance #next upward (for appendGroup auto-increment).
54
- if (group.sequence >= (this.#next ?? 0)) {
55
- this.#next = group.sequence + 1;
406
+ const existing = this.#cache.findIndex((entry) => entry.group.sequence === group.sequence);
407
+ if (existing >= 0) {
408
+ const entry = this.#cache[existing];
409
+ if (!(entry.group.closed.peek() instanceof Error)) {
410
+ throw new Error(`duplicate group: sequence=${group.sequence}`);
411
+ }
412
+ this.#evict(entry);
413
+ this.#cache.splice(existing, 1);
56
414
  }
57
- this.state.groups.mutate((groups) => {
58
- groups.push(group);
59
- groups.sort((a, b) => a.sequence - b.sequence);
60
- });
415
+ // Only advance the shared counter upward (for appendGroup auto-increment).
416
+ const sequence = this.#sequence;
417
+ if (group.sequence >= sequence.next) {
418
+ sequence.next = group.sequence + 1;
419
+ }
420
+ this.#publish(group);
421
+ }
422
+ // Fan a datagram out to every live subscriber, dropping the oldest once the ring is full.
423
+ // Late subscribers do NOT replay old datagrams (best-effort, unlike the group cache).
424
+ #publishDatagram(datagram) {
425
+ const now = performance.now();
426
+ for (const sink of this.#sinks) {
427
+ sink.datagrams.mutate((list) => {
428
+ list.push({ datagram, time: now });
429
+ // Drop anything older than the send-buffer window.
430
+ while (list.length > 0 && now - list[0].time > MAX_DATAGRAM_AGE_MS)
431
+ list.shift();
432
+ });
433
+ }
434
+ }
435
+ /**
436
+ * Append a datagram with the next sequence number, returning the assigned sequence.
437
+ *
438
+ * A datagram is delivered best-effort over a single QUIC datagram, parallel to the track's
439
+ * groups but drawing from the same sequence namespace (interleaving with {@link appendGroup}
440
+ * never reuses a number). The payload must fit the negotiated transport datagram size minus
441
+ * a small header; an oversize payload is dropped at each hop (there is no group fallback), so
442
+ * keep datagram payloads small (e.g. a single audio frame). Datagrams are never delivered
443
+ * over IETF moq-transport or stream-only transports (the WebSocket fallback). A payload over
444
+ * 65535 bytes (the QUIC datagram frame ceiling) throws. An origin publisher uses this; a
445
+ * relay preserving upstream numbering uses {@link writeDatagram}.
446
+ */
447
+ appendDatagram(timestamp, payload) {
448
+ if (this.#state.closed.peek() !== undefined)
449
+ throw new Error("track is closed");
450
+ if (payload.byteLength > MAX_DATAGRAM_BYTES)
451
+ throw new Error("datagram payload too large");
452
+ const counter = this.#sequence;
453
+ const sequence = counter.next;
454
+ counter.next = sequence + 1;
455
+ this.#publishDatagram({ sequence, timestamp, payload });
456
+ return sequence;
61
457
  }
62
458
  /**
63
- * Appends a frame to the track in its own group.
459
+ * Write a datagram with an explicit sequence number.
64
460
  *
65
- * @param frame - The frame to append
461
+ * Preserves the supplied sequence (advancing the shared counter if needed) so a relay can
462
+ * forward a datagram without renumbering it. The size limits of {@link appendDatagram}
463
+ * apply. Most origin publishers want {@link appendDatagram} instead.
66
464
  */
465
+ writeDatagram(datagram) {
466
+ if (this.#state.closed.peek() !== undefined)
467
+ throw new Error("track is closed");
468
+ if (datagram.payload.byteLength > MAX_DATAGRAM_BYTES)
469
+ throw new Error("datagram payload too large");
470
+ const sequence = this.#sequence;
471
+ if (datagram.sequence >= sequence.next) {
472
+ sequence.next = datagram.sequence + 1;
473
+ }
474
+ this.#publishDatagram(datagram);
475
+ }
476
+ /** Close the track and every subscriber, mirroring the abort to their groups. Idempotent. */
477
+ close(abort) {
478
+ closeTrackState(this.#state, abort);
479
+ for (const { group } of this.#cache)
480
+ group.close(abort);
481
+ for (const sink of this.#sinks) {
482
+ for (const group of sink.groups.peek())
483
+ group.close(abort);
484
+ closeTrackState(sink, abort);
485
+ }
486
+ this.#sinks.clear();
487
+ }
488
+ /** Append a frame as its own single-frame group. */
67
489
  writeFrame(frame) {
68
490
  const group = this.appendGroup();
69
491
  group.writeFrame(frame);
@@ -87,101 +509,299 @@ export class Track {
87
509
  group.writeBool(bool);
88
510
  group.close();
89
511
  }
512
+ }
513
+ /**
514
+ * The read side of a live track subscription, mirroring the Rust `Subscriber`.
515
+ *
516
+ * Obtained from `Broadcast.Consumer.subscribe` / `Track.Consumer.subscribe`, or from
517
+ * {@link Producer.subscribe} for an in-process track. Reads the groups a
518
+ * {@link Producer} on the same underlying state writes.
519
+ */
520
+ export class Subscriber {
521
+ /** The track name. */
522
+ name;
523
+ #state;
524
+ #nextSequence = 0;
525
+ #cursor = new Signal({ start: 0 });
526
+ constructor(name, state) {
527
+ this.name = name;
528
+ this.#state = state;
529
+ }
530
+ static {
531
+ makeSubscriber = (name, state) => new Subscriber(name, state);
532
+ }
90
533
  /**
91
- * Receive the next group available on this track, in arrival order.
534
+ * Resolve this track's immutable publisher properties.
92
535
  *
93
- * Groups may arrive out of order or with gaps due to network conditions.
94
- * Use {@link nextGroupOrdered} if you need groups in sequence order,
95
- * skipping those that arrive too late.
536
+ * Resolves once the wire layer commits the TRACK_INFO it received (lite-05+) or
537
+ * defaults (older drafts), so awaiting it never yields a placeholder. Rejects if
538
+ * the track is closed before the properties are known (e.g. a rejected subscription).
539
+ */
540
+ info() {
541
+ return resolveInfo(this.#state);
542
+ }
543
+ /** Settles once the track closes; see {@link Producer.closed}. */
544
+ get closed() {
545
+ return this.#state.closed;
546
+ }
547
+ /** This subscriber's current options, including defaults and the last {@link update}. */
548
+ get subscription() {
549
+ return this.#state.update;
550
+ }
551
+ /** Return the latest group sequence observed on this track, if any. */
552
+ latest() {
553
+ return this.#state.latest;
554
+ }
555
+ /**
556
+ * The newest frame this track has produced, or `undefined` while it has none.
557
+ *
558
+ * Read from the groups buffered for this subscriber, so nothing is consumed. A newest
559
+ * group that has no frames yet falls back to the newest older group that does. It
560
+ * reads as `undefined` once the newest group has been evicted or received: what is
561
+ * left can no longer say where the edge is.
562
+ */
563
+ largest() {
564
+ const latest = this.#state.latest;
565
+ if (latest === undefined)
566
+ return undefined;
567
+ const groups = this.#state.groups.peek();
568
+ const newest = groups[groups.length - 1];
569
+ if (!newest || newest.sequence !== latest)
570
+ return undefined;
571
+ for (let i = groups.length - 1; i >= 0; i--) {
572
+ const count = groups[i].frameCount;
573
+ if (count > 0)
574
+ return { group: groups[i].sequence, frame: count - 1 };
575
+ }
576
+ return undefined;
577
+ }
578
+ /**
579
+ * An independent subscriber to the same track, with its own cursor and its own replay of
580
+ * the retained window.
581
+ *
582
+ * Reads here do not consume anything this one would deliver, which is what lets a
583
+ * publisher read the cache alongside the cursor it is serving from. Resolving the track
584
+ * again through the broadcast would not do: a dynamic serve is one request per peer
585
+ * subscription, so that mints a second producer the application has to answer separately.
586
+ *
587
+ * @internal Wire layers only.
588
+ */
589
+ fork(options) {
590
+ const producer = this.#state.producer;
591
+ if (!producer)
592
+ throw new Error("track has no producer to fork from");
593
+ return producer.subscribe(options);
594
+ }
595
+ /** Start this subscriber's local read cursor at `sequence`, without changing its wire request. */
596
+ startAt(sequence) {
597
+ this.#cursor.update((cursor) => ({ ...cursor, start: sequence }));
598
+ }
599
+ /**
600
+ * Cap {@link nextGroup} and {@link recvGroup} at `sequence` inclusively, or omit it to
601
+ * remove the cap. Groups above the cap remain buffered and become readable if the cap
602
+ * is raised. This local cursor does not change the subscription's wire request.
603
+ */
604
+ endAt(sequence) {
605
+ this.#cursor.update((cursor) => ({ ...cursor, end: sequence }));
606
+ }
607
+ /** Close the track (optionally with an error), closing any pending groups. Idempotent. */
608
+ close(abort) {
609
+ // Settle if we're first (the producer may already have); either way drop anything
610
+ // still buffered. Groups parked at the endAt cap deliberately outlive a clean
611
+ // producer close, so the subscriber leaving is what must release them: closing
612
+ // and clearing wakes a pending read to observe the end instead of hanging.
613
+ closeTrackState(this.#state, abort);
614
+ this.#state.groups.mutate((groups) => {
615
+ for (const group of groups)
616
+ group.close(abort);
617
+ groups.length = 0;
618
+ });
619
+ }
620
+ /**
621
+ * Receive every group on this track exactly once, as it becomes available.
622
+ *
623
+ * Groups may arrive out of order or with gaps due to network conditions; unlike
624
+ * {@link nextGroup}, one that arrives after a newer group was already returned is
625
+ * still delivered. When several groups are buffered, the lowest sequence is
626
+ * returned first.
627
+ *
628
+ * Honors the floor set by {@link startAt} and the cap set by {@link endAt}: a group
629
+ * beyond the cap stays buffered (not dropped) and is offered once the cap rises, even
630
+ * after a clean close, without blocking in-range groups that arrive behind it.
96
631
  */
97
632
  async recvGroup() {
98
633
  for (;;) {
99
- const groups = this.state.groups.peek();
100
- if (groups.length > 0) {
101
- return groups.shift();
102
- }
103
- const closed = this.state.closed.peek();
634
+ const group = this.tryRecvGroup();
635
+ if (group)
636
+ return group;
637
+ const closed = this.#state.closed.peek();
104
638
  if (closed instanceof Error)
105
639
  throw closed;
106
- if (closed)
640
+ // A group beyond the cap outlives a clean close: it becomes deliverable if
641
+ // the cap rises, so the track isn't over while any are held.
642
+ if (closed !== undefined && !this.#state.groups.peek()[0])
107
643
  return undefined;
108
- await Signal.race(this.state.groups, this.state.closed);
644
+ await Signal.race(this.#state.groups, this.#cursor, this.#state.closed);
109
645
  }
110
646
  }
111
647
  /**
112
- * @deprecated Use {@link recvGroup} for arrival order, or {@link nextGroupOrdered} for sequence order.
648
+ * Take the next buffered group without blocking, honoring the same cursor bounds as
649
+ * {@link recvGroup}.
650
+ *
651
+ * Returns `undefined` when nothing is deliverable right now, which is not by itself the
652
+ * end of the track: a group may still arrive, or one may be parked beyond the
653
+ * {@link endAt} cap. Use it to drain what the retained window already holds, where
654
+ * waiting for a sequence nothing will republish would park forever.
113
655
  */
114
- async nextGroup() {
115
- return this.recvGroup();
656
+ tryRecvGroup() {
657
+ const groups = this.#state.groups.peek();
658
+ const { start, end } = this.#cursor.peek();
659
+ while (groups.length > 0 && groups[0].sequence < start)
660
+ groups.shift()?.close();
661
+ // The buffer is sequence-sorted, so an in-range group that arrives behind a
662
+ // beyond-cap one sorts in front of it and is never blocked by it.
663
+ const group = groups[0];
664
+ if (group && (end === undefined || group.sequence <= end)) {
665
+ groups.shift();
666
+ return group;
667
+ }
668
+ return undefined;
116
669
  }
117
670
  /**
118
- * Return the next group with a strictly-greater sequence number than the last returned.
119
- *
120
- * Late arrivals (with a sequence number at or below the last one returned) are silently skipped.
671
+ * Receive the next datagram in arrival order.
121
672
  *
122
- * NOTE: This will be renamed to `nextGroup` in the next major version.
673
+ * Datagrams are a separate best-effort channel from groups (see
674
+ * {@link Producer.appendDatagram}); they share only the sequence namespace. A consumer
675
+ * that falls too far behind silently loses the oldest datagrams. Read this alongside
676
+ * {@link recvGroup} (e.g. in a separate loop) to receive both channels concurrently. Returning
677
+ * a datagram advances {@link nextGroup} past that sequence.
123
678
  */
124
- async nextGroupOrdered() {
679
+ async recvDatagram() {
125
680
  for (;;) {
126
- const group = await this.recvGroup();
127
- if (!group)
681
+ const datagrams = this.#state.datagrams.peek();
682
+ // Evict datagrams older than the send-buffer window (also enforced on write), so a
683
+ // reader that stalled skips stale datagrams instead of replaying them.
684
+ const cutoff = performance.now() - MAX_DATAGRAM_AGE_MS;
685
+ while (datagrams.length > 0 && datagrams[0].time < cutoff)
686
+ datagrams.shift();
687
+ if (datagrams.length > 0) {
688
+ const datagram = datagrams.shift()?.datagram;
689
+ if (datagram) {
690
+ this.#nextSequence = Math.max(this.#nextSequence, datagram.sequence + 1);
691
+ }
692
+ return datagram;
693
+ }
694
+ const closed = this.#state.closed.peek();
695
+ if (closed instanceof Error)
696
+ throw closed;
697
+ if (closed !== undefined)
128
698
  return undefined;
129
- if (group.sequence < this.#nextSequence) {
130
- group.close();
131
- continue;
699
+ await Signal.race(this.#state.datagrams, this.#state.closed);
700
+ }
701
+ }
702
+ /**
703
+ * Return the next group with a strictly-greater sequence number than the last returned.
704
+ *
705
+ * Late arrivals (sequence at or below the last returned) are silently skipped.
706
+ * Use {@link recvGroup} to see every group in arrival order instead.
707
+ */
708
+ async nextGroup() {
709
+ for (;;) {
710
+ const groups = this.#state.groups.peek();
711
+ const cursor = this.#cursor.peek();
712
+ const start = Math.max(cursor.start, this.#nextSequence);
713
+ while (groups.length > 0 && groups[0].sequence < start)
714
+ groups.shift()?.close();
715
+ const group = groups[0];
716
+ if (group && (cursor.end === undefined || group.sequence <= cursor.end)) {
717
+ groups.shift();
718
+ this.#nextSequence = group.sequence + 1;
719
+ return group;
132
720
  }
133
- this.#nextSequence = group.sequence + 1;
134
- return group;
721
+ const closed = this.#state.closed.peek();
722
+ if (closed instanceof Error)
723
+ throw closed;
724
+ if (closed !== undefined && !group)
725
+ return undefined;
726
+ await Signal.race(this.#state.groups, this.#cursor, this.#state.closed);
135
727
  }
136
728
  }
137
- /** Reads the next frame across all groups, discarding older groups. */
729
+ /**
730
+ * Reads the next frame across all groups, discarding older groups.
731
+ * Treat the returned frame bytes as read-only; they are shared with other consumers.
732
+ */
138
733
  async readFrame() {
139
- return (await this.readFrameSequence())?.data;
734
+ const next = await this.readFrameSequence();
735
+ return next ? { payload: next.payload, timestamp: next.timestamp } : undefined;
140
736
  }
141
- /** Reads the next frame along with its group and frame sequence numbers. */
737
+ /**
738
+ * Reads the next frame along with its group and frame sequence numbers.
739
+ * Treat the returned frame bytes as read-only; they are shared with other consumers.
740
+ */
142
741
  async readFrameSequence() {
143
742
  for (;;) {
144
- const groups = this.state.groups.peek();
145
- // Discard old groups.
743
+ const groups = this.#state.groups.peek();
744
+ const { start } = this.#cursor.peek();
745
+ while (groups.length > 0 && groups[0].sequence < start)
746
+ groups.shift()?.close();
747
+ // Drain older groups first, dropping each once empty.
146
748
  while (groups.length > 1) {
749
+ if (groups[0].skipped) {
750
+ // The reader fell behind this group's eviction window. Drop it and
751
+ // signal the gap; the next read resyncs from the following group.
752
+ groups.shift()?.close();
753
+ throw new Lagged();
754
+ }
147
755
  const next = groups[0].tryReadFrameSequence();
148
756
  if (next) {
149
- return { group: groups[0].sequence, frame: next.sequence, data: next.data };
757
+ return {
758
+ group: groups[0].sequence,
759
+ frame: next.sequence,
760
+ payload: next.payload,
761
+ timestamp: next.timestamp,
762
+ };
150
763
  }
151
- // Skip this old group
152
764
  groups.shift()?.close();
153
765
  }
154
- // If there's no groups, wait for a new one.
155
766
  if (groups.length === 0) {
156
- const closed = this.state.closed.peek();
767
+ const closed = this.#state.closed.peek();
157
768
  if (closed instanceof Error)
158
769
  throw closed;
159
- if (closed)
770
+ if (closed !== undefined)
160
771
  return undefined;
161
- await Signal.race(this.state.groups, this.state.closed);
772
+ await Signal.race(this.#state.groups, this.#cursor, this.#state.closed);
162
773
  continue;
163
774
  }
164
- // If there's a group, wait for a frame.
165
775
  const group = groups[0];
166
- const next = group.tryReadFrameSequence();
167
- if (next) {
168
- return { group: group.sequence, frame: next.sequence, data: next.data };
776
+ if (group.skipped) {
777
+ // Fell behind this group's eviction window. Drop it and signal the gap;
778
+ // the next read resyncs from the following group.
779
+ groups.shift()?.close();
780
+ throw new Lagged();
169
781
  }
170
- // If the track is closed, return undefined.
171
- const closed = this.state.closed.peek();
782
+ const next = group.tryReadFrameSequence();
783
+ if (next)
784
+ return {
785
+ group: group.sequence,
786
+ frame: next.sequence,
787
+ payload: next.payload,
788
+ timestamp: next.timestamp,
789
+ };
790
+ const closed = this.#state.closed.peek();
172
791
  if (closed instanceof Error)
173
792
  throw closed;
174
- if (closed)
793
+ if (closed !== undefined)
175
794
  return undefined;
176
- // Wake on a new group or the track closing. Only fold in the current group's readability
177
- // while it can still deliver frames: a finished group's readable() resolves immediately, so
178
- // racing it would busy-loop until a newer group arrives instead of waiting.
795
+ // A finished (drained + closed) group has nothing left: drop it and loop, rather than
796
+ // busy-waiting on its already-resolved readable() (which would livelock and starve the
797
+ // macrotask that delivers the next group).
179
798
  if (group.done) {
180
- await Signal.race(this.state.groups, this.state.closed);
181
- }
182
- else {
183
- await Promise.race([Signal.race(this.state.groups, this.state.closed), group.readable()]);
799
+ groups.shift()?.close();
800
+ continue;
184
801
  }
802
+ // Lone open group with nothing buffered yet: wait for a frame on it, a new group, or
803
+ // the track closing.
804
+ await Promise.race([Signal.race(this.#state.groups, this.#cursor, this.#state.closed), group.readable()]);
185
805
  }
186
806
  }
187
807
  /** Reads the next frame and decodes it as a UTF-8 string. */
@@ -189,7 +809,7 @@ export class Track {
189
809
  const next = await this.readFrame();
190
810
  if (!next)
191
811
  return undefined;
192
- return new TextDecoder().decode(next);
812
+ return new TextDecoder().decode(next.payload);
193
813
  }
194
814
  /** Reads the next frame and parses it as JSON. */
195
815
  async readJson() {
@@ -203,25 +823,17 @@ export class Track {
203
823
  const next = await this.readFrame();
204
824
  if (!next)
205
825
  return undefined;
206
- if (next.byteLength !== 1 || !(next[0] === 0 || next[0] === 1))
826
+ const payload = next.payload;
827
+ if (payload.byteLength !== 1 || !(payload[0] === 0 || payload[0] === 1))
207
828
  throw new Error("invalid bool frame");
208
- return next[0] === 1;
209
- }
210
- /**
211
- * Update the subscription priority. Triggers a SUBSCRIBE_UPDATE
212
- * to the publisher when used on a subscribed track.
213
- */
214
- updatePriority(priority) {
215
- this.state.priority.set(priority, true);
829
+ return payload[0] === 1;
216
830
  }
217
831
  /**
218
- * Closes the publisher and all associated groups.
832
+ * Update this subscription's options (e.g. priority), triggering a SUBSCRIBE_UPDATE to the
833
+ * publisher. Mirrors the Rust `Subscriber::update`.
219
834
  */
220
- close(abort) {
221
- this.state.closed.set(abort ?? true);
222
- for (const group of this.state.groups.peek()) {
223
- group.close(abort);
224
- }
835
+ update(options) {
836
+ this.#state.update.set(subscriptionDefaults(options));
225
837
  }
226
838
  }
227
839
  //# sourceMappingURL=track.js.map