@norskvideo/moq-net 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (274) hide show
  1. package/README.md +70 -0
  2. package/announced.d.ts +45 -0
  3. package/announced.d.ts.map +1 -0
  4. package/announced.js +70 -0
  5. package/announced.js.map +1 -0
  6. package/bandwidth.d.ts +11 -0
  7. package/bandwidth.d.ts.map +1 -0
  8. package/bandwidth.js +7 -0
  9. package/bandwidth.js.map +1 -0
  10. package/broadcast.d.ts +39 -0
  11. package/broadcast.d.ts.map +1 -0
  12. package/broadcast.js +76 -0
  13. package/broadcast.js.map +1 -0
  14. package/connection/accept.d.ts +15 -0
  15. package/connection/accept.d.ts.map +1 -0
  16. package/connection/accept.js +137 -0
  17. package/connection/accept.js.map +1 -0
  18. package/connection/cert.test.d.ts +2 -0
  19. package/connection/cert.test.d.ts.map +1 -0
  20. package/connection/cert.test.js +26 -0
  21. package/connection/cert.test.js.map +1 -0
  22. package/connection/connect.d.ts +37 -0
  23. package/connection/connect.d.ts.map +1 -0
  24. package/connection/connect.js +348 -0
  25. package/connection/connect.js.map +1 -0
  26. package/connection/established.d.ts +30 -0
  27. package/connection/established.d.ts.map +1 -0
  28. package/connection/established.js +3 -0
  29. package/connection/established.js.map +1 -0
  30. package/connection/handshake.d.ts +10 -0
  31. package/connection/handshake.d.ts.map +1 -0
  32. package/connection/handshake.js +48 -0
  33. package/connection/handshake.js.map +1 -0
  34. package/connection/index.d.ts +5 -0
  35. package/connection/index.d.ts.map +1 -0
  36. package/connection/index.js +6 -0
  37. package/connection/index.js.map +1 -0
  38. package/connection/reload.d.ts +47 -0
  39. package/connection/reload.d.ts.map +1 -0
  40. package/connection/reload.js +137 -0
  41. package/connection/reload.js.map +1 -0
  42. package/group.d.ts +66 -0
  43. package/group.d.ts.map +1 -0
  44. package/group.js +135 -0
  45. package/group.js.map +1 -0
  46. package/group.test.d.ts +2 -0
  47. package/group.test.d.ts.map +1 -0
  48. package/group.test.js +66 -0
  49. package/group.test.js.map +1 -0
  50. package/ietf/adapter.d.ts +65 -0
  51. package/ietf/adapter.d.ts.map +1 -0
  52. package/ietf/adapter.js +626 -0
  53. package/ietf/adapter.js.map +1 -0
  54. package/ietf/connection.d.ts +65 -0
  55. package/ietf/connection.d.ts.map +1 -0
  56. package/ietf/connection.js +248 -0
  57. package/ietf/connection.js.map +1 -0
  58. package/ietf/control.d.ts +135 -0
  59. package/ietf/control.d.ts.map +1 -0
  60. package/ietf/control.js +242 -0
  61. package/ietf/control.js.map +1 -0
  62. package/ietf/fetch.d.ts +64 -0
  63. package/ietf/fetch.d.ts.map +1 -0
  64. package/ietf/fetch.js +100 -0
  65. package/ietf/fetch.js.map +1 -0
  66. package/ietf/goaway.d.ts +15 -0
  67. package/ietf/goaway.d.ts.map +1 -0
  68. package/ietf/goaway.js +34 -0
  69. package/ietf/goaway.js.map +1 -0
  70. package/ietf/ietf.test.d.ts +2 -0
  71. package/ietf/ietf.test.d.ts.map +1 -0
  72. package/ietf/ietf.test.js +873 -0
  73. package/ietf/ietf.test.js.map +1 -0
  74. package/ietf/index.d.ts +18 -0
  75. package/ietf/index.d.ts.map +1 -0
  76. package/ietf/index.js +19 -0
  77. package/ietf/index.js.map +1 -0
  78. package/ietf/message.d.ts +4 -0
  79. package/ietf/message.d.ts.map +1 -0
  80. package/ietf/message.js +54 -0
  81. package/ietf/message.js.map +1 -0
  82. package/ietf/namespace.d.ts +5 -0
  83. package/ietf/namespace.d.ts.map +1 -0
  84. package/ietf/namespace.js +27 -0
  85. package/ietf/namespace.js.map +1 -0
  86. package/ietf/object.d.ts +40 -0
  87. package/ietf/object.d.ts.map +1 -0
  88. package/ietf/object.js +160 -0
  89. package/ietf/object.js.map +1 -0
  90. package/ietf/parameters.d.ts +56 -0
  91. package/ietf/parameters.d.ts.map +1 -0
  92. package/ietf/parameters.js +337 -0
  93. package/ietf/parameters.js.map +1 -0
  94. package/ietf/properties.d.ts +4 -0
  95. package/ietf/properties.d.ts.map +1 -0
  96. package/ietf/properties.js +33 -0
  97. package/ietf/properties.js.map +1 -0
  98. package/ietf/publish.d.ts +68 -0
  99. package/ietf/publish.d.ts.map +1 -0
  100. package/ietf/publish.js +192 -0
  101. package/ietf/publish.js.map +1 -0
  102. package/ietf/publish_namespace.d.ts +68 -0
  103. package/ietf/publish_namespace.d.ts.map +1 -0
  104. package/ietf/publish_namespace.js +176 -0
  105. package/ietf/publish_namespace.js.map +1 -0
  106. package/ietf/publisher.d.ts +50 -0
  107. package/ietf/publisher.d.ts.map +1 -0
  108. package/ietf/publisher.js +330 -0
  109. package/ietf/publisher.js.map +1 -0
  110. package/ietf/request.d.ts +52 -0
  111. package/ietf/request.d.ts.map +1 -0
  112. package/ietf/request.js +119 -0
  113. package/ietf/request.js.map +1 -0
  114. package/ietf/setup.d.ts +38 -0
  115. package/ietf/setup.d.ts.map +1 -0
  116. package/ietf/setup.js +129 -0
  117. package/ietf/setup.js.map +1 -0
  118. package/ietf/subscribe.d.ts +66 -0
  119. package/ietf/subscribe.d.ts.map +1 -0
  120. package/ietf/subscribe.js +276 -0
  121. package/ietf/subscribe.js.map +1 -0
  122. package/ietf/subscribe_namespace.d.ts +101 -0
  123. package/ietf/subscribe_namespace.d.ts.map +1 -0
  124. package/ietf/subscribe_namespace.js +256 -0
  125. package/ietf/subscribe_namespace.js.map +1 -0
  126. package/ietf/subscriber.d.ts +53 -0
  127. package/ietf/subscriber.d.ts.map +1 -0
  128. package/ietf/subscriber.js +404 -0
  129. package/ietf/subscriber.js.map +1 -0
  130. package/ietf/track.d.ts +40 -0
  131. package/ietf/track.d.ts.map +1 -0
  132. package/ietf/track.js +108 -0
  133. package/ietf/track.js.map +1 -0
  134. package/ietf/version.d.ts +56 -0
  135. package/ietf/version.d.ts.map +1 -0
  136. package/ietf/version.js +63 -0
  137. package/ietf/version.js.map +1 -0
  138. package/index.d.ts +22 -0
  139. package/index.d.ts.map +1 -0
  140. package/index.js +23 -0
  141. package/index.js.map +1 -0
  142. package/integration.test.d.ts +2 -0
  143. package/integration.test.d.ts.map +1 -0
  144. package/integration.test.js +90 -0
  145. package/integration.test.js.map +1 -0
  146. package/lite/announce.d.ts +35 -0
  147. package/lite/announce.d.ts.map +1 -0
  148. package/lite/announce.js +166 -0
  149. package/lite/announce.js.map +1 -0
  150. package/lite/connection.d.ts +47 -0
  151. package/lite/connection.d.ts.map +1 -0
  152. package/lite/connection.js +247 -0
  153. package/lite/connection.js.map +1 -0
  154. package/lite/fetch.d.ts +14 -0
  155. package/lite/fetch.d.ts.map +1 -0
  156. package/lite/fetch.js +47 -0
  157. package/lite/fetch.js.map +1 -0
  158. package/lite/goaway.d.ts +10 -0
  159. package/lite/goaway.d.ts.map +1 -0
  160. package/lite/goaway.js +37 -0
  161. package/lite/goaway.js.map +1 -0
  162. package/lite/group.d.ts +18 -0
  163. package/lite/group.d.ts.map +1 -0
  164. package/lite/group.js +46 -0
  165. package/lite/group.js.map +1 -0
  166. package/lite/index.d.ts +11 -0
  167. package/lite/index.d.ts.map +1 -0
  168. package/lite/index.js +12 -0
  169. package/lite/index.js.map +1 -0
  170. package/lite/message.d.ts +5 -0
  171. package/lite/message.d.ts.map +1 -0
  172. package/lite/message.js +51 -0
  173. package/lite/message.js.map +1 -0
  174. package/lite/origin.d.ts +25 -0
  175. package/lite/origin.d.ts.map +1 -0
  176. package/lite/origin.js +35 -0
  177. package/lite/origin.js.map +1 -0
  178. package/lite/probe.d.ts +12 -0
  179. package/lite/probe.d.ts.map +1 -0
  180. package/lite/probe.js +60 -0
  181. package/lite/probe.js.map +1 -0
  182. package/lite/publisher.d.ts +63 -0
  183. package/lite/publisher.d.ts.map +1 -0
  184. package/lite/publisher.js +350 -0
  185. package/lite/publisher.js.map +1 -0
  186. package/lite/session.d.ts +36 -0
  187. package/lite/session.d.ts.map +1 -0
  188. package/lite/session.js +130 -0
  189. package/lite/session.js.map +1 -0
  190. package/lite/setup.d.ts +18 -0
  191. package/lite/setup.d.ts.map +1 -0
  192. package/lite/setup.js +55 -0
  193. package/lite/setup.js.map +1 -0
  194. package/lite/stream.d.ts +23 -0
  195. package/lite/stream.d.ts.map +1 -0
  196. package/lite/stream.js +20 -0
  197. package/lite/stream.js.map +1 -0
  198. package/lite/subscribe.d.ts +111 -0
  199. package/lite/subscribe.d.ts.map +1 -0
  200. package/lite/subscribe.js +341 -0
  201. package/lite/subscribe.js.map +1 -0
  202. package/lite/subscriber.d.ts +80 -0
  203. package/lite/subscriber.d.ts.map +1 -0
  204. package/lite/subscriber.js +316 -0
  205. package/lite/subscriber.js.map +1 -0
  206. package/lite/track.d.ts +38 -0
  207. package/lite/track.d.ts.map +1 -0
  208. package/lite/track.js +81 -0
  209. package/lite/track.js.map +1 -0
  210. package/lite/track.test.d.ts +2 -0
  211. package/lite/track.test.d.ts.map +1 -0
  212. package/lite/track.test.js +53 -0
  213. package/lite/track.test.js.map +1 -0
  214. package/lite/version.d.ts +16 -0
  215. package/lite/version.d.ts.map +1 -0
  216. package/lite/version.js +41 -0
  217. package/lite/version.js.map +1 -0
  218. package/mock.d.ts +16 -0
  219. package/mock.d.ts.map +1 -0
  220. package/mock.js +154 -0
  221. package/mock.js.map +1 -0
  222. package/package.json +27 -0
  223. package/path.d.ts +111 -0
  224. package/path.d.ts.map +1 -0
  225. package/path.js +133 -0
  226. package/path.js.map +1 -0
  227. package/path.test.d.ts +2 -0
  228. package/path.test.d.ts.map +1 -0
  229. package/path.test.js +169 -0
  230. package/path.test.js.map +1 -0
  231. package/stream.d.ts +57 -0
  232. package/stream.d.ts.map +1 -0
  233. package/stream.js +326 -0
  234. package/stream.js.map +1 -0
  235. package/stream.test.d.ts +2 -0
  236. package/stream.test.d.ts.map +1 -0
  237. package/stream.test.js +210 -0
  238. package/stream.test.js.map +1 -0
  239. package/time.d.ts +109 -0
  240. package/time.d.ts.map +1 -0
  241. package/time.js +94 -0
  242. package/time.js.map +1 -0
  243. package/track.d.ts +85 -0
  244. package/track.d.ts.map +1 -0
  245. package/track.js +227 -0
  246. package/track.js.map +1 -0
  247. package/track.test.d.ts +2 -0
  248. package/track.test.d.ts.map +1 -0
  249. package/track.test.js +56 -0
  250. package/track.test.js.map +1 -0
  251. package/util/error.d.ts +3 -0
  252. package/util/error.d.ts.map +1 -0
  253. package/util/error.js +9 -0
  254. package/util/error.js.map +1 -0
  255. package/util/hex.d.ts +3 -0
  256. package/util/hex.d.ts.map +1 -0
  257. package/util/hex.js +16 -0
  258. package/util/hex.js.map +1 -0
  259. package/util/index.d.ts +2 -0
  260. package/util/index.d.ts.map +1 -0
  261. package/util/index.js +3 -0
  262. package/util/index.js.map +1 -0
  263. package/varint.d.ts +36 -0
  264. package/varint.d.ts.map +1 -0
  265. package/varint.js +288 -0
  266. package/varint.js.map +1 -0
  267. package/varint.test.d.ts +2 -0
  268. package/varint.test.d.ts.map +1 -0
  269. package/varint.test.js +106 -0
  270. package/varint.test.js.map +1 -0
  271. package/zod.d.ts +13 -0
  272. package/zod.d.ts.map +1 -0
  273. package/zod.js +19 -0
  274. package/zod.js.map +1 -0
@@ -0,0 +1,137 @@
1
+ /* @ts-self-types="./reload.d.ts" */
2
+ import { Effect, Signal } from "@norskvideo/moq-signals";
3
+ import { empty as emptyPath } from "../path.js";
4
+ import { connect } from "./connect.js";
5
+ /** Maintains a MoQ connection, reconnecting with exponential backoff when it drops. */
6
+ export class Reload {
7
+ /** Relay URL to connect to; updating it triggers a reconnect. */
8
+ url;
9
+ /** Whether reconnecting is active. */
10
+ enabled;
11
+ /** Current connection status. */
12
+ status = new Signal("disconnected");
13
+ /** The currently established session, or undefined while disconnected. */
14
+ established = new Signal(undefined);
15
+ // All actively announced broadcast paths, updated reactively.
16
+ #announced = new Signal(new Set());
17
+ /** The set of broadcast paths currently announced by the server, updated reactively. */
18
+ announced = this.#announced;
19
+ /** WebTransport options applied to each connection attempt (not reactive). */
20
+ webtransport;
21
+ /** WebSocket fallback options applied to each connection attempt (not reactive). */
22
+ websocket;
23
+ /** Backoff settings for the reconnect loop. */
24
+ delay;
25
+ /** The reactive effect scope driving the connect loop; closed by {@link Reload.close}. */
26
+ signals = new Effect();
27
+ /** Resolves when the reconnect loop stops via {@link Reload.close} or the retry timeout. */
28
+ closed;
29
+ #closedResolve;
30
+ #closedReject;
31
+ #delay;
32
+ // Timestamp when the current retry sequence started (for timeout).
33
+ #retryStart;
34
+ // Increased by 1 each time to trigger a reload.
35
+ #tick = new Signal(0);
36
+ constructor(props) {
37
+ this.url = Signal.from(props?.url);
38
+ this.enabled = Signal.from(props?.enabled ?? false);
39
+ this.delay = props?.delay ?? { initial: 1000, multiplier: 2, max: 30000 };
40
+ this.webtransport = props?.webtransport;
41
+ this.websocket = props?.websocket;
42
+ this.#delay = this.delay.initial;
43
+ this.closed = new Promise((resolve, reject) => {
44
+ this.#closedResolve = resolve;
45
+ this.#closedReject = reject;
46
+ });
47
+ // Create a reactive root so cleanup is easier.
48
+ this.signals.run(this.#connect.bind(this));
49
+ this.signals.run(this.#runAnnounced.bind(this));
50
+ }
51
+ #connect(effect) {
52
+ // Will retry when the tick changes.
53
+ effect.get(this.#tick);
54
+ const enabled = effect.get(this.enabled);
55
+ if (!enabled)
56
+ return;
57
+ const url = effect.get(this.url);
58
+ if (!url)
59
+ return;
60
+ effect.set(this.status, "connecting", "disconnected");
61
+ effect.spawn(async () => {
62
+ try {
63
+ const pending = connect(url, { websocket: this.websocket, webtransport: this.webtransport });
64
+ const connection = await Promise.race([effect.cancel, pending]);
65
+ if (!connection) {
66
+ pending.then((conn) => conn.close()).catch(() => { });
67
+ return;
68
+ }
69
+ effect.set(this.established, connection);
70
+ effect.cleanup(() => connection.close());
71
+ effect.set(this.status, "connected", "disconnected");
72
+ // Reset the exponential backoff and timeout on success.
73
+ this.#delay = this.delay.initial;
74
+ this.#retryStart = undefined;
75
+ await Promise.race([effect.cancel, connection.closed]);
76
+ }
77
+ catch (err) {
78
+ console.warn("connection error:", err);
79
+ // Track retry start for timeout.
80
+ this.#retryStart ??= performance.now();
81
+ const timeout = this.delay.timeout ?? 300000;
82
+ if (timeout > 0) {
83
+ const elapsed = performance.now() - this.#retryStart;
84
+ if (elapsed >= timeout) {
85
+ console.warn("reconnect timed out");
86
+ this.#closedReject(err instanceof Error ? err : new Error(String(err)));
87
+ return;
88
+ }
89
+ }
90
+ const tick = this.#tick.peek() + 1;
91
+ effect.timer(() => this.#tick.update((prev) => Math.max(prev, tick)), this.#delay);
92
+ this.#delay = Math.min(this.#delay * this.delay.multiplier, this.delay.max);
93
+ }
94
+ });
95
+ }
96
+ #runAnnounced(effect) {
97
+ this.#announced.set(new Set());
98
+ const conn = effect.get(this.established);
99
+ if (!conn)
100
+ return;
101
+ effect.cleanup(() => this.#announced.set(new Set()));
102
+ // Cloudflare's relay does not yet support SUBSCRIBE_NAMESPACE, so
103
+ // skip announce subscriptions entirely for those hosts.
104
+ if (conn.url.hostname.endsWith("mediaoverquic.com")) {
105
+ return;
106
+ }
107
+ const announced = conn.announced(emptyPath());
108
+ effect.cleanup(() => announced.close());
109
+ effect.spawn(async () => {
110
+ try {
111
+ for (;;) {
112
+ const entry = await Promise.race([effect.cancel, announced.next()]);
113
+ if (!entry)
114
+ break;
115
+ this.#announced.mutate((active) => {
116
+ if (entry.active) {
117
+ active.add(entry.path);
118
+ }
119
+ else {
120
+ active.delete(entry.path);
121
+ }
122
+ });
123
+ }
124
+ }
125
+ catch (err) {
126
+ this.#announced.set(new Set());
127
+ throw err;
128
+ }
129
+ });
130
+ }
131
+ /** Stop reconnecting, close the current connection, and resolve {@link Reload.closed}. */
132
+ close() {
133
+ this.signals.close();
134
+ this.#closedResolve();
135
+ }
136
+ }
137
+ //# sourceMappingURL=reload.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"reload.js","sourceRoot":"","sources":["../../src/connection/reload.ts"],"names":[],"mappings":"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","sourcesContent":["import { Effect, type Getter, Signal } from \"@moq/signals\";\nimport type * as Path from \"../path.ts\";\nimport { empty as emptyPath } from \"../path.ts\";\nimport { type ConnectProps, connect, type WebSocketOptions, type WebTransportProps } from \"./connect.ts\";\nimport type { Established } from \"./established.ts\";\n\n/** Exponential backoff settings for {@link Reload}'s reconnect loop. */\nexport type ReloadDelay = {\n\t// The delay in milliseconds before reconnecting.\n\t// default: 1000\n\tinitial: DOMHighResTimeStamp;\n\n\t// The multiplier for the delay.\n\t// default: 2\n\tmultiplier: number;\n\n\t// The maximum delay in milliseconds.\n\t// default: 30000\n\tmax: DOMHighResTimeStamp;\n\n\t// Maximum total time in milliseconds to spend retrying before giving up.\n\t// Resets after each successful connection. Set to 0 for unlimited retries.\n\t// default: 300000 (5 minutes)\n\ttimeout?: DOMHighResTimeStamp;\n};\n\n/** Options for {@link Reload}: connect options plus reactive URL/enabled signals and backoff tuning. */\nexport type ReloadProps = ConnectProps & {\n\t// Whether to reload the connection when it disconnects.\n\t// default: true\n\tenabled?: boolean | Signal<boolean>;\n\n\t// The URL of the relay server.\n\turl?: URL | Signal<URL | undefined>;\n\n\t// The delay for the reload.\n\tdelay?: ReloadDelay;\n};\n\n/** Current state of a {@link Reload} connection. */\nexport type ReloadStatus = \"connecting\" | \"connected\" | \"disconnected\";\n\n/** Maintains a MoQ connection, reconnecting with exponential backoff when it drops. */\nexport class Reload {\n\t/** Relay URL to connect to; updating it triggers a reconnect. */\n\turl: Signal<URL | undefined>;\n\n\t/** Whether reconnecting is active. */\n\tenabled: Signal<boolean>;\n\n\t/** Current connection status. */\n\tstatus = new Signal<ReloadStatus>(\"disconnected\");\n\n\t/** The currently established session, or undefined while disconnected. */\n\testablished = new Signal<Established | undefined>(undefined);\n\n\t// All actively announced broadcast paths, updated reactively.\n\t#announced = new Signal<Set<Path.Valid>>(new Set());\n\n\t/** The set of broadcast paths currently announced by the server, updated reactively. */\n\treadonly announced: Getter<Set<Path.Valid>> = this.#announced;\n\n\t/** WebTransport options applied to each connection attempt (not reactive). */\n\twebtransport?: WebTransportProps;\n\n\t/** WebSocket fallback options applied to each connection attempt (not reactive). */\n\twebsocket: WebSocketOptions | undefined;\n\n\t/** Backoff settings for the reconnect loop. */\n\tdelay: ReloadDelay;\n\n\t/** The reactive effect scope driving the connect loop; closed by {@link Reload.close}. */\n\tsignals = new Effect();\n\n\t/** Resolves when the reconnect loop stops via {@link Reload.close} or the retry timeout. */\n\tclosed: Promise<void>;\n\t#closedResolve!: () => void;\n\t#closedReject!: (err: Error) => void;\n\n\t#delay: DOMHighResTimeStamp;\n\n\t// Timestamp when the current retry sequence started (for timeout).\n\t#retryStart: DOMHighResTimeStamp | undefined;\n\n\t// Increased by 1 each time to trigger a reload.\n\t#tick = new Signal(0);\n\n\tconstructor(props?: ReloadProps) {\n\t\tthis.url = Signal.from(props?.url);\n\t\tthis.enabled = Signal.from(props?.enabled ?? false);\n\t\tthis.delay = props?.delay ?? { initial: 1000, multiplier: 2, max: 30000 };\n\t\tthis.webtransport = props?.webtransport;\n\t\tthis.websocket = props?.websocket;\n\n\t\tthis.#delay = this.delay.initial;\n\n\t\tthis.closed = new Promise((resolve, reject) => {\n\t\t\tthis.#closedResolve = resolve;\n\t\t\tthis.#closedReject = reject;\n\t\t});\n\n\t\t// Create a reactive root so cleanup is easier.\n\t\tthis.signals.run(this.#connect.bind(this));\n\t\tthis.signals.run(this.#runAnnounced.bind(this));\n\t}\n\n\t#connect(effect: Effect): void {\n\t\t// Will retry when the tick changes.\n\t\teffect.get(this.#tick);\n\n\t\tconst enabled = effect.get(this.enabled);\n\t\tif (!enabled) return;\n\n\t\tconst url = effect.get(this.url);\n\t\tif (!url) return;\n\n\t\teffect.set(this.status, \"connecting\", \"disconnected\");\n\n\t\teffect.spawn(async () => {\n\t\t\ttry {\n\t\t\t\tconst pending = connect(url, { websocket: this.websocket, webtransport: this.webtransport });\n\n\t\t\t\tconst connection = await Promise.race([effect.cancel, pending]);\n\t\t\t\tif (!connection) {\n\t\t\t\t\tpending.then((conn) => conn.close()).catch(() => {});\n\t\t\t\t\treturn;\n\t\t\t\t}\n\n\t\t\t\teffect.set(this.established, connection);\n\t\t\t\teffect.cleanup(() => connection.close());\n\n\t\t\t\teffect.set(this.status, \"connected\", \"disconnected\");\n\n\t\t\t\t// Reset the exponential backoff and timeout on success.\n\t\t\t\tthis.#delay = this.delay.initial;\n\t\t\t\tthis.#retryStart = undefined;\n\n\t\t\t\tawait Promise.race([effect.cancel, connection.closed]);\n\t\t\t} catch (err) {\n\t\t\t\tconsole.warn(\"connection error:\", err);\n\n\t\t\t\t// Track retry start for timeout.\n\t\t\t\tthis.#retryStart ??= performance.now();\n\n\t\t\t\tconst timeout = this.delay.timeout ?? 300000;\n\t\t\t\tif (timeout > 0) {\n\t\t\t\t\tconst elapsed = performance.now() - this.#retryStart;\n\t\t\t\t\tif (elapsed >= timeout) {\n\t\t\t\t\t\tconsole.warn(\"reconnect timed out\");\n\t\t\t\t\t\tthis.#closedReject(err instanceof Error ? err : new Error(String(err)));\n\t\t\t\t\t\treturn;\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\tconst tick = this.#tick.peek() + 1;\n\t\t\t\teffect.timer(() => this.#tick.update((prev) => Math.max(prev, tick)), this.#delay);\n\n\t\t\t\tthis.#delay = Math.min(this.#delay * this.delay.multiplier, this.delay.max);\n\t\t\t}\n\t\t});\n\t}\n\n\t#runAnnounced(effect: Effect): void {\n\t\tthis.#announced.set(new Set());\n\n\t\tconst conn = effect.get(this.established);\n\t\tif (!conn) return;\n\n\t\teffect.cleanup(() => this.#announced.set(new Set()));\n\n\t\t// Cloudflare's relay does not yet support SUBSCRIBE_NAMESPACE, so\n\t\t// skip announce subscriptions entirely for those hosts.\n\t\tif (conn.url.hostname.endsWith(\"mediaoverquic.com\")) {\n\t\t\treturn;\n\t\t}\n\n\t\tconst announced = conn.announced(emptyPath());\n\t\teffect.cleanup(() => announced.close());\n\n\t\teffect.spawn(async () => {\n\t\t\ttry {\n\t\t\t\tfor (;;) {\n\t\t\t\t\tconst entry = await Promise.race([effect.cancel, announced.next()]);\n\t\t\t\t\tif (!entry) break;\n\n\t\t\t\t\tthis.#announced.mutate((active) => {\n\t\t\t\t\t\tif (entry.active) {\n\t\t\t\t\t\t\tactive.add(entry.path);\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\tactive.delete(entry.path);\n\t\t\t\t\t\t}\n\t\t\t\t\t});\n\t\t\t\t}\n\t\t\t} catch (err) {\n\t\t\t\tthis.#announced.set(new Set());\n\t\t\t\tthrow err;\n\t\t\t}\n\t\t});\n\t}\n\n\t/** Stop reconnecting, close the current connection, and resolve {@link Reload.closed}. */\n\tclose() {\n\t\tthis.signals.close();\n\t\tthis.#closedResolve();\n\t}\n}\n"]}
package/group.d.ts ADDED
@@ -0,0 +1,66 @@
1
+ /** An ordered stream of frames within a track, delivered over a single QUIC stream. */
2
+ export declare class Group {
3
+ #private;
4
+ /** Sequence number of this group within its track. */
5
+ readonly sequence: number;
6
+ /** Resolves with the abort error (or undefined) once closed. */
7
+ readonly closed: Promise<Error | undefined>;
8
+ constructor(sequence: number);
9
+ /**
10
+ * Writes a frame to the group.
11
+ * @param frame - The frame to write
12
+ */
13
+ writeFrame(frame: Uint8Array, extensions?: Uint8Array): void;
14
+ /** Write a string as a single UTF-8 encoded frame. */
15
+ writeString(str: string): void;
16
+ /** Write a value as a single JSON-encoded frame. */
17
+ writeJson(json: unknown): void;
18
+ /** Write a boolean as a single one-byte frame. */
19
+ writeBool(bool: boolean): void;
20
+ /** True once no further frames can be read: the group has closed and every buffered frame is read. */
21
+ get done(): boolean;
22
+ /**
23
+ * Reads the next already-buffered frame without blocking.
24
+ *
25
+ * Returns `undefined` when nothing is buffered right now. That is *not* by itself end-of-group:
26
+ * check {@link done} to tell "no frame buffered yet" (more may arrive) from "the group finished".
27
+ * Drain a backlog by looping until this returns `undefined`, then branch on {@link done}: if not
28
+ * done, {@link readable} resolves when the next frame arrives.
29
+ */
30
+ tryReadFrame(): Uint8Array | undefined;
31
+ /** Like {@link tryReadFrame} but also reports the frame's sequence number within the group. */
32
+ tryReadFrameSequence(): {
33
+ sequence: number;
34
+ data: Uint8Array;
35
+ extensions?: Uint8Array;
36
+ } | undefined;
37
+ /**
38
+ * Resolves once {@link readFrame} would not block: a frame is buffered, or the group has closed.
39
+ * Always settles (never hangs), so on a finished group it resolves immediately; pair it with
40
+ * {@link done} to avoid re-waiting on a group that has nothing left.
41
+ *
42
+ * Lets a caller fold "this group has a frame" into a larger wait (e.g. racing it against a new
43
+ * group arriving) without touching the group's internal signals.
44
+ */
45
+ readable(): Promise<void>;
46
+ /**
47
+ * Reads the next frame from the group.
48
+ * @returns A promise that resolves to the next frame or undefined
49
+ */
50
+ readFrame(): Promise<Uint8Array | undefined>;
51
+ /** Reads the next frame along with its sequence number within the group. */
52
+ readFrameSequence(): Promise<{
53
+ sequence: number;
54
+ data: Uint8Array;
55
+ extensions?: Uint8Array;
56
+ } | undefined>;
57
+ /** Reads the next frame and decodes it as a UTF-8 string. */
58
+ readString(): Promise<string | undefined>;
59
+ /** Reads the next frame and parses it as JSON. */
60
+ readJson(): Promise<unknown | undefined>;
61
+ /** Reads the next frame and decodes it as a one-byte boolean. */
62
+ readBool(): Promise<boolean | undefined>;
63
+ /** Closes the group, optionally with an error to abort readers. */
64
+ close(abort?: Error): void;
65
+ }
66
+ //# sourceMappingURL=group.d.ts.map
package/group.d.ts.map ADDED
@@ -0,0 +1 @@
1
+ {"version":3,"file":"group.d.ts","sourceRoot":"","sources":["../src/group.ts"],"names":[],"mappings":"AAeA,uFAAuF;AACvF,qBAAa,KAAK;;IACjB,sDAAsD;IACtD,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAM1B,gEAAgE;IAChE,QAAQ,CAAC,MAAM,EAAE,OAAO,CAAC,KAAK,GAAG,SAAS,CAAC,CAAC;IAE5C,YAAY,QAAQ,EAAE,MAAM,EAW3B;IAED;;;OAGG;IACH,UAAU,CAAC,KAAK,EAAE,UAAU,EAAE,UAAU,CAAC,EAAE,UAAU,QAQpD;IAED,sDAAsD;IACtD,WAAW,CAAC,GAAG,EAAE,MAAM,QAEtB;IAED,oDAAoD;IACpD,SAAS,CAAC,IAAI,EAAE,OAAO,QAEtB;IAED,kDAAkD;IAClD,SAAS,CAAC,IAAI,EAAE,OAAO,QAEtB;IAED,sGAAsG;IACtG,IAAI,IAAI,IAAI,OAAO,CAElB;IAED;;;;;;;OAOG;IACH,YAAY,IAAI,UAAU,GAAG,SAAS,CAErC;IAED,+FAA+F;IAC/F,oBAAoB,IAAI;QAAE,QAAQ,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,UAAU,CAAC;QAAC,UAAU,CAAC,EAAE,UAAU,CAAA;KAAE,GAAG,SAAS,CAKlG;IAED;;;;;;;OAOG;IACG,QAAQ,IAAI,OAAO,CAAC,IAAI,CAAC,CAM9B;IAED;;;OAGG;IACG,SAAS,IAAI,OAAO,CAAC,UAAU,GAAG,SAAS,CAAC,CAEjD;IAED,4EAA4E;IACtE,iBAAiB,IAAI,OAAO,CAAC;QAAE,QAAQ,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,UAAU,CAAC;QAAC,UAAU,CAAC,EAAE,UAAU,CAAA;KAAE,GAAG,SAAS,CAAC,CAY9G;IAED,6DAA6D;IACvD,UAAU,IAAI,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC,CAG9C;IAED,kDAAkD;IAC5C,QAAQ,IAAI,OAAO,CAAC,OAAO,GAAG,SAAS,CAAC,CAG7C;IAED,iEAAiE;IAC3D,QAAQ,IAAI,OAAO,CAAC,OAAO,GAAG,SAAS,CAAC,CAG7C;IAED,mEAAmE;IACnE,KAAK,CAAC,KAAK,CAAC,EAAE,KAAK,QAElB;CACD"}
package/group.js ADDED
@@ -0,0 +1,135 @@
1
+ /* @ts-self-types="./group.d.ts" */
2
+ import { Signal } from "@norskvideo/moq-signals";
3
+ class GroupState {
4
+ frames = new Signal([]);
5
+ closed = new Signal(false);
6
+ total = new Signal(0); // The total number of frames in the group thus far
7
+ }
8
+ /** An ordered stream of frames within a track, delivered over a single QUIC stream. */
9
+ export class Group {
10
+ /** Sequence number of this group within its track. */
11
+ sequence;
12
+ // Reactive backing state, deliberately private: read through the read* methods so callers can't
13
+ // poke the signals directly (and so the internal representation can change).
14
+ #state = new GroupState();
15
+ /** Resolves with the abort error (or undefined) once closed. */
16
+ closed;
17
+ constructor(sequence) {
18
+ this.sequence = sequence;
19
+ // Cache the closed promise to avoid recreating it every time.
20
+ this.closed = new Promise((resolve) => {
21
+ const dispose = this.#state.closed.subscribe((closed) => {
22
+ if (!closed)
23
+ return;
24
+ resolve(closed instanceof Error ? closed : undefined);
25
+ dispose();
26
+ });
27
+ });
28
+ }
29
+ /**
30
+ * Writes a frame to the group.
31
+ * @param frame - The frame to write
32
+ */
33
+ writeFrame(frame, extensions) {
34
+ if (this.#state.closed.peek())
35
+ throw new Error("group is closed");
36
+ this.#state.frames.mutate((frames) => {
37
+ frames.push({ data: frame, extensions });
38
+ });
39
+ this.#state.total.update((total) => total + 1);
40
+ }
41
+ /** Write a string as a single UTF-8 encoded frame. */
42
+ writeString(str) {
43
+ this.writeFrame(new TextEncoder().encode(str));
44
+ }
45
+ /** Write a value as a single JSON-encoded frame. */
46
+ writeJson(json) {
47
+ this.writeString(JSON.stringify(json));
48
+ }
49
+ /** Write a boolean as a single one-byte frame. */
50
+ writeBool(bool) {
51
+ this.writeFrame(new Uint8Array([bool ? 1 : 0]));
52
+ }
53
+ /** True once no further frames can be read: the group has closed and every buffered frame is read. */
54
+ get done() {
55
+ return this.#state.frames.peek().length === 0 && this.#state.closed.peek() !== false;
56
+ }
57
+ /**
58
+ * Reads the next already-buffered frame without blocking.
59
+ *
60
+ * Returns `undefined` when nothing is buffered right now. That is *not* by itself end-of-group:
61
+ * check {@link done} to tell "no frame buffered yet" (more may arrive) from "the group finished".
62
+ * Drain a backlog by looping until this returns `undefined`, then branch on {@link done}: if not
63
+ * done, {@link readable} resolves when the next frame arrives.
64
+ */
65
+ tryReadFrame() {
66
+ return this.tryReadFrameSequence()?.data;
67
+ }
68
+ /** Like {@link tryReadFrame} but also reports the frame's sequence number within the group. */
69
+ tryReadFrameSequence() {
70
+ const frames = this.#state.frames.peek();
71
+ const frame = frames.shift();
72
+ if (frame === undefined)
73
+ return undefined;
74
+ return { sequence: this.#state.total.peek() - frames.length - 1, data: frame.data, extensions: frame.extensions };
75
+ }
76
+ /**
77
+ * Resolves once {@link readFrame} would not block: a frame is buffered, or the group has closed.
78
+ * Always settles (never hangs), so on a finished group it resolves immediately; pair it with
79
+ * {@link done} to avoid re-waiting on a group that has nothing left.
80
+ *
81
+ * Lets a caller fold "this group has a frame" into a larger wait (e.g. racing it against a new
82
+ * group arriving) without touching the group's internal signals.
83
+ */
84
+ async readable() {
85
+ for (;;) {
86
+ if (this.#state.frames.peek().length > 0)
87
+ return;
88
+ if (this.#state.closed.peek())
89
+ return;
90
+ await Signal.race(this.#state.frames, this.#state.closed);
91
+ }
92
+ }
93
+ /**
94
+ * Reads the next frame from the group.
95
+ * @returns A promise that resolves to the next frame or undefined
96
+ */
97
+ async readFrame() {
98
+ return (await this.readFrameSequence())?.data;
99
+ }
100
+ /** Reads the next frame along with its sequence number within the group. */
101
+ async readFrameSequence() {
102
+ for (;;) {
103
+ const next = this.tryReadFrameSequence();
104
+ if (next)
105
+ return next;
106
+ // Drain buffered frames before observing the close, so a closed group still yields them.
107
+ const closed = this.#state.closed.peek();
108
+ if (closed instanceof Error)
109
+ throw closed;
110
+ if (closed)
111
+ return undefined;
112
+ await this.readable();
113
+ }
114
+ }
115
+ /** Reads the next frame and decodes it as a UTF-8 string. */
116
+ async readString() {
117
+ const frame = await this.readFrame();
118
+ return frame ? new TextDecoder().decode(frame) : undefined;
119
+ }
120
+ /** Reads the next frame and parses it as JSON. */
121
+ async readJson() {
122
+ const frame = await this.readString();
123
+ return frame ? JSON.parse(frame) : undefined;
124
+ }
125
+ /** Reads the next frame and decodes it as a one-byte boolean. */
126
+ async readBool() {
127
+ const frame = await this.readFrame();
128
+ return frame ? frame[0] === 1 : undefined;
129
+ }
130
+ /** Closes the group, optionally with an error to abort readers. */
131
+ close(abort) {
132
+ this.#state.closed.set(abort ?? true);
133
+ }
134
+ }
135
+ //# sourceMappingURL=group.js.map
package/group.js.map ADDED
@@ -0,0 +1 @@
1
+ {"version":3,"file":"group.js","sourceRoot":"","sources":["../src/group.ts"],"names":[],"mappings":"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","sourcesContent":["import { Signal } from \"@moq/signals\";\n\n/** Reactive backing state for a {@link Group}: buffered frames, a closed flag, and the running frame count. */\n/** A buffered frame: its payload plus any raw MOQ Object Properties bytes. */\ninterface BufferedFrame {\n\tdata: Uint8Array;\n\textensions?: Uint8Array;\n}\n\nclass GroupState {\n\tframes = new Signal<BufferedFrame[]>([]);\n\tclosed = new Signal<boolean | Error>(false);\n\ttotal = new Signal<number>(0); // The total number of frames in the group thus far\n}\n\n/** An ordered stream of frames within a track, delivered over a single QUIC stream. */\nexport class Group {\n\t/** Sequence number of this group within its track. */\n\treadonly sequence: number;\n\n\t// Reactive backing state, deliberately private: read through the read* methods so callers can't\n\t// poke the signals directly (and so the internal representation can change).\n\t#state = new GroupState();\n\n\t/** Resolves with the abort error (or undefined) once closed. */\n\treadonly closed: Promise<Error | undefined>;\n\n\tconstructor(sequence: number) {\n\t\tthis.sequence = sequence;\n\n\t\t// Cache the closed promise to avoid recreating it every time.\n\t\tthis.closed = new Promise((resolve) => {\n\t\t\tconst dispose = this.#state.closed.subscribe((closed) => {\n\t\t\t\tif (!closed) return;\n\t\t\t\tresolve(closed instanceof Error ? closed : undefined);\n\t\t\t\tdispose();\n\t\t\t});\n\t\t});\n\t}\n\n\t/**\n\t * Writes a frame to the group.\n\t * @param frame - The frame to write\n\t */\n\twriteFrame(frame: Uint8Array, extensions?: Uint8Array) {\n\t\tif (this.#state.closed.peek()) throw new Error(\"group is closed\");\n\n\t\tthis.#state.frames.mutate((frames) => {\n\t\t\tframes.push({ data: frame, extensions });\n\t\t});\n\n\t\tthis.#state.total.update((total) => total + 1);\n\t}\n\n\t/** Write a string as a single UTF-8 encoded frame. */\n\twriteString(str: string) {\n\t\tthis.writeFrame(new TextEncoder().encode(str));\n\t}\n\n\t/** Write a value as a single JSON-encoded frame. */\n\twriteJson(json: unknown) {\n\t\tthis.writeString(JSON.stringify(json));\n\t}\n\n\t/** Write a boolean as a single one-byte frame. */\n\twriteBool(bool: boolean) {\n\t\tthis.writeFrame(new Uint8Array([bool ? 1 : 0]));\n\t}\n\n\t/** True once no further frames can be read: the group has closed and every buffered frame is read. */\n\tget done(): boolean {\n\t\treturn this.#state.frames.peek().length === 0 && this.#state.closed.peek() !== false;\n\t}\n\n\t/**\n\t * Reads the next already-buffered frame without blocking.\n\t *\n\t * Returns `undefined` when nothing is buffered right now. That is *not* by itself end-of-group:\n\t * check {@link done} to tell \"no frame buffered yet\" (more may arrive) from \"the group finished\".\n\t * Drain a backlog by looping until this returns `undefined`, then branch on {@link done}: if not\n\t * done, {@link readable} resolves when the next frame arrives.\n\t */\n\ttryReadFrame(): Uint8Array | undefined {\n\t\treturn this.tryReadFrameSequence()?.data;\n\t}\n\n\t/** Like {@link tryReadFrame} but also reports the frame's sequence number within the group. */\n\ttryReadFrameSequence(): { sequence: number; data: Uint8Array; extensions?: Uint8Array } | undefined {\n\t\tconst frames = this.#state.frames.peek();\n\t\tconst frame = frames.shift();\n\t\tif (frame === undefined) return undefined;\n\t\treturn { sequence: this.#state.total.peek() - frames.length - 1, data: frame.data, extensions: frame.extensions };\n\t}\n\n\t/**\n\t * Resolves once {@link readFrame} would not block: a frame is buffered, or the group has closed.\n\t * Always settles (never hangs), so on a finished group it resolves immediately; pair it with\n\t * {@link done} to avoid re-waiting on a group that has nothing left.\n\t *\n\t * Lets a caller fold \"this group has a frame\" into a larger wait (e.g. racing it against a new\n\t * group arriving) without touching the group's internal signals.\n\t */\n\tasync readable(): Promise<void> {\n\t\tfor (;;) {\n\t\t\tif (this.#state.frames.peek().length > 0) return;\n\t\t\tif (this.#state.closed.peek()) return;\n\t\t\tawait Signal.race(this.#state.frames, this.#state.closed);\n\t\t}\n\t}\n\n\t/**\n\t * Reads the next frame from the group.\n\t * @returns A promise that resolves to the next frame or undefined\n\t */\n\tasync readFrame(): Promise<Uint8Array | undefined> {\n\t\treturn (await this.readFrameSequence())?.data;\n\t}\n\n\t/** Reads the next frame along with its sequence number within the group. */\n\tasync readFrameSequence(): Promise<{ sequence: number; data: Uint8Array; extensions?: Uint8Array } | undefined> {\n\t\tfor (;;) {\n\t\t\tconst next = this.tryReadFrameSequence();\n\t\t\tif (next) return next;\n\n\t\t\t// Drain buffered frames before observing the close, so a closed group still yields them.\n\t\t\tconst closed = this.#state.closed.peek();\n\t\t\tif (closed instanceof Error) throw closed;\n\t\t\tif (closed) return undefined;\n\n\t\t\tawait this.readable();\n\t\t}\n\t}\n\n\t/** Reads the next frame and decodes it as a UTF-8 string. */\n\tasync readString(): Promise<string | undefined> {\n\t\tconst frame = await this.readFrame();\n\t\treturn frame ? new TextDecoder().decode(frame) : undefined;\n\t}\n\n\t/** Reads the next frame and parses it as JSON. */\n\tasync readJson(): Promise<unknown | undefined> {\n\t\tconst frame = await this.readString();\n\t\treturn frame ? JSON.parse(frame) : undefined;\n\t}\n\n\t/** Reads the next frame and decodes it as a one-byte boolean. */\n\tasync readBool(): Promise<boolean | undefined> {\n\t\tconst frame = await this.readFrame();\n\t\treturn frame ? frame[0] === 1 : undefined;\n\t}\n\n\t/** Closes the group, optionally with an error to abort readers. */\n\tclose(abort?: Error) {\n\t\tthis.#state.closed.set(abort ?? true);\n\t}\n}\n"]}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=group.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"group.test.d.ts","sourceRoot":"","sources":["../src/group.test.ts"],"names":[],"mappings":""}
package/group.test.js ADDED
@@ -0,0 +1,66 @@
1
+ /* @ts-self-types="./group.test.d.ts" */
2
+ import { expect, test } from "bun:test";
3
+ import { Group } from "./group.js";
4
+ const dec = new TextDecoder();
5
+ test("tryReadFrame drains buffered frames then returns undefined", () => {
6
+ const group = new Group(0);
7
+ group.writeString("a");
8
+ group.writeString("b");
9
+ expect(dec.decode(group.tryReadFrame())).toBe("a");
10
+ expect(dec.decode(group.tryReadFrame())).toBe("b");
11
+ // Nothing buffered: undefined, and the group is not closed so this is not end-of-group.
12
+ expect(group.tryReadFrame()).toBeUndefined();
13
+ });
14
+ test("tryReadFrameSequence reports per-frame sequence numbers", () => {
15
+ const group = new Group(7);
16
+ group.writeString("a");
17
+ group.writeString("b");
18
+ expect(group.tryReadFrameSequence()).toEqual({ sequence: 0, data: new TextEncoder().encode("a") });
19
+ expect(group.tryReadFrameSequence()).toEqual({ sequence: 1, data: new TextEncoder().encode("b") });
20
+ expect(group.tryReadFrameSequence()).toBeUndefined();
21
+ });
22
+ test("done distinguishes a finished group from one that is merely empty", () => {
23
+ const group = new Group(0);
24
+ // Open and empty: not done (more frames may arrive), and tryReadFrame is undefined.
25
+ expect(group.tryReadFrame()).toBeUndefined();
26
+ expect(group.done).toBe(false);
27
+ group.writeString("a");
28
+ // Buffered but closed: still not done until the frame is drained.
29
+ group.close();
30
+ expect(group.done).toBe(false);
31
+ group.tryReadFrame();
32
+ // Drained and closed: now done.
33
+ expect(group.tryReadFrame()).toBeUndefined();
34
+ expect(group.done).toBe(true);
35
+ });
36
+ test("readable resolves once a frame is buffered", async () => {
37
+ const group = new Group(0);
38
+ // No frame yet: readable() must stay pending for an empty, open group.
39
+ const readable = group.readable();
40
+ let settled = false;
41
+ void readable.then(() => {
42
+ settled = true;
43
+ });
44
+ await Promise.resolve();
45
+ expect(settled).toBe(false);
46
+ // Writing makes it resolve.
47
+ group.writeString("hi");
48
+ await readable; // must not hang
49
+ expect(dec.decode(group.tryReadFrame())).toBe("hi");
50
+ });
51
+ test("readable resolves once the group closes, even with nothing buffered", async () => {
52
+ const group = new Group(0);
53
+ const readable = group.readable();
54
+ group.close();
55
+ await readable; // resolves on close so callers don't wait forever
56
+ expect(group.tryReadFrame()).toBeUndefined();
57
+ });
58
+ test("buffered frames are still readable after the group closes", async () => {
59
+ const group = new Group(0);
60
+ group.writeString("a");
61
+ group.close();
62
+ // Closing doesn't discard buffered frames; the blocking reader drains them before ending.
63
+ expect(await group.readString()).toBe("a");
64
+ expect(await group.readFrame()).toBeUndefined();
65
+ });
66
+ //# sourceMappingURL=group.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"group.test.js","sourceRoot":"","sources":["../src/group.test.ts"],"names":[],"mappings":"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","sourcesContent":["import { expect, test } from \"bun:test\";\nimport { Group } from \"./group.ts\";\n\nconst dec = new TextDecoder();\n\ntest(\"tryReadFrame drains buffered frames then returns undefined\", () => {\n\tconst group = new Group(0);\n\tgroup.writeString(\"a\");\n\tgroup.writeString(\"b\");\n\n\texpect(dec.decode(group.tryReadFrame())).toBe(\"a\");\n\texpect(dec.decode(group.tryReadFrame())).toBe(\"b\");\n\t// Nothing buffered: undefined, and the group is not closed so this is not end-of-group.\n\texpect(group.tryReadFrame()).toBeUndefined();\n});\n\ntest(\"tryReadFrameSequence reports per-frame sequence numbers\", () => {\n\tconst group = new Group(7);\n\tgroup.writeString(\"a\");\n\tgroup.writeString(\"b\");\n\n\texpect(group.tryReadFrameSequence()).toEqual({ sequence: 0, data: new TextEncoder().encode(\"a\") });\n\texpect(group.tryReadFrameSequence()).toEqual({ sequence: 1, data: new TextEncoder().encode(\"b\") });\n\texpect(group.tryReadFrameSequence()).toBeUndefined();\n});\n\ntest(\"done distinguishes a finished group from one that is merely empty\", () => {\n\tconst group = new Group(0);\n\t// Open and empty: not done (more frames may arrive), and tryReadFrame is undefined.\n\texpect(group.tryReadFrame()).toBeUndefined();\n\texpect(group.done).toBe(false);\n\n\tgroup.writeString(\"a\");\n\t// Buffered but closed: still not done until the frame is drained.\n\tgroup.close();\n\texpect(group.done).toBe(false);\n\n\tgroup.tryReadFrame();\n\t// Drained and closed: now done.\n\texpect(group.tryReadFrame()).toBeUndefined();\n\texpect(group.done).toBe(true);\n});\n\ntest(\"readable resolves once a frame is buffered\", async () => {\n\tconst group = new Group(0);\n\t// No frame yet: readable() must stay pending for an empty, open group.\n\tconst readable = group.readable();\n\tlet settled = false;\n\tvoid readable.then(() => {\n\t\tsettled = true;\n\t});\n\tawait Promise.resolve();\n\texpect(settled).toBe(false);\n\n\t// Writing makes it resolve.\n\tgroup.writeString(\"hi\");\n\tawait readable; // must not hang\n\texpect(dec.decode(group.tryReadFrame())).toBe(\"hi\");\n});\n\ntest(\"readable resolves once the group closes, even with nothing buffered\", async () => {\n\tconst group = new Group(0);\n\tconst readable = group.readable();\n\tgroup.close();\n\tawait readable; // resolves on close so callers don't wait forever\n\texpect(group.tryReadFrame()).toBeUndefined();\n});\n\ntest(\"buffered frames are still readable after the group closes\", async () => {\n\tconst group = new Group(0);\n\tgroup.writeString(\"a\");\n\tgroup.close();\n\n\t// Closing doesn't discard buffered frames; the blocking reader drains them before ending.\n\texpect(await group.readString()).toBe(\"a\");\n\texpect(await group.readFrame()).toBeUndefined();\n});\n"]}
@@ -0,0 +1,65 @@
1
+ import { Stream } from "../stream";
2
+ import { type IetfVersion } from "./version";
3
+ /**
4
+ * Interface for opening outgoing bidi streams and allocating request IDs.
5
+ * Implemented by both ControlStreamAdapter (v14-v16) and NativeSession (v17).
6
+ */
7
+ export interface Session {
8
+ openBi(): Stream | Promise<Stream>;
9
+ openNativeBi?(): Promise<Stream>;
10
+ acceptBi(): Promise<Stream | undefined>;
11
+ nextRequestId(): Promise<bigint | undefined>;
12
+ close?(): void;
13
+ readonly version: IetfVersion;
14
+ }
15
+ /**
16
+ * v17 native session — thin wrapper around WebTransport.
17
+ * Each request gets its own real bidi stream; no control stream multiplexing.
18
+ */
19
+ export declare class NativeSession implements Session {
20
+ #private;
21
+ readonly version: IetfVersion;
22
+ constructor(quic: WebTransport, version: IetfVersion);
23
+ openBi(): Promise<Stream>;
24
+ acceptBi(): Promise<Stream | undefined>;
25
+ nextRequestId(): Promise<bigint | undefined>;
26
+ }
27
+ /**
28
+ * Converts v14-v16 control stream multiplexing into virtual bidi streams.
29
+ *
30
+ * Reads control messages, classifies them, and routes to virtual Stream objects.
31
+ * Each request/response pair gets its own virtual Stream, making all versions
32
+ * look like v17's stream-per-request model.
33
+ */
34
+ export declare class ControlStreamAdapter implements Session {
35
+ #private;
36
+ readonly version: IetfVersion;
37
+ constructor(quic: WebTransport, controlStream: Stream, version: IetfVersion, maxRequestId: bigint);
38
+ /**
39
+ * Accept the next incoming virtual bidi stream.
40
+ * Blocks until a new request arrives on the control stream.
41
+ */
42
+ acceptBi(): Promise<Stream | undefined>;
43
+ /**
44
+ * Open an outgoing virtual bidi stream.
45
+ * Buffers writes until the first full message is available, parses the
46
+ * requestId (and namespace for PublishNamespace), self-registers, then
47
+ * flushes. Subsequent writes go directly to the control stream.
48
+ */
49
+ openBi(): Stream;
50
+ /**
51
+ * Open a real WebTransport bidi stream (for v16 SubscribeNamespace).
52
+ */
53
+ openNativeBi(): Promise<Stream>;
54
+ /**
55
+ * Allocate the next request ID, blocking if flow control limit reached.
56
+ */
57
+ nextRequestId(): Promise<bigint | undefined>;
58
+ /**
59
+ * Main run loop — reads control stream messages and routes to virtual streams.
60
+ * Must be called after construction. Runs until the control stream closes.
61
+ */
62
+ run(): Promise<void>;
63
+ close(): void;
64
+ }
65
+ //# sourceMappingURL=adapter.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"adapter.d.ts","sourceRoot":"","sources":["../../src/ietf/adapter.ts"],"names":[],"mappings":"AACA,OAAO,EAAU,MAAM,EAAe,MAAM,cAAc,CAAC;AAG3D,OAAO,EAAE,KAAK,WAAW,EAAW,MAAM,cAAc,CAAC;AAEzD;;;GAGG;AACH,MAAM,WAAW,OAAO;IACvB,MAAM,IAAI,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC;IACnC,YAAY,CAAC,IAAI,OAAO,CAAC,MAAM,CAAC,CAAC;IACjC,QAAQ,IAAI,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC,CAAC;IACxC,aAAa,IAAI,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC,CAAC;IAC7C,KAAK,CAAC,IAAI,IAAI,CAAC;IACf,QAAQ,CAAC,OAAO,EAAE,WAAW,CAAC;CAC9B;AAED;;;GAGG;AACH,qBAAa,aAAc,YAAW,OAAO;;IAG5C,QAAQ,CAAC,OAAO,EAAE,WAAW,CAAC;IAE9B,YAAY,IAAI,EAAE,YAAY,EAAE,OAAO,EAAE,WAAW,EAGnD;IAEK,MAAM,IAAI,OAAO,CAAC,MAAM,CAAC,CAE9B;IAEK,QAAQ,IAAI,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC,CAE5C;IAEK,aAAa,IAAI,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC,CAIjD;CACD;AAmBD;;;;;;GAMG;AACH,qBAAa,oBAAqB,YAAW,OAAO;;IAQnD,QAAQ,CAAC,OAAO,EAAE,WAAW,CAAC;IAyB9B,YAAY,IAAI,EAAE,YAAY,EAAE,aAAa,EAAE,MAAM,EAAE,OAAO,EAAE,WAAW,EAAE,YAAY,EAAE,MAAM,EAQhG;IAED;;;OAGG;IACG,QAAQ,IAAI,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC,CAS5C;IAED;;;;;OAKG;IACH,MAAM,IAAI,MAAM,CAsDf;IAED;;OAEG;IACG,YAAY,IAAI,OAAO,CAAC,MAAM,CAAC,CAEpC;IAED;;OAEG;IACG,aAAa,IAAI,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC,CAYjD;IAED;;;OAGG;IACG,GAAG,IAAI,OAAO,CAAC,IAAI,CAAC,CAmDzB;IAwYD,KAAK,SA6BJ;CACD"}