tempest-react-sdk 0.56.0 → 0.58.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 (337) hide show
  1. package/bin/lib/icons/generate.test.mjs +4 -4
  2. package/dist/br/geocode.cjs +1 -1
  3. package/dist/br/geocode.cjs.map +1 -1
  4. package/dist/br/geocode.js +1 -1
  5. package/dist/br/geocode.js.map +1 -1
  6. package/dist/capture/use-barcode-scanner.cjs +1 -1
  7. package/dist/capture/use-barcode-scanner.cjs.map +1 -1
  8. package/dist/capture/use-barcode-scanner.js +1 -1
  9. package/dist/capture/use-barcode-scanner.js.map +1 -1
  10. package/dist/components/AIChat/AIChat.cjs +1 -1
  11. package/dist/components/AIChat/AIChat.cjs.map +1 -1
  12. package/dist/components/AIChat/AIChat.js +1 -1
  13. package/dist/components/AIChat/AIChat.js.map +1 -1
  14. package/dist/components/AIChat/AIChatTurn.cjs +1 -1
  15. package/dist/components/AIChat/AIChatTurn.cjs.map +1 -1
  16. package/dist/components/AIChat/AIChatTurn.js +1 -1
  17. package/dist/components/AIChat/AIChatTurn.js.map +1 -1
  18. package/dist/components/AppBar/AppBar.module.cjs.map +1 -1
  19. package/dist/components/AppBar/AppBar.module.js.map +1 -1
  20. package/dist/components/AppBar/use-sticky-body-warning.cjs +1 -1
  21. package/dist/components/AppBar/use-sticky-body-warning.cjs.map +1 -1
  22. package/dist/components/AppBar/use-sticky-body-warning.js +1 -1
  23. package/dist/components/AppBar/use-sticky-body-warning.js.map +1 -1
  24. package/dist/components/AudioPlayer/AudioPlayer.cjs +1 -1
  25. package/dist/components/AudioPlayer/AudioPlayer.cjs.map +1 -1
  26. package/dist/components/AudioPlayer/AudioPlayer.js +2 -2
  27. package/dist/components/AudioPlayer/AudioPlayer.js.map +1 -1
  28. package/dist/components/AudioRecorder/AudioRecorder.cjs +1 -1
  29. package/dist/components/AudioRecorder/AudioRecorder.cjs.map +1 -1
  30. package/dist/components/AudioRecorder/AudioRecorder.js +1 -1
  31. package/dist/components/AudioRecorder/AudioRecorder.js.map +1 -1
  32. package/dist/components/Chat/Chat.cjs +1 -1
  33. package/dist/components/Chat/Chat.cjs.map +1 -1
  34. package/dist/components/Chat/Chat.js +1 -1
  35. package/dist/components/Chat/Chat.js.map +1 -1
  36. package/dist/components/FilterBar/FilterBar.cjs +1 -1
  37. package/dist/components/FilterBar/FilterBar.cjs.map +1 -1
  38. package/dist/components/FilterBar/FilterBar.js +1 -1
  39. package/dist/components/FilterBar/FilterBar.js.map +1 -1
  40. package/dist/components/FilterBar/filter-apply.cjs +1 -1
  41. package/dist/components/FilterBar/filter-apply.cjs.map +1 -1
  42. package/dist/components/FilterBar/filter-apply.js +1 -1
  43. package/dist/components/FilterBar/filter-apply.js.map +1 -1
  44. package/dist/components/ImageCropper/ImageCropper.cjs +1 -1
  45. package/dist/components/ImageCropper/ImageCropper.cjs.map +1 -1
  46. package/dist/components/ImageCropper/ImageCropper.js +1 -1
  47. package/dist/components/ImageCropper/ImageCropper.js.map +1 -1
  48. package/dist/components/Scheduler/Scheduler.module.cjs.map +1 -1
  49. package/dist/components/Scheduler/Scheduler.module.js.map +1 -1
  50. package/dist/components/VideoPlayer/VideoPlayer.cjs +1 -1
  51. package/dist/components/VideoPlayer/VideoPlayer.cjs.map +1 -1
  52. package/dist/components/VideoPlayer/VideoPlayer.js +2 -2
  53. package/dist/components/VideoPlayer/VideoPlayer.js.map +1 -1
  54. package/dist/components/VirtualTable/VirtualTable.cjs +1 -1
  55. package/dist/components/VirtualTable/VirtualTable.cjs.map +1 -1
  56. package/dist/components/VirtualTable/VirtualTable.js +1 -1
  57. package/dist/components/VirtualTable/VirtualTable.js.map +1 -1
  58. package/dist/feature-flags/growthbook-adapter.cjs +1 -1
  59. package/dist/feature-flags/growthbook-adapter.cjs.map +1 -1
  60. package/dist/feature-flags/growthbook-adapter.js +1 -1
  61. package/dist/feature-flags/growthbook-adapter.js.map +1 -1
  62. package/dist/hooks/use-focus-trap.cjs +1 -1
  63. package/dist/hooks/use-focus-trap.cjs.map +1 -1
  64. package/dist/hooks/use-focus-trap.js +1 -1
  65. package/dist/hooks/use-focus-trap.js.map +1 -1
  66. package/dist/hooks/use-push-to-talk.cjs +1 -1
  67. package/dist/hooks/use-push-to-talk.cjs.map +1 -1
  68. package/dist/hooks/use-push-to-talk.js +2 -2
  69. package/dist/hooks/use-push-to-talk.js.map +1 -1
  70. package/dist/http/resumable-upload.cjs +1 -1
  71. package/dist/http/resumable-upload.cjs.map +1 -1
  72. package/dist/http/resumable-upload.js +1 -1
  73. package/dist/http/resumable-upload.js.map +1 -1
  74. package/dist/icons/generated/aliases.cjs +1 -1
  75. package/dist/icons/generated/aliases.cjs.map +1 -1
  76. package/dist/icons/generated/aliases.js +1 -0
  77. package/dist/icons/generated/aliases.js.map +1 -1
  78. package/dist/icons/generated/icon-names.cjs +1 -1
  79. package/dist/icons/generated/icon-names.cjs.map +1 -1
  80. package/dist/icons/generated/icon-names.js +1 -1
  81. package/dist/icons/generated/icon-names.js.map +1 -1
  82. package/dist/icons/generated/loaders.cjs +1 -1
  83. package/dist/icons/generated/loaders.cjs.map +1 -1
  84. package/dist/icons/generated/loaders.js +48 -42
  85. package/dist/icons/generated/loaders.js.map +1 -1
  86. package/dist/icons/generated/shard-02.cjs +1 -1
  87. package/dist/icons/generated/shard-02.cjs.map +1 -1
  88. package/dist/icons/generated/shard-02.js +20 -20
  89. package/dist/icons/generated/shard-02.js.map +1 -1
  90. package/dist/icons/generated/shard-03.cjs +1 -1
  91. package/dist/icons/generated/shard-03.cjs.map +1 -1
  92. package/dist/icons/generated/shard-03.js +41 -41
  93. package/dist/icons/generated/shard-03.js.map +1 -1
  94. package/dist/icons/generated/shard-04.cjs +1 -1
  95. package/dist/icons/generated/shard-04.cjs.map +1 -1
  96. package/dist/icons/generated/shard-04.js +41 -41
  97. package/dist/icons/generated/shard-04.js.map +1 -1
  98. package/dist/icons/generated/shard-05.cjs +1 -1
  99. package/dist/icons/generated/shard-05.cjs.map +1 -1
  100. package/dist/icons/generated/shard-05.js +41 -41
  101. package/dist/icons/generated/shard-05.js.map +1 -1
  102. package/dist/icons/generated/shard-06.cjs +1 -1
  103. package/dist/icons/generated/shard-06.cjs.map +1 -1
  104. package/dist/icons/generated/shard-06.js +41 -41
  105. package/dist/icons/generated/shard-06.js.map +1 -1
  106. package/dist/icons/generated/shard-07.cjs +1 -1
  107. package/dist/icons/generated/shard-07.cjs.map +1 -1
  108. package/dist/icons/generated/shard-07.js +41 -41
  109. package/dist/icons/generated/shard-07.js.map +1 -1
  110. package/dist/icons/generated/shard-08.cjs +1 -1
  111. package/dist/icons/generated/shard-08.cjs.map +1 -1
  112. package/dist/icons/generated/shard-08.js +41 -41
  113. package/dist/icons/generated/shard-08.js.map +1 -1
  114. package/dist/icons/generated/shard-09.cjs +1 -1
  115. package/dist/icons/generated/shard-09.cjs.map +1 -1
  116. package/dist/icons/generated/shard-09.js +41 -41
  117. package/dist/icons/generated/shard-09.js.map +1 -1
  118. package/dist/icons/generated/shard-10.cjs +1 -1
  119. package/dist/icons/generated/shard-10.cjs.map +1 -1
  120. package/dist/icons/generated/shard-10.js +41 -41
  121. package/dist/icons/generated/shard-10.js.map +1 -1
  122. package/dist/icons/generated/shard-11.cjs +1 -1
  123. package/dist/icons/generated/shard-11.cjs.map +1 -1
  124. package/dist/icons/generated/shard-11.js +41 -41
  125. package/dist/icons/generated/shard-11.js.map +1 -1
  126. package/dist/icons/generated/shard-12.cjs +1 -1
  127. package/dist/icons/generated/shard-12.cjs.map +1 -1
  128. package/dist/icons/generated/shard-12.js +41 -41
  129. package/dist/icons/generated/shard-12.js.map +1 -1
  130. package/dist/icons/generated/shard-13.cjs +1 -1
  131. package/dist/icons/generated/shard-13.cjs.map +1 -1
  132. package/dist/icons/generated/shard-13.js +41 -41
  133. package/dist/icons/generated/shard-13.js.map +1 -1
  134. package/dist/icons/generated/shard-14.cjs +1 -1
  135. package/dist/icons/generated/shard-14.cjs.map +1 -1
  136. package/dist/icons/generated/shard-14.js +41 -41
  137. package/dist/icons/generated/shard-14.js.map +1 -1
  138. package/dist/icons/generated/shard-15.cjs +1 -1
  139. package/dist/icons/generated/shard-15.cjs.map +1 -1
  140. package/dist/icons/generated/shard-15.js +41 -41
  141. package/dist/icons/generated/shard-15.js.map +1 -1
  142. package/dist/icons/generated/shard-16.cjs +1 -1
  143. package/dist/icons/generated/shard-16.cjs.map +1 -1
  144. package/dist/icons/generated/shard-16.js +41 -41
  145. package/dist/icons/generated/shard-16.js.map +1 -1
  146. package/dist/icons/generated/shard-17.cjs +1 -1
  147. package/dist/icons/generated/shard-17.cjs.map +1 -1
  148. package/dist/icons/generated/shard-17.js +41 -41
  149. package/dist/icons/generated/shard-17.js.map +1 -1
  150. package/dist/icons/generated/shard-18.cjs +1 -1
  151. package/dist/icons/generated/shard-18.cjs.map +1 -1
  152. package/dist/icons/generated/shard-18.js +41 -41
  153. package/dist/icons/generated/shard-18.js.map +1 -1
  154. package/dist/icons/generated/shard-19.cjs +1 -1
  155. package/dist/icons/generated/shard-19.cjs.map +1 -1
  156. package/dist/icons/generated/shard-19.js +41 -41
  157. package/dist/icons/generated/shard-19.js.map +1 -1
  158. package/dist/icons/generated/shard-20.cjs +1 -1
  159. package/dist/icons/generated/shard-20.cjs.map +1 -1
  160. package/dist/icons/generated/shard-20.js +41 -41
  161. package/dist/icons/generated/shard-20.js.map +1 -1
  162. package/dist/icons/generated/shard-21.cjs +1 -1
  163. package/dist/icons/generated/shard-21.cjs.map +1 -1
  164. package/dist/icons/generated/shard-21.js +41 -41
  165. package/dist/icons/generated/shard-21.js.map +1 -1
  166. package/dist/icons/generated/shard-22.cjs +1 -1
  167. package/dist/icons/generated/shard-22.cjs.map +1 -1
  168. package/dist/icons/generated/shard-22.js +41 -41
  169. package/dist/icons/generated/shard-22.js.map +1 -1
  170. package/dist/icons/generated/shard-23.cjs +1 -1
  171. package/dist/icons/generated/shard-23.cjs.map +1 -1
  172. package/dist/icons/generated/shard-23.js +41 -41
  173. package/dist/icons/generated/shard-23.js.map +1 -1
  174. package/dist/icons/generated/shard-24.cjs +1 -1
  175. package/dist/icons/generated/shard-24.cjs.map +1 -1
  176. package/dist/icons/generated/shard-24.js +41 -41
  177. package/dist/icons/generated/shard-24.js.map +1 -1
  178. package/dist/icons/generated/shard-25.cjs +1 -1
  179. package/dist/icons/generated/shard-25.cjs.map +1 -1
  180. package/dist/icons/generated/shard-25.js +41 -41
  181. package/dist/icons/generated/shard-25.js.map +1 -1
  182. package/dist/icons/generated/shard-26.cjs +1 -1
  183. package/dist/icons/generated/shard-26.cjs.map +1 -1
  184. package/dist/icons/generated/shard-26.js +41 -41
  185. package/dist/icons/generated/shard-26.js.map +1 -1
  186. package/dist/icons/generated/shard-27.cjs +1 -1
  187. package/dist/icons/generated/shard-27.cjs.map +1 -1
  188. package/dist/icons/generated/shard-27.js +41 -41
  189. package/dist/icons/generated/shard-27.js.map +1 -1
  190. package/dist/icons/generated/shard-28.cjs +1 -1
  191. package/dist/icons/generated/shard-28.cjs.map +1 -1
  192. package/dist/icons/generated/shard-28.js +41 -41
  193. package/dist/icons/generated/shard-28.js.map +1 -1
  194. package/dist/icons/generated/shard-29.cjs +1 -1
  195. package/dist/icons/generated/shard-29.cjs.map +1 -1
  196. package/dist/icons/generated/shard-29.js +41 -41
  197. package/dist/icons/generated/shard-29.js.map +1 -1
  198. package/dist/icons/generated/shard-30.cjs +1 -1
  199. package/dist/icons/generated/shard-30.cjs.map +1 -1
  200. package/dist/icons/generated/shard-30.js +41 -41
  201. package/dist/icons/generated/shard-30.js.map +1 -1
  202. package/dist/icons/generated/shard-31.cjs +1 -1
  203. package/dist/icons/generated/shard-31.cjs.map +1 -1
  204. package/dist/icons/generated/shard-31.js +41 -41
  205. package/dist/icons/generated/shard-31.js.map +1 -1
  206. package/dist/icons/generated/shard-32.cjs +1 -1
  207. package/dist/icons/generated/shard-32.cjs.map +1 -1
  208. package/dist/icons/generated/shard-32.js +41 -41
  209. package/dist/icons/generated/shard-32.js.map +1 -1
  210. package/dist/icons/generated/shard-33.cjs +1 -1
  211. package/dist/icons/generated/shard-33.cjs.map +1 -1
  212. package/dist/icons/generated/shard-33.js +41 -41
  213. package/dist/icons/generated/shard-33.js.map +1 -1
  214. package/dist/icons/generated/shard-34.cjs +1 -1
  215. package/dist/icons/generated/shard-34.cjs.map +1 -1
  216. package/dist/icons/generated/shard-34.js +41 -41
  217. package/dist/icons/generated/shard-34.js.map +1 -1
  218. package/dist/icons/generated/shard-35.cjs +1 -1
  219. package/dist/icons/generated/shard-35.cjs.map +1 -1
  220. package/dist/icons/generated/shard-35.js +41 -41
  221. package/dist/icons/generated/shard-35.js.map +1 -1
  222. package/dist/icons/generated/shard-36.cjs +1 -1
  223. package/dist/icons/generated/shard-36.cjs.map +1 -1
  224. package/dist/icons/generated/shard-36.js +41 -41
  225. package/dist/icons/generated/shard-36.js.map +1 -1
  226. package/dist/icons/generated/shard-37.cjs +1 -1
  227. package/dist/icons/generated/shard-37.cjs.map +1 -1
  228. package/dist/icons/generated/shard-37.js +41 -41
  229. package/dist/icons/generated/shard-37.js.map +1 -1
  230. package/dist/icons/generated/shard-38.cjs +1 -1
  231. package/dist/icons/generated/shard-38.cjs.map +1 -1
  232. package/dist/icons/generated/shard-38.js +41 -41
  233. package/dist/icons/generated/shard-38.js.map +1 -1
  234. package/dist/icons/generated/shard-39.cjs +1 -1
  235. package/dist/icons/generated/shard-39.cjs.map +1 -1
  236. package/dist/icons/generated/shard-39.js +41 -41
  237. package/dist/icons/generated/shard-39.js.map +1 -1
  238. package/dist/icons/generated/shard-40.cjs +1 -1
  239. package/dist/icons/generated/shard-40.cjs.map +1 -1
  240. package/dist/icons/generated/shard-40.js +41 -41
  241. package/dist/icons/generated/shard-40.js.map +1 -1
  242. package/dist/icons/generated/shard-41.cjs +1 -1
  243. package/dist/icons/generated/shard-41.cjs.map +1 -1
  244. package/dist/icons/generated/shard-41.js +41 -41
  245. package/dist/icons/generated/shard-41.js.map +1 -1
  246. package/dist/icons/generated/shard-42.cjs +1 -1
  247. package/dist/icons/generated/shard-42.cjs.map +1 -1
  248. package/dist/icons/generated/shard-42.js +41 -41
  249. package/dist/icons/generated/shard-42.js.map +1 -1
  250. package/dist/icons/generated/shard-43.cjs +1 -1
  251. package/dist/icons/generated/shard-43.cjs.map +1 -1
  252. package/dist/icons/generated/shard-43.js +41 -41
  253. package/dist/icons/generated/shard-43.js.map +1 -1
  254. package/dist/icons/generated/shard-44.cjs +1 -1
  255. package/dist/icons/generated/shard-44.cjs.map +1 -1
  256. package/dist/icons/generated/shard-44.js +43 -10
  257. package/dist/icons/generated/shard-44.js.map +1 -1
  258. package/dist/icons/generated/shard-45.cjs +2 -0
  259. package/dist/icons/generated/shard-45.cjs.map +1 -0
  260. package/dist/icons/generated/shard-45.js +15 -0
  261. package/dist/icons/generated/shard-45.js.map +1 -0
  262. package/dist/icons/material-symbols.cjs +1 -1
  263. package/dist/icons/material-symbols.cjs.map +1 -1
  264. package/dist/icons/material-symbols.js +3 -3
  265. package/dist/icons/material-symbols.js.map +1 -1
  266. package/dist/icons.d.ts +4 -4
  267. package/dist/query/foreign-client-warning.cjs +1 -1
  268. package/dist/query/foreign-client-warning.cjs.map +1 -1
  269. package/dist/query/foreign-client-warning.js +1 -1
  270. package/dist/query/foreign-client-warning.js.map +1 -1
  271. package/dist/styles/AppBar.css +1 -1
  272. package/dist/styles/Scheduler.css +2 -2
  273. package/dist/styles/core.css +1 -1
  274. package/dist/styles/layout.css +2 -2
  275. package/dist/styles/navigation.css +1 -1
  276. package/dist/styles.css +1 -1
  277. package/dist/tempest-react-sdk.cjs +1 -1
  278. package/dist/tempest-react-sdk.d.ts +175 -12
  279. package/dist/tempest-react-sdk.js +33 -33
  280. package/dist/utils/json-frame.cjs +1 -1
  281. package/dist/utils/json-frame.cjs.map +1 -1
  282. package/dist/utils/json-frame.js +1 -1
  283. package/dist/utils/json-frame.js.map +1 -1
  284. package/dist/vision/core/providers.cjs +1 -1
  285. package/dist/vision/core/providers.cjs.map +1 -1
  286. package/dist/vision/core/providers.js +22 -3
  287. package/dist/vision/core/providers.js.map +1 -1
  288. package/dist/vision/core/session.cjs +1 -1
  289. package/dist/vision/core/session.cjs.map +1 -1
  290. package/dist/vision/core/session.js +22 -15
  291. package/dist/vision/core/session.js.map +1 -1
  292. package/dist/vision/fusion.cjs.map +1 -1
  293. package/dist/vision/fusion.js.map +1 -1
  294. package/dist/vision/index.cjs +1 -1
  295. package/dist/vision/index.cjs.map +1 -1
  296. package/dist/vision/index.js +17 -16
  297. package/dist/vision/index.js.map +1 -1
  298. package/dist/vision/normalization.cjs +2 -0
  299. package/dist/vision/normalization.cjs.map +1 -0
  300. package/dist/vision/normalization.js +49 -0
  301. package/dist/vision/normalization.js.map +1 -0
  302. package/dist/vision/results.cjs +1 -1
  303. package/dist/vision/results.cjs.map +1 -1
  304. package/dist/vision/results.js +8 -2
  305. package/dist/vision/results.js.map +1 -1
  306. package/dist/vision/tasks/classifier.cjs +1 -1
  307. package/dist/vision/tasks/classifier.cjs.map +1 -1
  308. package/dist/vision/tasks/classifier.js +36 -32
  309. package/dist/vision/tasks/classifier.js.map +1 -1
  310. package/dist/vision.cjs +1 -1
  311. package/dist/vision.d.ts +218 -13
  312. package/dist/vision.js +19 -18
  313. package/dist/vite/tempest-pwa-manifest.cjs +1 -1
  314. package/dist/vite/tempest-pwa-manifest.cjs.map +1 -1
  315. package/dist/vite/tempest-pwa-manifest.js +1 -1
  316. package/dist/vite/tempest-pwa-manifest.js.map +1 -1
  317. package/dist/webrtc/link-stats.cjs +1 -1
  318. package/dist/webrtc/link-stats.cjs.map +1 -1
  319. package/dist/webrtc/link-stats.js +134 -44
  320. package/dist/webrtc/link-stats.js.map +1 -1
  321. package/dist/webrtc/mesh-quality.cjs +1 -1
  322. package/dist/webrtc/mesh-quality.cjs.map +1 -1
  323. package/dist/webrtc/mesh-quality.js +8 -2
  324. package/dist/webrtc/mesh-quality.js.map +1 -1
  325. package/dist/webrtc/peer-link.cjs +1 -1
  326. package/dist/webrtc/peer-link.cjs.map +1 -1
  327. package/dist/webrtc/peer-link.js +1 -1
  328. package/dist/webrtc/peer-link.js.map +1 -1
  329. package/dist/webrtc/peer-mesh.cjs +1 -1
  330. package/dist/webrtc/peer-mesh.cjs.map +1 -1
  331. package/dist/webrtc/peer-mesh.js +86 -64
  332. package/dist/webrtc/peer-mesh.js.map +1 -1
  333. package/dist/ws/create-web-socket.cjs +1 -1
  334. package/dist/ws/create-web-socket.cjs.map +1 -1
  335. package/dist/ws/create-web-socket.js +1 -1
  336. package/dist/ws/create-web-socket.js.map +1 -1
  337. package/package.json +1 -1
@@ -1,2 +1,2 @@
1
- const e=require("./mesh-quality.cjs"),t=require("./peer-link.cjs");function n(n){let{slots:r,send:i}=n,a=new Map,o=new Map,s=new Map,c=n.iceServers??[],l=n.setLocalDescription??((e,t)=>e.setLocalDescription(t)),u=n.quality??{},d=!1,f=()=>Object.fromEntries(r.map(e=>[e.name,null])),p=()=>{n.onPeers?.([...o.values()].map(e=>({...e})))},m=()=>{if(d)return;let e=[...a.values()].map(e=>e.pc.connectionState);e.some(e=>e===`connected`)?n.onState?.(`connected`):e.length>0?n.onState?.(`connecting`,`negotiating`):n.onState?.(`connected`,`alone`)},h=async t=>{u=t;let n=e.scaleForRoom(t,a.size),i=e.resolveDegradation(t,n);for(let t of a.values())await e.applyQualityToLink(t.transceivers,r,n,i)},g=async e=>{if(!e.makingOffer){e.makingOffer=!0;try{let t=await e.pc.createOffer();if(!t.sdp){n.onNotice?.(`empty_local_offer_sdp`);return}await l(e.pc,t),i({type:`offer`,to:e.peerId,sdp:e.pc.localDescription?.sdp??t.sdp})}catch{n.onNotice?.(`offer_failed`)}finally{e.makingOffer=!1}}},_=e=>e.streams?.[0]||(typeof MediaStream>`u`?null:new MediaStream([e.track])),v=(e,n)=>{let i=t.slotOf(n.transceiver,e,r),a=o.get(e.peerId);!i||!a||(a.streams={...a.streams,[i.name]:_(n)},n.track.onended=()=>{let t=o.get(e.peerId);t&&(t.streams={...t.streams,[i.name]:null},p())},p())},y=e=>{let t=a.get(e);t&&(t.pc.close(),a.delete(e)),o.delete(e),h(u),p(),m()},b=async(e,{offerer:n})=>{if(a.has(e))return;let l=t.createPeerConnection(c),u={peerId:e,pc:l,transceivers:[],pendingCandidates:[],remoteReady:!1,makingOffer:!1,isOfferer:n};a.set(e,u),o.set(e,{peerId:e,connection:l.connectionState,streams:o.get(e)?.streams??f()}),n&&(u.transceivers=r.map(e=>l.addTransceiver(e.kind,{direction:`sendrecv`})),await t.attachLocalTracks(u,r,s)),l.onicecandidate=t=>{i({type:`ice`,to:e,candidate:t.candidate?.candidate??null,sdpMid:t.candidate?.sdpMid??null,sdpMLineIndex:t.candidate?.sdpMLineIndex??null})},l.ontrack=e=>v(u,e),l.onconnectionstatechange=()=>{let t=o.get(e);if(t&&(t.connection=l.connectionState),l.connectionState===`failed`||l.connectionState===`closed`){y(e);return}p(),m()},l.onnegotiationneeded=()=>{u.isOfferer&&g(u)},p(),m(),n&&await g(u)},x=async(e,o)=>{a.has(e)||await b(e,{offerer:!1});let c=a.get(e);if(!c)return;await c.pc.setRemoteDescription({type:`offer`,sdp:o}),c.remoteReady=!0,await t.drainCandidates(c),c.isOfferer||(t.adoptTransceivers(c),await t.attachLocalTracks(c,r,s));let d=await c.pc.createAnswer();if(!d.sdp){n.onNotice?.(`empty_local_answer_sdp`);return}await l(c.pc,d),i({type:`answer`,to:e,sdp:c.pc.localDescription?.sdp??d.sdp}),await h(u)};return n.onState?.(`connecting`,`signaling`),{addPeer:b,removePeer:y,accept:async e=>{if(e.type===`offer`){await x(e.from,e.sdp);return}let n=a.get(e.from);if(n){if(e.type===`answer`){await n.pc.setRemoteDescription({type:`answer`,sdp:e.sdp}),n.remoteReady=!0,await t.drainCandidates(n),await h(u);return}e.candidate!==null&&await t.acceptCandidate(n,{candidate:e.candidate,sdpMid:e.sdpMid??void 0,sdpMLineIndex:e.sdpMLineIndex??void 0})}},setLocalTrack:async(e,t)=>{s.set(e,t);let n=r.findIndex(t=>t.name===e);if(!(n<0)){for(let e of a.values()){let r=e.transceivers[n];if(r)try{await r.sender.replaceTrack(t)}catch{}}await h(u)}},applyQuality:h,setIceServers:e=>{c=e},stop:()=>{for(let e of a.values())e.pc.close();a.clear(),o.clear(),d=!0,p(),n.onState?.(`closed`)},get peers(){return[...o.values()].map(e=>({...e}))}}}exports.createPeerMesh=n;
1
+ const e=require("./link-stats.cjs"),t=require("./mesh-quality.cjs"),n=require("./peer-link.cjs");function r(r){let{slots:i,send:a}=r,o=new Map,s=new Map,c=new Map,l=r.iceServers??[],u=r.setLocalDescription??((e,t)=>e.setLocalDescription(t)),d=r.quality??{},f=!1,p=new Map,m=null,h=()=>Object.fromEntries(i.map(e=>[e.name,null])),g=()=>{r.onPeers?.([...s.values()].map(e=>({...e})))},_=async t=>{if(f)return;let n=!1;for(let r of[...o.values()]){if(r.pc.connectionState!==`connected`)continue;let i=p.get(r.peerId);i===void 0&&(i=e.createLinkStatsSampler(t.kind===void 0?{}:{kind:t.kind}),p.set(r.peerId,i));let a=await i.sample(r.pc),o=s.get(r.peerId);o!==void 0&&(o.stats=a,n=!0,t.onStats?.(r.peerId,a))}n&&g()},v=()=>{let e=r.stats;if(e===void 0)return;let t=!f&&o.size>0;if(t&&m===null){m=setInterval(()=>void _(e),e.intervalMs??2e3);return}!t&&m!==null&&(clearInterval(m),m=null)},y=()=>{if(f)return;let e=[...o.values()].map(e=>e.pc.connectionState);e.some(e=>e===`connected`)?r.onState?.(`connected`):e.length>0?r.onState?.(`connecting`,`negotiating`):r.onState?.(`connected`,`alone`)},b=async e=>{d=e;let n=t.scaleForRoom(e,o.size),r=t.resolveDegradation(e,n);for(let e of o.values())await t.applyQualityToLink(e.transceivers,i,n,r)},x=async e=>{if(!e.makingOffer){e.makingOffer=!0;try{let t=await e.pc.createOffer();if(!t.sdp){r.onNotice?.(`empty_local_offer_sdp`);return}await u(e.pc,t),a({type:`offer`,to:e.peerId,sdp:e.pc.localDescription?.sdp??t.sdp})}catch{r.onNotice?.(`offer_failed`)}finally{e.makingOffer=!1}}},S=e=>e.streams?.[0]||(typeof MediaStream>`u`?null:new MediaStream([e.track])),C=(e,t)=>{let r=n.slotOf(t.transceiver,e,i),a=s.get(e.peerId);r&&a&&(a.streams={...a.streams,[r.name]:S(t)},t.track.onended=()=>{let t=s.get(e.peerId);t&&(t.streams={...t.streams,[r.name]:null},g())},g())},w=e=>{let t=o.get(e);t&&(t.pc.close(),o.delete(e)),s.delete(e),p.delete(e),b(d),v(),g(),y()},T=async(e,{offerer:t})=>{if(o.has(e))return;let r=n.createPeerConnection(l),u={peerId:e,pc:r,transceivers:[],pendingCandidates:[],remoteReady:!1,makingOffer:!1,isOfferer:t};o.set(e,u),s.set(e,{peerId:e,connection:r.connectionState,streams:s.get(e)?.streams??h()}),t&&(u.transceivers=i.map(e=>r.addTransceiver(e.kind,{direction:`sendrecv`})),await n.attachLocalTracks(u,i,c)),r.onicecandidate=t=>{a({type:`ice`,to:e,candidate:t.candidate?.candidate??null,sdpMid:t.candidate?.sdpMid??null,sdpMLineIndex:t.candidate?.sdpMLineIndex??null})},r.ontrack=e=>C(u,e),r.onconnectionstatechange=()=>{let t=s.get(e);if(t&&(t.connection=r.connectionState),r.connectionState===`failed`||r.connectionState===`closed`){w(e);return}g(),y()},r.onnegotiationneeded=()=>{u.isOfferer&&x(u)},v(),g(),y(),t&&await x(u)},E=async(e,t)=>{o.has(e)||await T(e,{offerer:!1});let s=o.get(e);if(!s)return;await s.pc.setRemoteDescription({type:`offer`,sdp:t}),s.remoteReady=!0,await n.drainCandidates(s),s.isOfferer||(n.adoptTransceivers(s),await n.attachLocalTracks(s,i,c));let l=await s.pc.createAnswer();if(!l.sdp){r.onNotice?.(`empty_local_answer_sdp`);return}await u(s.pc,l),a({type:`answer`,to:e,sdp:s.pc.localDescription?.sdp??l.sdp}),await b(d)};return r.onState?.(`connecting`,`signaling`),{addPeer:T,removePeer:w,accept:async e=>{if(e.type===`offer`){await E(e.from,e.sdp);return}let t=o.get(e.from);if(t){if(e.type===`answer`){await t.pc.setRemoteDescription({type:`answer`,sdp:e.sdp}),t.remoteReady=!0,await n.drainCandidates(t),await b(d);return}e.candidate!==null&&await n.acceptCandidate(t,{candidate:e.candidate,sdpMid:e.sdpMid??void 0,sdpMLineIndex:e.sdpMLineIndex??void 0})}},setLocalTrack:async(e,t)=>{c.set(e,t);let n=i.findIndex(t=>t.name===e);if(!(n<0)){for(let e of o.values()){let r=e.transceivers[n];if(r)try{await r.sender.replaceTrack(t)}catch{}}await b(d)}},applyQuality:b,setIceServers:e=>{l=e},stop:()=>{for(let e of o.values())e.pc.close();o.clear(),s.clear(),p.clear(),f=!0,v(),g(),r.onState?.(`closed`)},getConnection:e=>o.get(e)?.pc??null,get peers(){return[...s.values()].map(e=>({...e}))}}}exports.createPeerMesh=r;
2
2
  //# sourceMappingURL=peer-mesh.cjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"peer-mesh.cjs","names":[],"sources":["../../src/webrtc/peer-mesh.ts"],"sourcesContent":["/**\n * @tempest-limits file-lines — the mesh is one object's lifetime: a link's\n * transceivers, the offer it owes, the tracks published on it and the share of\n * the uplink it costs all change together, and every method here reads or writes\n * the same two maps. The pieces that *could* stand alone already do —\n * `peer-link.ts` for the per-connection mechanics and `mesh-quality.ts` for the\n * division — and what is left is the coordination between them.\n */\nimport { applyQualityToLink, resolveDegradation, scaleForRoom } from \"./mesh-quality\";\nimport {\n acceptCandidate,\n adoptTransceivers,\n attachLocalTracks,\n createPeerConnection,\n drainCandidates,\n slotOf,\n type PeerLink,\n} from \"./peer-link\";\nimport type { MeshMessage, MeshPeer, MeshQuality, PeerMeshOptions } from \"./mesh-types\";\n\n/** A running mesh. */\nexport interface PeerMesh {\n /**\n * Open a link to a peer.\n *\n * `offerer` decides which side sends the offer, and exactly one side of each\n * link must say `true`. Drive it from the arrival order your server already\n * imposes — the peer that joined **later** offers, the one already in the\n * room answers. That avoids glare by construction, without the rollback\n * dance of perfect negotiation and without depending on a browser\n * implementing argument-less `setLocalDescription`.\n */\n addPeer: (peerId: string, options: { offerer: boolean }) => Promise<void>;\n /** Close one link and give its share of the uplink back to the others. */\n removePeer: (peerId: string) => void;\n /** Feed the mesh an `offer`, `answer` or `ice` message from `from`. */\n accept: (message: MeshMessage & { from: string }) => Promise<void>;\n /**\n * Publish or retract one slot across every live link.\n *\n * `null` retracts: the sender keeps its negotiated transceiver and stops\n * producing, which is what mutes a microphone without disturbing the other\n * slots or the connection. Nothing renegotiates — the m-lines were allocated\n * before any track existed, which is what makes a mesh survive toggling: a\n * renegotiation per toggle is N-1 simultaneous offer/answer rounds every time\n * somebody unmutes.\n */\n setLocalTrack: (slot: string, track: MediaStreamTrack | null) => Promise<void>;\n /** Apply encoder limits, divided by the current size of the room. */\n applyQuality: (quality: MeshQuality) => Promise<void>;\n /** Replace the ICE servers used for links opened from now on. */\n setIceServers: (servers: RTCIceServer[]) => void;\n /** Close every link and report `closed`. */\n stop: () => void;\n /** The peers as the mesh currently sees them. */\n readonly peers: MeshPeer[];\n}\n\n/**\n * One `RTCPeerConnection` per peer, with the slots negotiated up front.\n *\n * The signalling protocol stays yours: this produces and accepts three message\n * shapes (`offer`, `answer`, `ice`) and leaves delivery, room membership and\n * identity to your server. What it owns is the part that is the same in every\n * mesh and wrong in most of them — see {@link PeerMesh} for the traps each\n * method exists to avoid.\n *\n * @param options - See {@link PeerMeshOptions}.\n * @returns The mesh.\n *\n * @example\n * const mesh = createPeerMesh({\n * slots: [\n * { name: \"mic\", kind: \"audio\" },\n * { name: \"cam\", kind: \"video\" },\n * ],\n * send: (message) => socket.send(message),\n * onPeers: setPeers,\n * });\n * socket.onMessage = (message) => void mesh.accept(message);\n * await mesh.addPeer(\"peer-2\", { offerer: true });\n * await mesh.setLocalTrack(\"mic\", micTrack);\n */\nexport function createPeerMesh(options: PeerMeshOptions): PeerMesh {\n const { slots, send } = options;\n const links = new Map<string, PeerLink>();\n const peers = new Map<string, MeshPeer>();\n const localTracks = new Map<string, MediaStreamTrack | null>();\n let iceServers = options.iceServers ?? [];\n const describe =\n options.setLocalDescription ??\n ((connection: RTCPeerConnection, description: RTCSessionDescriptionInit) =>\n connection.setLocalDescription(description));\n let quality: MeshQuality = options.quality ?? {};\n let stopped = false;\n\n const emptyStreams = (): Record<string, MediaStream | null> =>\n Object.fromEntries(slots.map((slot) => [slot.name, null]));\n\n const emitPeers = (): void => {\n options.onPeers?.([...peers.values()].map((peer) => ({ ...peer })));\n };\n\n /**\n * Collapse the per-link states into the one badge a call shows.\n *\n * No links at all is `connected`, not a failure: it means nobody else is in\n * the room yet, which is a legitimate state a call sits in for as long as the\n * first person is early.\n */\n const refreshState = (): void => {\n if (stopped) return;\n const states = [...links.values()].map((link) => link.pc.connectionState);\n if (states.some((state) => state === \"connected\")) options.onState?.(\"connected\");\n else if (states.length > 0) options.onState?.(\"connecting\", \"negotiating\");\n else options.onState?.(\"connected\", \"alone\");\n };\n\n const applyQuality = async (next: MeshQuality): Promise<void> => {\n quality = next;\n const effective = scaleForRoom(next, links.size);\n const degradation = resolveDegradation(next, effective);\n for (const link of links.values()) {\n await applyQualityToLink(link.transceivers, slots, effective, degradation);\n }\n };\n\n const makeOffer = async (link: PeerLink): Promise<void> => {\n if (link.makingOffer) return;\n link.makingOffer = true;\n try {\n const offer = await link.pc.createOffer();\n if (!offer.sdp) {\n options.onNotice?.(\"empty_local_offer_sdp\");\n return;\n }\n await describe(link.pc, offer);\n send({\n type: \"offer\",\n to: link.peerId,\n sdp: link.pc.localDescription?.sdp ?? offer.sdp,\n });\n } catch {\n options.onNotice?.(\"offer_failed\");\n } finally {\n link.makingOffer = false;\n }\n };\n\n /**\n * Wrap an inbound track in the stream the caller renders.\n *\n * The event's own stream is preferred: it is the one the sending side\n * grouped the track into, so a caller that reads `stream.id` sees what the\n * peer published rather than a wrapper invented here. Browsers only fill it\n * when the offer named a stream, hence the fallback — and the guard after\n * that is for a runtime with no `MediaStream` at all, where routing still\n * has to not throw.\n */\n const streamFor = (event: RTCTrackEvent): MediaStream | null => {\n const provided = event.streams?.[0];\n if (provided) return provided;\n return typeof MediaStream === \"undefined\" ? null : new MediaStream([event.track]);\n };\n\n const attachRemoteTrack = (link: PeerLink, event: RTCTrackEvent): void => {\n const slot = slotOf(event.transceiver, link, slots);\n const peer = peers.get(link.peerId);\n if (!slot || !peer) return;\n\n peer.streams = { ...peer.streams, [slot.name]: streamFor(event) };\n event.track.onended = (): void => {\n const current = peers.get(link.peerId);\n if (!current) return;\n current.streams = { ...current.streams, [slot.name]: null };\n emitPeers();\n };\n emitPeers();\n };\n\n const removePeer = (peerId: string): void => {\n const link = links.get(peerId);\n if (link) {\n link.pc.close();\n links.delete(peerId);\n }\n peers.delete(peerId);\n void applyQuality(quality);\n emitPeers();\n refreshState();\n };\n\n const addPeer = async (peerId: string, { offerer }: { offerer: boolean }): Promise<void> => {\n if (links.has(peerId)) return;\n\n const pc = createPeerConnection(iceServers);\n const link: PeerLink = {\n peerId,\n pc,\n transceivers: [],\n pendingCandidates: [],\n remoteReady: false,\n makingOffer: false,\n isOfferer: offerer,\n };\n links.set(peerId, link);\n peers.set(peerId, {\n peerId,\n connection: pc.connectionState,\n streams: peers.get(peerId)?.streams ?? emptyStreams(),\n });\n\n if (offerer) {\n link.transceivers = slots.map((slot) =>\n pc.addTransceiver(slot.kind, { direction: \"sendrecv\" }),\n );\n await attachLocalTracks(link, slots, localTracks);\n }\n\n pc.onicecandidate = (event): void => {\n send({\n type: \"ice\",\n to: peerId,\n candidate: event.candidate?.candidate ?? null,\n sdpMid: event.candidate?.sdpMid ?? null,\n sdpMLineIndex: event.candidate?.sdpMLineIndex ?? null,\n });\n };\n\n pc.ontrack = (event): void => attachRemoteTrack(link, event);\n\n pc.onconnectionstatechange = (): void => {\n const peer = peers.get(peerId);\n if (peer) peer.connection = pc.connectionState;\n if (pc.connectionState === \"failed\" || pc.connectionState === \"closed\") {\n removePeer(peerId);\n return;\n }\n emitPeers();\n refreshState();\n };\n\n pc.onnegotiationneeded = (): void => {\n if (link.isOfferer) void makeOffer(link);\n };\n\n emitPeers();\n refreshState();\n if (offerer) await makeOffer(link);\n };\n\n const acceptOffer = async (from: string, sdp: string): Promise<void> => {\n if (!links.has(from)) await addPeer(from, { offerer: false });\n const link = links.get(from);\n if (!link) return;\n\n await link.pc.setRemoteDescription({ type: \"offer\", sdp });\n link.remoteReady = true;\n await drainCandidates(link);\n\n if (!link.isOfferer) {\n adoptTransceivers(link);\n await attachLocalTracks(link, slots, localTracks);\n }\n\n const answer = await link.pc.createAnswer();\n if (!answer.sdp) {\n options.onNotice?.(\"empty_local_answer_sdp\");\n return;\n }\n await describe(link.pc, answer);\n send({ type: \"answer\", to: from, sdp: link.pc.localDescription?.sdp ?? answer.sdp });\n await applyQuality(quality);\n };\n\n const accept = async (message: MeshMessage & { from: string }): Promise<void> => {\n if (message.type === \"offer\") {\n await acceptOffer(message.from, message.sdp);\n return;\n }\n\n const link = links.get(message.from);\n if (!link) return;\n\n if (message.type === \"answer\") {\n await link.pc.setRemoteDescription({ type: \"answer\", sdp: message.sdp });\n link.remoteReady = true;\n await drainCandidates(link);\n await applyQuality(quality);\n return;\n }\n\n if (message.candidate === null) return;\n await acceptCandidate(link, {\n candidate: message.candidate,\n sdpMid: message.sdpMid ?? undefined,\n sdpMLineIndex: message.sdpMLineIndex ?? undefined,\n });\n };\n\n const setLocalTrack = async (slot: string, track: MediaStreamTrack | null): Promise<void> => {\n localTracks.set(slot, track);\n const index = slots.findIndex((entry) => entry.name === slot);\n if (index < 0) return;\n\n for (const link of links.values()) {\n const transceiver = link.transceivers[index];\n if (!transceiver) continue;\n try {\n await transceiver.sender.replaceTrack(track);\n } catch {\n /* the link is tearing down; its state handler will remove it */\n }\n }\n await applyQuality(quality);\n };\n\n const stop = (): void => {\n for (const link of links.values()) link.pc.close();\n links.clear();\n peers.clear();\n stopped = true;\n emitPeers();\n options.onState?.(\"closed\");\n };\n\n options.onState?.(\"connecting\", \"signaling\");\n\n return {\n addPeer,\n removePeer,\n accept,\n setLocalTrack,\n applyQuality,\n setIceServers: (servers): void => {\n iceServers = servers;\n },\n stop,\n get peers(): MeshPeer[] {\n return [...peers.values()].map((peer) => ({ ...peer }));\n },\n };\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"peer-mesh.cjs","names":[],"sources":["../../src/webrtc/peer-mesh.ts"],"sourcesContent":["/**\n * @tempest-limits file-lines — the mesh is one object's lifetime: a link's\n * transceivers, the offer it owes, the tracks published on it and the share of\n * the uplink it costs all change together, and every method here reads or writes\n * the same two maps. The pieces that *could* stand alone already do —\n * `peer-link.ts` for the per-connection mechanics and `mesh-quality.ts` for the\n * division — and what is left is the coordination between them.\n */\nimport { applyQualityToLink, resolveDegradation, scaleForRoom } from \"./mesh-quality\";\nimport {\n acceptCandidate,\n adoptTransceivers,\n attachLocalTracks,\n createPeerConnection,\n drainCandidates,\n slotOf,\n type PeerLink,\n} from \"./peer-link\";\nimport { createLinkStatsSampler, type LinkStatsSampler } from \"./link-stats\";\nimport type {\n MeshMessage,\n MeshPeer,\n MeshQuality,\n MeshStatsOptions,\n PeerMeshOptions,\n} from \"./mesh-types\";\n\n/** A running mesh. */\nexport interface PeerMesh {\n /**\n * Open a link to a peer.\n *\n * `offerer` decides which side sends the offer, and exactly one side of each\n * link must say `true`. Drive it from the arrival order your server already\n * imposes — the peer that joined **later** offers, the one already in the\n * room answers. That avoids glare by construction, without the rollback\n * dance of perfect negotiation and without depending on a browser\n * implementing argument-less `setLocalDescription`.\n */\n addPeer: (peerId: string, options: { offerer: boolean }) => Promise<void>;\n /** Close one link and give its share of the uplink back to the others. */\n removePeer: (peerId: string) => void;\n /** Feed the mesh an `offer`, `answer` or `ice` message from `from`. */\n accept: (message: MeshMessage & { from: string }) => Promise<void>;\n /**\n * Publish or retract one slot across every live link.\n *\n * `null` retracts: the sender keeps its negotiated transceiver and stops\n * producing, which is what mutes a microphone without disturbing the other\n * slots or the connection. Nothing renegotiates — the m-lines were allocated\n * before any track existed, which is what makes a mesh survive toggling: a\n * renegotiation per toggle is N-1 simultaneous offer/answer rounds every time\n * somebody unmutes.\n */\n setLocalTrack: (slot: string, track: MediaStreamTrack | null) => Promise<void>;\n /** Apply encoder limits, divided by the current size of the room. */\n applyQuality: (quality: MeshQuality) => Promise<void>;\n /** Replace the ICE servers used for links opened from now on. */\n setIceServers: (servers: RTCIceServer[]) => void;\n /** Close every link and report `closed`. */\n stop: () => void;\n /** The peers as the mesh currently sees them. */\n readonly peers: MeshPeer[];\n /**\n * The connection carrying one peer's link, or `null`.\n *\n * The escape hatch for everything this mesh does not model. `getStats()`,\n * `getSenders()`, an `RTCDataChannel`, an encoding tweak the `quality`\n * shape has no field for — all of it lives on the connection, and a mesh\n * that keeps it private turns adopting the mesh into losing a feature.\n *\n * {@link MeshPeer.connection} is the *state*, which is what a view needs;\n * this is the object, which is what a measurement needs. For the common\n * case of measuring, {@link PeerMeshOptions.stats} spares you the loop —\n * including the one-sampler-per-connection rule that a hand-rolled version\n * gets wrong.\n *\n * @param peerId - The id the peer was added under.\n * @returns The connection, or `null` when there is no link for that id.\n */\n getConnection: (peerId: string) => RTCPeerConnection | null;\n}\n\n/**\n * One `RTCPeerConnection` per peer, with the slots negotiated up front.\n *\n * The signalling protocol stays yours: this produces and accepts three message\n * shapes (`offer`, `answer`, `ice`) and leaves delivery, room membership and\n * identity to your server. What it owns is the part that is the same in every\n * mesh and wrong in most of them — see {@link PeerMesh} for the traps each\n * method exists to avoid.\n *\n * @param options - See {@link PeerMeshOptions}.\n * @returns The mesh.\n *\n * @example\n * const mesh = createPeerMesh({\n * slots: [\n * { name: \"mic\", kind: \"audio\" },\n * { name: \"cam\", kind: \"video\" },\n * ],\n * send: (message) => socket.send(message),\n * onPeers: setPeers,\n * });\n * socket.onMessage = (message) => void mesh.accept(message);\n * await mesh.addPeer(\"peer-2\", { offerer: true });\n * await mesh.setLocalTrack(\"mic\", micTrack);\n */\nexport function createPeerMesh(options: PeerMeshOptions): PeerMesh {\n const { slots, send } = options;\n const links = new Map<string, PeerLink>();\n const peers = new Map<string, MeshPeer>();\n const localTracks = new Map<string, MediaStreamTrack | null>();\n let iceServers = options.iceServers ?? [];\n const describe =\n options.setLocalDescription ??\n ((connection: RTCPeerConnection, description: RTCSessionDescriptionInit) =>\n connection.setLocalDescription(description));\n let quality: MeshQuality = options.quality ?? {};\n let stopped = false;\n const samplers = new Map<string, LinkStatsSampler>();\n let statsTimer: ReturnType<typeof setInterval> | null = null;\n\n const emptyStreams = (): Record<string, MediaStream | null> =>\n Object.fromEntries(slots.map((slot) => [slot.name, null]));\n\n const emitPeers = (): void => {\n options.onPeers?.([...peers.values()].map((peer) => ({ ...peer })));\n };\n\n /**\n * Sample every connected link once.\n *\n * Only `connected` links are asked. A link still gathering candidates has\n * no traffic to measure, and asking anyway spends a `getStats()` round trip\n * to learn that — per link, on every tick.\n *\n * `settings` arrives as an argument rather than being read from `options`\n * here: the timer is only ever armed past the check that they exist, so\n * reading them again would add a branch that nothing can reach.\n *\n * @param settings - The measuring options, already known to be present.\n */\n const sampleLinks = async (settings: MeshStatsOptions): Promise<void> => {\n if (stopped) return;\n let changed = false;\n\n for (const link of [...links.values()]) {\n if (link.pc.connectionState !== \"connected\") continue;\n let sampler = samplers.get(link.peerId);\n if (sampler === undefined) {\n sampler = createLinkStatsSampler(\n settings.kind === undefined ? {} : { kind: settings.kind },\n );\n samplers.set(link.peerId, sampler);\n }\n const stats = await sampler.sample(link.pc);\n const peer = peers.get(link.peerId);\n /*\n * Belt and braces, and the belt is the state check above: a peer\n * removed from inside `onStats` has its connection closed first, so\n * the next iteration skips it there rather than here. This catches\n * the entry disappearing any other way across the await.\n */\n if (peer === undefined) continue;\n peer.stats = stats;\n changed = true;\n settings.onStats?.(link.peerId, stats);\n }\n\n if (changed) emitPeers();\n };\n\n /**\n * Keep the timer alive exactly while there is something to measure.\n *\n * One timer for the whole mesh, not one per peer — and none at all in an\n * empty room, which is where a call sits for as long as the first person is\n * early.\n */\n const syncStatsTimer = (): void => {\n const settings = options.stats;\n if (settings === undefined) return;\n const wanted = !stopped && links.size > 0;\n if (wanted && statsTimer === null) {\n statsTimer = setInterval(() => void sampleLinks(settings), settings.intervalMs ?? 2000);\n return;\n }\n if (!wanted && statsTimer !== null) {\n clearInterval(statsTimer);\n statsTimer = null;\n }\n };\n\n /**\n * Collapse the per-link states into the one badge a call shows.\n *\n * No links at all is `connected`, not a failure: it means nobody else is in\n * the room yet, which is a legitimate state a call sits in for as long as the\n * first person is early.\n */\n const refreshState = (): void => {\n if (stopped) return;\n const states = [...links.values()].map((link) => link.pc.connectionState);\n if (states.some((state) => state === \"connected\")) options.onState?.(\"connected\");\n else if (states.length > 0) options.onState?.(\"connecting\", \"negotiating\");\n else options.onState?.(\"connected\", \"alone\");\n };\n\n const applyQuality = async (next: MeshQuality): Promise<void> => {\n quality = next;\n const effective = scaleForRoom(next, links.size);\n const degradation = resolveDegradation(next, effective);\n for (const link of links.values()) {\n await applyQualityToLink(link.transceivers, slots, effective, degradation);\n }\n };\n\n const makeOffer = async (link: PeerLink): Promise<void> => {\n if (link.makingOffer) return;\n link.makingOffer = true;\n try {\n const offer = await link.pc.createOffer();\n if (!offer.sdp) {\n options.onNotice?.(\"empty_local_offer_sdp\");\n return;\n }\n await describe(link.pc, offer);\n send({\n type: \"offer\",\n to: link.peerId,\n sdp: link.pc.localDescription?.sdp ?? offer.sdp,\n });\n } catch {\n options.onNotice?.(\"offer_failed\");\n } finally {\n link.makingOffer = false;\n }\n };\n\n /**\n * Wrap an inbound track in the stream the caller renders.\n *\n * The event's own stream is preferred: it is the one the sending side\n * grouped the track into, so a caller that reads `stream.id` sees what the\n * peer published rather than a wrapper invented here. Browsers only fill it\n * when the offer named a stream, hence the fallback — and the guard after\n * that is for a runtime with no `MediaStream` at all, where routing still\n * has to not throw.\n */\n const streamFor = (event: RTCTrackEvent): MediaStream | null => {\n const provided = event.streams?.[0];\n if (provided) return provided;\n return typeof MediaStream === \"undefined\" ? null : new MediaStream([event.track]);\n };\n\n const attachRemoteTrack = (link: PeerLink, event: RTCTrackEvent): void => {\n const slot = slotOf(event.transceiver, link, slots);\n const peer = peers.get(link.peerId);\n if (!slot || !peer) return;\n\n peer.streams = { ...peer.streams, [slot.name]: streamFor(event) };\n event.track.onended = (): void => {\n const current = peers.get(link.peerId);\n if (!current) return;\n current.streams = { ...current.streams, [slot.name]: null };\n emitPeers();\n };\n emitPeers();\n };\n\n const removePeer = (peerId: string): void => {\n const link = links.get(peerId);\n if (link) {\n link.pc.close();\n links.delete(peerId);\n }\n peers.delete(peerId);\n samplers.delete(peerId);\n void applyQuality(quality);\n syncStatsTimer();\n emitPeers();\n refreshState();\n };\n\n const addPeer = async (peerId: string, { offerer }: { offerer: boolean }): Promise<void> => {\n if (links.has(peerId)) return;\n\n const pc = createPeerConnection(iceServers);\n const link: PeerLink = {\n peerId,\n pc,\n transceivers: [],\n pendingCandidates: [],\n remoteReady: false,\n makingOffer: false,\n isOfferer: offerer,\n };\n links.set(peerId, link);\n peers.set(peerId, {\n peerId,\n connection: pc.connectionState,\n streams: peers.get(peerId)?.streams ?? emptyStreams(),\n });\n\n if (offerer) {\n link.transceivers = slots.map((slot) =>\n pc.addTransceiver(slot.kind, { direction: \"sendrecv\" }),\n );\n await attachLocalTracks(link, slots, localTracks);\n }\n\n pc.onicecandidate = (event): void => {\n send({\n type: \"ice\",\n to: peerId,\n candidate: event.candidate?.candidate ?? null,\n sdpMid: event.candidate?.sdpMid ?? null,\n sdpMLineIndex: event.candidate?.sdpMLineIndex ?? null,\n });\n };\n\n pc.ontrack = (event): void => attachRemoteTrack(link, event);\n\n pc.onconnectionstatechange = (): void => {\n const peer = peers.get(peerId);\n if (peer) peer.connection = pc.connectionState;\n if (pc.connectionState === \"failed\" || pc.connectionState === \"closed\") {\n removePeer(peerId);\n return;\n }\n emitPeers();\n refreshState();\n };\n\n pc.onnegotiationneeded = (): void => {\n if (link.isOfferer) void makeOffer(link);\n };\n\n syncStatsTimer();\n emitPeers();\n refreshState();\n if (offerer) await makeOffer(link);\n };\n\n const acceptOffer = async (from: string, sdp: string): Promise<void> => {\n if (!links.has(from)) await addPeer(from, { offerer: false });\n const link = links.get(from);\n if (!link) return;\n\n await link.pc.setRemoteDescription({ type: \"offer\", sdp });\n link.remoteReady = true;\n await drainCandidates(link);\n\n if (!link.isOfferer) {\n adoptTransceivers(link);\n await attachLocalTracks(link, slots, localTracks);\n }\n\n const answer = await link.pc.createAnswer();\n if (!answer.sdp) {\n options.onNotice?.(\"empty_local_answer_sdp\");\n return;\n }\n await describe(link.pc, answer);\n send({ type: \"answer\", to: from, sdp: link.pc.localDescription?.sdp ?? answer.sdp });\n await applyQuality(quality);\n };\n\n const accept = async (message: MeshMessage & { from: string }): Promise<void> => {\n if (message.type === \"offer\") {\n await acceptOffer(message.from, message.sdp);\n return;\n }\n\n const link = links.get(message.from);\n if (!link) return;\n\n if (message.type === \"answer\") {\n await link.pc.setRemoteDescription({ type: \"answer\", sdp: message.sdp });\n link.remoteReady = true;\n await drainCandidates(link);\n await applyQuality(quality);\n return;\n }\n\n if (message.candidate === null) return;\n await acceptCandidate(link, {\n candidate: message.candidate,\n sdpMid: message.sdpMid ?? undefined,\n sdpMLineIndex: message.sdpMLineIndex ?? undefined,\n });\n };\n\n const setLocalTrack = async (slot: string, track: MediaStreamTrack | null): Promise<void> => {\n localTracks.set(slot, track);\n const index = slots.findIndex((entry) => entry.name === slot);\n if (index < 0) return;\n\n for (const link of links.values()) {\n const transceiver = link.transceivers[index];\n if (!transceiver) continue;\n try {\n await transceiver.sender.replaceTrack(track);\n } catch {\n /* the link is tearing down; its state handler will remove it */\n }\n }\n await applyQuality(quality);\n };\n\n const stop = (): void => {\n for (const link of links.values()) link.pc.close();\n links.clear();\n peers.clear();\n samplers.clear();\n stopped = true;\n syncStatsTimer();\n emitPeers();\n options.onState?.(\"closed\");\n };\n\n options.onState?.(\"connecting\", \"signaling\");\n\n return {\n addPeer,\n removePeer,\n accept,\n setLocalTrack,\n applyQuality,\n setIceServers: (servers): void => {\n iceServers = servers;\n },\n stop,\n getConnection: (peerId: string): RTCPeerConnection | null => links.get(peerId)?.pc ?? null,\n get peers(): MeshPeer[] {\n return [...peers.values()].map((peer) => ({ ...peer }));\n },\n };\n}\n"],"mappings":"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"}
@@ -1,55 +1,76 @@
1
- import { applyQualityToLink as e, resolveDegradation as t, scaleForRoom as n } from "./mesh-quality.js";
2
- import { acceptCandidate as r, adoptTransceivers as i, attachLocalTracks as a, createPeerConnection as o, drainCandidates as s, slotOf as c } from "./peer-link.js";
1
+ import { createLinkStatsSampler as e } from "./link-stats.js";
2
+ import { applyQualityToLink as t, resolveDegradation as n, scaleForRoom as r } from "./mesh-quality.js";
3
+ import { acceptCandidate as i, adoptTransceivers as a, attachLocalTracks as o, createPeerConnection as s, drainCandidates as c, slotOf as l } from "./peer-link.js";
3
4
  //#region src/webrtc/peer-mesh.ts
4
- function l(l) {
5
- let { slots: u, send: d } = l, f = /* @__PURE__ */ new Map(), p = /* @__PURE__ */ new Map(), m = /* @__PURE__ */ new Map(), h = l.iceServers ?? [], g = l.setLocalDescription ?? ((e, t) => e.setLocalDescription(t)), _ = l.quality ?? {}, v = !1, y = () => Object.fromEntries(u.map((e) => [e.name, null])), b = () => {
6
- l.onPeers?.([...p.values()].map((e) => ({ ...e })));
7
- }, x = () => {
8
- if (v) return;
9
- let e = [...f.values()].map((e) => e.pc.connectionState);
10
- e.some((e) => e === "connected") ? l.onState?.("connected") : e.length > 0 ? l.onState?.("connecting", "negotiating") : l.onState?.("connected", "alone");
11
- }, S = async (r) => {
12
- _ = r;
13
- let i = n(r, f.size), a = t(r, i);
14
- for (let t of f.values()) await e(t.transceivers, u, i, a);
15
- }, C = async (e) => {
5
+ function u(u) {
6
+ let { slots: d, send: f } = u, p = /* @__PURE__ */ new Map(), m = /* @__PURE__ */ new Map(), h = /* @__PURE__ */ new Map(), g = u.iceServers ?? [], _ = u.setLocalDescription ?? ((e, t) => e.setLocalDescription(t)), v = u.quality ?? {}, y = !1, b = /* @__PURE__ */ new Map(), x = null, S = () => Object.fromEntries(d.map((e) => [e.name, null])), C = () => {
7
+ u.onPeers?.([...m.values()].map((e) => ({ ...e })));
8
+ }, w = async (t) => {
9
+ if (y) return;
10
+ let n = !1;
11
+ for (let r of [...p.values()]) {
12
+ if (r.pc.connectionState !== "connected") continue;
13
+ let i = b.get(r.peerId);
14
+ i === void 0 && (i = e(t.kind === void 0 ? {} : { kind: t.kind }), b.set(r.peerId, i));
15
+ let a = await i.sample(r.pc), o = m.get(r.peerId);
16
+ o !== void 0 && (o.stats = a, n = !0, t.onStats?.(r.peerId, a));
17
+ }
18
+ n && C();
19
+ }, T = () => {
20
+ let e = u.stats;
21
+ if (e === void 0) return;
22
+ let t = !y && p.size > 0;
23
+ if (t && x === null) {
24
+ x = setInterval(() => void w(e), e.intervalMs ?? 2e3);
25
+ return;
26
+ }
27
+ !t && x !== null && (clearInterval(x), x = null);
28
+ }, E = () => {
29
+ if (y) return;
30
+ let e = [...p.values()].map((e) => e.pc.connectionState);
31
+ e.some((e) => e === "connected") ? u.onState?.("connected") : e.length > 0 ? u.onState?.("connecting", "negotiating") : u.onState?.("connected", "alone");
32
+ }, D = async (e) => {
33
+ v = e;
34
+ let i = r(e, p.size), a = n(e, i);
35
+ for (let e of p.values()) await t(e.transceivers, d, i, a);
36
+ }, O = async (e) => {
16
37
  if (!e.makingOffer) {
17
38
  e.makingOffer = !0;
18
39
  try {
19
40
  let t = await e.pc.createOffer();
20
41
  if (!t.sdp) {
21
- l.onNotice?.("empty_local_offer_sdp");
42
+ u.onNotice?.("empty_local_offer_sdp");
22
43
  return;
23
44
  }
24
- await g(e.pc, t), d({
45
+ await _(e.pc, t), f({
25
46
  type: "offer",
26
47
  to: e.peerId,
27
48
  sdp: e.pc.localDescription?.sdp ?? t.sdp
28
49
  });
29
50
  } catch {
30
- l.onNotice?.("offer_failed");
51
+ u.onNotice?.("offer_failed");
31
52
  } finally {
32
53
  e.makingOffer = !1;
33
54
  }
34
55
  }
35
- }, w = (e) => e.streams?.[0] || (typeof MediaStream > "u" ? null : new MediaStream([e.track])), T = (e, t) => {
36
- let n = c(t.transceiver, e, u), r = p.get(e.peerId);
37
- !n || !r || (r.streams = {
56
+ }, k = (e) => e.streams?.[0] || (typeof MediaStream > "u" ? null : new MediaStream([e.track])), A = (e, t) => {
57
+ let n = l(t.transceiver, e, d), r = m.get(e.peerId);
58
+ n && r && (r.streams = {
38
59
  ...r.streams,
39
- [n.name]: w(t)
60
+ [n.name]: k(t)
40
61
  }, t.track.onended = () => {
41
- let t = p.get(e.peerId);
62
+ let t = m.get(e.peerId);
42
63
  t && (t.streams = {
43
64
  ...t.streams,
44
65
  [n.name]: null
45
- }, b());
46
- }, b());
47
- }, E = (e) => {
48
- let t = f.get(e);
49
- t && (t.pc.close(), f.delete(e)), p.delete(e), S(_), b(), x();
50
- }, D = async (e, { offerer: t }) => {
51
- if (f.has(e)) return;
52
- let n = o(h), r = {
66
+ }, C());
67
+ }, C());
68
+ }, j = (e) => {
69
+ let t = p.get(e);
70
+ t && (t.pc.close(), p.delete(e)), m.delete(e), b.delete(e), D(v), T(), C(), E();
71
+ }, M = async (e, { offerer: t }) => {
72
+ if (p.has(e)) return;
73
+ let n = s(g), r = {
53
74
  peerId: e,
54
75
  pc: n,
55
76
  transceivers: [],
@@ -58,65 +79,65 @@ function l(l) {
58
79
  makingOffer: !1,
59
80
  isOfferer: t
60
81
  };
61
- f.set(e, r), p.set(e, {
82
+ p.set(e, r), m.set(e, {
62
83
  peerId: e,
63
84
  connection: n.connectionState,
64
- streams: p.get(e)?.streams ?? y()
65
- }), t && (r.transceivers = u.map((e) => n.addTransceiver(e.kind, { direction: "sendrecv" })), await a(r, u, m)), n.onicecandidate = (t) => {
66
- d({
85
+ streams: m.get(e)?.streams ?? S()
86
+ }), t && (r.transceivers = d.map((e) => n.addTransceiver(e.kind, { direction: "sendrecv" })), await o(r, d, h)), n.onicecandidate = (t) => {
87
+ f({
67
88
  type: "ice",
68
89
  to: e,
69
90
  candidate: t.candidate?.candidate ?? null,
70
91
  sdpMid: t.candidate?.sdpMid ?? null,
71
92
  sdpMLineIndex: t.candidate?.sdpMLineIndex ?? null
72
93
  });
73
- }, n.ontrack = (e) => T(r, e), n.onconnectionstatechange = () => {
74
- let t = p.get(e);
94
+ }, n.ontrack = (e) => A(r, e), n.onconnectionstatechange = () => {
95
+ let t = m.get(e);
75
96
  if (t && (t.connection = n.connectionState), n.connectionState === "failed" || n.connectionState === "closed") {
76
- E(e);
97
+ j(e);
77
98
  return;
78
99
  }
79
- b(), x();
100
+ C(), E();
80
101
  }, n.onnegotiationneeded = () => {
81
- r.isOfferer && C(r);
82
- }, b(), x(), t && await C(r);
83
- }, O = async (e, t) => {
84
- f.has(e) || await D(e, { offerer: !1 });
85
- let n = f.get(e);
102
+ r.isOfferer && O(r);
103
+ }, T(), C(), E(), t && await O(r);
104
+ }, N = async (e, t) => {
105
+ p.has(e) || await M(e, { offerer: !1 });
106
+ let n = p.get(e);
86
107
  if (!n) return;
87
108
  await n.pc.setRemoteDescription({
88
109
  type: "offer",
89
110
  sdp: t
90
- }), n.remoteReady = !0, await s(n), n.isOfferer || (i(n), await a(n, u, m));
111
+ }), n.remoteReady = !0, await c(n), n.isOfferer || (a(n), await o(n, d, h));
91
112
  let r = await n.pc.createAnswer();
92
113
  if (!r.sdp) {
93
- l.onNotice?.("empty_local_answer_sdp");
114
+ u.onNotice?.("empty_local_answer_sdp");
94
115
  return;
95
116
  }
96
- await g(n.pc, r), d({
117
+ await _(n.pc, r), f({
97
118
  type: "answer",
98
119
  to: e,
99
120
  sdp: n.pc.localDescription?.sdp ?? r.sdp
100
- }), await S(_);
121
+ }), await D(v);
101
122
  };
102
- return l.onState?.("connecting", "signaling"), {
103
- addPeer: D,
104
- removePeer: E,
123
+ return u.onState?.("connecting", "signaling"), {
124
+ addPeer: M,
125
+ removePeer: j,
105
126
  accept: async (e) => {
106
127
  if (e.type === "offer") {
107
- await O(e.from, e.sdp);
128
+ await N(e.from, e.sdp);
108
129
  return;
109
130
  }
110
- let t = f.get(e.from);
131
+ let t = p.get(e.from);
111
132
  if (t) {
112
133
  if (e.type === "answer") {
113
134
  await t.pc.setRemoteDescription({
114
135
  type: "answer",
115
136
  sdp: e.sdp
116
- }), t.remoteReady = !0, await s(t), await S(_);
137
+ }), t.remoteReady = !0, await c(t), await D(v);
117
138
  return;
118
139
  }
119
- e.candidate !== null && await r(t, {
140
+ e.candidate !== null && await i(t, {
120
141
  candidate: e.candidate,
121
142
  sdpMid: e.sdpMid ?? void 0,
122
143
  sdpMLineIndex: e.sdpMLineIndex ?? void 0
@@ -124,32 +145,33 @@ function l(l) {
124
145
  }
125
146
  },
126
147
  setLocalTrack: async (e, t) => {
127
- m.set(e, t);
128
- let n = u.findIndex((t) => t.name === e);
148
+ h.set(e, t);
149
+ let n = d.findIndex((t) => t.name === e);
129
150
  if (!(n < 0)) {
130
- for (let e of f.values()) {
151
+ for (let e of p.values()) {
131
152
  let r = e.transceivers[n];
132
153
  if (r) try {
133
154
  await r.sender.replaceTrack(t);
134
155
  } catch {}
135
156
  }
136
- await S(_);
157
+ await D(v);
137
158
  }
138
159
  },
139
- applyQuality: S,
160
+ applyQuality: D,
140
161
  setIceServers: (e) => {
141
- h = e;
162
+ g = e;
142
163
  },
143
164
  stop: () => {
144
- for (let e of f.values()) e.pc.close();
145
- f.clear(), p.clear(), v = !0, b(), l.onState?.("closed");
165
+ for (let e of p.values()) e.pc.close();
166
+ p.clear(), m.clear(), b.clear(), y = !0, T(), C(), u.onState?.("closed");
146
167
  },
168
+ getConnection: (e) => p.get(e)?.pc ?? null,
147
169
  get peers() {
148
- return [...p.values()].map((e) => ({ ...e }));
170
+ return [...m.values()].map((e) => ({ ...e }));
149
171
  }
150
172
  };
151
173
  }
152
174
  //#endregion
153
- export { l as createPeerMesh };
175
+ export { u as createPeerMesh };
154
176
 
155
177
  //# sourceMappingURL=peer-mesh.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"peer-mesh.js","names":[],"sources":["../../src/webrtc/peer-mesh.ts"],"sourcesContent":["/**\n * @tempest-limits file-lines — the mesh is one object's lifetime: a link's\n * transceivers, the offer it owes, the tracks published on it and the share of\n * the uplink it costs all change together, and every method here reads or writes\n * the same two maps. The pieces that *could* stand alone already do —\n * `peer-link.ts` for the per-connection mechanics and `mesh-quality.ts` for the\n * division — and what is left is the coordination between them.\n */\nimport { applyQualityToLink, resolveDegradation, scaleForRoom } from \"./mesh-quality\";\nimport {\n acceptCandidate,\n adoptTransceivers,\n attachLocalTracks,\n createPeerConnection,\n drainCandidates,\n slotOf,\n type PeerLink,\n} from \"./peer-link\";\nimport type { MeshMessage, MeshPeer, MeshQuality, PeerMeshOptions } from \"./mesh-types\";\n\n/** A running mesh. */\nexport interface PeerMesh {\n /**\n * Open a link to a peer.\n *\n * `offerer` decides which side sends the offer, and exactly one side of each\n * link must say `true`. Drive it from the arrival order your server already\n * imposes — the peer that joined **later** offers, the one already in the\n * room answers. That avoids glare by construction, without the rollback\n * dance of perfect negotiation and without depending on a browser\n * implementing argument-less `setLocalDescription`.\n */\n addPeer: (peerId: string, options: { offerer: boolean }) => Promise<void>;\n /** Close one link and give its share of the uplink back to the others. */\n removePeer: (peerId: string) => void;\n /** Feed the mesh an `offer`, `answer` or `ice` message from `from`. */\n accept: (message: MeshMessage & { from: string }) => Promise<void>;\n /**\n * Publish or retract one slot across every live link.\n *\n * `null` retracts: the sender keeps its negotiated transceiver and stops\n * producing, which is what mutes a microphone without disturbing the other\n * slots or the connection. Nothing renegotiates — the m-lines were allocated\n * before any track existed, which is what makes a mesh survive toggling: a\n * renegotiation per toggle is N-1 simultaneous offer/answer rounds every time\n * somebody unmutes.\n */\n setLocalTrack: (slot: string, track: MediaStreamTrack | null) => Promise<void>;\n /** Apply encoder limits, divided by the current size of the room. */\n applyQuality: (quality: MeshQuality) => Promise<void>;\n /** Replace the ICE servers used for links opened from now on. */\n setIceServers: (servers: RTCIceServer[]) => void;\n /** Close every link and report `closed`. */\n stop: () => void;\n /** The peers as the mesh currently sees them. */\n readonly peers: MeshPeer[];\n}\n\n/**\n * One `RTCPeerConnection` per peer, with the slots negotiated up front.\n *\n * The signalling protocol stays yours: this produces and accepts three message\n * shapes (`offer`, `answer`, `ice`) and leaves delivery, room membership and\n * identity to your server. What it owns is the part that is the same in every\n * mesh and wrong in most of them — see {@link PeerMesh} for the traps each\n * method exists to avoid.\n *\n * @param options - See {@link PeerMeshOptions}.\n * @returns The mesh.\n *\n * @example\n * const mesh = createPeerMesh({\n * slots: [\n * { name: \"mic\", kind: \"audio\" },\n * { name: \"cam\", kind: \"video\" },\n * ],\n * send: (message) => socket.send(message),\n * onPeers: setPeers,\n * });\n * socket.onMessage = (message) => void mesh.accept(message);\n * await mesh.addPeer(\"peer-2\", { offerer: true });\n * await mesh.setLocalTrack(\"mic\", micTrack);\n */\nexport function createPeerMesh(options: PeerMeshOptions): PeerMesh {\n const { slots, send } = options;\n const links = new Map<string, PeerLink>();\n const peers = new Map<string, MeshPeer>();\n const localTracks = new Map<string, MediaStreamTrack | null>();\n let iceServers = options.iceServers ?? [];\n const describe =\n options.setLocalDescription ??\n ((connection: RTCPeerConnection, description: RTCSessionDescriptionInit) =>\n connection.setLocalDescription(description));\n let quality: MeshQuality = options.quality ?? {};\n let stopped = false;\n\n const emptyStreams = (): Record<string, MediaStream | null> =>\n Object.fromEntries(slots.map((slot) => [slot.name, null]));\n\n const emitPeers = (): void => {\n options.onPeers?.([...peers.values()].map((peer) => ({ ...peer })));\n };\n\n /**\n * Collapse the per-link states into the one badge a call shows.\n *\n * No links at all is `connected`, not a failure: it means nobody else is in\n * the room yet, which is a legitimate state a call sits in for as long as the\n * first person is early.\n */\n const refreshState = (): void => {\n if (stopped) return;\n const states = [...links.values()].map((link) => link.pc.connectionState);\n if (states.some((state) => state === \"connected\")) options.onState?.(\"connected\");\n else if (states.length > 0) options.onState?.(\"connecting\", \"negotiating\");\n else options.onState?.(\"connected\", \"alone\");\n };\n\n const applyQuality = async (next: MeshQuality): Promise<void> => {\n quality = next;\n const effective = scaleForRoom(next, links.size);\n const degradation = resolveDegradation(next, effective);\n for (const link of links.values()) {\n await applyQualityToLink(link.transceivers, slots, effective, degradation);\n }\n };\n\n const makeOffer = async (link: PeerLink): Promise<void> => {\n if (link.makingOffer) return;\n link.makingOffer = true;\n try {\n const offer = await link.pc.createOffer();\n if (!offer.sdp) {\n options.onNotice?.(\"empty_local_offer_sdp\");\n return;\n }\n await describe(link.pc, offer);\n send({\n type: \"offer\",\n to: link.peerId,\n sdp: link.pc.localDescription?.sdp ?? offer.sdp,\n });\n } catch {\n options.onNotice?.(\"offer_failed\");\n } finally {\n link.makingOffer = false;\n }\n };\n\n /**\n * Wrap an inbound track in the stream the caller renders.\n *\n * The event's own stream is preferred: it is the one the sending side\n * grouped the track into, so a caller that reads `stream.id` sees what the\n * peer published rather than a wrapper invented here. Browsers only fill it\n * when the offer named a stream, hence the fallback — and the guard after\n * that is for a runtime with no `MediaStream` at all, where routing still\n * has to not throw.\n */\n const streamFor = (event: RTCTrackEvent): MediaStream | null => {\n const provided = event.streams?.[0];\n if (provided) return provided;\n return typeof MediaStream === \"undefined\" ? null : new MediaStream([event.track]);\n };\n\n const attachRemoteTrack = (link: PeerLink, event: RTCTrackEvent): void => {\n const slot = slotOf(event.transceiver, link, slots);\n const peer = peers.get(link.peerId);\n if (!slot || !peer) return;\n\n peer.streams = { ...peer.streams, [slot.name]: streamFor(event) };\n event.track.onended = (): void => {\n const current = peers.get(link.peerId);\n if (!current) return;\n current.streams = { ...current.streams, [slot.name]: null };\n emitPeers();\n };\n emitPeers();\n };\n\n const removePeer = (peerId: string): void => {\n const link = links.get(peerId);\n if (link) {\n link.pc.close();\n links.delete(peerId);\n }\n peers.delete(peerId);\n void applyQuality(quality);\n emitPeers();\n refreshState();\n };\n\n const addPeer = async (peerId: string, { offerer }: { offerer: boolean }): Promise<void> => {\n if (links.has(peerId)) return;\n\n const pc = createPeerConnection(iceServers);\n const link: PeerLink = {\n peerId,\n pc,\n transceivers: [],\n pendingCandidates: [],\n remoteReady: false,\n makingOffer: false,\n isOfferer: offerer,\n };\n links.set(peerId, link);\n peers.set(peerId, {\n peerId,\n connection: pc.connectionState,\n streams: peers.get(peerId)?.streams ?? emptyStreams(),\n });\n\n if (offerer) {\n link.transceivers = slots.map((slot) =>\n pc.addTransceiver(slot.kind, { direction: \"sendrecv\" }),\n );\n await attachLocalTracks(link, slots, localTracks);\n }\n\n pc.onicecandidate = (event): void => {\n send({\n type: \"ice\",\n to: peerId,\n candidate: event.candidate?.candidate ?? null,\n sdpMid: event.candidate?.sdpMid ?? null,\n sdpMLineIndex: event.candidate?.sdpMLineIndex ?? null,\n });\n };\n\n pc.ontrack = (event): void => attachRemoteTrack(link, event);\n\n pc.onconnectionstatechange = (): void => {\n const peer = peers.get(peerId);\n if (peer) peer.connection = pc.connectionState;\n if (pc.connectionState === \"failed\" || pc.connectionState === \"closed\") {\n removePeer(peerId);\n return;\n }\n emitPeers();\n refreshState();\n };\n\n pc.onnegotiationneeded = (): void => {\n if (link.isOfferer) void makeOffer(link);\n };\n\n emitPeers();\n refreshState();\n if (offerer) await makeOffer(link);\n };\n\n const acceptOffer = async (from: string, sdp: string): Promise<void> => {\n if (!links.has(from)) await addPeer(from, { offerer: false });\n const link = links.get(from);\n if (!link) return;\n\n await link.pc.setRemoteDescription({ type: \"offer\", sdp });\n link.remoteReady = true;\n await drainCandidates(link);\n\n if (!link.isOfferer) {\n adoptTransceivers(link);\n await attachLocalTracks(link, slots, localTracks);\n }\n\n const answer = await link.pc.createAnswer();\n if (!answer.sdp) {\n options.onNotice?.(\"empty_local_answer_sdp\");\n return;\n }\n await describe(link.pc, answer);\n send({ type: \"answer\", to: from, sdp: link.pc.localDescription?.sdp ?? answer.sdp });\n await applyQuality(quality);\n };\n\n const accept = async (message: MeshMessage & { from: string }): Promise<void> => {\n if (message.type === \"offer\") {\n await acceptOffer(message.from, message.sdp);\n return;\n }\n\n const link = links.get(message.from);\n if (!link) return;\n\n if (message.type === \"answer\") {\n await link.pc.setRemoteDescription({ type: \"answer\", sdp: message.sdp });\n link.remoteReady = true;\n await drainCandidates(link);\n await applyQuality(quality);\n return;\n }\n\n if (message.candidate === null) return;\n await acceptCandidate(link, {\n candidate: message.candidate,\n sdpMid: message.sdpMid ?? undefined,\n sdpMLineIndex: message.sdpMLineIndex ?? undefined,\n });\n };\n\n const setLocalTrack = async (slot: string, track: MediaStreamTrack | null): Promise<void> => {\n localTracks.set(slot, track);\n const index = slots.findIndex((entry) => entry.name === slot);\n if (index < 0) return;\n\n for (const link of links.values()) {\n const transceiver = link.transceivers[index];\n if (!transceiver) continue;\n try {\n await transceiver.sender.replaceTrack(track);\n } catch {\n /* the link is tearing down; its state handler will remove it */\n }\n }\n await applyQuality(quality);\n };\n\n const stop = (): void => {\n for (const link of links.values()) link.pc.close();\n links.clear();\n peers.clear();\n stopped = true;\n emitPeers();\n options.onState?.(\"closed\");\n };\n\n options.onState?.(\"connecting\", \"signaling\");\n\n return {\n addPeer,\n removePeer,\n accept,\n setLocalTrack,\n applyQuality,\n setIceServers: (servers): void => {\n iceServers = servers;\n },\n stop,\n get peers(): MeshPeer[] {\n return [...peers.values()].map((peer) => ({ ...peer }));\n },\n };\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"peer-mesh.js","names":[],"sources":["../../src/webrtc/peer-mesh.ts"],"sourcesContent":["/**\n * @tempest-limits file-lines — the mesh is one object's lifetime: a link's\n * transceivers, the offer it owes, the tracks published on it and the share of\n * the uplink it costs all change together, and every method here reads or writes\n * the same two maps. The pieces that *could* stand alone already do —\n * `peer-link.ts` for the per-connection mechanics and `mesh-quality.ts` for the\n * division — and what is left is the coordination between them.\n */\nimport { applyQualityToLink, resolveDegradation, scaleForRoom } from \"./mesh-quality\";\nimport {\n acceptCandidate,\n adoptTransceivers,\n attachLocalTracks,\n createPeerConnection,\n drainCandidates,\n slotOf,\n type PeerLink,\n} from \"./peer-link\";\nimport { createLinkStatsSampler, type LinkStatsSampler } from \"./link-stats\";\nimport type {\n MeshMessage,\n MeshPeer,\n MeshQuality,\n MeshStatsOptions,\n PeerMeshOptions,\n} from \"./mesh-types\";\n\n/** A running mesh. */\nexport interface PeerMesh {\n /**\n * Open a link to a peer.\n *\n * `offerer` decides which side sends the offer, and exactly one side of each\n * link must say `true`. Drive it from the arrival order your server already\n * imposes — the peer that joined **later** offers, the one already in the\n * room answers. That avoids glare by construction, without the rollback\n * dance of perfect negotiation and without depending on a browser\n * implementing argument-less `setLocalDescription`.\n */\n addPeer: (peerId: string, options: { offerer: boolean }) => Promise<void>;\n /** Close one link and give its share of the uplink back to the others. */\n removePeer: (peerId: string) => void;\n /** Feed the mesh an `offer`, `answer` or `ice` message from `from`. */\n accept: (message: MeshMessage & { from: string }) => Promise<void>;\n /**\n * Publish or retract one slot across every live link.\n *\n * `null` retracts: the sender keeps its negotiated transceiver and stops\n * producing, which is what mutes a microphone without disturbing the other\n * slots or the connection. Nothing renegotiates — the m-lines were allocated\n * before any track existed, which is what makes a mesh survive toggling: a\n * renegotiation per toggle is N-1 simultaneous offer/answer rounds every time\n * somebody unmutes.\n */\n setLocalTrack: (slot: string, track: MediaStreamTrack | null) => Promise<void>;\n /** Apply encoder limits, divided by the current size of the room. */\n applyQuality: (quality: MeshQuality) => Promise<void>;\n /** Replace the ICE servers used for links opened from now on. */\n setIceServers: (servers: RTCIceServer[]) => void;\n /** Close every link and report `closed`. */\n stop: () => void;\n /** The peers as the mesh currently sees them. */\n readonly peers: MeshPeer[];\n /**\n * The connection carrying one peer's link, or `null`.\n *\n * The escape hatch for everything this mesh does not model. `getStats()`,\n * `getSenders()`, an `RTCDataChannel`, an encoding tweak the `quality`\n * shape has no field for — all of it lives on the connection, and a mesh\n * that keeps it private turns adopting the mesh into losing a feature.\n *\n * {@link MeshPeer.connection} is the *state*, which is what a view needs;\n * this is the object, which is what a measurement needs. For the common\n * case of measuring, {@link PeerMeshOptions.stats} spares you the loop —\n * including the one-sampler-per-connection rule that a hand-rolled version\n * gets wrong.\n *\n * @param peerId - The id the peer was added under.\n * @returns The connection, or `null` when there is no link for that id.\n */\n getConnection: (peerId: string) => RTCPeerConnection | null;\n}\n\n/**\n * One `RTCPeerConnection` per peer, with the slots negotiated up front.\n *\n * The signalling protocol stays yours: this produces and accepts three message\n * shapes (`offer`, `answer`, `ice`) and leaves delivery, room membership and\n * identity to your server. What it owns is the part that is the same in every\n * mesh and wrong in most of them — see {@link PeerMesh} for the traps each\n * method exists to avoid.\n *\n * @param options - See {@link PeerMeshOptions}.\n * @returns The mesh.\n *\n * @example\n * const mesh = createPeerMesh({\n * slots: [\n * { name: \"mic\", kind: \"audio\" },\n * { name: \"cam\", kind: \"video\" },\n * ],\n * send: (message) => socket.send(message),\n * onPeers: setPeers,\n * });\n * socket.onMessage = (message) => void mesh.accept(message);\n * await mesh.addPeer(\"peer-2\", { offerer: true });\n * await mesh.setLocalTrack(\"mic\", micTrack);\n */\nexport function createPeerMesh(options: PeerMeshOptions): PeerMesh {\n const { slots, send } = options;\n const links = new Map<string, PeerLink>();\n const peers = new Map<string, MeshPeer>();\n const localTracks = new Map<string, MediaStreamTrack | null>();\n let iceServers = options.iceServers ?? [];\n const describe =\n options.setLocalDescription ??\n ((connection: RTCPeerConnection, description: RTCSessionDescriptionInit) =>\n connection.setLocalDescription(description));\n let quality: MeshQuality = options.quality ?? {};\n let stopped = false;\n const samplers = new Map<string, LinkStatsSampler>();\n let statsTimer: ReturnType<typeof setInterval> | null = null;\n\n const emptyStreams = (): Record<string, MediaStream | null> =>\n Object.fromEntries(slots.map((slot) => [slot.name, null]));\n\n const emitPeers = (): void => {\n options.onPeers?.([...peers.values()].map((peer) => ({ ...peer })));\n };\n\n /**\n * Sample every connected link once.\n *\n * Only `connected` links are asked. A link still gathering candidates has\n * no traffic to measure, and asking anyway spends a `getStats()` round trip\n * to learn that — per link, on every tick.\n *\n * `settings` arrives as an argument rather than being read from `options`\n * here: the timer is only ever armed past the check that they exist, so\n * reading them again would add a branch that nothing can reach.\n *\n * @param settings - The measuring options, already known to be present.\n */\n const sampleLinks = async (settings: MeshStatsOptions): Promise<void> => {\n if (stopped) return;\n let changed = false;\n\n for (const link of [...links.values()]) {\n if (link.pc.connectionState !== \"connected\") continue;\n let sampler = samplers.get(link.peerId);\n if (sampler === undefined) {\n sampler = createLinkStatsSampler(\n settings.kind === undefined ? {} : { kind: settings.kind },\n );\n samplers.set(link.peerId, sampler);\n }\n const stats = await sampler.sample(link.pc);\n const peer = peers.get(link.peerId);\n /*\n * Belt and braces, and the belt is the state check above: a peer\n * removed from inside `onStats` has its connection closed first, so\n * the next iteration skips it there rather than here. This catches\n * the entry disappearing any other way across the await.\n */\n if (peer === undefined) continue;\n peer.stats = stats;\n changed = true;\n settings.onStats?.(link.peerId, stats);\n }\n\n if (changed) emitPeers();\n };\n\n /**\n * Keep the timer alive exactly while there is something to measure.\n *\n * One timer for the whole mesh, not one per peer — and none at all in an\n * empty room, which is where a call sits for as long as the first person is\n * early.\n */\n const syncStatsTimer = (): void => {\n const settings = options.stats;\n if (settings === undefined) return;\n const wanted = !stopped && links.size > 0;\n if (wanted && statsTimer === null) {\n statsTimer = setInterval(() => void sampleLinks(settings), settings.intervalMs ?? 2000);\n return;\n }\n if (!wanted && statsTimer !== null) {\n clearInterval(statsTimer);\n statsTimer = null;\n }\n };\n\n /**\n * Collapse the per-link states into the one badge a call shows.\n *\n * No links at all is `connected`, not a failure: it means nobody else is in\n * the room yet, which is a legitimate state a call sits in for as long as the\n * first person is early.\n */\n const refreshState = (): void => {\n if (stopped) return;\n const states = [...links.values()].map((link) => link.pc.connectionState);\n if (states.some((state) => state === \"connected\")) options.onState?.(\"connected\");\n else if (states.length > 0) options.onState?.(\"connecting\", \"negotiating\");\n else options.onState?.(\"connected\", \"alone\");\n };\n\n const applyQuality = async (next: MeshQuality): Promise<void> => {\n quality = next;\n const effective = scaleForRoom(next, links.size);\n const degradation = resolveDegradation(next, effective);\n for (const link of links.values()) {\n await applyQualityToLink(link.transceivers, slots, effective, degradation);\n }\n };\n\n const makeOffer = async (link: PeerLink): Promise<void> => {\n if (link.makingOffer) return;\n link.makingOffer = true;\n try {\n const offer = await link.pc.createOffer();\n if (!offer.sdp) {\n options.onNotice?.(\"empty_local_offer_sdp\");\n return;\n }\n await describe(link.pc, offer);\n send({\n type: \"offer\",\n to: link.peerId,\n sdp: link.pc.localDescription?.sdp ?? offer.sdp,\n });\n } catch {\n options.onNotice?.(\"offer_failed\");\n } finally {\n link.makingOffer = false;\n }\n };\n\n /**\n * Wrap an inbound track in the stream the caller renders.\n *\n * The event's own stream is preferred: it is the one the sending side\n * grouped the track into, so a caller that reads `stream.id` sees what the\n * peer published rather than a wrapper invented here. Browsers only fill it\n * when the offer named a stream, hence the fallback — and the guard after\n * that is for a runtime with no `MediaStream` at all, where routing still\n * has to not throw.\n */\n const streamFor = (event: RTCTrackEvent): MediaStream | null => {\n const provided = event.streams?.[0];\n if (provided) return provided;\n return typeof MediaStream === \"undefined\" ? null : new MediaStream([event.track]);\n };\n\n const attachRemoteTrack = (link: PeerLink, event: RTCTrackEvent): void => {\n const slot = slotOf(event.transceiver, link, slots);\n const peer = peers.get(link.peerId);\n if (!slot || !peer) return;\n\n peer.streams = { ...peer.streams, [slot.name]: streamFor(event) };\n event.track.onended = (): void => {\n const current = peers.get(link.peerId);\n if (!current) return;\n current.streams = { ...current.streams, [slot.name]: null };\n emitPeers();\n };\n emitPeers();\n };\n\n const removePeer = (peerId: string): void => {\n const link = links.get(peerId);\n if (link) {\n link.pc.close();\n links.delete(peerId);\n }\n peers.delete(peerId);\n samplers.delete(peerId);\n void applyQuality(quality);\n syncStatsTimer();\n emitPeers();\n refreshState();\n };\n\n const addPeer = async (peerId: string, { offerer }: { offerer: boolean }): Promise<void> => {\n if (links.has(peerId)) return;\n\n const pc = createPeerConnection(iceServers);\n const link: PeerLink = {\n peerId,\n pc,\n transceivers: [],\n pendingCandidates: [],\n remoteReady: false,\n makingOffer: false,\n isOfferer: offerer,\n };\n links.set(peerId, link);\n peers.set(peerId, {\n peerId,\n connection: pc.connectionState,\n streams: peers.get(peerId)?.streams ?? emptyStreams(),\n });\n\n if (offerer) {\n link.transceivers = slots.map((slot) =>\n pc.addTransceiver(slot.kind, { direction: \"sendrecv\" }),\n );\n await attachLocalTracks(link, slots, localTracks);\n }\n\n pc.onicecandidate = (event): void => {\n send({\n type: \"ice\",\n to: peerId,\n candidate: event.candidate?.candidate ?? null,\n sdpMid: event.candidate?.sdpMid ?? null,\n sdpMLineIndex: event.candidate?.sdpMLineIndex ?? null,\n });\n };\n\n pc.ontrack = (event): void => attachRemoteTrack(link, event);\n\n pc.onconnectionstatechange = (): void => {\n const peer = peers.get(peerId);\n if (peer) peer.connection = pc.connectionState;\n if (pc.connectionState === \"failed\" || pc.connectionState === \"closed\") {\n removePeer(peerId);\n return;\n }\n emitPeers();\n refreshState();\n };\n\n pc.onnegotiationneeded = (): void => {\n if (link.isOfferer) void makeOffer(link);\n };\n\n syncStatsTimer();\n emitPeers();\n refreshState();\n if (offerer) await makeOffer(link);\n };\n\n const acceptOffer = async (from: string, sdp: string): Promise<void> => {\n if (!links.has(from)) await addPeer(from, { offerer: false });\n const link = links.get(from);\n if (!link) return;\n\n await link.pc.setRemoteDescription({ type: \"offer\", sdp });\n link.remoteReady = true;\n await drainCandidates(link);\n\n if (!link.isOfferer) {\n adoptTransceivers(link);\n await attachLocalTracks(link, slots, localTracks);\n }\n\n const answer = await link.pc.createAnswer();\n if (!answer.sdp) {\n options.onNotice?.(\"empty_local_answer_sdp\");\n return;\n }\n await describe(link.pc, answer);\n send({ type: \"answer\", to: from, sdp: link.pc.localDescription?.sdp ?? answer.sdp });\n await applyQuality(quality);\n };\n\n const accept = async (message: MeshMessage & { from: string }): Promise<void> => {\n if (message.type === \"offer\") {\n await acceptOffer(message.from, message.sdp);\n return;\n }\n\n const link = links.get(message.from);\n if (!link) return;\n\n if (message.type === \"answer\") {\n await link.pc.setRemoteDescription({ type: \"answer\", sdp: message.sdp });\n link.remoteReady = true;\n await drainCandidates(link);\n await applyQuality(quality);\n return;\n }\n\n if (message.candidate === null) return;\n await acceptCandidate(link, {\n candidate: message.candidate,\n sdpMid: message.sdpMid ?? undefined,\n sdpMLineIndex: message.sdpMLineIndex ?? undefined,\n });\n };\n\n const setLocalTrack = async (slot: string, track: MediaStreamTrack | null): Promise<void> => {\n localTracks.set(slot, track);\n const index = slots.findIndex((entry) => entry.name === slot);\n if (index < 0) return;\n\n for (const link of links.values()) {\n const transceiver = link.transceivers[index];\n if (!transceiver) continue;\n try {\n await transceiver.sender.replaceTrack(track);\n } catch {\n /* the link is tearing down; its state handler will remove it */\n }\n }\n await applyQuality(quality);\n };\n\n const stop = (): void => {\n for (const link of links.values()) link.pc.close();\n links.clear();\n peers.clear();\n samplers.clear();\n stopped = true;\n syncStatsTimer();\n emitPeers();\n options.onState?.(\"closed\");\n };\n\n options.onState?.(\"connecting\", \"signaling\");\n\n return {\n addPeer,\n removePeer,\n accept,\n setLocalTrack,\n applyQuality,\n setIceServers: (servers): void => {\n iceServers = servers;\n },\n stop,\n getConnection: (peerId: string): RTCPeerConnection | null => links.get(peerId)?.pc ?? null,\n get peers(): MeshPeer[] {\n return [...peers.values()].map((peer) => ({ ...peer }));\n },\n };\n}\n"],"mappings":"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"}
@@ -1,2 +1,2 @@
1
- const e=require("../utils/json-frame.cjs"),t=require("./resilience.cjs");function n(n,r={}){let{protocols:i,maxRetries:a=10,initialBackoff:o=1e3,maxBackoff:s=3e4,jitter:ee=.3,handshakeTimeout:c=8e3,silenceTimeout:l=0,waitForOnline:u=!0,pingInterval:d=0,pingPayload:f=JSON.stringify({type:`ping`}),respondToPing:p=!0,pongPayload:m=JSON.stringify({type:`pong`}),queueWhileClosed:h=!1,maxQueuedMessages:te=100,parser:ne,onOpen:g,onMessage:_,onClose:v,onError:y,onParseError:b,onStatusChange:re,onReconnecting:ie,onReconnected:x,onLost:S}=r,C=null,w=null,T=null,E=null,D=null,O=null,k=l,A=0,j=`idle`,M=!1,N=!1,P=[],F=null,I=null,L=new Promise((e,t)=>{F=e,I=t});L.catch(()=>void 0);function R(e){return typeof e==`object`&&!!e&&e.type===`ping`}function z(e){for(;P.length>0&&e.readyState===WebSocket.OPEN;)e.send(P.shift())}function B(e){j!==e&&(j=e,re?.(e))}function V(){T&&=(clearInterval(T),null)}function H(){E&&=(clearTimeout(E),null)}function U(){D&&=(clearTimeout(D),null)}function W(){!d||d<=0||(V(),T=setInterval(()=>{C?.readyState===WebSocket.OPEN&&C.send(f)},d))}function G(){U(),!(M||k<=0)&&(D=setTimeout(K,k))}function K(){U(),!(M||!C)&&(J(C),C=null,B(`closed`),X())}function q(e){H(),!(M||e.readyState!==WebSocket.CONNECTING)&&(J(e),C===e&&(C=null),B(`closed`),X())}function J(e){e.onopen=null,e.onmessage=null,e.onerror=null,e.onclose=null;try{e.close()}catch{}}function Y(e){if(U(),H(),Z(),e===`rejected`&&(M=!0),B(`error`),!N&&I){let t=I;I=null,F=null,t(Error(`websocket_${e}`))}S?.(e)}function X(){if(M)return;if(A>=a){Y(`exhausted`);return}let e=t.backoffDelay(A,{initialBackoff:o,maxBackoff:s,jitter:ee});if(A+=1,ie?.(A,a),u&&typeof navigator<`u`&&navigator.onLine===!1){ae();return}w=setTimeout(Q,e)}function ae(){if(O||typeof window>`u`)return;let e=()=>{Z(),M||Q()};O=e,window.addEventListener(`online`,e)}function Z(){!O||typeof window>`u`||(window.removeEventListener(`online`,O),O=null)}function Q(){if(M)return;if(w=null,H(),C){let e=C;e.onmessage=null,e.onclose=null,e.onerror=null,e.readyState!==WebSocket.CONNECTING&&(e.onopen=null),$(e)}B(`connecting`);let r=new WebSocket(n,i);C=r,c>0&&(E=setTimeout(()=>q(r),c)),r.onopen=e=>{H();let t=A>0;if(A=0,N=!0,B(`open`),W(),G(),z(r),F){let e=F;F=null,I=null,e()}g?.(e),t&&x?.()},r.onmessage=t=>{G();let n=typeof t.data==`string`?t.data:``,i=e.decodeFrame(n,ne,b,`createWebSocket`);if(!i.delivered)return;let a=i.data;p&&R(a)&&r.readyState===WebSocket.OPEN&&r.send(m),_?.({data:a,raw:t})},r.onerror=e=>{y?.(e)},r.onclose=e=>{if(H(),V(),U(),v?.(e),C=null,B(`closed`),!M){if(t.isRejectionCloseCode(e.code)){Y(`rejected`);return}if(t.shouldRetryClose(e.code,e.wasClean)){X();return}N||Y(`rejected`)}}}function oe(e){return C?.readyState===WebSocket.OPEN?(C.send(e),!0):!h||M?!1:(P.length>=te&&P.shift(),P.push(e),!0)}function se(e,t){if(M=!0,w&&=(clearTimeout(w),null),V(),H(),U(),Z(),A=0,P.length=0,I){let e=I;I=null,F=null,e(Error(`websocket_closed`))}C&&=(B(`closing`),$(C,e,t),null),B(`closed`)}function $(e,t,n){if(e.readyState===WebSocket.CONNECTING){e.onopen=()=>e.close(t,n),e.onmessage=null,e.onerror=null,e.onclose=null;return}e.close(t,n)}function ce(){w&&=(clearTimeout(w),null),Z(),A=0,M=!1,Q()}function le(e){k=Number.isFinite(e)&&e>0?e:0,C?.readyState===WebSocket.OPEN?G():U()}return Q(),{send:oe,close:se,reconnect:ce,setSilenceTimeout:le,opened:L,get status(){return j}}}exports.createWebSocket=n;
1
+ const e=require("../utils/json-frame.cjs"),t=require("./resilience.cjs");function n(n,r={}){let{protocols:i,maxRetries:a=10,initialBackoff:o=1e3,maxBackoff:s=3e4,jitter:ee=.3,handshakeTimeout:c=8e3,silenceTimeout:l=0,waitForOnline:u=!0,pingInterval:d=0,pingPayload:f=JSON.stringify({type:`ping`}),respondToPing:p=!0,pongPayload:m=JSON.stringify({type:`pong`}),queueWhileClosed:h=!1,maxQueuedMessages:te=100,parser:ne,onOpen:g,onMessage:_,onClose:v,onError:y,onParseError:b,onStatusChange:re,onReconnecting:ie,onReconnected:x,onLost:S}=r,C=null,w=null,T=null,E=null,D=null,O=null,k=l,A=0,j=`idle`,M=!1,N=!1,P=[],F=null,I=null,L=new Promise((e,t)=>{F=e,I=t});L.catch(()=>void 0);function R(e){return typeof e==`object`&&!!e&&e.type===`ping`}function z(e){for(;P.length>0&&e.readyState===WebSocket.OPEN;)e.send(P.shift())}function B(e){j!==e&&(j=e,re?.(e))}function V(){T&&=(clearInterval(T),null)}function H(){E&&=(clearTimeout(E),null)}function U(){D&&=(clearTimeout(D),null)}function W(){!d||d<=0||(V(),T=setInterval(()=>{C?.readyState===WebSocket.OPEN&&C.send(f)},d))}function G(){U(),!(M||k<=0)&&(D=setTimeout(K,k))}function K(){U(),!M&&C&&(J(C),C=null,B(`closed`),X())}function q(e){H(),!(M||e.readyState!==WebSocket.CONNECTING)&&(J(e),C===e&&(C=null),B(`closed`),X())}function J(e){e.onopen=null,e.onmessage=null,e.onerror=null,e.onclose=null;try{e.close()}catch{}}function Y(e){if(U(),H(),Z(),e===`rejected`&&(M=!0),B(`error`),!N&&I){let t=I;I=null,F=null,t(Error(`websocket_${e}`))}S?.(e)}function X(){if(M)return;if(A>=a){Y(`exhausted`);return}let e=t.backoffDelay(A,{initialBackoff:o,maxBackoff:s,jitter:ee});if(A+=1,ie?.(A,a),u&&typeof navigator<`u`&&navigator.onLine===!1){ae();return}w=setTimeout(Q,e)}function ae(){if(O||typeof window>`u`)return;let e=()=>{Z(),M||Q()};O=e,window.addEventListener(`online`,e)}function Z(){!O||typeof window>`u`||(window.removeEventListener(`online`,O),O=null)}function Q(){if(M)return;if(w=null,H(),C){let e=C;e.onmessage=null,e.onclose=null,e.onerror=null,e.readyState!==WebSocket.CONNECTING&&(e.onopen=null),$(e)}B(`connecting`);let r=new WebSocket(n,i);C=r,c>0&&(E=setTimeout(()=>q(r),c)),r.onopen=e=>{H();let t=A>0;if(A=0,N=!0,B(`open`),W(),G(),z(r),F){let e=F;F=null,I=null,e()}g?.(e),t&&x?.()},r.onmessage=t=>{G();let n=typeof t.data==`string`?t.data:``,i=e.decodeFrame(n,ne,b,`createWebSocket`);if(!i.delivered)return;let a=i.data;p&&R(a)&&r.readyState===WebSocket.OPEN&&r.send(m),_?.({data:a,raw:t})},r.onerror=e=>{y?.(e)},r.onclose=e=>{if(H(),V(),U(),v?.(e),C=null,B(`closed`),!M){if(t.isRejectionCloseCode(e.code)){Y(`rejected`);return}if(t.shouldRetryClose(e.code,e.wasClean)){X();return}N||Y(`rejected`)}}}function oe(e){return C?.readyState===WebSocket.OPEN?(C.send(e),!0):!h||M?!1:(P.length>=te&&P.shift(),P.push(e),!0)}function se(e,t){if(M=!0,w&&=(clearTimeout(w),null),V(),H(),U(),Z(),A=0,P.length=0,I){let e=I;I=null,F=null,e(Error(`websocket_closed`))}C&&=(B(`closing`),$(C,e,t),null),B(`closed`)}function $(e,t,n){if(e.readyState===WebSocket.CONNECTING){e.onopen=()=>e.close(t,n),e.onmessage=null,e.onerror=null,e.onclose=null;return}e.close(t,n)}function ce(){w&&=(clearTimeout(w),null),Z(),A=0,M=!1,Q()}function le(e){k=Number.isFinite(e)&&e>0?e:0,C?.readyState===WebSocket.OPEN?G():U()}return Q(),{send:oe,close:se,reconnect:ce,setSilenceTimeout:le,opened:L,get status(){return j}}}exports.createWebSocket=n;
2
2
  //# sourceMappingURL=create-web-socket.cjs.map