@q9labsai/chalk-client 4.0.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 (253) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +173 -0
  3. package/dist/access/base64-url.d.ts +2 -0
  4. package/dist/access/base64-url.d.ts.map +1 -0
  5. package/dist/access/episode-diagnostic-credential.d.ts +11 -0
  6. package/dist/access/episode-diagnostic-credential.d.ts.map +1 -0
  7. package/dist/access/grant.d.ts +53 -0
  8. package/dist/access/grant.d.ts.map +1 -0
  9. package/dist/access/grant.test.helpers.d.ts +10 -0
  10. package/dist/access/grant.test.helpers.d.ts.map +1 -0
  11. package/dist/access/manager.d.ts +36 -0
  12. package/dist/access/manager.d.ts.map +1 -0
  13. package/dist/chat-files/http-transport.d.ts +8 -0
  14. package/dist/chat-files/http-transport.d.ts.map +1 -0
  15. package/dist/chat-files/index.d.ts +3 -0
  16. package/dist/chat-files/index.d.ts.map +1 -0
  17. package/dist/chat-files/types.d.ts +37 -0
  18. package/dist/chat-files/types.d.ts.map +1 -0
  19. package/dist/chunk-5VXGLI3T.js +3385 -0
  20. package/dist/chunk-O22WFBKF.js +5161 -0
  21. package/dist/chunk-PBCGWE2V.js +811 -0
  22. package/dist/chunk-PBSYXPNJ.js +23 -0
  23. package/dist/chunk-QVHR3RMB.js +1229 -0
  24. package/dist/client.d.ts +6785 -0
  25. package/dist/client.d.ts.map +1 -0
  26. package/dist/collaboration/index.d.ts +3 -0
  27. package/dist/collaboration/index.d.ts.map +1 -0
  28. package/dist/collaboration/types.d.ts +94 -0
  29. package/dist/collaboration/types.d.ts.map +1 -0
  30. package/dist/collaboration/wire.d.ts +19 -0
  31. package/dist/collaboration/wire.d.ts.map +1 -0
  32. package/dist/connection/dependencies.d.ts +112 -0
  33. package/dist/connection/dependencies.d.ts.map +1 -0
  34. package/dist/connection/diagnostics.d.ts +52 -0
  35. package/dist/connection/diagnostics.d.ts.map +1 -0
  36. package/dist/connection/index.d.ts +47 -0
  37. package/dist/connection/index.d.ts.map +1 -0
  38. package/dist/connection/lifecycle.d.ts +58 -0
  39. package/dist/connection/lifecycle.d.ts.map +1 -0
  40. package/dist/connection/media-devices.d.ts +6 -0
  41. package/dist/connection/media-devices.d.ts.map +1 -0
  42. package/dist/connection/production.d.ts +13 -0
  43. package/dist/connection/production.d.ts.map +1 -0
  44. package/dist/connection/snapshot.d.ts +23 -0
  45. package/dist/connection/snapshot.d.ts.map +1 -0
  46. package/dist/connection/types.d.ts +128 -0
  47. package/dist/connection/types.d.ts.map +1 -0
  48. package/dist/effect.d.ts +19 -0
  49. package/dist/effect.d.ts.map +1 -0
  50. package/dist/effect.js +7846 -0
  51. package/dist/episode-diagnostic-runtime.test.helpers.d.ts +3 -0
  52. package/dist/episode-diagnostic-runtime.test.helpers.d.ts.map +1 -0
  53. package/dist/generated/http-api.d.ts +6658 -0
  54. package/dist/generated/http-api.d.ts.map +1 -0
  55. package/dist/generated/schemas.d.ts +12076 -0
  56. package/dist/generated/schemas.d.ts.map +1 -0
  57. package/dist/generated/sync.d.ts +3947 -0
  58. package/dist/generated/sync.d.ts.map +1 -0
  59. package/dist/generated/whiteboard-v1.d.ts +334 -0
  60. package/dist/generated/whiteboard-v1.d.ts.map +1 -0
  61. package/dist/index.d.ts +10 -0
  62. package/dist/index.d.ts.map +1 -0
  63. package/dist/index.js +152 -0
  64. package/dist/media/client.d.ts +18 -0
  65. package/dist/media/client.d.ts.map +1 -0
  66. package/dist/media/cloudflare-sfu.d.ts +5 -0
  67. package/dist/media/cloudflare-sfu.d.ts.map +1 -0
  68. package/dist/media/index.d.ts +3 -0
  69. package/dist/media/index.d.ts.map +1 -0
  70. package/dist/media/index.js +24 -0
  71. package/dist/media/plane.d.ts +24 -0
  72. package/dist/media/plane.d.ts.map +1 -0
  73. package/dist/media/tracks.d.ts +21 -0
  74. package/dist/media/tracks.d.ts.map +1 -0
  75. package/dist/media/transport.d.ts +3 -0
  76. package/dist/media/transport.d.ts.map +1 -0
  77. package/dist/media/types.d.ts +114 -0
  78. package/dist/media/types.d.ts.map +1 -0
  79. package/dist/server/chunk-A6YMG5FM.js +61 -0
  80. package/dist/server/client.d.cts +2 -0
  81. package/dist/server/client.d.ts +3 -0
  82. package/dist/server/client.d.ts.map +1 -0
  83. package/dist/server/errors.d.cts +18 -0
  84. package/dist/server/errors.d.ts +19 -0
  85. package/dist/server/errors.d.ts.map +1 -0
  86. package/dist/server/index.cjs +551 -0
  87. package/dist/server/index.d.cts +3 -0
  88. package/dist/server/index.d.ts +4 -0
  89. package/dist/server/index.d.ts.map +1 -0
  90. package/dist/server/index.js +474 -0
  91. package/dist/server/server-only.cjs +40 -0
  92. package/dist/server/server-only.d.cts +1 -0
  93. package/dist/server/server-only.d.ts +2 -0
  94. package/dist/server/server-only.d.ts.map +1 -0
  95. package/dist/server/server-only.js +9 -0
  96. package/dist/server/transport.d.cts +12 -0
  97. package/dist/server/transport.d.ts +13 -0
  98. package/dist/server/transport.d.ts.map +1 -0
  99. package/dist/server/types.d.cts +201 -0
  100. package/dist/server/types.d.ts +202 -0
  101. package/dist/server/types.d.ts.map +1 -0
  102. package/dist/space-client/chat-controller-helpers.d.ts +15 -0
  103. package/dist/space-client/chat-controller-helpers.d.ts.map +1 -0
  104. package/dist/space-client/chat-controller.d.ts +34 -0
  105. package/dist/space-client/chat-controller.d.ts.map +1 -0
  106. package/dist/space-client/controller-effects.d.ts +45 -0
  107. package/dist/space-client/controller-effects.d.ts.map +1 -0
  108. package/dist/space-client/controller-parity.test.helpers.d.ts +150 -0
  109. package/dist/space-client/controller-parity.test.helpers.d.ts.map +1 -0
  110. package/dist/space-client/core.d.ts +52 -0
  111. package/dist/space-client/core.d.ts.map +1 -0
  112. package/dist/space-client/core.test.helpers.d.ts +14 -0
  113. package/dist/space-client/core.test.helpers.d.ts.map +1 -0
  114. package/dist/space-client/effect.d.ts +44 -0
  115. package/dist/space-client/effect.d.ts.map +1 -0
  116. package/dist/space-client/episode-diagnostic-registry.d.ts +13 -0
  117. package/dist/space-client/episode-diagnostic-registry.d.ts.map +1 -0
  118. package/dist/space-client/episode-diagnostic-render-registry.d.ts +4 -0
  119. package/dist/space-client/episode-diagnostic-render-registry.d.ts.map +1 -0
  120. package/dist/space-client/episode-diagnostic-runtime.d.ts +102 -0
  121. package/dist/space-client/episode-diagnostic-runtime.d.ts.map +1 -0
  122. package/dist/space-client/errors.d.ts +11 -0
  123. package/dist/space-client/errors.d.ts.map +1 -0
  124. package/dist/space-client/index.d.ts +5 -0
  125. package/dist/space-client/index.d.ts.map +1 -0
  126. package/dist/space-client/media-controller.d.ts +30 -0
  127. package/dist/space-client/media-controller.d.ts.map +1 -0
  128. package/dist/space-client/media-controller.test.helpers.d.ts +105 -0
  129. package/dist/space-client/media-controller.test.helpers.d.ts.map +1 -0
  130. package/dist/space-client/media-device-selection.d.ts +13 -0
  131. package/dist/space-client/media-device-selection.d.ts.map +1 -0
  132. package/dist/space-client/participants-controller.d.ts +33 -0
  133. package/dist/space-client/participants-controller.d.ts.map +1 -0
  134. package/dist/space-client/promise-facade.d.ts +12 -0
  135. package/dist/space-client/promise-facade.d.ts.map +1 -0
  136. package/dist/space-client/reactions-controller.d.ts +19 -0
  137. package/dist/space-client/reactions-controller.d.ts.map +1 -0
  138. package/dist/space-client/space-client.d.ts +5 -0
  139. package/dist/space-client/space-client.d.ts.map +1 -0
  140. package/dist/space-client/store.d.ts +29 -0
  141. package/dist/space-client/store.d.ts.map +1 -0
  142. package/dist/space-client/types.d.ts +222 -0
  143. package/dist/space-client/types.d.ts.map +1 -0
  144. package/dist/space-client/whiteboard-controller.d.ts +17 -0
  145. package/dist/space-client/whiteboard-controller.d.ts.map +1 -0
  146. package/dist/sync/browser.d.ts +9 -0
  147. package/dist/sync/browser.d.ts.map +1 -0
  148. package/dist/sync/canonical.d.ts +7 -0
  149. package/dist/sync/canonical.d.ts.map +1 -0
  150. package/dist/sync/errors.d.ts +9 -0
  151. package/dist/sync/errors.d.ts.map +1 -0
  152. package/dist/sync/index.d.ts +13 -0
  153. package/dist/sync/index.d.ts.map +1 -0
  154. package/dist/sync/index.js +51 -0
  155. package/dist/sync/react-native.d.ts +42 -0
  156. package/dist/sync/react-native.d.ts.map +1 -0
  157. package/dist/sync/types.d.ts +28 -0
  158. package/dist/sync/types.d.ts.map +1 -0
  159. package/dist/sync/v1-client.d.ts +59 -0
  160. package/dist/sync/v1-client.d.ts.map +1 -0
  161. package/dist/sync/v1-codec.d.ts +5 -0
  162. package/dist/sync/v1-codec.d.ts.map +1 -0
  163. package/dist/sync/v1-create.d.ts +12 -0
  164. package/dist/sync/v1-create.d.ts.map +1 -0
  165. package/dist/sync/v1-persistence.d.ts +10 -0
  166. package/dist/sync/v1-persistence.d.ts.map +1 -0
  167. package/dist/sync/v1-platform-persistence.d.ts +26 -0
  168. package/dist/sync/v1-platform-persistence.d.ts.map +1 -0
  169. package/dist/sync/v1-reducer.d.ts +17 -0
  170. package/dist/sync/v1-reducer.d.ts.map +1 -0
  171. package/dist/sync/v1-types.d.ts +197 -0
  172. package/dist/sync/v1-types.d.ts.map +1 -0
  173. package/dist/telemetry/attributes.d.ts +7 -0
  174. package/dist/telemetry/attributes.d.ts.map +1 -0
  175. package/dist/telemetry/client.d.ts +44 -0
  176. package/dist/telemetry/client.d.ts.map +1 -0
  177. package/dist/telemetry/delivery.d.ts +107 -0
  178. package/dist/telemetry/delivery.d.ts.map +1 -0
  179. package/dist/telemetry/exporter.d.ts +38 -0
  180. package/dist/telemetry/exporter.d.ts.map +1 -0
  181. package/dist/telemetry/index.d.ts +7 -0
  182. package/dist/telemetry/index.d.ts.map +1 -0
  183. package/dist/telemetry/index.js +48 -0
  184. package/dist/telemetry/journey.d.ts +52 -0
  185. package/dist/telemetry/journey.d.ts.map +1 -0
  186. package/dist/telemetry/random.d.ts +18 -0
  187. package/dist/telemetry/random.d.ts.map +1 -0
  188. package/dist/telemetry/rtc.d.ts +35 -0
  189. package/dist/telemetry/rtc.d.ts.map +1 -0
  190. package/dist/telemetry/storage.d.ts +22 -0
  191. package/dist/telemetry/storage.d.ts.map +1 -0
  192. package/dist/telemetry/sync.d.ts +10 -0
  193. package/dist/telemetry/sync.d.ts.map +1 -0
  194. package/dist/telemetry/test-support.d.ts +51 -0
  195. package/dist/telemetry/test-support.d.ts.map +1 -0
  196. package/dist/telemetry/trace.d.ts +18 -0
  197. package/dist/telemetry/trace.d.ts.map +1 -0
  198. package/dist/telemetry/types.d.ts +109 -0
  199. package/dist/telemetry/types.d.ts.map +1 -0
  200. package/dist/webhooks/chunk-UNMJZNTZ.js +31 -0
  201. package/dist/webhooks/errors.d.cts +8 -0
  202. package/dist/webhooks/errors.d.ts +9 -0
  203. package/dist/webhooks/errors.d.ts.map +1 -0
  204. package/dist/webhooks/generated/event-v1.d.cts +764 -0
  205. package/dist/webhooks/generated/event-v1.d.ts +765 -0
  206. package/dist/webhooks/generated/event-v1.d.ts.map +1 -0
  207. package/dist/webhooks/generated/fixtures-v1.d.cts +46 -0
  208. package/dist/webhooks/generated/fixtures-v1.d.ts +47 -0
  209. package/dist/webhooks/generated/fixtures-v1.d.ts.map +1 -0
  210. package/dist/webhooks/index.cjs +1242 -0
  211. package/dist/webhooks/index.d.cts +5 -0
  212. package/dist/webhooks/index.d.ts +6 -0
  213. package/dist/webhooks/index.d.ts.map +1 -0
  214. package/dist/webhooks/index.js +1197 -0
  215. package/dist/webhooks/processor.d.cts +21 -0
  216. package/dist/webhooks/processor.d.ts +22 -0
  217. package/dist/webhooks/processor.d.ts.map +1 -0
  218. package/dist/webhooks/server-only.cjs +36 -0
  219. package/dist/webhooks/server-only.d.cts +1 -0
  220. package/dist/webhooks/server-only.d.ts +2 -0
  221. package/dist/webhooks/server-only.d.ts.map +1 -0
  222. package/dist/webhooks/server-only.js +9 -0
  223. package/dist/webhooks/test.cjs +160 -0
  224. package/dist/webhooks/test.d.cts +42 -0
  225. package/dist/webhooks/test.d.ts +43 -0
  226. package/dist/webhooks/test.d.ts.map +1 -0
  227. package/dist/webhooks/test.js +137 -0
  228. package/dist/webhooks/types.d.cts +55 -0
  229. package/dist/webhooks/types.d.ts +56 -0
  230. package/dist/webhooks/types.d.ts.map +1 -0
  231. package/dist/webhooks/validate.d.cts +5 -0
  232. package/dist/webhooks/validate.d.ts +6 -0
  233. package/dist/webhooks/validate.d.ts.map +1 -0
  234. package/dist/webhooks/verify.d.cts +10 -0
  235. package/dist/webhooks/verify.d.ts +11 -0
  236. package/dist/webhooks/verify.d.ts.map +1 -0
  237. package/dist/whiteboard/file-http-transport.d.ts +9 -0
  238. package/dist/whiteboard/file-http-transport.d.ts.map +1 -0
  239. package/dist/whiteboard/index.d.ts +7 -0
  240. package/dist/whiteboard/index.d.ts.map +1 -0
  241. package/dist/whiteboard/types.d.ts +139 -0
  242. package/dist/whiteboard/types.d.ts.map +1 -0
  243. package/dist/whiteboard/v1-client.d.ts +18 -0
  244. package/dist/whiteboard/v1-client.d.ts.map +1 -0
  245. package/dist/whiteboard/v1-codec.d.ts +5 -0
  246. package/dist/whiteboard/v1-codec.d.ts.map +1 -0
  247. package/dist/whiteboard/v1-create.d.ts +8 -0
  248. package/dist/whiteboard/v1-create.d.ts.map +1 -0
  249. package/dist/whiteboard/v1-multipart.d.ts +30 -0
  250. package/dist/whiteboard/v1-multipart.d.ts.map +1 -0
  251. package/dist/whiteboard/v1-persistence.d.ts +9 -0
  252. package/dist/whiteboard/v1-persistence.d.ts.map +1 -0
  253. package/package.json +222 -0
@@ -0,0 +1,811 @@
1
+ // src/media/types.ts
2
+ var CloudflareSFUError = class extends Error {
3
+ constructor(message, code) {
4
+ super(message);
5
+ this.code = code;
6
+ this.name = "CloudflareSFUError";
7
+ }
8
+ code;
9
+ };
10
+
11
+ // src/media/tracks.ts
12
+ function parseCloudflareSFUPublicationID(publicationId) {
13
+ if (publicationId.startsWith("chalk_pub_v1.")) return parseVersionOnePublicationReference(publicationId.slice("chalk_pub_v1.".length));
14
+ if (publicationId.startsWith("chalk_pub_")) throw new CloudflareSFUError("Cloudflare SFU publication ID uses an unsupported version", "invalid_publication");
15
+ return parseLegacyPublicationReference(publicationId);
16
+ }
17
+ function parseVersionOnePublicationReference(encoded) {
18
+ if (!encoded || !/^[A-Za-z0-9_-]+$/.test(encoded) || encoded.length % 4 === 1) {
19
+ throw new CloudflareSFUError("Cloudflare SFU publication ID is malformed", "invalid_publication");
20
+ }
21
+ try {
22
+ const standard = encoded.replaceAll("-", "+").replaceAll("_", "/");
23
+ const padded = standard.padEnd(standard.length + (4 - standard.length % 4) % 4, "=");
24
+ const binary = globalThis.atob(padded);
25
+ const canonical = globalThis.btoa(binary).replaceAll("+", "-").replaceAll("/", "_").replace(/=+$/, "");
26
+ if (canonical !== encoded) throw new Error("non-canonical publication payload");
27
+ const bytes = Uint8Array.from(binary, (character) => character.charCodeAt(0));
28
+ const decoded = JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(bytes));
29
+ if (!isVersionOnePublicationPayload(decoded)) throw new Error("invalid publication payload");
30
+ return { connectionId: decoded.c, trackName: decoded.t };
31
+ } catch {
32
+ throw new CloudflareSFUError("Cloudflare SFU publication ID is malformed", "invalid_publication");
33
+ }
34
+ }
35
+ function isVersionOnePublicationPayload(value) {
36
+ if (!isRecord(value) || !hasExactKeys(value, ["c", "g", "m", "t"])) return false;
37
+ return isTrimmedNonEmptyString(value.c) && isTrimmedNonEmptyString(value.m) && isTrimmedNonEmptyString(value.t) && isPositiveSafeInteger(value.g);
38
+ }
39
+ function isRecord(value) {
40
+ return typeof value === "object" && value !== null && !Array.isArray(value);
41
+ }
42
+ function hasExactKeys(value, expected) {
43
+ const actual = Object.keys(value).sort();
44
+ return actual.length === expected.length && actual.every((key, index) => key === expected[index]);
45
+ }
46
+ function isTrimmedNonEmptyString(value) {
47
+ return typeof value === "string" && value.length > 0 && value.trim() === value;
48
+ }
49
+ function isPositiveSafeInteger(value) {
50
+ return typeof value === "number" && Number.isSafeInteger(value) && value > 0;
51
+ }
52
+ function parseLegacyPublicationReference(publicationId) {
53
+ const separator = publicationId.indexOf("|");
54
+ if (separator <= 0 || separator === publicationId.length - 1 || publicationId.indexOf("|", separator + 1) !== -1) {
55
+ throw new CloudflareSFUError("Cloudflare SFU publication ID is malformed", "invalid_publication");
56
+ }
57
+ const connectionId = publicationId.slice(0, separator);
58
+ const trackName = publicationId.slice(separator + 1);
59
+ if (connectionId.trim() !== connectionId || trackName.trim() !== trackName) throw new CloudflareSFUError("Cloudflare SFU publication ID is malformed", "invalid_publication");
60
+ return { connectionId, trackName };
61
+ }
62
+ function requireDescription(description) {
63
+ if (description.type !== "offer" && description.type !== "answer" || !description.sdp) {
64
+ throw new CloudflareSFUError("Browser SDP description is incomplete", "media_failed");
65
+ }
66
+ return { type: description.type, sdp: description.sdp };
67
+ }
68
+ function requireSFUDescription(description) {
69
+ if (!description) throw new CloudflareSFUError("Cloudflare did not return an SDP answer", "signaling_failed");
70
+ return description;
71
+ }
72
+ function validatePublicationSnapshot(snapshot) {
73
+ if (!Number.isSafeInteger(snapshot.incarnation) || snapshot.incarnation < 0 || !Number.isSafeInteger(snapshot.sequence) || snapshot.sequence < 0) {
74
+ throw new CloudflareSFUError("Cloudflare SFU publication cursor is invalid", "invalid_publication");
75
+ }
76
+ const seen = /* @__PURE__ */ new Set();
77
+ for (const publication of snapshot.publications) {
78
+ if (!publication.participantId.trim() || !publication.publicationId.trim()) {
79
+ throw new CloudflareSFUError("Cloudflare SFU publication is incomplete", "invalid_publication");
80
+ }
81
+ const key = publicationKey(publication);
82
+ if (seen.has(key)) throw new CloudflareSFUError("Cloudflare SFU publication snapshot contains duplicate participant sources", "invalid_publication");
83
+ seen.add(key);
84
+ parseCloudflareSFUPublicationID(publication.publicationId);
85
+ }
86
+ return { incarnation: snapshot.incarnation, sequence: snapshot.sequence, signature: publicationSignature(snapshot.publications) };
87
+ }
88
+ function comparePublicationCursor(current, next) {
89
+ if (!current || next.incarnation > current.incarnation || next.incarnation === current.incarnation && next.sequence > current.sequence) return "newer";
90
+ if (next.incarnation < current.incarnation || next.sequence < current.sequence) return "stale";
91
+ if (next.signature !== current.signature) throw new CloudflareSFUError("Cloudflare SFU returned conflicting data for one publication cursor", "invalid_publication");
92
+ return "same";
93
+ }
94
+ function publicationKey(publication) {
95
+ return `${publication.participantId}\0${publication.source}`;
96
+ }
97
+ function publicationSignature(publications) {
98
+ return [...publications].sort((left, right) => publicationKey(left).localeCompare(publicationKey(right))).map((publication) => `${publicationKey(publication)}\0${publication.publicationId}`).join("");
99
+ }
100
+ async function waitFor(predicate, timeoutMs, timeoutMessage = "Timed out waiting for a remote media track") {
101
+ const deadline = Date.now() + timeoutMs;
102
+ while (!predicate()) {
103
+ if (Date.now() >= deadline) throw new CloudflareSFUError(timeoutMessage, "media_failed");
104
+ await new Promise((resolve) => globalThis.setTimeout(resolve, 20));
105
+ }
106
+ }
107
+
108
+ // src/media/client.ts
109
+ var EMPTY_LOCAL = Object.freeze([]);
110
+ var EMPTY_REMOTE = Object.freeze([]);
111
+ var CONNECTION_TIMEOUT_MS = 8e3;
112
+ var CloudflareSFUClient = class {
113
+ #localListeners = /* @__PURE__ */ new Set();
114
+ #onError;
115
+ #onRemoteTrack;
116
+ #onScreenEnded;
117
+ #participantId;
118
+ #peerConnectionFactory;
119
+ #pollIntervalMs;
120
+ #remoteListeners = /* @__PURE__ */ new Set();
121
+ #snapshotListeners = /* @__PURE__ */ new Set();
122
+ #localTracks = /* @__PURE__ */ new Map();
123
+ #reusableLocalTransceivers = /* @__PURE__ */ new Map();
124
+ #remoteTracks = /* @__PURE__ */ new Map();
125
+ #bootstrap;
126
+ #connection;
127
+ #cursor = null;
128
+ #generation = 0;
129
+ #polling = false;
130
+ #pollTimer;
131
+ #sdpTail = Promise.resolve();
132
+ #snapshot;
133
+ #started = false;
134
+ #stopped = false;
135
+ #transport;
136
+ constructor(options) {
137
+ validateClientOptions(options);
138
+ this.#participantId = options.participantId;
139
+ this.#bootstrap = options.bootstrap;
140
+ this.#transport = options.transport;
141
+ this.#pollIntervalMs = options.pollIntervalMs ?? 1e3;
142
+ this.#onError = options.onError;
143
+ this.#onRemoteTrack = options.onRemoteTrack;
144
+ this.#onScreenEnded = options.onScreenEnded;
145
+ this.#peerConnectionFactory = options.peerConnectionFactory;
146
+ this.#connection = this.#createPeerConnection(options.bootstrap);
147
+ this.#snapshot = freezeSnapshot({
148
+ connection: { phase: "idle", peerConnectionState: this.#connection.connectionState, iceConnectionState: this.#connection.iceConnectionState },
149
+ cursor: null,
150
+ localTracks: EMPTY_LOCAL,
151
+ remoteTracks: EMPTY_REMOTE,
152
+ failure: null
153
+ });
154
+ this.#observeConnection(this.#connection, this.#generation);
155
+ }
156
+ getSnapshot() {
157
+ return this.#snapshot;
158
+ }
159
+ subscribe(listener) {
160
+ this.#snapshotListeners.add(listener);
161
+ return () => this.#snapshotListeners.delete(listener);
162
+ }
163
+ prepareLocalTrack(source, track) {
164
+ this.#requireActive();
165
+ validateTrackSource(source, track);
166
+ if (this.#localTracks.has(source)) throw new CloudflareSFUError(`A ${source} track is already prepared`, "media_failed");
167
+ const state = {
168
+ source,
169
+ track,
170
+ transceiver: this.#reusableLocalTransceivers.get(source) ?? null,
171
+ providerPublicationId: null,
172
+ pendingOperationId: null,
173
+ pendingTrackName: null,
174
+ desiredEnabled: false,
175
+ enabled: false,
176
+ endedListener: null
177
+ };
178
+ this.#reusableLocalTransceivers.delete(source);
179
+ if (source === "screen") {
180
+ state.endedListener = () => {
181
+ if (this.#localTracks.get("screen") === state) this.#invokeListener(() => this.#onScreenEnded?.());
182
+ };
183
+ track.addEventListener("ended", state.endedListener);
184
+ }
185
+ this.#localTracks.set(source, state);
186
+ this.#publishSnapshot();
187
+ this.#emitLocal();
188
+ }
189
+ async clearPreparedLocalTrack(source) {
190
+ const state = this.#localTracks.get(source);
191
+ if (!state) return;
192
+ if (state.enabled) await this.#setPreparedTrackEnabled(state, false);
193
+ if (state.transceiver) this.#reusableLocalTransceivers.set(source, state.transceiver);
194
+ this.#removeOwnedLocalTrack(state);
195
+ this.#localTracks.delete(source);
196
+ this.#publishSnapshot();
197
+ this.#emitLocal();
198
+ }
199
+ async start(localMedia) {
200
+ if (this.#started) return;
201
+ this.#requireActive();
202
+ const tracks = localMedia.getTracks().filter((track) => track.kind === "audio" || track.kind === "video");
203
+ for (const track of tracks) this.prepareLocalTrack(track.kind === "audio" ? "microphone" : "camera", track);
204
+ const generation = this.#generation;
205
+ this.#setPhase("connecting", null);
206
+ await this.#activatePreparedTracks(
207
+ [...this.#localTracks.values()].filter((state) => state.source !== "screen"),
208
+ generation
209
+ );
210
+ }
211
+ async refreshRemotePublications() {
212
+ if (!this.#started || this.#stopped || this.#polling) return;
213
+ this.#polling = true;
214
+ const generation = this.#generation;
215
+ try {
216
+ const transport = this.#requireTransport();
217
+ const authoritative = await transport.listPublications();
218
+ this.#requireGeneration(generation);
219
+ await this.#reconcileRemotePublications(authoritative, generation);
220
+ } catch (error) {
221
+ if (generation === this.#generation && !this.#stopped) this.#reportError(error);
222
+ throw error;
223
+ } finally {
224
+ if (generation === this.#generation) this.#polling = false;
225
+ }
226
+ }
227
+ async setLocalPublicationTarget(target) {
228
+ if (target.participantId !== this.#participantId) return { outcome: "terminal_failure", errorCode: "invalid_participant" };
229
+ if (this.#stopped) return { outcome: "terminal_failure", errorCode: "media_stopped" };
230
+ const state = this.#localTracks.get(target.source);
231
+ if (!state) return { outcome: "terminal_failure", errorCode: "source_unavailable" };
232
+ if (state.enabled === target.enabled) return { outcome: "satisfied", errorCode: null };
233
+ try {
234
+ await this.#setPreparedTrackEnabled(state, target.enabled, target.operationId);
235
+ return { outcome: "confirmed", errorCode: null };
236
+ } catch (error) {
237
+ if (!this.#stopped) this.#reportError(error);
238
+ return { outcome: "retryable_failure", errorCode: error instanceof CloudflareSFUError ? error.code : "media_failed" };
239
+ }
240
+ }
241
+ observeLocalPublications(listener) {
242
+ this.#localListeners.add(listener);
243
+ this.#invokeListener(() => listener(this.#projectLocalPublications()));
244
+ return () => this.#localListeners.delete(listener);
245
+ }
246
+ observeRemotePublications(listener) {
247
+ this.#remoteListeners.add(listener);
248
+ this.#invokeListener(() => listener(this.#projectRemotePublications()));
249
+ return () => this.#remoteListeners.delete(listener);
250
+ }
251
+ async restart(input) {
252
+ this.#requireActive();
253
+ const options = "connectionId" in input ? { bootstrap: input } : input;
254
+ validateBootstrap(options.bootstrap);
255
+ const generation = ++this.#generation;
256
+ this.#polling = false;
257
+ this.#clearPoll();
258
+ this.#disposeConnection(false);
259
+ this.#reusableLocalTransceivers.clear();
260
+ this.#clearRemoteTracks();
261
+ this.#cursor = null;
262
+ this.#bootstrap = options.bootstrap;
263
+ if (options.transport) this.#transport = options.transport;
264
+ this.#connection = this.#createPeerConnection(options.bootstrap);
265
+ this.#observeConnection(this.#connection, generation);
266
+ for (const state of this.#localTracks.values()) {
267
+ state.transceiver = null;
268
+ state.enabled = false;
269
+ }
270
+ const enabled = [...this.#localTracks.values()].filter((state) => state.desiredEnabled && state.track.readyState !== "ended");
271
+ this.#setPhase("recovering", null);
272
+ await this.#activatePreparedTracks(enabled, generation);
273
+ }
274
+ async #activatePreparedTracks(states, generation) {
275
+ try {
276
+ await this.#publishPreparedTracks(states, generation);
277
+ this.#requireGeneration(generation);
278
+ this.#started = true;
279
+ this.#setPhase("live", null);
280
+ this.#schedulePoll(0);
281
+ } catch (error) {
282
+ if (generation === this.#generation && !this.#stopped) this.#setFailure(error, "media_failed");
283
+ throw error;
284
+ }
285
+ }
286
+ stop() {
287
+ if (this.#stopped) return;
288
+ this.#stopped = true;
289
+ this.#generation++;
290
+ this.#clearPoll();
291
+ this.#polling = false;
292
+ this.#disposeConnection(true);
293
+ this.#reusableLocalTransceivers.clear();
294
+ this.#clearRemoteTracks();
295
+ for (const state of this.#localTracks.values()) this.#removeOwnedLocalTrack(state);
296
+ this.#localTracks.clear();
297
+ this.#transport = void 0;
298
+ this.#started = false;
299
+ this.#cursor = null;
300
+ this.#publishSnapshot("stopped", null);
301
+ this.#emitLocal();
302
+ this.#emitRemote();
303
+ this.#localListeners.clear();
304
+ this.#remoteListeners.clear();
305
+ this.#snapshotListeners.clear();
306
+ }
307
+ async #setPreparedTrackEnabled(state, enabled, operationId) {
308
+ if (enabled) return this.#enablePreparedTrack(state, operationId);
309
+ return this.#disablePreparedTrack(state);
310
+ }
311
+ async #enablePreparedTrack(state, operationId) {
312
+ if (operationId && state.pendingOperationId !== operationId) {
313
+ state.pendingOperationId = operationId;
314
+ state.pendingTrackName = `${state.source}-${operationId}`;
315
+ }
316
+ state.desiredEnabled = true;
317
+ state.track.enabled = true;
318
+ try {
319
+ await this.#publishPreparedTracks([state], this.#generation);
320
+ } catch (error) {
321
+ state.desiredEnabled = false;
322
+ state.enabled = false;
323
+ state.track.enabled = false;
324
+ throw error;
325
+ }
326
+ }
327
+ async #disablePreparedTrack(state) {
328
+ const transceiver = state.transceiver;
329
+ state.desiredEnabled = false;
330
+ state.track.enabled = false;
331
+ try {
332
+ if (transceiver) await transceiver.sender.replaceTrack(null);
333
+ } catch (error) {
334
+ state.desiredEnabled = true;
335
+ state.track.enabled = true;
336
+ throw error;
337
+ }
338
+ state.enabled = false;
339
+ state.providerPublicationId = null;
340
+ state.pendingOperationId = null;
341
+ state.pendingTrackName = null;
342
+ this.#publishSnapshot();
343
+ this.#emitLocal();
344
+ }
345
+ async #publishPreparedTracks(states, generation) {
346
+ if (states.length === 0) return;
347
+ const connection = this.#connection;
348
+ const bootstrap = this.#bootstrap;
349
+ await this.#serializeSDP(() => this.#publishPreparedTracksSerialized(states, generation, connection, bootstrap.connectionId));
350
+ }
351
+ async #publishPreparedTracksSerialized(states, generation, connection, connectionId) {
352
+ this.#requireGeneration(generation);
353
+ const pendingStates = states.filter((state) => !state.enabled);
354
+ if (pendingStates.length === 0) return;
355
+ const wasLive = connectionIsLive(connection);
356
+ const publications = [];
357
+ try {
358
+ for (const state of pendingStates) publications.push(await this.#prepareLocalPublication(connection, state));
359
+ const response = await this.#negotiateLocalPublications(connection, connectionId, publications, generation);
360
+ if (!wasLive) await this.#waitForConnection(connection, generation);
361
+ this.#requireGeneration(generation);
362
+ this.#confirmLocalPublications(publications, response.tracks);
363
+ this.#publishSnapshot();
364
+ this.#emitLocal();
365
+ } catch (error) {
366
+ await this.#rollbackLocalOffer(connection);
367
+ await this.#discardLocalPublications(publications);
368
+ throw error;
369
+ }
370
+ }
371
+ async #prepareLocalPublication(connection, state) {
372
+ const reusedTransceiver = state.transceiver !== null;
373
+ const transceiver = state.transceiver ?? connection.addTransceiver(state.track, { direction: "sendonly" });
374
+ if (reusedTransceiver) await transceiver.sender.replaceTrack(state.track);
375
+ state.transceiver = transceiver;
376
+ return { state, transceiver, trackName: state.pendingTrackName ?? `${state.source}-${globalThis.crypto.randomUUID()}`, reusedTransceiver };
377
+ }
378
+ async #negotiateLocalPublications(connection, connectionId, publications, generation) {
379
+ const offer = await connection.createOffer();
380
+ await connection.setLocalDescription(offer);
381
+ const tracks = publications.map(
382
+ ({ state, transceiver, trackName }) => ({
383
+ location: "local",
384
+ mid: requireTransceiverMid(transceiver),
385
+ trackName,
386
+ source: state.source
387
+ })
388
+ );
389
+ const response = await this.#requireTransport().addTracks({ connectionId, sessionDescription: requireDescription(offer), tracks });
390
+ this.#requireGeneration(generation);
391
+ await connection.setRemoteDescription(requireSFUDescription(response.sessionDescription));
392
+ return response;
393
+ }
394
+ #confirmLocalPublications(publications, tracks) {
395
+ for (const { state, transceiver } of publications) {
396
+ const authoritative = tracks?.find((track) => track.location === "local" && track.mid === transceiver.mid && track.source === state.source);
397
+ if (!authoritative?.publicationId) throw new CloudflareSFUError("Chalk did not return an authoritative local publication ID", "invalid_publication");
398
+ state.desiredEnabled = true;
399
+ state.enabled = true;
400
+ state.providerPublicationId = authoritative.publicationId;
401
+ state.pendingOperationId = null;
402
+ state.pendingTrackName = null;
403
+ }
404
+ }
405
+ async #discardLocalPublications(publications) {
406
+ for (const { state, transceiver, reusedTransceiver } of publications) {
407
+ if (reusedTransceiver) {
408
+ try {
409
+ await transceiver.sender.replaceTrack(null);
410
+ } catch (error) {
411
+ this.#reportError(error);
412
+ }
413
+ continue;
414
+ }
415
+ try {
416
+ transceiver.stop();
417
+ } catch (error) {
418
+ this.#reportError(error);
419
+ }
420
+ if (state.transceiver === transceiver) state.transceiver = null;
421
+ }
422
+ }
423
+ async #rollbackLocalOffer(connection) {
424
+ if (connection.signalingState !== "have-local-offer") return;
425
+ try {
426
+ await connection.setLocalDescription({ type: "rollback" });
427
+ } catch (error) {
428
+ this.#reportError(error);
429
+ }
430
+ }
431
+ async #reconcileRemotePublications(authoritative, generation) {
432
+ const cursor = validatePublicationSnapshot(authoritative);
433
+ const ordering = comparePublicationCursor(this.#cursor, cursor);
434
+ if (ordering !== "newer") return;
435
+ const desired = desiredRemotePublications(authoritative.publications, this.#participantId);
436
+ const toPull = [...desired].filter(([key, publication]) => this.#remoteTracks.get(key)?.publicationId !== publication.publicationId).map(([, publication]) => publication);
437
+ const pulled = await this.#pull(toPull, generation);
438
+ this.#requireGeneration(generation);
439
+ const next = reconcileRemoteTracks(desired, pulled, this.#remoteTracks);
440
+ stopReplacedRemoteTracks(this.#remoteTracks, next, this.#reportError.bind(this));
441
+ this.#remoteTracks.clear();
442
+ for (const [key, publication] of next) this.#remoteTracks.set(key, publication);
443
+ this.#cursor = cursor;
444
+ for (const publication of pulled) this.#invokeListener(() => this.#onRemoteTrack?.(publication));
445
+ this.#publishSnapshot();
446
+ this.#emitRemote();
447
+ }
448
+ async #pull(publications, generation) {
449
+ if (publications.length === 0) return [];
450
+ const connection = this.#connection;
451
+ const connectionId = this.#bootstrap.connectionId;
452
+ return this.#serializeSDP(async () => {
453
+ const requested = publications.map((publication) => {
454
+ const reference = parseCloudflareSFUPublicationID(publication.publicationId);
455
+ return { location: "remote", sessionId: reference.connectionId, trackName: reference.trackName };
456
+ });
457
+ const received = /* @__PURE__ */ new Map();
458
+ const onTrack = (event) => {
459
+ if (event.transceiver.mid !== null) received.set(event.transceiver.mid, event.track);
460
+ };
461
+ connection.addEventListener("track", onTrack);
462
+ try {
463
+ this.#requireGeneration(generation);
464
+ const response = await this.#requireTransport().addTracks({ connectionId, tracks: requested });
465
+ this.#requireGeneration(generation);
466
+ const responseTracks = response.tracks ?? [];
467
+ await this.#completeRenegotiation(response, connection, connectionId, generation);
468
+ await this.#waitForConnection(connection, generation);
469
+ await waitFor(() => responseTracks.every((track) => track.mid !== void 0 && received.has(track.mid)), 5e3);
470
+ this.#requireGeneration(generation);
471
+ return publications.map((publication, index) => {
472
+ const responseTrack = responseTracks[index];
473
+ const track = responseTrack?.mid === void 0 ? void 0 : received.get(responseTrack.mid);
474
+ if (!track) throw new CloudflareSFUError("A negotiated remote track did not arrive", "media_failed");
475
+ return Object.freeze({ ...publication, track });
476
+ });
477
+ } catch (error) {
478
+ for (const track of received.values()) safeStopTrack(track, this.#reportError.bind(this));
479
+ throw error;
480
+ } finally {
481
+ connection.removeEventListener("track", onTrack);
482
+ }
483
+ });
484
+ }
485
+ async #completeRenegotiation(response, connection, connectionId, generation) {
486
+ if (!response.requiresImmediateRenegotiation) return;
487
+ if (!response.sessionDescription) throw new CloudflareSFUError("Cloudflare did not return a remote-track offer", "signaling_failed");
488
+ await connection.setRemoteDescription(response.sessionDescription);
489
+ const answer = await connection.createAnswer();
490
+ await connection.setLocalDescription(answer);
491
+ await this.#requireTransport().renegotiate({ connectionId, sessionDescription: requireDescription(answer) });
492
+ this.#requireGeneration(generation);
493
+ }
494
+ #serializeSDP(operation) {
495
+ const result = this.#sdpTail.then(operation, operation);
496
+ this.#sdpTail = result.then(
497
+ () => void 0,
498
+ () => void 0
499
+ );
500
+ return result;
501
+ }
502
+ #waitForConnection(connection, generation) {
503
+ return waitFor(
504
+ () => {
505
+ this.#requireGeneration(generation);
506
+ return connectionIsLive(connection);
507
+ },
508
+ CONNECTION_TIMEOUT_MS,
509
+ "Timed out waiting for the Cloudflare SFU peer connection"
510
+ );
511
+ }
512
+ #observeConnection(connection, generation) {
513
+ const observe = () => {
514
+ if (generation !== this.#generation || this.#stopped) return;
515
+ const observation = observeConnectionState(connection.connectionState, connection.iceConnectionState, this.#started);
516
+ if (observation) this.#publishSnapshot(observation.phase, observation.failure);
517
+ else this.#publishSnapshot();
518
+ };
519
+ connection.addEventListener("connectionstatechange", observe);
520
+ connection.addEventListener("iceconnectionstatechange", observe);
521
+ }
522
+ #createPeerConnection(bootstrap) {
523
+ const create = this.#peerConnectionFactory ?? ((configuration) => new RTCPeerConnection(configuration));
524
+ return create({ iceServers: [{ urls: bootstrap.stunServer }], bundlePolicy: "max-bundle" });
525
+ }
526
+ #disposeConnection(stopSenders) {
527
+ if (stopSenders) {
528
+ for (const sender of this.#connection.getSenders()) {
529
+ if (sender.track) safeStopTrack(sender.track, this.#reportError.bind(this));
530
+ }
531
+ }
532
+ for (const transceiver of this.#connection.getTransceivers()) {
533
+ try {
534
+ transceiver.stop();
535
+ } catch (error) {
536
+ this.#reportError(error);
537
+ }
538
+ }
539
+ try {
540
+ this.#connection.close();
541
+ } catch (error) {
542
+ this.#reportError(error);
543
+ }
544
+ }
545
+ #removeOwnedLocalTrack(state) {
546
+ if (state.endedListener) state.track.removeEventListener("ended", state.endedListener);
547
+ safeStopTrack(state.track, this.#reportError.bind(this));
548
+ state.enabled = false;
549
+ state.desiredEnabled = false;
550
+ state.transceiver = null;
551
+ state.providerPublicationId = null;
552
+ state.pendingOperationId = null;
553
+ state.pendingTrackName = null;
554
+ state.endedListener = null;
555
+ }
556
+ #clearRemoteTracks() {
557
+ for (const publication of this.#remoteTracks.values()) safeStopTrack(publication.track, this.#reportError.bind(this));
558
+ this.#remoteTracks.clear();
559
+ this.#publishSnapshot();
560
+ this.#emitRemote();
561
+ }
562
+ #schedulePoll(delayMs = this.#pollIntervalMs) {
563
+ if (this.#stopped || !this.#started || this.#pollTimer !== void 0) return;
564
+ this.#pollTimer = globalThis.setTimeout(async () => {
565
+ this.#pollTimer = void 0;
566
+ try {
567
+ await this.refreshRemotePublications();
568
+ } catch {
569
+ } finally {
570
+ this.#schedulePoll();
571
+ }
572
+ }, delayMs);
573
+ }
574
+ #clearPoll() {
575
+ if (this.#pollTimer !== void 0) globalThis.clearTimeout(this.#pollTimer);
576
+ this.#pollTimer = void 0;
577
+ }
578
+ #projectLocalPublications() {
579
+ return [...this.#localTracks.values()].map((state) => ({
580
+ participantId: this.#participantId,
581
+ source: state.source,
582
+ enabled: state.enabled,
583
+ publicationId: state.enabled ? state.providerPublicationId : null
584
+ }));
585
+ }
586
+ #projectRemotePublications() {
587
+ return [...this.#remoteTracks.values()].map(({ participantId, source, publicationId }) => ({ participantId, source, publicationId, enabled: true }));
588
+ }
589
+ #publishSnapshot(phase = this.#snapshot.connection.phase, failure = this.#snapshot.failure) {
590
+ const next = freezeSnapshot({
591
+ connection: {
592
+ phase,
593
+ peerConnectionState: this.#connection.connectionState,
594
+ iceConnectionState: this.#connection.iceConnectionState
595
+ },
596
+ cursor: this.#cursor ? { incarnation: this.#cursor.incarnation, sequence: this.#cursor.sequence } : null,
597
+ localTracks: this.#localTracks.size ? [...this.#localTracks.values()].map(
598
+ (state) => Object.freeze({
599
+ source: state.source,
600
+ enabled: state.enabled,
601
+ publicationId: state.enabled ? state.providerPublicationId : null,
602
+ track: state.track
603
+ })
604
+ ) : EMPTY_LOCAL,
605
+ remoteTracks: this.#remoteTracks.size ? [...this.#remoteTracks.values()] : EMPTY_REMOTE,
606
+ failure
607
+ });
608
+ if (snapshotEqual(this.#snapshot, next)) return;
609
+ this.#snapshot = next;
610
+ for (const listener of this.#snapshotListeners) this.#invokeListener(listener);
611
+ }
612
+ #setPhase(phase, failure) {
613
+ this.#publishSnapshot(phase, failure);
614
+ }
615
+ #setFailure(error, fallback) {
616
+ const code = error instanceof CloudflareSFUError ? error.code : fallback;
617
+ this.#publishSnapshot("failed", { code, recoverable: code !== "invalid_bootstrap" && code !== "invalid_target" && code !== "invalid_publication" });
618
+ this.#reportError(error);
619
+ }
620
+ #emitLocal() {
621
+ const publications = this.#projectLocalPublications();
622
+ for (const listener of this.#localListeners) this.#invokeListener(() => listener(publications));
623
+ }
624
+ #emitRemote() {
625
+ const publications = this.#projectRemotePublications();
626
+ for (const listener of this.#remoteListeners) this.#invokeListener(() => listener(publications));
627
+ }
628
+ #invokeListener(listener) {
629
+ try {
630
+ listener();
631
+ } catch (error) {
632
+ this.#reportError(error);
633
+ }
634
+ }
635
+ #reportError(error) {
636
+ try {
637
+ this.#onError?.(error);
638
+ } catch {
639
+ }
640
+ }
641
+ #requireGeneration(generation) {
642
+ if (generation !== this.#generation || this.#stopped) throw new CloudflareSFUError("Cloudflare SFU operation belongs to a stale connection generation", "stale_generation");
643
+ }
644
+ #requireActive() {
645
+ if (this.#stopped) throw new CloudflareSFUError("Cloudflare SFU client is stopped", "media_failed");
646
+ }
647
+ #requireTransport() {
648
+ if (!this.#transport) throw new CloudflareSFUError("Cloudflare SFU signaling transport is unavailable", "signaling_failed");
649
+ return this.#transport;
650
+ }
651
+ };
652
+ function validateClientOptions(options) {
653
+ validateBootstrap(options.bootstrap);
654
+ if (!options.participantId.trim()) throw new CloudflareSFUError("Cloudflare SFU participant is missing", "invalid_bootstrap");
655
+ if (options.pollIntervalMs !== void 0 && (!Number.isFinite(options.pollIntervalMs) || options.pollIntervalMs < 0)) {
656
+ throw new CloudflareSFUError("Cloudflare SFU polling interval is invalid", "invalid_bootstrap");
657
+ }
658
+ }
659
+ function validateBootstrap(bootstrap) {
660
+ if (!bootstrap.connectionId.trim() || !bootstrap.stunServer.trim()) throw new CloudflareSFUError("Cloudflare SFU bootstrap is incomplete", "invalid_bootstrap");
661
+ }
662
+ function validateTrackSource(source, track) {
663
+ const valid = source === "microphone" ? track.kind === "audio" : track.kind === "video";
664
+ if (!valid) throw new CloudflareSFUError(`The prepared ${source} track has an incompatible kind`, "media_failed");
665
+ }
666
+ function desiredRemotePublications(publications, participantId) {
667
+ return new Map(publications.filter((publication) => publication.participantId !== participantId).map((publication) => [publicationKey(publication), publication]));
668
+ }
669
+ function reconcileRemoteTracks(desired, pulled, current) {
670
+ const pulledByKey = new Map(pulled.map((publication) => [publicationKey(publication), publication]));
671
+ return new Map(
672
+ [...desired].map(([key, publication]) => {
673
+ const track = pulledByKey.get(key) ?? current.get(key);
674
+ if (!track || track.publicationId !== publication.publicationId) throw new CloudflareSFUError("Cloudflare SFU did not return a requested remote track", "media_failed");
675
+ return [key, track];
676
+ })
677
+ );
678
+ }
679
+ function stopReplacedRemoteTracks(current, next, onError) {
680
+ for (const [key, previous] of current) {
681
+ if (next.get(key) !== previous) safeStopTrack(previous.track, onError);
682
+ }
683
+ }
684
+ function observeConnectionState(peerState, iceState, started) {
685
+ if (peerState === "failed") return { phase: "failed", failure: { code: "peer_connection_failed", recoverable: true } };
686
+ if (iceState === "failed") return { phase: "failed", failure: { code: "ice_connection_failed", recoverable: true } };
687
+ if (peerState === "disconnected" || iceState === "disconnected") return { phase: "recovering", failure: null };
688
+ if (started && peerState === "connected" && (iceState === "connected" || iceState === "completed")) return { phase: "live", failure: null };
689
+ return null;
690
+ }
691
+ function connectionIsLive(connection) {
692
+ return connection.connectionState === "connected" && (connection.iceConnectionState === "connected" || connection.iceConnectionState === "completed");
693
+ }
694
+ function requireTransceiverMid(transceiver) {
695
+ if (transceiver.mid === null) throw new CloudflareSFUError("Browser did not assign a media section", "media_failed");
696
+ return transceiver.mid;
697
+ }
698
+ function safeStopTrack(track, onError) {
699
+ try {
700
+ track.stop();
701
+ } catch (error) {
702
+ onError(error);
703
+ }
704
+ }
705
+ function freezeSnapshot(snapshot) {
706
+ const failure = snapshot.failure ? Object.freeze(snapshot.failure) : null;
707
+ return Object.freeze({
708
+ ...snapshot,
709
+ connection: Object.freeze(snapshot.connection),
710
+ cursor: snapshot.cursor ? Object.freeze(snapshot.cursor) : null,
711
+ localTracks: Object.freeze(snapshot.localTracks),
712
+ remoteTracks: Object.freeze(snapshot.remoteTracks),
713
+ failure
714
+ });
715
+ }
716
+ function snapshotEqual(left, right) {
717
+ return snapshotMetadataEqual(left, right) && left.localTracks.every((publication, index) => publicationEqual(publication, right.localTracks[index])) && left.remoteTracks.every((publication, index) => remotePublicationEqual(publication, right.remoteTracks[index]));
718
+ }
719
+ function snapshotMetadataEqual(left, right) {
720
+ return connectionEqual(left.connection, right.connection) && cursorEqual(left.cursor, right.cursor) && failureEqual(left.failure, right.failure) && left.localTracks.length === right.localTracks.length && left.remoteTracks.length === right.remoteTracks.length;
721
+ }
722
+ function connectionEqual(left, right) {
723
+ return left.phase === right.phase && left.peerConnectionState === right.peerConnectionState && left.iceConnectionState === right.iceConnectionState;
724
+ }
725
+ function cursorEqual(left, right) {
726
+ return left?.incarnation === right?.incarnation && left?.sequence === right?.sequence;
727
+ }
728
+ function failureEqual(left, right) {
729
+ return left?.code === right?.code && left?.recoverable === right?.recoverable;
730
+ }
731
+ function publicationEqual(left, right) {
732
+ return right !== void 0 && left.source === right.source && left.enabled === right.enabled && left.publicationId === right.publicationId && left.track === right.track;
733
+ }
734
+ function remotePublicationEqual(left, right) {
735
+ return right !== void 0 && left.participantId === right.participantId && left.source === right.source && left.publicationId === right.publicationId && left.track === right.track;
736
+ }
737
+
738
+ // src/media/transport.ts
739
+ function createCloudflareSFUHTTPTransport(options) {
740
+ const fetch = options.fetch ?? globalThis.fetch;
741
+ const credential = requireCredential(options);
742
+ const base = options.apiBaseURL.replace(/\/$/, "");
743
+ const mediaPath = `${base}/v1/tenants/${encodeURIComponent(options.tenantId)}/spaces/${encodeURIComponent(options.spaceId)}/episodes/${encodeURIComponent(options.episodeId)}/participants/${encodeURIComponent(options.participantId)}/media/sfu`;
744
+ const request = async (path, init) => {
745
+ const token = await credential();
746
+ if (!token.trim()) throw new CloudflareSFUError("The media credential provider returned an empty token", "signaling_failed");
747
+ const response = await fetch(`${mediaPath}/${path}`, {
748
+ ...init,
749
+ headers: { Authorization: `Bearer ${token}`, "Content-Type": "application/json", ...init?.headers }
750
+ });
751
+ if (!response.ok) throw new CloudflareSFUError(`Chalk SFU signaling failed with HTTP ${response.status}`, "signaling_failed");
752
+ return await response.json();
753
+ };
754
+ return {
755
+ addTracks: async (input) => {
756
+ const response = await request("tracks", {
757
+ method: "POST",
758
+ body: JSON.stringify({ connection_id: input.connectionId, session_description: input.sessionDescription, tracks: input.tracks })
759
+ });
760
+ return {
761
+ ...response,
762
+ tracks: response.tracks?.map(({ publication_id, ...track }) => ({ ...track, publicationId: publication_id }))
763
+ };
764
+ },
765
+ closeTracks: async (input) => request("tracks/close", {
766
+ method: "PUT",
767
+ body: JSON.stringify({
768
+ connection_id: input.connectionId,
769
+ session_description: input.sessionDescription,
770
+ tracks: input.tracks.map((track) => ({ mid: track.mid, source: track.source, publication_id: track.publicationId })),
771
+ force: input.force
772
+ })
773
+ }),
774
+ renegotiate: async (input) => {
775
+ await request("renegotiate", {
776
+ method: "POST",
777
+ body: JSON.stringify({ connection_id: input.connectionId, session_description: input.sessionDescription })
778
+ });
779
+ },
780
+ listPublications: async () => {
781
+ const response = await request("publications");
782
+ return {
783
+ incarnation: response.incarnation,
784
+ sequence: response.sequence,
785
+ publications: response.publications.map((publication) => ({
786
+ participantId: publication.participant_id ?? publication.participant_session_id ?? "",
787
+ source: publication.source,
788
+ publicationId: publication.publication_id
789
+ }))
790
+ };
791
+ }
792
+ };
793
+ }
794
+ function requireCredential(options) {
795
+ if (options.credential) return options.credential;
796
+ if (options.bearerToken !== void 0) return () => options.bearerToken ?? "";
797
+ throw new CloudflareSFUError("A media credential provider is required", "signaling_failed");
798
+ }
799
+
800
+ export {
801
+ CloudflareSFUError,
802
+ parseCloudflareSFUPublicationID,
803
+ requireDescription,
804
+ requireSFUDescription,
805
+ validatePublicationSnapshot,
806
+ comparePublicationCursor,
807
+ publicationKey,
808
+ waitFor,
809
+ CloudflareSFUClient,
810
+ createCloudflareSFUHTTPTransport
811
+ };