@burdenoff/microfe-comradecircle 2026.1025.1 → 2026.1025.3

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 (51) hide show
  1. package/dist/comradecircle/components/LiveStagePanel.d.ts +10 -0
  2. package/dist/comradecircle/components/LiveStagePanel.d.ts.map +1 -0
  3. package/dist/comradecircle/components/LiveStagePanel.js +263 -0
  4. package/dist/comradecircle/components/LiveStagePanel.js.map +1 -0
  5. package/dist/comradecircle/components/MediaTile.d.ts +16 -0
  6. package/dist/comradecircle/components/MediaTile.d.ts.map +1 -0
  7. package/dist/comradecircle/components/MediaTile.js +74 -0
  8. package/dist/comradecircle/components/MediaTile.js.map +1 -0
  9. package/dist/comradecircle/components/VoiceChannelPanel.d.ts +10 -0
  10. package/dist/comradecircle/components/VoiceChannelPanel.d.ts.map +1 -0
  11. package/dist/comradecircle/components/VoiceChannelPanel.js +123 -0
  12. package/dist/comradecircle/components/VoiceChannelPanel.js.map +1 -0
  13. package/dist/comradecircle/hooks/index.d.ts +3 -0
  14. package/dist/comradecircle/hooks/index.d.ts.map +1 -1
  15. package/dist/comradecircle/hooks/index.js +3 -0
  16. package/dist/comradecircle/hooks/useLiveStage.d.ts +20 -0
  17. package/dist/comradecircle/hooks/useLiveStage.d.ts.map +1 -0
  18. package/dist/comradecircle/hooks/useLiveStage.js +160 -0
  19. package/dist/comradecircle/hooks/useLiveStage.js.map +1 -0
  20. package/dist/comradecircle/hooks/useVoiceApi.d.ts +36 -0
  21. package/dist/comradecircle/hooks/useVoiceApi.d.ts.map +1 -0
  22. package/dist/comradecircle/hooks/useVoiceApi.js +113 -0
  23. package/dist/comradecircle/hooks/useVoiceApi.js.map +1 -0
  24. package/dist/comradecircle/hooks/useVoiceCall.d.ts +21 -0
  25. package/dist/comradecircle/hooks/useVoiceCall.d.ts.map +1 -0
  26. package/dist/comradecircle/hooks/useVoiceCall.js +154 -0
  27. package/dist/comradecircle/hooks/useVoiceCall.js.map +1 -0
  28. package/dist/comradecircle/lib/graphqlStream.d.ts +31 -0
  29. package/dist/comradecircle/lib/graphqlStream.d.ts.map +1 -0
  30. package/dist/comradecircle/lib/graphqlStream.js +112 -0
  31. package/dist/comradecircle/lib/graphqlStream.js.map +1 -0
  32. package/dist/comradecircle/lib/rtc/liveStage.d.ts +37 -0
  33. package/dist/comradecircle/lib/rtc/liveStage.d.ts.map +1 -0
  34. package/dist/comradecircle/lib/rtc/liveStage.js +97 -0
  35. package/dist/comradecircle/lib/rtc/liveStage.js.map +1 -0
  36. package/dist/comradecircle/lib/rtc/peerLink.d.ts +50 -0
  37. package/dist/comradecircle/lib/rtc/peerLink.d.ts.map +1 -0
  38. package/dist/comradecircle/lib/rtc/peerLink.js +110 -0
  39. package/dist/comradecircle/lib/rtc/peerLink.js.map +1 -0
  40. package/dist/comradecircle/lib/rtc/voiceMesh.d.ts +41 -0
  41. package/dist/comradecircle/lib/rtc/voiceMesh.d.ts.map +1 -0
  42. package/dist/comradecircle/lib/rtc/voiceMesh.js +101 -0
  43. package/dist/comradecircle/lib/rtc/voiceMesh.js.map +1 -0
  44. package/dist/comradecircle/pages/circles/CircleDetailPage.d.ts.map +1 -1
  45. package/dist/comradecircle/pages/circles/CircleDetailPage.js +299 -286
  46. package/dist/comradecircle/pages/circles/CircleDetailPage.js.map +1 -1
  47. package/dist/comradecircle/pages/circles/ModerationTab.js +2 -2
  48. package/dist/comradecircle/pages/circles/ModerationTab.js.map +1 -1
  49. package/dist/comradecircle/types/index.d.ts +51 -0
  50. package/dist/comradecircle/types/index.d.ts.map +1 -1
  51. package/package.json +1 -1
@@ -0,0 +1,112 @@
1
+ import { getStoredAuthToken as e, getStoredLocale as t, getStoredProjectId as n, getStoredWorkspaceId as r, getStoredWorkspaceToken as i, resolveGatewayUrl as a } from "@burdenoff/fe-libs/shared/graphql";
2
+ //#region src/comradecircle/lib/graphqlStream.ts
3
+ function o() {
4
+ let e = "";
5
+ return (t) => {
6
+ e += t.replace(/\r\n/g, "\n");
7
+ let n = [], r = e.indexOf("\n\n");
8
+ for (; r !== -1;) {
9
+ let t = e.slice(0, r);
10
+ e = e.slice(r + 2);
11
+ let i = "", a = [];
12
+ for (let e of t.split("\n")) e.startsWith(":") || (e.startsWith("event:") ? i = e.slice(6).trim() : e.startsWith("data:") && a.push(e.slice(5).replace(/^ /, "")));
13
+ (i || a.length > 0) && n.push({
14
+ event: i || "message",
15
+ data: a.join("\n")
16
+ }), r = e.indexOf("\n\n");
17
+ }
18
+ return n;
19
+ };
20
+ }
21
+ function s(e) {
22
+ return (e ?? []).map((e) => ({
23
+ message: e.message ?? "Subscription error",
24
+ code: e.extensions?.code
25
+ }));
26
+ }
27
+ function c() {
28
+ let a = {
29
+ "Content-Type": "application/json",
30
+ Accept: "text/event-stream",
31
+ "Apollo-Require-Preflight": "true"
32
+ }, o = e();
33
+ o && (a.Authorization = `Bearer ${o}`);
34
+ let s = i();
35
+ s && (a["X-Workspace-Authorization"] = `Bearer ${s}`);
36
+ let c = r();
37
+ c && (a["x-workspace-id"] = c);
38
+ let l = n();
39
+ l && (a["x-project-id"] = l);
40
+ let u = t();
41
+ return u && (a["Accept-Language"] = u), a;
42
+ }
43
+ var l = 1e3, u = 15e3;
44
+ function d(e) {
45
+ let t = !1, n = null, r = null, i = l, d = () => {
46
+ t || (r = setTimeout(() => {
47
+ r = null, f();
48
+ }, i), i = Math.min(i * 2, u));
49
+ }, f = async () => {
50
+ if (t) return;
51
+ n = new AbortController();
52
+ let r;
53
+ try {
54
+ r = await fetch(a("workspace", e.baseUrl), {
55
+ method: "POST",
56
+ credentials: "include",
57
+ headers: c(),
58
+ body: JSON.stringify({
59
+ query: e.query,
60
+ variables: e.variables,
61
+ operationName: e.operationName
62
+ }),
63
+ signal: n.signal
64
+ });
65
+ } catch {
66
+ d();
67
+ return;
68
+ }
69
+ if (!r.ok || !r.body) {
70
+ if (r.status >= 400 && r.status < 500 && r.status !== 429) {
71
+ e.onError?.([{ message: `Subscription rejected (HTTP ${r.status})` }]);
72
+ return;
73
+ }
74
+ d();
75
+ return;
76
+ }
77
+ e.onOpen?.(), i = l;
78
+ let u = r.body.getReader(), f = new TextDecoder(), p = o();
79
+ try {
80
+ for (;;) {
81
+ let { done: r, value: i } = await u.read();
82
+ if (r) break;
83
+ for (let r of p(f.decode(i, { stream: !0 }))) {
84
+ if (r.event === "complete") {
85
+ d();
86
+ return;
87
+ }
88
+ if (r.event !== "next" && r.event !== "message" || !r.data) continue;
89
+ let i;
90
+ try {
91
+ i = JSON.parse(r.data);
92
+ } catch {
93
+ continue;
94
+ }
95
+ if (i.errors?.length) {
96
+ t = !0, n.abort(), e.onError?.(s(i.errors));
97
+ return;
98
+ }
99
+ i.data && e.onData(i.data);
100
+ }
101
+ }
102
+ } catch {}
103
+ d();
104
+ };
105
+ return f(), () => {
106
+ t = !0, r && clearTimeout(r), n?.abort();
107
+ };
108
+ }
109
+ //#endregion
110
+ export { d as subscribeGraphqlStream };
111
+
112
+ //# sourceMappingURL=graphqlStream.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"graphqlStream.js","names":[],"sources":["../../../src/comradecircle/lib/graphqlStream.ts"],"sourcesContent":["/**\n * graphqlStream — GraphQL subscriptions over Server-Sent Events (BOFF-4450).\n *\n * The ComradeCircle app shell does not provide a graphql-ws client, so the\n * generic `useComSubscription` hook degrades to polling. That is fine for feed\n * refreshes but useless for WebRTC negotiation: voice and live-stream signals\n * are never persisted, so a signal that is not received live is lost. This\n * module opens the subscription directly against the workspace public gateway\n * using the GraphQL-over-SSE \"distinct connections\" mode (POST with\n * `Accept: text/event-stream`), which the gateway already serves.\n *\n * Authentication mirrors `graphqlFetch`: the platform bearer token, the\n * workspace token, `x-workspace-id` and the cookie (credentials: include) for\n * HttpOnly sessions. Tokens are re-read on every (re)connect so a refreshed\n * workspace token is picked up. Transport failures reconnect with capped\n * exponential backoff; GraphQL errors are surfaced to the caller and end the\n * stream (they are authorization or validation failures a retry won't fix).\n */\nimport {\n getStoredAuthToken,\n getStoredLocale,\n getStoredProjectId,\n getStoredWorkspaceId,\n getStoredWorkspaceToken,\n resolveGatewayUrl,\n} from '@burdenoff/fe-libs/shared/graphql';\n\nexport interface GraphqlStreamError {\n message: string;\n code?: string;\n}\n\nexport interface GraphqlStreamOptions<TData> {\n query: string;\n variables: Record<string, unknown>;\n operationName: string;\n /** Workspace gateway base URL override (from the ComradeCircle context). */\n baseUrl?: string;\n onData: (data: TData) => void;\n /** Called once the gateway has accepted the subscription (response headers received). */\n onOpen?: () => void;\n /** GraphQL errors end the stream; the caller decides what to do next. */\n onError?: (errors: GraphqlStreamError[]) => void;\n}\n\nexport interface SseEvent {\n event: string;\n data: string;\n}\n\n/**\n * Incremental SSE parser. Feed it decoded text chunks; it returns every\n * complete event and keeps the unterminated tail for the next call.\n */\nexport function createSseParser(): (chunk: string) => SseEvent[] {\n let buffer = '';\n return (chunk: string) => {\n buffer += chunk.replace(/\\r\\n/g, '\\n');\n const events: SseEvent[] = [];\n let boundary = buffer.indexOf('\\n\\n');\n while (boundary !== -1) {\n const block = buffer.slice(0, boundary);\n buffer = buffer.slice(boundary + 2);\n let event = '';\n const data: string[] = [];\n for (const line of block.split('\\n')) {\n if (line.startsWith(':')) continue;\n if (line.startsWith('event:')) event = line.slice(6).trim();\n else if (line.startsWith('data:')) data.push(line.slice(5).replace(/^ /, ''));\n }\n if (event || data.length > 0)\n events.push({ event: event || 'message', data: data.join('\\n') });\n boundary = buffer.indexOf('\\n\\n');\n }\n return events;\n };\n}\n\ninterface WirePayload<TData> {\n data?: TData | null;\n errors?: Array<{ message?: string; extensions?: { code?: string } }>;\n}\n\nfunction toStreamErrors(errors: WirePayload<unknown>['errors']): GraphqlStreamError[] {\n return (errors ?? []).map((e) => ({\n message: e.message ?? 'Subscription error',\n code: e.extensions?.code,\n }));\n}\n\nfunction buildHeaders(): Record<string, string> {\n const headers: Record<string, string> = {\n 'Content-Type': 'application/json',\n Accept: 'text/event-stream',\n 'Apollo-Require-Preflight': 'true',\n };\n const authToken = getStoredAuthToken();\n if (authToken) headers.Authorization = `Bearer ${authToken}`;\n const workspaceToken = getStoredWorkspaceToken();\n if (workspaceToken) headers['X-Workspace-Authorization'] = `Bearer ${workspaceToken}`;\n const workspaceId = getStoredWorkspaceId();\n if (workspaceId) headers['x-workspace-id'] = workspaceId;\n const projectId = getStoredProjectId();\n if (projectId) headers['x-project-id'] = projectId;\n const locale = getStoredLocale();\n if (locale) headers['Accept-Language'] = locale;\n return headers;\n}\n\nconst MIN_BACKOFF_MS = 1_000;\nconst MAX_BACKOFF_MS = 15_000;\n\n/**\n * Open a subscription. Returns a disposer that aborts the stream and stops\n * any pending reconnect.\n */\nexport function subscribeGraphqlStream<TData>(options: GraphqlStreamOptions<TData>): () => void {\n let disposed = false;\n let controller: AbortController | null = null;\n let retryTimer: ReturnType<typeof setTimeout> | null = null;\n let backoff = MIN_BACKOFF_MS;\n\n const scheduleReconnect = () => {\n if (disposed) return;\n retryTimer = setTimeout(() => {\n retryTimer = null;\n void connect();\n }, backoff);\n backoff = Math.min(backoff * 2, MAX_BACKOFF_MS);\n };\n\n const connect = async (): Promise<void> => {\n if (disposed) return;\n controller = new AbortController();\n let response: Response;\n try {\n response = await fetch(resolveGatewayUrl('workspace', options.baseUrl), {\n method: 'POST',\n credentials: 'include',\n headers: buildHeaders(),\n body: JSON.stringify({\n query: options.query,\n variables: options.variables,\n operationName: options.operationName,\n }),\n signal: controller.signal,\n });\n } catch {\n scheduleReconnect();\n return;\n }\n if (!response.ok || !response.body) {\n // 4xx other than throttling means the request itself is unacceptable.\n if (response.status >= 400 && response.status < 500 && response.status !== 429) {\n options.onError?.([{ message: `Subscription rejected (HTTP ${response.status})` }]);\n return;\n }\n scheduleReconnect();\n return;\n }\n\n options.onOpen?.();\n backoff = MIN_BACKOFF_MS;\n const reader = response.body.getReader();\n const decoder = new TextDecoder();\n const parse = createSseParser();\n try {\n for (;;) {\n const { done, value } = await reader.read();\n if (done) break;\n for (const evt of parse(decoder.decode(value, { stream: true }))) {\n if (evt.event === 'complete') {\n // The server finished the stream (e.g. a gateway lifetime cap):\n // resubscribe so the caller keeps receiving events.\n scheduleReconnect();\n return;\n }\n if (evt.event !== 'next' && evt.event !== 'message') continue;\n if (!evt.data) continue;\n let payload: WirePayload<TData>;\n try {\n payload = JSON.parse(evt.data) as WirePayload<TData>;\n } catch {\n continue;\n }\n if (payload.errors?.length) {\n disposed = true;\n controller.abort();\n options.onError?.(toStreamErrors(payload.errors));\n return;\n }\n if (payload.data) options.onData(payload.data);\n }\n }\n } catch {\n // Aborted by dispose, or the connection dropped.\n }\n scheduleReconnect();\n };\n\n void connect();\n\n return () => {\n disposed = true;\n if (retryTimer) clearTimeout(retryTimer);\n controller?.abort();\n };\n}\n"],"mappings":"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"}
@@ -0,0 +1,37 @@
1
+ import { ComRtcSignal, ComVoiceSignalType } from '../../types';
2
+
3
+ type Send = (toMemberId: string, signalType: ComVoiceSignalType, payload: unknown) => Promise<void>;
4
+ export declare class StageHost {
5
+ private readonly rtcConfig;
6
+ private readonly send;
7
+ private readonly onConnectedViewersChange?;
8
+ private readonly links;
9
+ private readonly watchdog;
10
+ private localStream;
11
+ constructor(rtcConfig: RTCConfiguration, localStream: MediaStream, send: Send, onConnectedViewersChange?: ((count: number) => void) | undefined);
12
+ handleSignal(signal: ComRtcSignal): Promise<void>;
13
+ setLocalStream(stream: MediaStream): void;
14
+ dispose(): void;
15
+ private prune;
16
+ private report;
17
+ }
18
+ export declare class StageViewer {
19
+ private readonly hostId;
20
+ private readonly rtcConfig;
21
+ private readonly send;
22
+ private readonly onStream;
23
+ private readonly onState?;
24
+ private link;
25
+ private readonly watchdog;
26
+ private ready;
27
+ private disposed;
28
+ constructor(hostId: string, rtcConfig: RTCConfiguration, send: Send, onStream: (stream: MediaStream | null) => void, onState?: ((state: RTCPeerConnectionState) => void) | undefined);
29
+ /** Call once the viewer's signal subscription has been accepted. */
30
+ markSignalingReady(): void;
31
+ handleSignal(signal: ComRtcSignal): Promise<void>;
32
+ dispose(): void;
33
+ private dial;
34
+ private check;
35
+ }
36
+ export {};
37
+ //# sourceMappingURL=liveStage.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"liveStage.d.ts","sourceRoot":"","sources":["../../../../src/comradecircle/lib/rtc/liveStage.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AACH,OAAO,KAAK,EAAE,YAAY,EAAE,kBAAkB,EAAE,MAAM,aAAa,CAAC;AAMpE,KAAK,IAAI,GAAG,CAAC,UAAU,EAAE,MAAM,EAAE,UAAU,EAAE,kBAAkB,EAAE,OAAO,EAAE,OAAO,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;AAEpG,qBAAa,SAAS;IAMlB,OAAO,CAAC,QAAQ,CAAC,SAAS;IAE1B,OAAO,CAAC,QAAQ,CAAC,IAAI;IACrB,OAAO,CAAC,QAAQ,CAAC,wBAAwB,CAAC;IAR5C,OAAO,CAAC,QAAQ,CAAC,KAAK,CAA+B;IACrD,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAiC;IAC1D,OAAO,CAAC,WAAW,CAAc;gBAGd,SAAS,EAAE,gBAAgB,EAC5C,WAAW,EAAE,WAAW,EACP,IAAI,EAAE,IAAI,EACV,wBAAwB,CAAC,GAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,aAAA;IAM/D,YAAY,CAAC,MAAM,EAAE,YAAY,GAAG,OAAO,CAAC,IAAI,CAAC;IA8BvD,cAAc,CAAC,MAAM,EAAE,WAAW,GAAG,IAAI;IAKzC,OAAO,IAAI,IAAI;IAOf,OAAO,CAAC,KAAK;IAUb,OAAO,CAAC,MAAM;CAMf;AAED,qBAAa,WAAW;IAOpB,OAAO,CAAC,QAAQ,CAAC,MAAM;IACvB,OAAO,CAAC,QAAQ,CAAC,SAAS;IAC1B,OAAO,CAAC,QAAQ,CAAC,IAAI;IACrB,OAAO,CAAC,QAAQ,CAAC,QAAQ;IACzB,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAC;IAV3B,OAAO,CAAC,IAAI,CAAyB;IACrC,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAiC;IAC1D,OAAO,CAAC,KAAK,CAAS;IACtB,OAAO,CAAC,QAAQ,CAAS;gBAGN,MAAM,EAAE,MAAM,EACd,SAAS,EAAE,gBAAgB,EAC3B,IAAI,EAAE,IAAI,EACV,QAAQ,EAAE,CAAC,MAAM,EAAE,WAAW,GAAG,IAAI,KAAK,IAAI,EAC9C,OAAO,CAAC,GAAE,CAAC,KAAK,EAAE,sBAAsB,KAAK,IAAI,aAAA;IAKpE,oEAAoE;IACpE,kBAAkB,IAAI,IAAI;IAKpB,YAAY,CAAC,MAAM,EAAE,YAAY,GAAG,OAAO,CAAC,IAAI,CAAC;IASvD,OAAO,IAAI,IAAI;IAQf,OAAO,CAAC,IAAI;IAgBZ,OAAO,CAAC,KAAK;CAMd"}
@@ -0,0 +1,97 @@
1
+ import { PeerLink as e } from "./peerLink.js";
2
+ import "./voiceMesh.js";
3
+ //#region src/comradecircle/lib/rtc/liveStage.ts
4
+ var t = 5e3, n = class {
5
+ rtcConfig;
6
+ send;
7
+ onConnectedViewersChange;
8
+ links = /* @__PURE__ */ new Map();
9
+ watchdog;
10
+ localStream;
11
+ constructor(e, n, r, i) {
12
+ this.rtcConfig = e, this.send = r, this.onConnectedViewersChange = i, this.localStream = n, this.watchdog = setInterval(() => this.prune(), t);
13
+ }
14
+ async handleSignal(t) {
15
+ let n = this.links.get(t.fromMemberId);
16
+ if (t.signalType === "OFFER" && n && n.connectionState !== "connected" && (n.close(), this.links.delete(t.fromMemberId), n = void 0), !n) {
17
+ if (t.signalType !== "OFFER") return;
18
+ let r = t.fromMemberId;
19
+ n = new e({
20
+ remoteId: r,
21
+ initiator: !1,
22
+ polite: !0,
23
+ rtcConfig: this.rtcConfig,
24
+ localStream: this.localStream,
25
+ send: (e, t) => this.send(r, e, t),
26
+ onRemoteStream: () => {},
27
+ onStateChange: () => this.report()
28
+ }), this.links.set(r, n);
29
+ }
30
+ try {
31
+ await n.handleSignal(t.signalType, t.payload);
32
+ } catch {}
33
+ }
34
+ setLocalStream(e) {
35
+ this.localStream = e;
36
+ for (let t of this.links.values()) t.setLocalStream(e);
37
+ }
38
+ dispose() {
39
+ clearInterval(this.watchdog);
40
+ for (let e of this.links.values()) e.close();
41
+ this.links.clear(), this.report();
42
+ }
43
+ prune() {
44
+ for (let [e, t] of [...this.links.entries()]) (t.connectionState === "failed" || t.connectionState === "closed") && (t.close(), this.links.delete(e));
45
+ this.report();
46
+ }
47
+ report() {
48
+ let e = 0;
49
+ for (let t of this.links.values()) t.connectionState === "connected" && (e += 1);
50
+ this.onConnectedViewersChange?.(e);
51
+ }
52
+ }, r = class {
53
+ hostId;
54
+ rtcConfig;
55
+ send;
56
+ onStream;
57
+ onState;
58
+ link = null;
59
+ watchdog;
60
+ ready = !1;
61
+ disposed = !1;
62
+ constructor(e, n, r, i, a) {
63
+ this.hostId = e, this.rtcConfig = n, this.send = r, this.onStream = i, this.onState = a, this.watchdog = setInterval(() => this.check(), t);
64
+ }
65
+ markSignalingReady() {
66
+ this.ready = !0, this.link || this.dial();
67
+ }
68
+ async handleSignal(e) {
69
+ if (!(!this.link || e.fromMemberId !== this.hostId)) try {
70
+ await this.link.handleSignal(e.signalType, e.payload);
71
+ } catch {}
72
+ }
73
+ dispose() {
74
+ this.disposed = !0, clearInterval(this.watchdog), this.link?.close(), this.link = null, this.onStream(null);
75
+ }
76
+ dial() {
77
+ this.disposed || !this.ready || (this.link?.close(), this.link = new e({
78
+ remoteId: this.hostId,
79
+ initiator: !0,
80
+ polite: !1,
81
+ receiveOnly: !0,
82
+ rtcConfig: this.rtcConfig,
83
+ send: (e, t) => this.send(this.hostId, e, t),
84
+ onRemoteStream: (e) => this.onStream(e),
85
+ onStateChange: (e) => this.onState?.(e)
86
+ }), this.onState?.("new"));
87
+ }
88
+ check() {
89
+ if (this.disposed || !this.link) return;
90
+ let e = this.link.connectionState, t = e !== "connected" && Date.now() - this.link.createdAt > 15e3;
91
+ (e === "failed" || t) && this.dial();
92
+ }
93
+ };
94
+ //#endregion
95
+ export { n as StageHost, r as StageViewer };
96
+
97
+ //# sourceMappingURL=liveStage.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"liveStage.js","names":[],"sources":["../../../../src/comradecircle/lib/rtc/liveStage.ts"],"sourcesContent":["/**\n * Live stage media (BOFF-4450): the host's browser fans its camera/screen +\n * microphone out to each viewer over a direct peer connection.\n *\n * Viewers always dial: a viewer opens a receive-only PeerLink to the host\n * once its own signal subscription is accepted, and re-dials if the link has\n * not connected within CONNECT_TIMEOUT_MS. The host only ever answers, so it\n * needs no viewer list — any authorised viewer's offer creates a link (the\n * relay already refuses signals from members who are not active viewers).\n */\nimport type { ComRtcSignal, ComVoiceSignalType } from '../../types';\nimport { PeerLink } from './peerLink';\nimport { CONNECT_TIMEOUT_MS } from './voiceMesh';\n\nconst WATCHDOG_INTERVAL_MS = 5_000;\n\ntype Send = (toMemberId: string, signalType: ComVoiceSignalType, payload: unknown) => Promise<void>;\n\nexport class StageHost {\n private readonly links = new Map<string, PeerLink>();\n private readonly watchdog: ReturnType<typeof setInterval>;\n private localStream: MediaStream;\n\n constructor(\n private readonly rtcConfig: RTCConfiguration,\n localStream: MediaStream,\n private readonly send: Send,\n private readonly onConnectedViewersChange?: (count: number) => void\n ) {\n this.localStream = localStream;\n this.watchdog = setInterval(() => this.prune(), WATCHDOG_INTERVAL_MS);\n }\n\n async handleSignal(signal: ComRtcSignal): Promise<void> {\n let link = this.links.get(signal.fromMemberId);\n if (signal.signalType === 'OFFER' && link && link.connectionState !== 'connected') {\n // A viewer re-dialled after a stalled attempt: start over.\n link.close();\n this.links.delete(signal.fromMemberId);\n link = undefined;\n }\n if (!link) {\n if (signal.signalType !== 'OFFER') return;\n const remoteId = signal.fromMemberId;\n link = new PeerLink({\n remoteId,\n initiator: false,\n polite: true,\n rtcConfig: this.rtcConfig,\n localStream: this.localStream,\n send: (type, payload) => this.send(remoteId, type, payload),\n onRemoteStream: () => {},\n onStateChange: () => this.report(),\n });\n this.links.set(remoteId, link);\n }\n try {\n await link.handleSignal(signal.signalType, signal.payload);\n } catch {\n // The viewer's watchdog re-dials.\n }\n }\n\n setLocalStream(stream: MediaStream): void {\n this.localStream = stream;\n for (const link of this.links.values()) link.setLocalStream(stream);\n }\n\n dispose(): void {\n clearInterval(this.watchdog);\n for (const link of this.links.values()) link.close();\n this.links.clear();\n this.report();\n }\n\n private prune(): void {\n for (const [id, link] of [...this.links.entries()]) {\n if (link.connectionState === 'failed' || link.connectionState === 'closed') {\n link.close();\n this.links.delete(id);\n }\n }\n this.report();\n }\n\n private report(): void {\n let connected = 0;\n for (const link of this.links.values())\n if (link.connectionState === 'connected') connected += 1;\n this.onConnectedViewersChange?.(connected);\n }\n}\n\nexport class StageViewer {\n private link: PeerLink | null = null;\n private readonly watchdog: ReturnType<typeof setInterval>;\n private ready = false;\n private disposed = false;\n\n constructor(\n private readonly hostId: string,\n private readonly rtcConfig: RTCConfiguration,\n private readonly send: Send,\n private readonly onStream: (stream: MediaStream | null) => void,\n private readonly onState?: (state: RTCPeerConnectionState) => void\n ) {\n this.watchdog = setInterval(() => this.check(), WATCHDOG_INTERVAL_MS);\n }\n\n /** Call once the viewer's signal subscription has been accepted. */\n markSignalingReady(): void {\n this.ready = true;\n if (!this.link) this.dial();\n }\n\n async handleSignal(signal: ComRtcSignal): Promise<void> {\n if (!this.link || signal.fromMemberId !== this.hostId) return;\n try {\n await this.link.handleSignal(signal.signalType, signal.payload);\n } catch {\n // Re-dialled by the watchdog.\n }\n }\n\n dispose(): void {\n this.disposed = true;\n clearInterval(this.watchdog);\n this.link?.close();\n this.link = null;\n this.onStream(null);\n }\n\n private dial(): void {\n if (this.disposed || !this.ready) return;\n this.link?.close();\n this.link = new PeerLink({\n remoteId: this.hostId,\n initiator: true,\n polite: false,\n receiveOnly: true,\n rtcConfig: this.rtcConfig,\n send: (type, payload) => this.send(this.hostId, type, payload),\n onRemoteStream: (stream) => this.onStream(stream),\n onStateChange: (state) => this.onState?.(state),\n });\n this.onState?.('new');\n }\n\n private check(): void {\n if (this.disposed || !this.link) return;\n const state = this.link.connectionState;\n const stalled = state !== 'connected' && Date.now() - this.link.createdAt > CONNECT_TIMEOUT_MS;\n if (state === 'failed' || stalled) this.dial();\n }\n}\n"],"mappings":"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"}
@@ -0,0 +1,50 @@
1
+ import { ComVoiceSignalType } from '../../types';
2
+
3
+ export type SendSignal = (signalType: ComVoiceSignalType, payload: unknown) => Promise<void>;
4
+ export interface PeerLinkOptions {
5
+ remoteId: string;
6
+ /** The polite peer yields on an offer collision. Must differ between the two sides. */
7
+ polite: boolean;
8
+ rtcConfig: RTCConfiguration;
9
+ send: SendSignal;
10
+ /** Tracks to publish. For an answering peer they are attached after the first remote offer. */
11
+ localStream?: MediaStream | null;
12
+ /** True when this side sends the first offer. */
13
+ initiator: boolean;
14
+ /** Receive-only viewer: negotiate inbound audio + video without publishing anything. */
15
+ receiveOnly?: boolean;
16
+ onRemoteStream: (stream: MediaStream) => void;
17
+ onStateChange?: (state: RTCPeerConnectionState) => void;
18
+ }
19
+ export declare class PeerLink {
20
+ readonly remoteId: string;
21
+ readonly initiator: boolean;
22
+ readonly createdAt: number;
23
+ private readonly pc;
24
+ private readonly polite;
25
+ private readonly send;
26
+ private readonly onRemoteStream;
27
+ private localStream;
28
+ private tracksAttached;
29
+ private makingOffer;
30
+ private ignoreOffer;
31
+ private pendingCandidates;
32
+ private remoteStream;
33
+ private closed;
34
+ constructor(options: PeerLinkOptions);
35
+ get connectionState(): RTCPeerConnectionState;
36
+ /** Publish (or re-publish) the local stream on this connection. */
37
+ setLocalStream(stream: MediaStream | null): void;
38
+ private attachLocalTracks;
39
+ private negotiate;
40
+ /** Apply a relayed signal from the remote peer. */
41
+ handleSignal(signalType: ComVoiceSignalType, payload: unknown): Promise<void>;
42
+ close(): void;
43
+ }
44
+ /** Convert the server's ICE server list into the browser RTCConfiguration shape. */
45
+ export declare function toRtcConfiguration(iceServers: Array<{
46
+ urls: string[];
47
+ username?: string | null;
48
+ credential?: string | null;
49
+ }>): RTCConfiguration;
50
+ //# sourceMappingURL=peerLink.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"peerLink.d.ts","sourceRoot":"","sources":["../../../../src/comradecircle/lib/rtc/peerLink.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AACH,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,aAAa,CAAC;AAEtD,MAAM,MAAM,UAAU,GAAG,CAAC,UAAU,EAAE,kBAAkB,EAAE,OAAO,EAAE,OAAO,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;AAE7F,MAAM,WAAW,eAAe;IAC9B,QAAQ,EAAE,MAAM,CAAC;IACjB,uFAAuF;IACvF,MAAM,EAAE,OAAO,CAAC;IAChB,SAAS,EAAE,gBAAgB,CAAC;IAC5B,IAAI,EAAE,UAAU,CAAC;IACjB,+FAA+F;IAC/F,WAAW,CAAC,EAAE,WAAW,GAAG,IAAI,CAAC;IACjC,iDAAiD;IACjD,SAAS,EAAE,OAAO,CAAC;IACnB,wFAAwF;IACxF,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB,cAAc,EAAE,CAAC,MAAM,EAAE,WAAW,KAAK,IAAI,CAAC;IAC9C,aAAa,CAAC,EAAE,CAAC,KAAK,EAAE,sBAAsB,KAAK,IAAI,CAAC;CACzD;AAmBD,qBAAa,QAAQ;IACnB,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,SAAS,EAAE,OAAO,CAAC;IAC5B,QAAQ,CAAC,SAAS,SAAc;IAChC,OAAO,CAAC,QAAQ,CAAC,EAAE,CAAoB;IACvC,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAU;IACjC,OAAO,CAAC,QAAQ,CAAC,IAAI,CAAa;IAClC,OAAO,CAAC,QAAQ,CAAC,cAAc,CAAgC;IAC/D,OAAO,CAAC,WAAW,CAAqB;IACxC,OAAO,CAAC,cAAc,CAAS;IAC/B,OAAO,CAAC,WAAW,CAAS;IAC5B,OAAO,CAAC,WAAW,CAAS;IAC5B,OAAO,CAAC,iBAAiB,CAA6B;IACtD,OAAO,CAAC,YAAY,CAA4B;IAChD,OAAO,CAAC,MAAM,CAAS;gBAEX,OAAO,EAAE,eAAe;IAwCpC,IAAI,eAAe,IAAI,sBAAsB,CAE5C;IAED,mEAAmE;IACnE,cAAc,CAAC,MAAM,EAAE,WAAW,GAAG,IAAI,GAAG,IAAI;IAQhD,OAAO,CAAC,iBAAiB;YA+BX,SAAS;IAevB,mDAAmD;IAC7C,YAAY,CAAC,UAAU,EAAE,kBAAkB,EAAE,OAAO,EAAE,OAAO,GAAG,OAAO,CAAC,IAAI,CAAC;IA2CnF,KAAK,IAAI,IAAI;CASd;AAED,oFAAoF;AACpF,wBAAgB,kBAAkB,CAChC,UAAU,EAAE,KAAK,CAAC;IAAE,IAAI,EAAE,MAAM,EAAE,CAAC;IAAC,QAAQ,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAAC,UAAU,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;CAAE,CAAC,GAC1F,gBAAgB,CAQlB"}
@@ -0,0 +1,110 @@
1
+ //#region src/comradecircle/lib/rtc/peerLink.ts
2
+ function e(e) {
3
+ if (!e || typeof e != "object") return null;
4
+ let t = e.description;
5
+ if (!t || typeof t != "object") return null;
6
+ let { type: n, sdp: r } = t;
7
+ return n !== "offer" && n !== "answer" || typeof r != "string" ? null : {
8
+ type: n,
9
+ sdp: r
10
+ };
11
+ }
12
+ function t(e) {
13
+ if (!e || typeof e != "object") return null;
14
+ let t = e.candidate;
15
+ if (!t || typeof t != "object") return null;
16
+ let n = t;
17
+ return typeof n.candidate == "string" ? n : null;
18
+ }
19
+ var n = class {
20
+ remoteId;
21
+ initiator;
22
+ createdAt = Date.now();
23
+ pc;
24
+ polite;
25
+ send;
26
+ onRemoteStream;
27
+ localStream;
28
+ tracksAttached = !1;
29
+ makingOffer = !1;
30
+ ignoreOffer = !1;
31
+ pendingCandidates = [];
32
+ remoteStream = null;
33
+ closed = !1;
34
+ constructor(e) {
35
+ this.remoteId = e.remoteId, this.initiator = e.initiator, this.polite = e.polite, this.send = e.send, this.onRemoteStream = e.onRemoteStream, this.localStream = e.localStream ?? null, this.pc = new RTCPeerConnection(e.rtcConfig), this.pc.onnegotiationneeded = () => {
36
+ this.negotiate();
37
+ }, this.pc.onicecandidate = ({ candidate: e }) => {
38
+ e && this.send("ICE_CANDIDATE", { candidate: e.toJSON() }).catch(() => {});
39
+ }, this.pc.ontrack = (e) => {
40
+ let t = (this.remoteStream?.getTracks() ?? []).filter((t) => t !== e.track && !(t.kind === e.track.kind && t.readyState === "ended")), n = new MediaStream([...t, e.track]);
41
+ this.remoteStream = n, this.onRemoteStream(n);
42
+ }, this.pc.onconnectionstatechange = () => {
43
+ e.onStateChange?.(this.pc.connectionState);
44
+ }, e.receiveOnly ? (this.pc.addTransceiver("audio", { direction: "recvonly" }), this.pc.addTransceiver("video", { direction: "recvonly" })) : e.initiator && this.attachLocalTracks();
45
+ }
46
+ get connectionState() {
47
+ return this.pc.connectionState;
48
+ }
49
+ setLocalStream(e) {
50
+ this.localStream = e, (this.tracksAttached || this.initiator) && (this.tracksAttached = !1, this.attachLocalTracks());
51
+ }
52
+ attachLocalTracks() {
53
+ if (this.closed || this.tracksAttached) return;
54
+ this.tracksAttached = !0;
55
+ let e = this.localStream, t = e?.getTracks() ?? [], n = this.pc.getTransceivers();
56
+ for (let r of t) {
57
+ if (n.some((e) => e.sender.track === r)) continue;
58
+ let t = n.find((e) => e.receiver.track.kind === r.kind && (e.direction === "sendrecv" || e.direction === "sendonly") && (e.sender.track === null || e.sender.track.kind === r.kind));
59
+ t ? t.sender.replaceTrack(r).catch(() => {}) : e && this.pc.addTrack(r, e);
60
+ }
61
+ for (let e of n) e.sender.track && !t.includes(e.sender.track) && e.sender.replaceTrack(null).catch(() => {});
62
+ }
63
+ async negotiate() {
64
+ if (!this.closed) try {
65
+ this.makingOffer = !0, await this.pc.setLocalDescription(), this.pc.localDescription && await this.send("OFFER", { description: this.pc.localDescription.toJSON() });
66
+ } catch {} finally {
67
+ this.makingOffer = !1;
68
+ }
69
+ }
70
+ async handleSignal(n, r) {
71
+ if (this.closed) return;
72
+ if (n === "ICE_CANDIDATE") {
73
+ let e = t(r);
74
+ if (!e) return;
75
+ if (!this.pc.remoteDescription) {
76
+ this.pendingCandidates.push(e);
77
+ return;
78
+ }
79
+ try {
80
+ await this.pc.addIceCandidate(e);
81
+ } catch {}
82
+ return;
83
+ }
84
+ let i = e(r);
85
+ if (!i) return;
86
+ let a = i.type === "offer" && (this.makingOffer || this.pc.signalingState !== "stable");
87
+ if (this.ignoreOffer = !this.polite && a, this.ignoreOffer) return;
88
+ await this.pc.setRemoteDescription(i);
89
+ let o = this.pendingCandidates;
90
+ this.pendingCandidates = [];
91
+ for (let e of o) try {
92
+ await this.pc.addIceCandidate(e);
93
+ } catch {}
94
+ i.type === "offer" && (this.attachLocalTracks(), await this.pc.setLocalDescription(), this.pc.localDescription && await this.send("ANSWER", { description: this.pc.localDescription.toJSON() }));
95
+ }
96
+ close() {
97
+ this.closed || (this.closed = !0, this.pc.onnegotiationneeded = null, this.pc.onicecandidate = null, this.pc.ontrack = null, this.pc.onconnectionstatechange = null, this.pc.close());
98
+ }
99
+ };
100
+ function r(e) {
101
+ return { iceServers: e.map((e) => ({
102
+ urls: e.urls,
103
+ ...e.username ? { username: e.username } : {},
104
+ ...e.credential ? { credential: e.credential } : {}
105
+ })) };
106
+ }
107
+ //#endregion
108
+ export { n as PeerLink, r as toRtcConfiguration };
109
+
110
+ //# sourceMappingURL=peerLink.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"peerLink.js","names":[],"sources":["../../../../src/comradecircle/lib/rtc/peerLink.ts"],"sourcesContent":["/**\n * PeerLink — one WebRTC peer connection negotiated over the ComradeCircle\n * signaling relay (BOFF-4450).\n *\n * Implements the W3C \"perfect negotiation\" pattern so either side may\n * (re)negotiate at any time — e.g. when a participant turns their camera on —\n * and offer collisions resolve deterministically: the polite peer rolls back\n * its own offer, the impolite peer ignores the colliding one. Signals carry\n * `{ description }` for OFFER/ANSWER and `{ candidate }` for ICE_CANDIDATE.\n *\n * ICE candidates that arrive before the remote description (the relay does\n * not guarantee ordering across separate HTTP requests) are queued and\n * flushed once the description is applied.\n */\nimport type { ComVoiceSignalType } from '../../types';\n\nexport type SendSignal = (signalType: ComVoiceSignalType, payload: unknown) => Promise<void>;\n\nexport interface PeerLinkOptions {\n remoteId: string;\n /** The polite peer yields on an offer collision. Must differ between the two sides. */\n polite: boolean;\n rtcConfig: RTCConfiguration;\n send: SendSignal;\n /** Tracks to publish. For an answering peer they are attached after the first remote offer. */\n localStream?: MediaStream | null;\n /** True when this side sends the first offer. */\n initiator: boolean;\n /** Receive-only viewer: negotiate inbound audio + video without publishing anything. */\n receiveOnly?: boolean;\n onRemoteStream: (stream: MediaStream) => void;\n onStateChange?: (state: RTCPeerConnectionState) => void;\n}\n\nfunction readDescription(payload: unknown): RTCSessionDescriptionInit | null {\n if (!payload || typeof payload !== 'object') return null;\n const description = (payload as { description?: unknown }).description;\n if (!description || typeof description !== 'object') return null;\n const { type, sdp } = description as { type?: unknown; sdp?: unknown };\n if ((type !== 'offer' && type !== 'answer') || typeof sdp !== 'string') return null;\n return { type, sdp };\n}\n\nfunction readCandidate(payload: unknown): RTCIceCandidateInit | null {\n if (!payload || typeof payload !== 'object') return null;\n const candidate = (payload as { candidate?: unknown }).candidate;\n if (!candidate || typeof candidate !== 'object') return null;\n const c = candidate as RTCIceCandidateInit;\n return typeof c.candidate === 'string' ? c : null;\n}\n\nexport class PeerLink {\n readonly remoteId: string;\n readonly initiator: boolean;\n readonly createdAt = Date.now();\n private readonly pc: RTCPeerConnection;\n private readonly polite: boolean;\n private readonly send: SendSignal;\n private readonly onRemoteStream: (stream: MediaStream) => void;\n private localStream: MediaStream | null;\n private tracksAttached = false;\n private makingOffer = false;\n private ignoreOffer = false;\n private pendingCandidates: RTCIceCandidateInit[] = [];\n private remoteStream: MediaStream | null = null;\n private closed = false;\n\n constructor(options: PeerLinkOptions) {\n this.remoteId = options.remoteId;\n this.initiator = options.initiator;\n this.polite = options.polite;\n this.send = options.send;\n this.onRemoteStream = options.onRemoteStream;\n this.localStream = options.localStream ?? null;\n this.pc = new RTCPeerConnection(options.rtcConfig);\n\n this.pc.onnegotiationneeded = () => {\n void this.negotiate();\n };\n this.pc.onicecandidate = ({ candidate }) => {\n if (candidate)\n void this.send('ICE_CANDIDATE', { candidate: candidate.toJSON() }).catch(() => {});\n };\n this.pc.ontrack = (event) => {\n // Aggregate every inbound track into one stream per peer. A renegotiated\n // track (e.g. the remote turning their camera on) can arrive under a new\n // msid stream; replacing the stream would silently drop the audio.\n // A fresh MediaStream per change gives React consumers a new identity.\n const kept = (this.remoteStream?.getTracks() ?? []).filter(\n (t) => t !== event.track && !(t.kind === event.track.kind && t.readyState === 'ended')\n );\n const stream = new MediaStream([...kept, event.track]);\n this.remoteStream = stream;\n this.onRemoteStream(stream);\n };\n this.pc.onconnectionstatechange = () => {\n options.onStateChange?.(this.pc.connectionState);\n };\n\n if (options.receiveOnly) {\n this.pc.addTransceiver('audio', { direction: 'recvonly' });\n this.pc.addTransceiver('video', { direction: 'recvonly' });\n } else if (options.initiator) {\n this.attachLocalTracks();\n }\n }\n\n get connectionState(): RTCPeerConnectionState {\n return this.pc.connectionState;\n }\n\n /** Publish (or re-publish) the local stream on this connection. */\n setLocalStream(stream: MediaStream | null): void {\n this.localStream = stream;\n if (this.tracksAttached || this.initiator) {\n this.tracksAttached = false;\n this.attachLocalTracks();\n }\n }\n\n private attachLocalTracks(): void {\n if (this.closed || this.tracksAttached) return;\n this.tracksAttached = true;\n const stream = this.localStream;\n const localTracks = stream?.getTracks() ?? [];\n const transceivers = this.pc.getTransceivers();\n for (const track of localTracks) {\n if (transceivers.some((t) => t.sender.track === track)) continue;\n // Re-use a sending transceiver of the same kind (e.g. the camera was\n // turned off and on again) so toggling never piles up m-lines.\n const reusable = transceivers.find(\n (t) =>\n t.receiver.track.kind === track.kind &&\n (t.direction === 'sendrecv' || t.direction === 'sendonly') &&\n (t.sender.track === null || t.sender.track.kind === track.kind)\n );\n if (reusable) {\n void reusable.sender.replaceTrack(track).catch(() => {});\n } else if (stream) {\n // addTrack also adopts an unused transceiver created by a remote offer.\n this.pc.addTrack(track, stream);\n }\n }\n // Stop sending tracks the local stream no longer carries (camera off).\n for (const t of transceivers) {\n if (t.sender.track && !localTracks.includes(t.sender.track)) {\n void t.sender.replaceTrack(null).catch(() => {});\n }\n }\n }\n\n private async negotiate(): Promise<void> {\n if (this.closed) return;\n try {\n this.makingOffer = true;\n await this.pc.setLocalDescription();\n if (this.pc.localDescription) {\n await this.send('OFFER', { description: this.pc.localDescription.toJSON() });\n }\n } catch {\n // A failed offer is retried by the caller's connection watchdog.\n } finally {\n this.makingOffer = false;\n }\n }\n\n /** Apply a relayed signal from the remote peer. */\n async handleSignal(signalType: ComVoiceSignalType, payload: unknown): Promise<void> {\n if (this.closed) return;\n if (signalType === 'ICE_CANDIDATE') {\n const candidate = readCandidate(payload);\n if (!candidate) return;\n if (!this.pc.remoteDescription) {\n this.pendingCandidates.push(candidate);\n return;\n }\n try {\n await this.pc.addIceCandidate(candidate);\n } catch {\n // Candidates for an ignored offer are expected to fail.\n }\n return;\n }\n\n const description = readDescription(payload);\n if (!description) return;\n const offerCollision =\n description.type === 'offer' && (this.makingOffer || this.pc.signalingState !== 'stable');\n this.ignoreOffer = !this.polite && offerCollision;\n if (this.ignoreOffer) return;\n\n await this.pc.setRemoteDescription(description);\n const queued = this.pendingCandidates;\n this.pendingCandidates = [];\n for (const candidate of queued) {\n try {\n await this.pc.addIceCandidate(candidate);\n } catch {\n // ignore stale candidates\n }\n }\n if (description.type === 'offer') {\n this.attachLocalTracks();\n await this.pc.setLocalDescription();\n if (this.pc.localDescription) {\n await this.send('ANSWER', { description: this.pc.localDescription.toJSON() });\n }\n }\n }\n\n close(): void {\n if (this.closed) return;\n this.closed = true;\n this.pc.onnegotiationneeded = null;\n this.pc.onicecandidate = null;\n this.pc.ontrack = null;\n this.pc.onconnectionstatechange = null;\n this.pc.close();\n }\n}\n\n/** Convert the server's ICE server list into the browser RTCConfiguration shape. */\nexport function toRtcConfiguration(\n iceServers: Array<{ urls: string[]; username?: string | null; credential?: string | null }>\n): RTCConfiguration {\n return {\n iceServers: iceServers.map((s) => ({\n urls: s.urls,\n ...(s.username ? { username: s.username } : {}),\n ...(s.credential ? { credential: s.credential } : {}),\n })),\n };\n}\n"],"mappings":";AAkCA,SAAS,EAAgB,GAAoD;CAC3E,IAAI,CAAC,KAAW,OAAO,KAAY,UAAU,OAAO;CACpD,IAAM,IAAe,EAAsC;CAC3D,IAAI,CAAC,KAAe,OAAO,KAAgB,UAAU,OAAO;CAC5D,IAAM,EAAE,SAAM,WAAQ;CAEtB,OADK,MAAS,WAAW,MAAS,YAAa,OAAO,KAAQ,WAAiB,OACxE;EAAE;EAAM;CAAI;AACrB;AAEA,SAAS,EAAc,GAA8C;CACnE,IAAI,CAAC,KAAW,OAAO,KAAY,UAAU,OAAO;CACpD,IAAM,IAAa,EAAoC;CACvD,IAAI,CAAC,KAAa,OAAO,KAAc,UAAU,OAAO;CACxD,IAAM,IAAI;CACV,OAAO,OAAO,EAAE,aAAc,WAAW,IAAI;AAC/C;AAEA,IAAa,IAAb,MAAsB;CACpB;CACA;CACA,YAAqB,KAAK,IAAI;CAC9B;CACA;CACA;CACA;CACA;CACA,iBAAyB;CACzB,cAAsB;CACtB,cAAsB;CACtB,oBAAmD,CAAC;CACpD,eAA2C;CAC3C,SAAiB;CAEjB,YAAY,GAA0B;EAgCpC,AA/BA,KAAK,WAAW,EAAQ,UACxB,KAAK,YAAY,EAAQ,WACzB,KAAK,SAAS,EAAQ,QACtB,KAAK,OAAO,EAAQ,MACpB,KAAK,iBAAiB,EAAQ,gBAC9B,KAAK,cAAc,EAAQ,eAAe,MAC1C,KAAK,KAAK,IAAI,kBAAkB,EAAQ,SAAS,GAEjD,KAAK,GAAG,4BAA4B;GAClC,KAAU,UAAU;EACtB,GACA,KAAK,GAAG,kBAAkB,EAAE,mBAAgB;GAC1C,AAAI,KACF,KAAU,KAAK,iBAAiB,EAAE,WAAW,EAAU,OAAO,EAAE,CAAC,EAAE,YAAY,CAAC,CAAC;EACrF,GACA,KAAK,GAAG,WAAW,MAAU;GAK3B,IAAM,KAAQ,KAAK,cAAc,UAAU,KAAK,CAAC,GAAG,QACjD,MAAM,MAAM,EAAM,SAAS,EAAE,EAAE,SAAS,EAAM,MAAM,QAAQ,EAAE,eAAe,QAChF,GACM,IAAS,IAAI,YAAY,CAAC,GAAG,GAAM,EAAM,KAAK,CAAC;GAErD,AADA,KAAK,eAAe,GACpB,KAAK,eAAe,CAAM;EAC5B,GACA,KAAK,GAAG,gCAAgC;GACtC,EAAQ,gBAAgB,KAAK,GAAG,eAAe;EACjD,GAEI,EAAQ,eACV,KAAK,GAAG,eAAe,SAAS,EAAE,WAAW,WAAW,CAAC,GACzD,KAAK,GAAG,eAAe,SAAS,EAAE,WAAW,WAAW,CAAC,KAChD,EAAQ,aACjB,KAAK,kBAAkB;CAE3B;CAEA,IAAI,kBAA0C;EAC5C,OAAO,KAAK,GAAG;CACjB;CAGA,eAAe,GAAkC;EAE/C,AADA,KAAK,cAAc,IACf,KAAK,kBAAkB,KAAK,eAC9B,KAAK,iBAAiB,IACtB,KAAK,kBAAkB;CAE3B;CAEA,oBAAkC;EAChC,IAAI,KAAK,UAAU,KAAK,gBAAgB;EACxC,KAAK,iBAAiB;EACtB,IAAM,IAAS,KAAK,aACd,IAAc,GAAQ,UAAU,KAAK,CAAC,GACtC,IAAe,KAAK,GAAG,gBAAgB;EAC7C,KAAK,IAAM,KAAS,GAAa;GAC/B,IAAI,EAAa,MAAM,MAAM,EAAE,OAAO,UAAU,CAAK,GAAG;GAGxD,IAAM,IAAW,EAAa,MAC3B,MACC,EAAE,SAAS,MAAM,SAAS,EAAM,SAC/B,EAAE,cAAc,cAAc,EAAE,cAAc,gBAC9C,EAAE,OAAO,UAAU,QAAQ,EAAE,OAAO,MAAM,SAAS,EAAM,KAC9D;GACA,AAAI,IACF,EAAc,OAAO,aAAa,CAAK,EAAE,YAAY,CAAC,CAAC,IAC9C,KAET,KAAK,GAAG,SAAS,GAAO,CAAM;EAElC;EAEA,KAAK,IAAM,KAAK,GACd,AAAI,EAAE,OAAO,SAAS,CAAC,EAAY,SAAS,EAAE,OAAO,KAAK,KACxD,EAAO,OAAO,aAAa,IAAI,EAAE,YAAY,CAAC,CAAC;CAGrD;CAEA,MAAc,YAA2B;EACnC,UAAK,QACT,IAAI;GAGF,AAFA,KAAK,cAAc,IACnB,MAAM,KAAK,GAAG,oBAAoB,GAC9B,KAAK,GAAG,oBACV,MAAM,KAAK,KAAK,SAAS,EAAE,aAAa,KAAK,GAAG,iBAAiB,OAAO,EAAE,CAAC;EAE/E,QAAQ,CAER,UAAU;GACR,KAAK,cAAc;EACrB;CACF;CAGA,MAAM,aAAa,GAAgC,GAAiC;EAClF,IAAI,KAAK,QAAQ;EACjB,IAAI,MAAe,iBAAiB;GAClC,IAAM,IAAY,EAAc,CAAO;GACvC,IAAI,CAAC,GAAW;GAChB,IAAI,CAAC,KAAK,GAAG,mBAAmB;IAC9B,KAAK,kBAAkB,KAAK,CAAS;IACrC;GACF;GACA,IAAI;IACF,MAAM,KAAK,GAAG,gBAAgB,CAAS;GACzC,QAAQ,CAER;GACA;EACF;EAEA,IAAM,IAAc,EAAgB,CAAO;EAC3C,IAAI,CAAC,GAAa;EAClB,IAAM,IACJ,EAAY,SAAS,YAAY,KAAK,eAAe,KAAK,GAAG,mBAAmB;EAElF,IADA,KAAK,cAAc,CAAC,KAAK,UAAU,GAC/B,KAAK,aAAa;EAEtB,MAAM,KAAK,GAAG,qBAAqB,CAAW;EAC9C,IAAM,IAAS,KAAK;EACpB,KAAK,oBAAoB,CAAC;EAC1B,KAAK,IAAM,KAAa,GACtB,IAAI;GACF,MAAM,KAAK,GAAG,gBAAgB,CAAS;EACzC,QAAQ,CAER;EAEF,AAAI,EAAY,SAAS,YACvB,KAAK,kBAAkB,GACvB,MAAM,KAAK,GAAG,oBAAoB,GAC9B,KAAK,GAAG,oBACV,MAAM,KAAK,KAAK,UAAU,EAAE,aAAa,KAAK,GAAG,iBAAiB,OAAO,EAAE,CAAC;CAGlF;CAEA,QAAc;EACR,KAAK,WACT,KAAK,SAAS,IACd,KAAK,GAAG,sBAAsB,MAC9B,KAAK,GAAG,iBAAiB,MACzB,KAAK,GAAG,UAAU,MAClB,KAAK,GAAG,0BAA0B,MAClC,KAAK,GAAG,MAAM;CAChB;AACF;AAGA,SAAgB,EACd,GACkB;CAClB,OAAO,EACL,YAAY,EAAW,KAAK,OAAO;EACjC,MAAM,EAAE;EACR,GAAI,EAAE,WAAW,EAAE,UAAU,EAAE,SAAS,IAAI,CAAC;EAC7C,GAAI,EAAE,aAAa,EAAE,YAAY,EAAE,WAAW,IAAI,CAAC;CACrD,EAAE,EACJ;AACF"}
@@ -0,0 +1,41 @@
1
+ import { ComRtcSignal, ComVoiceParticipant, ComVoiceSignalType } from '../../types';
2
+
3
+ export declare const CONNECT_TIMEOUT_MS = 15000;
4
+ export interface VoiceMeshOptions {
5
+ selfId: string;
6
+ rtcConfig: RTCConfiguration;
7
+ localStream: MediaStream | null;
8
+ sendSignal: (toMemberId: string, signalType: ComVoiceSignalType, payload: unknown) => Promise<void>;
9
+ onRemoteStreamsChange: (streams: Map<string, MediaStream>) => void;
10
+ onPeerStatesChange?: (states: Map<string, RTCPeerConnectionState>) => void;
11
+ }
12
+ /** True when `self` must dial `remote`: self joined later (ties broken by id). */
13
+ export declare function shouldInitiate(self: ComVoiceParticipant, remote: ComVoiceParticipant): boolean;
14
+ export declare class VoiceMesh {
15
+ private readonly options;
16
+ private readonly links;
17
+ private readonly streams;
18
+ private readonly states;
19
+ private readonly disconnectedSince;
20
+ private roster;
21
+ private localStream;
22
+ private signalingReady;
23
+ private watchdog;
24
+ private disposed;
25
+ constructor(options: VoiceMeshOptions);
26
+ /** Call once the signal subscription has been accepted by the gateway. */
27
+ markSignalingReady(): void;
28
+ /** Apply the latest active roster (from the join response, heartbeat or roster subscription). */
29
+ updateRoster(participants: ComVoiceParticipant[]): void;
30
+ /** Route an inbound relayed signal to its link, creating an answering link on a first offer. */
31
+ handleSignal(signal: ComRtcSignal): Promise<void>;
32
+ setLocalStream(stream: MediaStream | null): void;
33
+ dispose(): void;
34
+ private self;
35
+ private dialPending;
36
+ private createLink;
37
+ private dropLink;
38
+ private checkLinks;
39
+ private emit;
40
+ }
41
+ //# sourceMappingURL=voiceMesh.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"voiceMesh.d.ts","sourceRoot":"","sources":["../../../../src/comradecircle/lib/rtc/voiceMesh.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AACH,OAAO,KAAK,EAAE,YAAY,EAAE,mBAAmB,EAAE,kBAAkB,EAAE,MAAM,aAAa,CAAC;AAGzF,eAAO,MAAM,kBAAkB,QAAS,CAAC;AAGzC,MAAM,WAAW,gBAAgB;IAC/B,MAAM,EAAE,MAAM,CAAC;IACf,SAAS,EAAE,gBAAgB,CAAC;IAC5B,WAAW,EAAE,WAAW,GAAG,IAAI,CAAC;IAChC,UAAU,EAAE,CACV,UAAU,EAAE,MAAM,EAClB,UAAU,EAAE,kBAAkB,EAC9B,OAAO,EAAE,OAAO,KACb,OAAO,CAAC,IAAI,CAAC,CAAC;IACnB,qBAAqB,EAAE,CAAC,OAAO,EAAE,GAAG,CAAC,MAAM,EAAE,WAAW,CAAC,KAAK,IAAI,CAAC;IACnE,kBAAkB,CAAC,EAAE,CAAC,MAAM,EAAE,GAAG,CAAC,MAAM,EAAE,sBAAsB,CAAC,KAAK,IAAI,CAAC;CAC5E;AAED,kFAAkF;AAClF,wBAAgB,cAAc,CAAC,IAAI,EAAE,mBAAmB,EAAE,MAAM,EAAE,mBAAmB,GAAG,OAAO,CAK9F;AAED,qBAAa,SAAS;IACpB,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAmB;IAC3C,OAAO,CAAC,QAAQ,CAAC,KAAK,CAA+B;IACrD,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAkC;IAC1D,OAAO,CAAC,QAAQ,CAAC,MAAM,CAA6C;IACpE,OAAO,CAAC,QAAQ,CAAC,iBAAiB,CAA6B;IAC/D,OAAO,CAAC,MAAM,CAA6B;IAC3C,OAAO,CAAC,WAAW,CAAqB;IACxC,OAAO,CAAC,cAAc,CAAS;IAC/B,OAAO,CAAC,QAAQ,CAA+C;IAC/D,OAAO,CAAC,QAAQ,CAAS;gBAEb,OAAO,EAAE,gBAAgB;IAMrC,0EAA0E;IAC1E,kBAAkB,IAAI,IAAI;IAK1B,iGAAiG;IACjG,YAAY,CAAC,YAAY,EAAE,mBAAmB,EAAE,GAAG,IAAI;IAUvD,gGAAgG;IAC1F,YAAY,CAAC,MAAM,EAAE,YAAY,GAAG,OAAO,CAAC,IAAI,CAAC;IAcvD,cAAc,CAAC,MAAM,EAAE,WAAW,GAAG,IAAI,GAAG,IAAI;IAKhD,OAAO,IAAI,IAAI;IAOf,OAAO,CAAC,IAAI;IAIZ,OAAO,CAAC,WAAW;IAUnB,OAAO,CAAC,UAAU;IAuBlB,OAAO,CAAC,QAAQ;IAWhB,OAAO,CAAC,UAAU;IAwBlB,OAAO,CAAC,IAAI;CAGb"}
@@ -0,0 +1,101 @@
1
+ import { PeerLink as e } from "./peerLink.js";
2
+ //#region src/comradecircle/lib/rtc/voiceMesh.ts
3
+ var t = 15e3, n = 5e3;
4
+ function r(e, t) {
5
+ let n = Date.parse(e.joinedAt), r = Date.parse(t.joinedAt);
6
+ return n === r ? e.memberId > t.memberId : n > r;
7
+ }
8
+ var i = class {
9
+ options;
10
+ links = /* @__PURE__ */ new Map();
11
+ streams = /* @__PURE__ */ new Map();
12
+ states = /* @__PURE__ */ new Map();
13
+ disconnectedSince = /* @__PURE__ */ new Map();
14
+ roster = [];
15
+ localStream;
16
+ signalingReady = !1;
17
+ watchdog = null;
18
+ disposed = !1;
19
+ constructor(e) {
20
+ this.options = e, this.localStream = e.localStream, this.watchdog = setInterval(() => this.checkLinks(), n);
21
+ }
22
+ markSignalingReady() {
23
+ this.signalingReady = !0, this.dialPending();
24
+ }
25
+ updateRoster(e) {
26
+ if (this.disposed) return;
27
+ this.roster = e.filter((e) => !e.leftAt);
28
+ let t = new Set(this.roster.map((e) => e.memberId));
29
+ for (let e of [...this.links.keys()]) t.has(e) || this.dropLink(e);
30
+ this.dialPending();
31
+ }
32
+ async handleSignal(e) {
33
+ if (this.disposed || e.fromMemberId === this.options.selfId) return;
34
+ let t = this.links.get(e.fromMemberId);
35
+ if (!t) {
36
+ if (e.signalType !== "OFFER") return;
37
+ t = this.createLink(e.fromMemberId, !1);
38
+ }
39
+ try {
40
+ await t.handleSignal(e.signalType, e.payload);
41
+ } catch {}
42
+ }
43
+ setLocalStream(e) {
44
+ this.localStream = e;
45
+ for (let t of this.links.values()) t.setLocalStream(e);
46
+ }
47
+ dispose() {
48
+ this.disposed = !0, this.watchdog && clearInterval(this.watchdog);
49
+ for (let e of [...this.links.keys()]) this.dropLink(e, !1);
50
+ this.emit();
51
+ }
52
+ self() {
53
+ return this.roster.find((e) => e.memberId === this.options.selfId);
54
+ }
55
+ dialPending() {
56
+ if (!this.signalingReady || this.disposed) return;
57
+ let e = this.self();
58
+ if (e) for (let t of this.roster) t.memberId === e.memberId || this.links.has(t.memberId) || r(e, t) && this.createLink(t.memberId, !0);
59
+ }
60
+ createLink(t, n) {
61
+ let r = new e({
62
+ remoteId: t,
63
+ initiator: n,
64
+ polite: this.options.selfId > t,
65
+ rtcConfig: this.options.rtcConfig,
66
+ localStream: this.localStream,
67
+ send: (e, n) => this.options.sendSignal(t, e, n),
68
+ onRemoteStream: (e) => {
69
+ this.streams.set(t, e), this.emit();
70
+ },
71
+ onStateChange: (e) => {
72
+ this.states.set(t, e), this.options.onPeerStatesChange?.(new Map(this.states));
73
+ }
74
+ });
75
+ return this.links.set(t, r), this.states.set(t, "new"), this.options.onPeerStatesChange?.(new Map(this.states)), r;
76
+ }
77
+ dropLink(e, t = !0) {
78
+ this.links.get(e)?.close(), this.links.delete(e), this.streams.delete(e), this.states.delete(e), t && (this.emit(), this.options.onPeerStatesChange?.(new Map(this.states)));
79
+ }
80
+ checkLinks() {
81
+ if (this.disposed) return;
82
+ let e = Date.now();
83
+ for (let [n, r] of [...this.links.entries()]) {
84
+ let i = r.connectionState;
85
+ i === "disconnected" ? this.disconnectedSince.has(n) || this.disconnectedSince.set(n, e) : this.disconnectedSince.delete(n);
86
+ let a = (i === "new" || i === "connecting") && e - r.createdAt > 15e3, o = e - (this.disconnectedSince.get(n) ?? e) > t;
87
+ if (i === "failed" || i === "closed" || a || o) {
88
+ this.disconnectedSince.delete(n);
89
+ let e = r.initiator;
90
+ this.dropLink(n), e && this.dialPending();
91
+ }
92
+ }
93
+ }
94
+ emit() {
95
+ this.options.onRemoteStreamsChange(new Map(this.streams));
96
+ }
97
+ };
98
+ //#endregion
99
+ export { t as CONNECT_TIMEOUT_MS, i as VoiceMesh };
100
+
101
+ //# sourceMappingURL=voiceMesh.js.map