@dobuki/hello-worker 1.0.48 → 1.0.50
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.
- package/dist/enter-world.d.ts.map +1 -1
- package/dist/enter-world.js +2 -2
- package/dist/enter-world.js.map +5 -5
- package/dist/impl/signal-room.d.ts.map +1 -1
- package/dist/index.js +2 -2
- package/dist/index.js.map +5 -5
- package/dist/signal-room.js +2 -2
- package/dist/signal-room.js.map +3 -3
- package/dist/signal-room.worker.js +2 -2
- package/dist/signal-room.worker.js.map +3 -3
- package/dist/webrtc-peer-collector.d.ts.map +1 -1
- package/dist/webrtc-peer-collector.js +2 -2
- package/dist/webrtc-peer-collector.js.map +4 -4
- package/package.json +1 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"enter-world.d.ts","sourceRoot":"","sources":["../src/browser/enter-world.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAa,MAAM,eAAe,CAAC;AACrD,OAAO,EACL,OAAO,EACP,UAAU,EAEX,MAAM,yBAAyB,CAAC;AAEjC,KAAK,YAAY,GAAG,CAClB,IAAI,EAAE,MAAM,EACZ,MAAM,EAAE,MAAM,GAAG,OAAO,EACxB,KAAK,EAAE,MAAM,EAAE,KACZ,IAAI,CAAC;AAEV,wBAAgB,UAAU,CAAC,CAAC,SAAS,MAAM,GAAG,UAAU,EAAE,EACxD,KAAK,EACL,OAAuB,EACvB,iBAA6B,EAC7B,sBAAsB,EACtB,SAAS,EACT,WAAW,EACX,WAAW,EACX,kBAAkB,GACnB,EAAE;IACD,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,CAAC,EAAE,CAAC,SAAS,EAAE,MAAM,EAAE,GAAG,CAAC,EAAE,GAAG,KAAK,IAAI,CAAC;IACjD,iBAAiB,CAAC,EAAE,SAAS,CAAC,OAAO,EAAE,UAAU,CAAC,CAAC;IACnD,sBAAsB,CAAC,EAAE,MAAM,CAAC;IAChC,SAAS,CAAC,EAAE,GAAG,CAAC;IAChB,WAAW,CAAC,CAAC,IAAI,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAA;KAAE,GAAG,IAAI,CAAC;IACzD,WAAW,CAAC,CAAC,IAAI,EAAE;QACjB,IAAI,EAAE,MAAM,CAAC;QACb,IAAI,EAAE,MAAM,CAAC;QACb,EAAE,EAAE,IAAI,CAAC,UAAU,EAAE,QAAQ,GAAG,MAAM,GAAG,UAAU,CAAC,CAAC;KACtD,GAAG,IAAI,CAAC;IACT,kBAAkB,CAAC,EAAE,kBAAkB,CAAC;CACzC;;
|
|
1
|
+
{"version":3,"file":"enter-world.d.ts","sourceRoot":"","sources":["../src/browser/enter-world.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAa,MAAM,eAAe,CAAC;AACrD,OAAO,EACL,OAAO,EACP,UAAU,EAEX,MAAM,yBAAyB,CAAC;AAEjC,KAAK,YAAY,GAAG,CAClB,IAAI,EAAE,MAAM,EACZ,MAAM,EAAE,MAAM,GAAG,OAAO,EACxB,KAAK,EAAE,MAAM,EAAE,KACZ,IAAI,CAAC;AAEV,wBAAgB,UAAU,CAAC,CAAC,SAAS,MAAM,GAAG,UAAU,EAAE,EACxD,KAAK,EACL,OAAuB,EACvB,iBAA6B,EAC7B,sBAAsB,EACtB,SAAS,EACT,WAAW,EACX,WAAW,EACX,kBAAkB,GACnB,EAAE;IACD,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,CAAC,EAAE,CAAC,SAAS,EAAE,MAAM,EAAE,GAAG,CAAC,EAAE,GAAG,KAAK,IAAI,CAAC;IACjD,iBAAiB,CAAC,EAAE,SAAS,CAAC,OAAO,EAAE,UAAU,CAAC,CAAC;IACnD,sBAAsB,CAAC,EAAE,MAAM,CAAC;IAChC,SAAS,CAAC,EAAE,GAAG,CAAC;IAChB,WAAW,CAAC,CAAC,IAAI,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAA;KAAE,GAAG,IAAI,CAAC;IACzD,WAAW,CAAC,CAAC,IAAI,EAAE;QACjB,IAAI,EAAE,MAAM,CAAC;QACb,IAAI,EAAE,MAAM,CAAC;QACb,EAAE,EAAE,IAAI,CAAC,UAAU,EAAE,QAAQ,GAAG,MAAM,GAAG,UAAU,CAAC,CAAC;KACtD,GAAG,IAAI,CAAC;IACT,kBAAkB,CAAC,EAAE,kBAAkB,CAAC;CACzC;;iBAkFqB,CAAC,WAAW,MAAM;;;;;;;;;;;mCAaA,CAAC,IAAI,EAAE,CAAC,EAAE,IAAI,EAAE,MAAM,KAAK,IAAI;sCAJ5B,CAAC,IAAI,EAAE,CAAC,EAAE,IAAI,EAAE,MAAM,KAAK,IAAI;gCAerC,YAAY;mCAJT,YAAY;;EAkCnD"}
|
package/dist/enter-world.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
function
|
|
1
|
+
function w(M){let{userId:B,appId:C,room:j,host:P,autoRejoin:h=!0,logLine:T}=M,L=!1,H=0,S,R,W=!0,E=new Map,F=`wss://${P}/room/${C}/${j}?userId=${encodeURIComponent(B)}`;function K(q,$,Q){if(!S)return!1;if(L||S.readyState!==WebSocket.OPEN)return!1;let X={type:q,to:$,payload:Q};return S.send(JSON.stringify(X)),T?.("\uD83D\uDC64 ➡️ \uD83D\uDDA5️",X),!0}function O(){if(L)return;S=new WebSocket(F),S.onopen=()=>{if(W)M.onOpen?.(),W=!1;H=0},S.onmessage=(q)=>{try{let $=JSON.parse(q.data);console.log(">>",$),(Array.isArray($)?$:[$]).forEach((X)=>{if(T?.("\uD83D\uDDA5️ ➡️ \uD83D\uDC64",X),X.type==="peer-joined"||X.type==="peer-left")_(X.users);else if(X.type==="ice-server")M.onIceUrl?.(X.url);else if(X.userId)M.onMessage(X.type,X.payload,{userId:X.userId,receive:(b,D)=>K(b,X.userId,D)})})}catch{T?.("⚠️ ERROR",{error:"invalid-json"})}},S.onclose=(q)=>{let Q=[1001,1006,1011,1012,1013].includes(q.code);if(h&&!L&&Q){let X=Math.min(Math.pow(2,H)*1000,30000),b=Math.random()*1000,D=X+b;T?.("\uD83D\uDD04 RECONNECTING",{attempt:H+1,delayMs:Math.round(D)}),H++,R=setTimeout(O,D)}else M.onClose?.({code:q.code,reason:q.reason,wasClean:q.wasClean})},S.onerror=(q)=>{console.error("WS Error",q),M.onError?.()}}function _(q){let $=[],Q=[],X=new Set;q.forEach(({userId:b})=>{if(b===B)return;if(!E.has(B)){let D={userId:b,receive:(V,G)=>K(V,b,G)};E.set(B,D),$.push(D)}X.add(B)});for(let b of E.keys())if(!X.has(b))E.delete(b),Q.push({userId:b});if($.length)M.onPeerJoined($);if(Q.length)M.onPeerLeft(Q)}return O(),{exitRoom:()=>{L=!0,clearTimeout(R),S.close()}}}function c({userId:M,appId:B,room:C,host:j,autoRejoin:P=!0,onOpen:h,onClose:T,onError:L,onPeerJoined:H,onPeerLeft:S,onIceUrl:R,onMessage:W,logLine:E,workerUrl:F}){if(!F)return console.warn("Warning: enterRoom called without workerUrl; this may cause issues in some environments. You should pass workerUrl explicitly. Use:","https://cdn.jsdelivr.net/npm/@dobuki/hello-worker/dist/signal-room.worker.min.js"),w({userId:M,appId:B,room:C,host:j,autoRejoin:P,onOpen:h,onClose:T,onError:L,onPeerJoined:H,onPeerLeft:S,onIceUrl:R,onMessage:W});let K=new Worker(F,{type:"module"}),O=!1;function _({userId:$}){return{userId:$,receive:(Q,X)=>{if(O)return!1;return K.postMessage({cmd:"send",toUserId:$,host:j,room:C,type:Q,payload:X}),!0}}}let q=($)=>{let Q=$.data;if(Q.kind==="open")h?.();else if(Q.kind==="close")K.terminate(),T?.(Q.ev);else if(Q.kind==="error")L?.();else if(Q.kind==="peer-joined")H(Q.users.map((X)=>_({userId:X.userId})));else if(Q.kind==="peer-left")S(Q.users);else if(Q.kind==="ice-server")R?.(Q.url);else if(Q.kind==="message")W(Q.type,Q.payload,_({userId:Q.fromUserId}));else if(Q.kind==="log")E?.(Q.direction,Q.obj)};return K.addEventListener("message",q),K.postMessage({cmd:"enter",userId:M,appId:B,room:C,host:j,autoRejoin:P}),{exitRoom:()=>{O=!0,K.removeEventListener("message",q),K.postMessage({cmd:"exit"})}}}var g=c;function y({appId:M,receivePeerConnection:B,peerlessUserExpiration:C=5000,fallbackRtcConfig:j={iceServers:[{urls:"stun:stun.l.google.com:19302"}]},enterRoomFunction:P=g,logLine:h=console.debug,onLeaveUser:T,workerUrl:L,onRoomReady:H,onRoomClose:S}){let R=`user-${crypto.randomUUID()}`,W=new Map,E=void 0,F;async function K(){if(E)try{let D=await fetch(E);if(!D.ok)throw Error(`ICE endpoint failed: ${D.status}`);F=await D.json()}catch(D){console.warn("Using fallback rtcConfig:",D)}return j}async function O(D){return D.pc=new RTCPeerConnection(Date.now()-(F?.timestamp??0)<1e4?F:await K()),D.pc.onicecandidate=(V)=>{if(!V.candidate)return;D.peer.receive("ice",V.candidate.toJSON())},D.pc.onconnectionstatechange=()=>{h("\uD83D\uDCAC",{event:"pc-state",userId:D.userId,state:D.pc?.connectionState})},D.pc}async function _(D){let V=W.get(D.userId),G=!1;if(!V){let Y={userId:D.userId,pendingRemoteIce:[],peer:D};W.set(D.userId,Y),await O(Y),V=Y,W.set(V.userId,V),G=!0}else if(V)clearTimeout(V.expirationTimeout),V.expirationTimeout=0;if(!V.pc)await O(V);return V.peer=D,[V,G]}function q(D){T?.(D);let V=W.get(D);if(!V)return;try{V.pc?.close()}catch{}W.delete(D)}async function $(D){if(!D.pc?.remoteDescription)return;let V=D.pendingRemoteIce;D.pendingRemoteIce=[];for(let G of V)try{await D.pc.addIceCandidate(G)}catch(Y){h("⚠️ ERROR",{error:"add-ice-failed",userId:D.userId,detail:String(Y)})}}let Q=new Map;function X({room:D,host:V}){let G=`${V}/room/${D}`,Y=Q.get(G);if(Y)Y.exitRoom(),Q.delete(G)}function b({room:D,host:V}){return new Promise(async(G,Y)=>{async function x(Z){let[z]=await _(Z),A=z.pc,f=await A?.createOffer();await A?.setLocalDescription(f),Z.receive("offer",A?.localDescription?.toJSON())}let{exitRoom:N}=P({userId:R,appId:M,room:D,host:V,logLine:h,workerUrl:L,autoRejoin:!0,onOpen(){H?.({room:D,host:V}),G()},onError(){console.error("onError"),Y()},onClose(Z){S?.({room:D,host:V,ev:Z})},onPeerJoined(Z){Z.forEach(async(z)=>{let[A,f]=await _(z);if(!f)return;let J=A.pc;if(!J)return;async function k(){let n=W.get(z.userId);if(n){n.pc=void 0;let v=await O(n);B({pc:v,userId:z.userId,initiator:!0,restart:k}),await new Promise((U)=>setTimeout(U,3000)),x(z)}}B({pc:J,userId:z.userId,initiator:!0,restart:k}),x(z)})},onPeerLeft(Z){Z.forEach(({userId:z})=>{let A=W.get(z);if(!A)return;A.expirationTimeout=setTimeout(()=>q(z),C??0)})},onIceUrl(Z){E=Z},async onMessage(Z,z,A){let[f]=await _(A),J=f.pc;if(!J)return;if(Z==="offer"){B({pc:J,userId:A.userId,initiator:!1,restart(){f.pc=void 0}}),await J.setRemoteDescription(z);let k=await J.createAnswer();await J.setLocalDescription(k),A.receive("answer",J.localDescription?.toJSON()),await $(f);return}if(Z==="answer"){await J.setRemoteDescription(z),await $(f);return}if(Z==="ice"){let k=z;if(!J.remoteDescription){f.pendingRemoteIce.push(k);return}try{await J.addIceCandidate(k)}catch(n){h("⚠️ ERROR",{error:"add-ice-failed",userId:f.userId,detail:String(n)})}return}}});Q.set(`${V}/room/${D}`,{exitRoom:N,room:D,host:V})})}return{userId:R,enterRoom:b,exitRoom:X,leaveUser:q,end(){Q.forEach(({exitRoom:D})=>D()),Q.clear(),W.forEach(({userId:D})=>q(D)),W.clear()}}}function r({appId:M,logLine:B=console.debug,enterRoomFunction:C=c,peerlessUserExpiration:j,workerUrl:P,onRoomReady:h,onRoomClose:T,dataChannelOptions:L}){let H=[],S=new Set;function R(G,Y,x,N){if(x){let Z=G.createDataChannel("data",L);W(Y,Z,N),E.set(Y,Z)}else{let Z=function(z){let A=z.channel;W(Y,A,N),E.set(Y,A)};return G.addEventListener("datachannel",Z),()=>{G.removeEventListener("datachannel",Z)}}}function W(G,Y,x){Y.onopen=()=>{B("\uD83D\uDCAC",{event:"dc-open",userId:G}),H.push(G),F.forEach((Z)=>Z(G,"join",H))};let N=({data:Z})=>{S.forEach((z)=>z(Z,G)),B("\uD83D\uDCAC",{event:"dc-message",userId:G,data:Z})};Y.addEventListener("message",N),Y.addEventListener("close",()=>{B("\uD83D\uDCAC",{event:"dc-close",userId:G}),H.splice(H.indexOf(G),1),F.forEach((Z)=>Z(G,"leave",H)),Y.removeEventListener("message",N),x?.()}),Y.onerror=()=>B("⚠️ ERROR",{error:"dc-error",userId:G})}let E=new Map,F=new Set,{userId:K,enterRoom:O,exitRoom:_,leaveUser:q,end:$}=y({appId:M,enterRoomFunction:C,logLine:B,workerUrl:P,peerlessUserExpiration:j,onRoomReady:h,onRoomClose:T,onLeaveUser(G){let Y=E.get(G);try{Y?.close()}catch{}E.delete(G)},receivePeerConnection({pc:G,userId:Y,initiator:x,restart:N}){R(G,Y,x,N)}});function Q(G,Y){E.forEach((x,N)=>{if(Y&&N!==Y)return;if(x.readyState==="open")x.send(G)})}function X(G){S.delete(G)}function b(G){return S.add(G),()=>{X(G)}}function D(G){F.delete(G)}function V(G){return F.add(G),()=>{D(G)}}return{userId:K,send:Q,enterRoom:O,exitRoom:_,leaveUser:q,getUsers:()=>H,addMessageListener:b,removeMessageListener:X,addUserListener:V,removeUserListener:D,end(){E.forEach((G)=>{try{G.close()}catch{}}),E.clear(),$(),F.clear(),H.length=0}}}export{r as enterWorld};
|
|
2
2
|
|
|
3
|
-
//# debugId=
|
|
3
|
+
//# debugId=2F12279FCB78E6F264756E2164756E21
|
|
4
4
|
//# sourceMappingURL=enter-world.js.map
|
package/dist/enter-world.js.map
CHANGED
|
@@ -2,12 +2,12 @@
|
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../src/browser/impl/signal-room.ts", "../src/browser/signal-room.ts", "../src/browser/webrtc-peer-collector.ts", "../src/browser/enter-world.ts"],
|
|
4
4
|
"sourcesContent": [
|
|
5
|
-
"export interface IPeer<T extends string = string, P = any> {\n userId: string;\n receive(type: T, payload: P): boolean;\n}\n\n/**\n * enterRoom connects to the signaling room via WebSocket.\n */\nexport function enterRoom<T extends string, P = any>(params: {\n userId: string;\n appId: string;\n room: string;\n host: string;\n onOpen?: () => void;\n onClose?: (ev: Pick<CloseEvent, \"code\" | \"reason\" | \"wasClean\">) => void;\n onError?: () => void;\n logLine?: (direction: string, obj?: any) => void;\n onPeerJoined(users: IPeer<T, P>[]): void;\n onPeerLeft(users: { userId: string }[]): void;\n onIceUrl?(url: string): void;\n onMessage(type: T, payload: P, from: IPeer<T, P>): void;\n autoRejoin?: boolean;\n}): { exitRoom: () => void } {\n type Message = {\n type: \"peer-joined\" | \"peer-left\" | \"ice-server\" | T;\n userId: string;\n users: { userId: string }[];\n payload: P;\n url: string;\n };\n\n const { userId, appId, room, host, autoRejoin = true, logLine } = params;\n\n let exited = false;\n let retryCount = 0;\n let ws: WebSocket;\n let timeoutId: ReturnType<typeof setTimeout>;\n let initialConnection = true;\n\n const peers = new Map<string, IPeer<T, P>>();\n const wsUrl = `wss://${host}/room/${appId}/${room}?userId=${encodeURIComponent(\n userId\n )}`;\n\n // Helper for sending (uses the current ws instance)\n function send(type: T, to: string, payload: P) {\n if (!ws) return false;\n if (exited || ws.readyState !== WebSocket.OPEN) return false;\n const obj = { type, to, payload };\n ws.send(JSON.stringify(obj));\n logLine?.(\"👤 ➡️ 🖥️\", obj);\n return true;\n }\n\n function connect() {\n if (exited) return;\n\n ws = new WebSocket(wsUrl);\n\n ws.onopen = () => {\n if (initialConnection) {\n params.onOpen?.();\n initialConnection = false;\n }\n retryCount = 0; // Reset backoff on successful connection\n };\n\n ws.onmessage = (e: MessageEvent) => {\n try {\n const
|
|
5
|
+
"export interface IPeer<T extends string = string, P = any> {\n userId: string;\n receive(type: T, payload: P): boolean;\n}\n\n/**\n * enterRoom connects to the signaling room via WebSocket.\n */\nexport function enterRoom<T extends string, P = any>(params: {\n userId: string;\n appId: string;\n room: string;\n host: string;\n onOpen?: () => void;\n onClose?: (ev: Pick<CloseEvent, \"code\" | \"reason\" | \"wasClean\">) => void;\n onError?: () => void;\n logLine?: (direction: string, obj?: any) => void;\n onPeerJoined(users: IPeer<T, P>[]): void;\n onPeerLeft(users: { userId: string }[]): void;\n onIceUrl?(url: string): void;\n onMessage(type: T, payload: P, from: IPeer<T, P>): void;\n autoRejoin?: boolean;\n}): { exitRoom: () => void } {\n type Message = {\n type: \"peer-joined\" | \"peer-left\" | \"ice-server\" | T;\n userId: string;\n users: { userId: string }[];\n payload: P;\n url: string;\n };\n\n const { userId, appId, room, host, autoRejoin = true, logLine } = params;\n\n let exited = false;\n let retryCount = 0;\n let ws: WebSocket;\n let timeoutId: ReturnType<typeof setTimeout>;\n let initialConnection = true;\n\n const peers = new Map<string, IPeer<T, P>>();\n const wsUrl = `wss://${host}/room/${appId}/${room}?userId=${encodeURIComponent(\n userId\n )}`;\n\n // Helper for sending (uses the current ws instance)\n function send(type: T, to: string, payload: P) {\n if (!ws) return false;\n if (exited || ws.readyState !== WebSocket.OPEN) return false;\n const obj = { type, to, payload };\n ws.send(JSON.stringify(obj));\n logLine?.(\"👤 ➡️ 🖥️\", obj);\n return true;\n }\n\n function connect() {\n if (exited) return;\n\n ws = new WebSocket(wsUrl);\n\n ws.onopen = () => {\n if (initialConnection) {\n params.onOpen?.();\n initialConnection = false;\n }\n retryCount = 0; // Reset backoff on successful connection\n };\n\n ws.onmessage = (e: MessageEvent) => {\n try {\n const result = JSON.parse(e.data);\n console.log(\">>\", result);\n const msgs: Message[] = Array.isArray(result) ? result : [result];\n msgs.forEach((msg) => {\n logLine?.(\"🖥️ ➡️ 👤\", msg);\n if (msg.type === \"peer-joined\" || msg.type === \"peer-left\") {\n updatePeers(msg.users);\n } else if (msg.type === \"ice-server\") {\n params.onIceUrl?.(msg.url);\n } else if (msg.userId) {\n params.onMessage(msg.type, msg.payload, {\n userId: msg.userId,\n receive: (type: T, payload: P) => send(type, msg.userId, payload),\n });\n }\n });\n } catch {\n logLine?.(\"⚠️ ERROR\", { error: \"invalid-json\" });\n }\n };\n\n ws.onclose = (ev: CloseEvent) => {\n // 1. Check if we should even try to reconnect\n const recoverableCodes = [1001, 1006, 1011, 1012, 1013];\n const isRecoverable = recoverableCodes.includes(ev.code);\n\n if (autoRejoin && !exited && isRecoverable) {\n // 2. Exponential Backoff: 1s, 2s, 4s, 8s... capped at 30s\n const backoff = Math.min(Math.pow(2, retryCount) * 1000, 30000);\n // 3. Add Jitter: +/- 1000ms randomness\n const jitter = Math.random() * 1000;\n const delay = backoff + jitter;\n\n logLine?.(\"🔄 RECONNECTING\", {\n attempt: retryCount + 1,\n delayMs: Math.round(delay),\n });\n\n retryCount++;\n timeoutId = setTimeout(connect, delay);\n } else {\n params.onClose?.({\n code: ev.code,\n reason: ev.reason,\n wasClean: ev.wasClean,\n });\n }\n };\n\n ws.onerror = (ev) => {\n console.error(\"WS Error\", ev);\n params.onError?.();\n };\n }\n\n // Helper for peer tracking (logic from your original code)\n function updatePeers(updatedUsers: { userId: string }[]) {\n const joined: IPeer<T, P>[] = [];\n const left: { userId: string }[] = [];\n const updatedPeerSet = new Set<string>();\n\n updatedUsers.forEach(({ userId: pUserId }) => {\n if (pUserId === userId) return;\n if (!peers.has(userId)) {\n const newPeer = {\n userId: pUserId,\n receive: (t: T, p: P) => send(t, pUserId, p),\n };\n peers.set(userId, newPeer);\n joined.push(newPeer);\n }\n updatedPeerSet.add(userId);\n });\n\n for (const userId of peers.keys()) {\n if (!updatedPeerSet.has(userId)) {\n peers.delete(userId);\n left.push({ userId });\n }\n }\n // Notify peer joined first then peer left. (avoid disconnect in case the peer leaving / joining is on the same user).\n if (joined.length) params.onPeerJoined(joined);\n if (left.length) params.onPeerLeft(left);\n }\n\n // Start initial connection\n connect();\n\n return {\n exitRoom: () => {\n exited = true;\n clearTimeout(timeoutId);\n ws.close();\n },\n };\n}\n",
|
|
6
6
|
"import type { IPeer } from \"./impl/signal-room.js\";\nimport { enterRoom as baseEnterRoom } from \"./impl/signal-room.js\";\nimport { RoomEvent, WorkerCommand } from \"./signal-room.worker.js\";\n\nexport function enterRoom<T extends string, P = any>({\n userId,\n appId,\n room,\n host,\n autoRejoin = true,\n onOpen,\n onClose,\n onError,\n onPeerJoined,\n onPeerLeft,\n onIceUrl,\n onMessage,\n logLine,\n workerUrl,\n}: {\n userId: string;\n appId: string;\n room: string;\n host: string;\n autoRejoin?: boolean;\n onOpen?: () => void;\n onClose?: (ev: Pick<CloseEvent, \"code\" | \"reason\" | \"wasClean\">) => void;\n onError?: () => void;\n onPeerJoined: (users: IPeer<T, P>[]) => void;\n onPeerLeft: (users: { userId: string }[]) => void;\n onIceUrl?(url: string): void;\n onMessage: (type: T, payload: P, from: IPeer<T, P>) => void;\n logLine?: (direction: string, obj?: any) => void;\n\n // Pass the URL to your worker file (bundler will handle it)\n workerUrl?: URL;\n}): { exitRoom: () => void } {\n if (!workerUrl) {\n const CDN_WORKER_URL = `https://cdn.jsdelivr.net/npm/@dobuki/hello-worker/dist/signal-room.worker.min.js`;\n\n console.warn(\n \"Warning: enterRoom called without workerUrl; this may cause issues in some environments. You should pass workerUrl explicitly. Use:\",\n CDN_WORKER_URL\n );\n return baseEnterRoom<T, P>({\n userId,\n appId,\n room,\n host,\n autoRejoin,\n onOpen,\n onClose,\n onError,\n onPeerJoined,\n onPeerLeft,\n onIceUrl,\n onMessage,\n });\n }\n const worker = new Worker(workerUrl, { type: \"module\" });\n let exited = false;\n\n function makeUser({ userId }: { userId: string }): IPeer<T, P> {\n return {\n userId,\n receive: (type: T, payload: P) => {\n if (exited) return false;\n worker.postMessage({\n cmd: \"send\",\n toUserId: userId,\n host,\n room,\n type,\n payload,\n } as WorkerCommand);\n return true;\n },\n };\n }\n\n const onWorkerMessage = (e: MessageEvent<RoomEvent<T, P>>) => {\n const ev = e.data;\n\n if (ev.kind === \"open\") onOpen?.();\n else if (ev.kind === \"close\") {\n worker.terminate();\n onClose?.(ev.ev);\n } else if (ev.kind === \"error\") onError?.();\n else if (ev.kind === \"peer-joined\")\n onPeerJoined(ev.users.map((ev) => makeUser({ userId: ev.userId })));\n else if (ev.kind === \"peer-left\") onPeerLeft(ev.users);\n else if (ev.kind === \"ice-server\") onIceUrl?.(ev.url);\n else if (ev.kind === \"message\")\n onMessage(ev.type, ev.payload, makeUser({ userId: ev.fromUserId }));\n else if (ev.kind === \"log\") logLine?.(ev.direction, ev.obj);\n };\n\n worker.addEventListener(\"message\", onWorkerMessage);\n\n worker.postMessage({\n cmd: \"enter\",\n userId,\n appId,\n room,\n host,\n autoRejoin,\n } as WorkerCommand);\n\n return {\n exitRoom: () => {\n exited = true;\n worker.removeEventListener(\"message\", onWorkerMessage);\n worker.postMessage({ cmd: \"exit\" } as WorkerCommand);\n },\n };\n}\n\nexport type EnterRoom<T extends string, P> = typeof enterRoom<T, P>;\n",
|
|
7
|
-
"import { IPeer } from \"./impl/signal-room\";\nimport { EnterRoom, enterRoom } from \"./signal-room\";\n\nexport type SigType = \"offer\" | \"answer\" | \"ice\";\nexport type SigPayload = RTCSessionDescriptionInit | RTCIceCandidateInit;\n\ntype UserState = {\n userId: string;\n pc?: RTCPeerConnection;\n\n // ICE that arrived before we had remoteDescription\n pendingRemoteIce: RTCIceCandidateInit[];\n\n // the signaling \"user\" handle so we can send messages\n peer: IPeer<SigType, SigPayload>;\n\n expirationTimeout?: number;\n};\n\nconst DEFAULT_ENTER_ROOM = enterRoom;\n\nexport function collectPeerConnections({\n appId,\n receivePeerConnection,\n peerlessUserExpiration = 5000,\n fallbackRtcConfig = {\n iceServers: [{ urls: \"stun:stun.l.google.com:19302\" }],\n },\n enterRoomFunction: enterRoom = DEFAULT_ENTER_ROOM,\n logLine = console.debug,\n onLeaveUser,\n workerUrl,\n onRoomReady,\n onRoomClose,\n}: {\n appId: string;\n fallbackRtcConfig?: RTCConfiguration;\n enterRoomFunction?: EnterRoom<SigType, SigPayload>;\n onLeaveUser?: (userId: string) => void;\n logLine?: (direction: string, obj?: any) => void;\n workerUrl?: URL;\n peerlessUserExpiration?: number;\n receivePeerConnection(connection: {\n pc: RTCPeerConnection;\n userId: string;\n initiator: boolean;\n restart?: () => void;\n }): void;\n onRoomReady?(info: { host: string; room: string }): void;\n onRoomClose?(info: {\n host: string;\n room: string;\n ev: Pick<CloseEvent, \"reason\" | \"code\" | \"wasClean\">;\n }): void;\n}) {\n const userId = `user-${crypto.randomUUID()}`;\n const users: Map<string, UserState> = new Map();\n let iceUrl: string | undefined = undefined;\n let rtcConfig: RTCConfiguration | undefined;\n let rtcConfigExpiration: number = 0;\n\n async function getRtcConfig(): Promise<RTCConfiguration> {\n if (iceUrl) {\n try {\n const r = await fetch(iceUrl);\n if (!r.ok) throw new Error(`ICE endpoint failed: ${r.status}`);\n const result = (await r.json()) as RTCConfiguration & {\n expire: number;\n };\n rtcConfigExpiration = Date.now() + result.expire * 1000;\n rtcConfig = result;\n } catch (e) {\n console.warn(\"Using fallback rtcConfig:\", e);\n }\n }\n return fallbackRtcConfig;\n }\n\n async function setupPC(state: UserState) {\n state.pc = new RTCPeerConnection(\n rtcConfigExpiration - Date.now() < 10000\n ? await getRtcConfig()\n : rtcConfig\n );\n // Send local ICE candidates to this peer\n state.pc.onicecandidate = (ev) => {\n if (!ev.candidate) return;\n state.peer.receive(\"ice\", ev.candidate.toJSON());\n };\n\n state.pc.onconnectionstatechange = () => {\n logLine(\"💬\", {\n event: \"pc-state\",\n userId: state.userId,\n state: state.pc?.connectionState,\n });\n };\n return state.pc;\n }\n\n async function getPeer(\n peer: IPeer<SigType, SigPayload>\n ): Promise<[UserState, boolean]> {\n let state = users.get(peer.userId);\n let isNewPeer = false;\n if (!state) {\n const newState: UserState = {\n userId: peer.userId,\n pendingRemoteIce: [],\n peer,\n };\n users.set(peer.userId, newState);\n\n await setupPC(newState);\n state = newState;\n\n // New user\n users.set(state.userId, state);\n isNewPeer = true;\n } else if (state) {\n clearTimeout(state.expirationTimeout);\n state.expirationTimeout = 0;\n }\n if (!state.pc) {\n await setupPC(state);\n }\n state.peer = peer;\n return [state, isNewPeer];\n }\n\n function leaveUser(userId: string) {\n onLeaveUser?.(userId);\n const p = users.get(userId);\n if (!p) return;\n try {\n p.pc?.close();\n } catch {}\n users.delete(userId);\n }\n\n async function flushRemoteIce(state: UserState) {\n if (!state.pc?.remoteDescription) return;\n\n const queued = state.pendingRemoteIce;\n state.pendingRemoteIce = [];\n\n for (const ice of queued) {\n try {\n await state.pc.addIceCandidate(ice);\n } catch (e) {\n logLine(\"⚠️ ERROR\", {\n error: \"add-ice-failed\",\n userId: state.userId,\n detail: String(e),\n });\n }\n }\n }\n\n const roomsEntered = new Map<\n string,\n { room: string; host: string; exitRoom: () => void }\n >();\n\n function exit({ room, host }: { room: string; host: string }) {\n const key = `${host}/room/${room}`;\n const session = roomsEntered.get(key);\n if (session) {\n session.exitRoom();\n roomsEntered.delete(key);\n }\n }\n\n function enter({ room, host }: { room: string; host: string }) {\n return new Promise<void>(async (resolve, reject) => {\n async function makeOffer(user: IPeer) {\n // Offer flow: createOffer -> setLocalDescription -> send localDescription\n const [state] = await getPeer(user);\n const pc = state.pc;\n const offer = await pc?.createOffer();\n await pc?.setLocalDescription(offer);\n user.receive(\"offer\", pc?.localDescription?.toJSON()!);\n }\n\n const { exitRoom } = enterRoom({\n userId,\n appId,\n room,\n host,\n logLine,\n workerUrl,\n autoRejoin: true,\n\n onOpen() {\n onRoomReady?.({ room, host });\n resolve();\n },\n onError() {\n console.error(\"onError\");\n reject();\n },\n onClose(ev) {\n onRoomClose?.({ room, host, ev });\n },\n\n // Existing peers initiate to the newcomer\n onPeerJoined(joiningUsers: IPeer<SigType, SigPayload>[]) {\n joiningUsers.forEach(async (user) => {\n const [state, isNewPeer] = await getPeer(user);\n if (!isNewPeer) return;\n const pc = state.pc;\n if (!pc) return;\n\n async function restart() {\n const state = users.get(user.userId);\n if (state) {\n state.pc = undefined;\n const pc = await setupPC(state);\n receivePeerConnection({\n pc,\n userId: user.userId,\n initiator: true,\n restart,\n });\n await new Promise((resolve) => setTimeout(resolve, 3000));\n makeOffer(user);\n }\n }\n\n receivePeerConnection({\n pc,\n userId: user.userId,\n initiator: true,\n restart,\n });\n makeOffer(user);\n });\n },\n\n onPeerLeft(leavingUsers: { userId: string }[]) {\n leavingUsers.forEach(({ userId }) => {\n const state = users.get(userId);\n if (!state) return;\n state.expirationTimeout = setTimeout(\n () => leaveUser(userId),\n peerlessUserExpiration ?? 0\n );\n });\n },\n\n onIceUrl(url) {\n iceUrl = url;\n },\n\n async onMessage(type: SigType, payload: any, from: IPeer) {\n const [state] = await getPeer(from);\n const pc = state.pc;\n if (!pc) return;\n\n if (type === \"offer\") {\n receivePeerConnection({\n pc,\n userId: from.userId,\n initiator: false,\n restart() {\n // reset PC\n state.pc = undefined;\n },\n });\n // Responder: set remote offer\n await pc.setRemoteDescription(payload as RTCSessionDescriptionInit);\n\n // Create and send answer\n const answer = await pc.createAnswer();\n await pc.setLocalDescription(answer);\n\n from.receive(\"answer\", pc.localDescription?.toJSON()!);\n\n // Now safe to apply any queued ICE from this peer\n await flushRemoteIce(state);\n return;\n }\n\n if (type === \"answer\") {\n // Initiator: set remote answer\n await pc.setRemoteDescription(payload as RTCSessionDescriptionInit);\n await flushRemoteIce(state);\n return;\n }\n\n if (type === \"ice\") {\n const ice = payload as RTCIceCandidateInit;\n\n // If we don't have remoteDescription yet, queue it\n if (!pc.remoteDescription) {\n state.pendingRemoteIce.push(ice);\n return;\n }\n\n try {\n await pc.addIceCandidate(ice);\n } catch (e) {\n logLine(\"⚠️ ERROR\", {\n error: \"add-ice-failed\",\n userId: state.userId,\n detail: String(e),\n });\n }\n return;\n }\n },\n });\n roomsEntered.set(`${host}/room/${room}`, { exitRoom, room, host });\n });\n }\n\n return {\n userId,\n enterRoom: enter,\n exitRoom: exit,\n leaveUser,\n end() {\n roomsEntered.forEach(({ exitRoom }) => exitRoom());\n roomsEntered.clear();\n users.forEach(({ userId }) => leaveUser(userId));\n users.clear();\n },\n };\n}\n\n/*\nTurn Token ID\n<CF_TURN_TOKEN_ID>\n\nAPI Token\n<CF_RTC_API_TOKEN>\n\nCURL\ncurl \\\n\t-H \"Authorization: Bearer <CF_RTC_API_TOKEN>\" \\\n\t-H \"Content-Type: application/json\" -d '{\"ttl\": 86400}' \\\n\thttps://rtc.live.cloudflare.com/v1/turn/keys/<CF_TURN_TOKEN_ID>/credentials/generate-ice-servers\n\nJSON\n{\n\t\"iceServers\": [\n {\n \"urls\": [\n \"stun:stun.cloudflare.com:3478\",\n \"turn:turn.cloudflare.com:3478?transport=udp\",\n \"turn:turn.cloudflare.com:3478?transport=tcp\",\n \"turns:turn.cloudflare.com:5349?transport=tcp\"\n ],\n \"username\": \"xxxx\",\n \"credential\": \"yyyy\",\n }\n ]\n}\n\n*/\n",
|
|
8
|
-
"import { EnterRoom, enterRoom } from \"./signal-room\";\nimport {\n SigType,\n SigPayload,\n collectPeerConnections,\n} from \"./webrtc-peer-collector\";\n\ntype UserListener = (\n user: string,\n action: \"join\" | \"leave\",\n users: string[]\n) => void;\n\nexport function enterWorld<D extends string | Uint8Array>({\n appId,\n logLine = console.debug,\n enterRoomFunction = enterRoom,\n peerlessUserExpiration,\n workerUrl,\n onRoomReady,\n onRoomClose,\n dataChannelOptions,\n}: {\n appId: string;\n logLine?: (direction: string, obj?: any) => void;\n enterRoomFunction?: EnterRoom<SigType, SigPayload>;\n peerlessUserExpiration?: number;\n workerUrl?: URL;\n onRoomReady?(info: { host: string; room: string }): void;\n onRoomClose?(info: {\n host: string;\n room: string;\n ev: Pick<CloseEvent, \"reason\" | \"code\" | \"wasClean\">;\n }): void;\n dataChannelOptions?: RTCDataChannelInit;\n}) {\n const userIds: string[] = [];\n\n const messagesListeners = new Set<(data: any, from: string) => void>();\n\n function createDataChannel(\n pc: RTCPeerConnection,\n peerUserId: string,\n initiator: boolean,\n restart?: () => void\n ) {\n if (initiator) {\n const dc = pc.createDataChannel(\"data\", dataChannelOptions);\n wireDataChannel(peerUserId, dc, restart);\n dataChannels.set(peerUserId, dc);\n } else {\n function listener(ev: RTCDataChannelEvent) {\n const dc = ev.channel;\n wireDataChannel(peerUserId, dc, restart);\n dataChannels.set(peerUserId, dc);\n
|
|
7
|
+
"import { IPeer } from \"./impl/signal-room\";\nimport { EnterRoom, enterRoom } from \"./signal-room\";\n\nexport type SigType = \"offer\" | \"answer\" | \"ice\";\nexport type SigPayload = RTCSessionDescriptionInit | RTCIceCandidateInit;\n\ntype UserState = {\n userId: string;\n pc?: RTCPeerConnection;\n\n // ICE that arrived before we had remoteDescription\n pendingRemoteIce: RTCIceCandidateInit[];\n\n // the signaling \"user\" handle so we can send messages\n peer: IPeer<SigType, SigPayload>;\n\n expirationTimeout?: number;\n};\n\nconst DEFAULT_ENTER_ROOM = enterRoom;\n\nexport function collectPeerConnections({\n appId,\n receivePeerConnection,\n peerlessUserExpiration = 5000,\n fallbackRtcConfig = {\n iceServers: [{ urls: \"stun:stun.l.google.com:19302\" }],\n },\n enterRoomFunction: enterRoom = DEFAULT_ENTER_ROOM,\n logLine = console.debug,\n onLeaveUser,\n workerUrl,\n onRoomReady,\n onRoomClose,\n}: {\n appId: string;\n fallbackRtcConfig?: RTCConfiguration;\n enterRoomFunction?: EnterRoom<SigType, SigPayload>;\n onLeaveUser?: (userId: string) => void;\n logLine?: (direction: string, obj?: any) => void;\n workerUrl?: URL;\n peerlessUserExpiration?: number;\n receivePeerConnection(connection: {\n pc: RTCPeerConnection;\n userId: string;\n initiator: boolean;\n restart?: () => void;\n }): void;\n onRoomReady?(info: { host: string; room: string }): void;\n onRoomClose?(info: {\n host: string;\n room: string;\n ev: Pick<CloseEvent, \"reason\" | \"code\" | \"wasClean\">;\n }): void;\n}) {\n const userId = `user-${crypto.randomUUID()}`;\n const users: Map<string, UserState> = new Map();\n let iceUrl: string | undefined = undefined;\n let rtcConfig: (RTCConfiguration & { timestamp: number }) | undefined;\n\n async function getRtcConfig(): Promise<RTCConfiguration> {\n if (iceUrl) {\n try {\n const r = await fetch(iceUrl);\n if (!r.ok) throw new Error(`ICE endpoint failed: ${r.status}`);\n rtcConfig = (await r.json()) as RTCConfiguration & {\n timestamp: number;\n };\n } catch (e) {\n console.warn(\"Using fallback rtcConfig:\", e);\n }\n }\n return fallbackRtcConfig;\n }\n\n async function setupPC(state: UserState) {\n state.pc = new RTCPeerConnection(\n Date.now() - (rtcConfig?.timestamp ?? 0) < 10000\n ? rtcConfig\n : await getRtcConfig()\n );\n // Send local ICE candidates to this peer\n state.pc.onicecandidate = (ev) => {\n if (!ev.candidate) return;\n state.peer.receive(\"ice\", ev.candidate.toJSON());\n };\n\n state.pc.onconnectionstatechange = () => {\n logLine(\"💬\", {\n event: \"pc-state\",\n userId: state.userId,\n state: state.pc?.connectionState,\n });\n };\n return state.pc;\n }\n\n async function getPeer(\n peer: IPeer<SigType, SigPayload>\n ): Promise<[UserState, boolean]> {\n let state = users.get(peer.userId);\n let isNewPeer = false;\n if (!state) {\n const newState: UserState = {\n userId: peer.userId,\n pendingRemoteIce: [],\n peer,\n };\n users.set(peer.userId, newState);\n\n await setupPC(newState);\n state = newState;\n\n // New user\n users.set(state.userId, state);\n isNewPeer = true;\n } else if (state) {\n clearTimeout(state.expirationTimeout);\n state.expirationTimeout = 0;\n }\n if (!state.pc) {\n await setupPC(state);\n }\n state.peer = peer;\n return [state, isNewPeer];\n }\n\n function leaveUser(userId: string) {\n onLeaveUser?.(userId);\n const p = users.get(userId);\n if (!p) return;\n try {\n p.pc?.close();\n } catch {}\n users.delete(userId);\n }\n\n async function flushRemoteIce(state: UserState) {\n if (!state.pc?.remoteDescription) return;\n\n const queued = state.pendingRemoteIce;\n state.pendingRemoteIce = [];\n\n for (const ice of queued) {\n try {\n await state.pc.addIceCandidate(ice);\n } catch (e) {\n logLine(\"⚠️ ERROR\", {\n error: \"add-ice-failed\",\n userId: state.userId,\n detail: String(e),\n });\n }\n }\n }\n\n const roomsEntered = new Map<\n string,\n { room: string; host: string; exitRoom: () => void }\n >();\n\n function exit({ room, host }: { room: string; host: string }) {\n const key = `${host}/room/${room}`;\n const session = roomsEntered.get(key);\n if (session) {\n session.exitRoom();\n roomsEntered.delete(key);\n }\n }\n\n function enter({ room, host }: { room: string; host: string }) {\n return new Promise<void>(async (resolve, reject) => {\n async function makeOffer(user: IPeer) {\n // Offer flow: createOffer -> setLocalDescription -> send localDescription\n const [state] = await getPeer(user);\n const pc = state.pc;\n const offer = await pc?.createOffer();\n await pc?.setLocalDescription(offer);\n user.receive(\"offer\", pc?.localDescription?.toJSON()!);\n }\n\n const { exitRoom } = enterRoom({\n userId,\n appId,\n room,\n host,\n logLine,\n workerUrl,\n autoRejoin: true,\n\n onOpen() {\n onRoomReady?.({ room, host });\n resolve();\n },\n onError() {\n console.error(\"onError\");\n reject();\n },\n onClose(ev) {\n onRoomClose?.({ room, host, ev });\n },\n\n // Existing peers initiate to the newcomer\n onPeerJoined(joiningUsers: IPeer<SigType, SigPayload>[]) {\n joiningUsers.forEach(async (user) => {\n const [state, isNewPeer] = await getPeer(user);\n if (!isNewPeer) return;\n const pc = state.pc;\n if (!pc) return;\n\n async function restart() {\n const state = users.get(user.userId);\n if (state) {\n state.pc = undefined;\n const pc = await setupPC(state);\n receivePeerConnection({\n pc,\n userId: user.userId,\n initiator: true,\n restart,\n });\n await new Promise((resolve) => setTimeout(resolve, 3000));\n makeOffer(user);\n }\n }\n\n receivePeerConnection({\n pc,\n userId: user.userId,\n initiator: true,\n restart,\n });\n makeOffer(user);\n });\n },\n\n onPeerLeft(leavingUsers: { userId: string }[]) {\n leavingUsers.forEach(({ userId }) => {\n const state = users.get(userId);\n if (!state) return;\n state.expirationTimeout = setTimeout(\n () => leaveUser(userId),\n peerlessUserExpiration ?? 0\n );\n });\n },\n\n onIceUrl(url) {\n iceUrl = url;\n },\n\n async onMessage(type: SigType, payload: any, from: IPeer) {\n const [state] = await getPeer(from);\n const pc = state.pc;\n if (!pc) return;\n\n if (type === \"offer\") {\n receivePeerConnection({\n pc,\n userId: from.userId,\n initiator: false,\n restart() {\n // reset PC\n state.pc = undefined;\n },\n });\n // Responder: set remote offer\n await pc.setRemoteDescription(payload as RTCSessionDescriptionInit);\n\n // Create and send answer\n const answer = await pc.createAnswer();\n await pc.setLocalDescription(answer);\n\n from.receive(\"answer\", pc.localDescription?.toJSON()!);\n\n // Now safe to apply any queued ICE from this peer\n await flushRemoteIce(state);\n return;\n }\n\n if (type === \"answer\") {\n // Initiator: set remote answer\n await pc.setRemoteDescription(payload as RTCSessionDescriptionInit);\n await flushRemoteIce(state);\n return;\n }\n\n if (type === \"ice\") {\n const ice = payload as RTCIceCandidateInit;\n\n // If we don't have remoteDescription yet, queue it\n if (!pc.remoteDescription) {\n state.pendingRemoteIce.push(ice);\n return;\n }\n\n try {\n await pc.addIceCandidate(ice);\n } catch (e) {\n logLine(\"⚠️ ERROR\", {\n error: \"add-ice-failed\",\n userId: state.userId,\n detail: String(e),\n });\n }\n return;\n }\n },\n });\n roomsEntered.set(`${host}/room/${room}`, { exitRoom, room, host });\n });\n }\n\n return {\n userId,\n enterRoom: enter,\n exitRoom: exit,\n leaveUser,\n end() {\n roomsEntered.forEach(({ exitRoom }) => exitRoom());\n roomsEntered.clear();\n users.forEach(({ userId }) => leaveUser(userId));\n users.clear();\n },\n };\n}\n\n/*\nTurn Token ID\n<CF_TURN_TOKEN_ID>\n\nAPI Token\n<CF_RTC_API_TOKEN>\n\nCURL\ncurl \\\n\t-H \"Authorization: Bearer <CF_RTC_API_TOKEN>\" \\\n\t-H \"Content-Type: application/json\" -d '{\"ttl\": 86400}' \\\n\thttps://rtc.live.cloudflare.com/v1/turn/keys/<CF_TURN_TOKEN_ID>/credentials/generate-ice-servers\n\nJSON\n{\n\t\"iceServers\": [\n {\n \"urls\": [\n \"stun:stun.cloudflare.com:3478\",\n \"turn:turn.cloudflare.com:3478?transport=udp\",\n \"turn:turn.cloudflare.com:3478?transport=tcp\",\n \"turns:turn.cloudflare.com:5349?transport=tcp\"\n ],\n \"username\": \"xxxx\",\n \"credential\": \"yyyy\",\n }\n ]\n}\n\n*/\n",
|
|
8
|
+
"import { EnterRoom, enterRoom } from \"./signal-room\";\nimport {\n SigType,\n SigPayload,\n collectPeerConnections,\n} from \"./webrtc-peer-collector\";\n\ntype UserListener = (\n user: string,\n action: \"join\" | \"leave\",\n users: string[]\n) => void;\n\nexport function enterWorld<D extends string | Uint8Array>({\n appId,\n logLine = console.debug,\n enterRoomFunction = enterRoom,\n peerlessUserExpiration,\n workerUrl,\n onRoomReady,\n onRoomClose,\n dataChannelOptions,\n}: {\n appId: string;\n logLine?: (direction: string, obj?: any) => void;\n enterRoomFunction?: EnterRoom<SigType, SigPayload>;\n peerlessUserExpiration?: number;\n workerUrl?: URL;\n onRoomReady?(info: { host: string; room: string }): void;\n onRoomClose?(info: {\n host: string;\n room: string;\n ev: Pick<CloseEvent, \"reason\" | \"code\" | \"wasClean\">;\n }): void;\n dataChannelOptions?: RTCDataChannelInit;\n}) {\n const userIds: string[] = [];\n\n const messagesListeners = new Set<(data: any, from: string) => void>();\n\n function createDataChannel(\n pc: RTCPeerConnection,\n peerUserId: string,\n initiator: boolean,\n restart?: () => void\n ) {\n if (initiator) {\n const dc = pc.createDataChannel(\"data\", dataChannelOptions);\n wireDataChannel(peerUserId, dc, restart);\n dataChannels.set(peerUserId, dc);\n } else {\n function listener(ev: RTCDataChannelEvent) {\n const dc = ev.channel;\n wireDataChannel(peerUserId, dc, restart);\n dataChannels.set(peerUserId, dc);\n }\n pc.addEventListener(\"datachannel\", listener);\n return () => {\n pc.removeEventListener(\"datachannel\", listener);\n };\n }\n }\n\n function wireDataChannel(\n userId: string,\n dc: RTCDataChannel,\n restart?: () => void\n ) {\n dc.onopen = () => {\n logLine(\"💬\", { event: \"dc-open\", userId });\n userIds.push(userId);\n userListeners.forEach((listener) => listener(userId, \"join\", userIds));\n };\n const onmessage = ({ data }: MessageEvent) => {\n messagesListeners.forEach((listener) => listener(data as any, userId));\n logLine(\"💬\", { event: \"dc-message\", userId, data });\n };\n dc.addEventListener(\"message\", onmessage);\n dc.addEventListener(\"close\", () => {\n logLine(\"💬\", { event: \"dc-close\", userId });\n userIds.splice(userIds.indexOf(userId), 1);\n userListeners.forEach((listener) => listener(userId, \"leave\", userIds));\n dc.removeEventListener(\"message\", onmessage);\n restart?.();\n });\n dc.onerror = () => logLine(\"⚠️ ERROR\", { error: \"dc-error\", userId });\n }\n\n const dataChannels = new Map<string, RTCDataChannel>();\n const userListeners = new Set<UserListener>();\n\n const {\n userId,\n enterRoom,\n exitRoom,\n leaveUser,\n end: endPeerCollection,\n } = collectPeerConnections({\n appId,\n enterRoomFunction,\n logLine,\n workerUrl,\n peerlessUserExpiration,\n onRoomReady,\n onRoomClose,\n onLeaveUser(userId: string) {\n const dc = dataChannels.get(userId);\n try {\n dc?.close();\n } catch {}\n dataChannels.delete(userId);\n },\n receivePeerConnection({ pc, userId, initiator, restart }) {\n createDataChannel(pc, userId, initiator, restart);\n },\n });\n\n function send(data: D, userId?: string) {\n dataChannels.forEach((dataChannel, pUserId) => {\n if (userId && pUserId !== userId) return;\n if (dataChannel.readyState === \"open\") {\n dataChannel.send(data as any);\n }\n });\n }\n\n function removeMessageListener(listener: (data: D, from: string) => void) {\n messagesListeners.delete(listener);\n }\n\n function addMessageListener(listener: (data: D, from: string) => void) {\n messagesListeners.add(listener);\n return () => {\n removeMessageListener(listener);\n };\n }\n\n function removeUserListener(listener: UserListener) {\n userListeners.delete(listener);\n }\n\n function addUserListener(listener: UserListener) {\n userListeners.add(listener);\n return () => {\n removeUserListener(listener);\n };\n }\n\n return {\n userId,\n send,\n enterRoom,\n exitRoom,\n leaveUser,\n getUsers: () => userIds,\n addMessageListener,\n removeMessageListener,\n addUserListener,\n removeUserListener,\n end() {\n dataChannels.forEach((dataChannel) => {\n try {\n dataChannel.close();\n } catch {}\n });\n dataChannels.clear();\n endPeerCollection();\n userListeners.clear();\n userIds.length = 0;\n },\n };\n}\n"
|
|
9
9
|
],
|
|
10
|
-
"mappings": "AAQO,SAAS,CAAoC,CAAC,EAcxB,CAS3B,IAAQ,SAAQ,QAAO,OAAM,OAAM,aAAa,GAAM,WAAY,EAE9D,EAAS,GACT,EAAa,EACb,EACA,EACA,EAAoB,GAElB,EAAQ,IAAI,IACZ,EAAQ,SAAS,UAAa,KAAS,YAAe,mBAC1D,CACF,IAGA,SAAS,CAAI,CAAC,EAAS,EAAY,EAAY,CAC7C,GAAI,CAAC,EAAI,MAAO,GAChB,GAAI,GAAU,EAAG,aAAe,UAAU,KAAM,MAAO,GACvD,IAAM,EAAM,CAAE,OAAM,KAAI,SAAQ,EAGhC,OAFA,EAAG,KAAK,KAAK,UAAU,CAAG,CAAC,EAC3B,IAAU,gCAAY,CAAG,EAClB,GAGT,SAAS,CAAO,EAAG,CACjB,GAAI,EAAQ,OAEZ,EAAK,IAAI,UAAU,CAAK,EAExB,EAAG,OAAS,IAAM,CAChB,GAAI,EACF,EAAO,SAAS,EAChB,EAAoB,GAEtB,EAAa,GAGf,EAAG,UAAY,CAAC,IAAoB,CAClC,GAAI,CACF,IAAM,
|
|
11
|
-
"debugId": "
|
|
10
|
+
"mappings": "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",
|
|
11
|
+
"debugId": "2F12279FCB78E6F264756E2164756E21",
|
|
12
12
|
"names": []
|
|
13
13
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"signal-room.d.ts","sourceRoot":"","sources":["../../src/browser/impl/signal-room.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,KAAK,CAAC,CAAC,SAAS,MAAM,GAAG,MAAM,EAAE,CAAC,GAAG,GAAG;IACvD,MAAM,EAAE,MAAM,CAAC;IACf,OAAO,CAAC,IAAI,EAAE,CAAC,EAAE,OAAO,EAAE,CAAC,GAAG,OAAO,CAAC;CACvC;AAED;;GAEG;AACH,wBAAgB,SAAS,CAAC,CAAC,SAAS,MAAM,EAAE,CAAC,GAAG,GAAG,EAAE,MAAM,EAAE;IAC3D,MAAM,EAAE,MAAM,CAAC;IACf,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,CAAC,EAAE,MAAM,IAAI,CAAC;IACpB,OAAO,CAAC,EAAE,CAAC,EAAE,EAAE,IAAI,CAAC,UAAU,EAAE,MAAM,GAAG,QAAQ,GAAG,UAAU,CAAC,KAAK,IAAI,CAAC;IACzE,OAAO,CAAC,EAAE,MAAM,IAAI,CAAC;IACrB,OAAO,CAAC,EAAE,CAAC,SAAS,EAAE,MAAM,EAAE,GAAG,CAAC,EAAE,GAAG,KAAK,IAAI,CAAC;IACjD,YAAY,CAAC,KAAK,EAAE,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,EAAE,GAAG,IAAI,CAAC;IACzC,UAAU,CAAC,KAAK,EAAE;QAAE,MAAM,EAAE,MAAM,CAAA;KAAE,EAAE,GAAG,IAAI,CAAC;IAC9C,QAAQ,CAAC,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,CAAC;IAC7B,SAAS,CAAC,IAAI,EAAE,CAAC,EAAE,OAAO,EAAE,CAAC,EAAE,IAAI,EAAE,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,GAAG,IAAI,CAAC;IACxD,UAAU,CAAC,EAAE,OAAO,CAAC;CACtB,GAAG;IAAE,QAAQ,EAAE,MAAM,IAAI,CAAA;CAAE,
|
|
1
|
+
{"version":3,"file":"signal-room.d.ts","sourceRoot":"","sources":["../../src/browser/impl/signal-room.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,KAAK,CAAC,CAAC,SAAS,MAAM,GAAG,MAAM,EAAE,CAAC,GAAG,GAAG;IACvD,MAAM,EAAE,MAAM,CAAC;IACf,OAAO,CAAC,IAAI,EAAE,CAAC,EAAE,OAAO,EAAE,CAAC,GAAG,OAAO,CAAC;CACvC;AAED;;GAEG;AACH,wBAAgB,SAAS,CAAC,CAAC,SAAS,MAAM,EAAE,CAAC,GAAG,GAAG,EAAE,MAAM,EAAE;IAC3D,MAAM,EAAE,MAAM,CAAC;IACf,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,CAAC,EAAE,MAAM,IAAI,CAAC;IACpB,OAAO,CAAC,EAAE,CAAC,EAAE,EAAE,IAAI,CAAC,UAAU,EAAE,MAAM,GAAG,QAAQ,GAAG,UAAU,CAAC,KAAK,IAAI,CAAC;IACzE,OAAO,CAAC,EAAE,MAAM,IAAI,CAAC;IACrB,OAAO,CAAC,EAAE,CAAC,SAAS,EAAE,MAAM,EAAE,GAAG,CAAC,EAAE,GAAG,KAAK,IAAI,CAAC;IACjD,YAAY,CAAC,KAAK,EAAE,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,EAAE,GAAG,IAAI,CAAC;IACzC,UAAU,CAAC,KAAK,EAAE;QAAE,MAAM,EAAE,MAAM,CAAA;KAAE,EAAE,GAAG,IAAI,CAAC;IAC9C,QAAQ,CAAC,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,CAAC;IAC7B,SAAS,CAAC,IAAI,EAAE,CAAC,EAAE,OAAO,EAAE,CAAC,EAAE,IAAI,EAAE,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,GAAG,IAAI,CAAC;IACxD,UAAU,CAAC,EAAE,OAAO,CAAC;CACtB,GAAG;IAAE,QAAQ,EAAE,MAAM,IAAI,CAAA;CAAE,CA8I3B"}
|
package/dist/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
function w(
|
|
1
|
+
function w(H){let{userId:q,appId:_,room:j,host:n,autoRejoin:h=!0,logLine:T}=H,x=!1,E=0,z,N,B=!0,S=new Map,F=`wss://${n}/room/${_}/${j}?userId=${encodeURIComponent(q)}`;function K($,Z,Q){if(!z)return!1;if(x||z.readyState!==WebSocket.OPEN)return!1;let W={type:$,to:Z,payload:Q};return z.send(JSON.stringify(W)),T?.("\uD83D\uDC64 ➡️ \uD83D\uDDA5️",W),!0}function L(){if(x)return;z=new WebSocket(F),z.onopen=()=>{if(B)H.onOpen?.(),B=!1;E=0},z.onmessage=($)=>{try{let Z=JSON.parse($.data);console.log(">>",Z),(Array.isArray(Z)?Z:[Z]).forEach((W)=>{if(T?.("\uD83D\uDDA5️ ➡️ \uD83D\uDC64",W),W.type==="peer-joined"||W.type==="peer-left")R(W.users);else if(W.type==="ice-server")H.onIceUrl?.(W.url);else if(W.userId)H.onMessage(W.type,W.payload,{userId:W.userId,receive:(M,D)=>K(M,W.userId,D)})})}catch{T?.("⚠️ ERROR",{error:"invalid-json"})}},z.onclose=($)=>{let Q=[1001,1006,1011,1012,1013].includes($.code);if(h&&!x&&Q){let W=Math.min(Math.pow(2,E)*1000,30000),M=Math.random()*1000,D=W+M;T?.("\uD83D\uDD04 RECONNECTING",{attempt:E+1,delayMs:Math.round(D)}),E++,N=setTimeout(L,D)}else H.onClose?.({code:$.code,reason:$.reason,wasClean:$.wasClean})},z.onerror=($)=>{console.error("WS Error",$),H.onError?.()}}function R($){let Z=[],Q=[],W=new Set;$.forEach(({userId:M})=>{if(M===q)return;if(!S.has(q)){let D={userId:M,receive:(V,G)=>K(V,M,G)};S.set(q,D),Z.push(D)}W.add(q)});for(let M of S.keys())if(!W.has(M))S.delete(M),Q.push({userId:M});if(Z.length)H.onPeerJoined(Z);if(Q.length)H.onPeerLeft(Q)}return L(),{exitRoom:()=>{x=!0,clearTimeout(N),z.close()}}}function k({userId:H,appId:q,room:_,host:j,autoRejoin:n=!0,onOpen:h,onClose:T,onError:x,onPeerJoined:E,onPeerLeft:z,onIceUrl:N,onMessage:B,logLine:S,workerUrl:F}){if(!F)return console.warn("Warning: enterRoom called without workerUrl; this may cause issues in some environments. You should pass workerUrl explicitly. Use:","https://cdn.jsdelivr.net/npm/@dobuki/hello-worker/dist/signal-room.worker.min.js"),w({userId:H,appId:q,room:_,host:j,autoRejoin:n,onOpen:h,onClose:T,onError:x,onPeerJoined:E,onPeerLeft:z,onIceUrl:N,onMessage:B});let K=new Worker(F,{type:"module"}),L=!1;function R({userId:Z}){return{userId:Z,receive:(Q,W)=>{if(L)return!1;return K.postMessage({cmd:"send",toUserId:Z,host:j,room:_,type:Q,payload:W}),!0}}}let $=(Z)=>{let Q=Z.data;if(Q.kind==="open")h?.();else if(Q.kind==="close")K.terminate(),T?.(Q.ev);else if(Q.kind==="error")x?.();else if(Q.kind==="peer-joined")E(Q.users.map((W)=>R({userId:W.userId})));else if(Q.kind==="peer-left")z(Q.users);else if(Q.kind==="ice-server")N?.(Q.url);else if(Q.kind==="message")B(Q.type,Q.payload,R({userId:Q.fromUserId}));else if(Q.kind==="log")S?.(Q.direction,Q.obj)};return K.addEventListener("message",$),K.postMessage({cmd:"enter",userId:H,appId:q,room:_,host:j,autoRejoin:n}),{exitRoom:()=>{L=!0,K.removeEventListener("message",$),K.postMessage({cmd:"exit"})}}}var g=k;function y({appId:H,receivePeerConnection:q,peerlessUserExpiration:_=5000,fallbackRtcConfig:j={iceServers:[{urls:"stun:stun.l.google.com:19302"}]},enterRoomFunction:n=g,logLine:h=console.debug,onLeaveUser:T,workerUrl:x,onRoomReady:E,onRoomClose:z}){let N=`user-${crypto.randomUUID()}`,B=new Map,S=void 0,F;async function K(){if(S)try{let D=await fetch(S);if(!D.ok)throw Error(`ICE endpoint failed: ${D.status}`);F=await D.json()}catch(D){console.warn("Using fallback rtcConfig:",D)}return j}async function L(D){return D.pc=new RTCPeerConnection(Date.now()-(F?.timestamp??0)<1e4?F:await K()),D.pc.onicecandidate=(V)=>{if(!V.candidate)return;D.peer.receive("ice",V.candidate.toJSON())},D.pc.onconnectionstatechange=()=>{h("\uD83D\uDCAC",{event:"pc-state",userId:D.userId,state:D.pc?.connectionState})},D.pc}async function R(D){let V=B.get(D.userId),G=!1;if(!V){let X={userId:D.userId,pendingRemoteIce:[],peer:D};B.set(D.userId,X),await L(X),V=X,B.set(V.userId,V),G=!0}else if(V)clearTimeout(V.expirationTimeout),V.expirationTimeout=0;if(!V.pc)await L(V);return V.peer=D,[V,G]}function $(D){T?.(D);let V=B.get(D);if(!V)return;try{V.pc?.close()}catch{}B.delete(D)}async function Z(D){if(!D.pc?.remoteDescription)return;let V=D.pendingRemoteIce;D.pendingRemoteIce=[];for(let G of V)try{await D.pc.addIceCandidate(G)}catch(X){h("⚠️ ERROR",{error:"add-ice-failed",userId:D.userId,detail:String(X)})}}let Q=new Map;function W({room:D,host:V}){let G=`${V}/room/${D}`,X=Q.get(G);if(X)X.exitRoom(),Q.delete(G)}function M({room:D,host:V}){return new Promise(async(G,X)=>{async function f(Y){let[b]=await R(Y),A=b.pc,C=await A?.createOffer();await A?.setLocalDescription(C),Y.receive("offer",A?.localDescription?.toJSON())}let{exitRoom:O}=n({userId:N,appId:H,room:D,host:V,logLine:h,workerUrl:x,autoRejoin:!0,onOpen(){E?.({room:D,host:V}),G()},onError(){console.error("onError"),X()},onClose(Y){z?.({room:D,host:V,ev:Y})},onPeerJoined(Y){Y.forEach(async(b)=>{let[A,C]=await R(b);if(!C)return;let J=A.pc;if(!J)return;async function c(){let P=B.get(b.userId);if(P){P.pc=void 0;let v=await L(P);q({pc:v,userId:b.userId,initiator:!0,restart:c}),await new Promise((U)=>setTimeout(U,3000)),f(b)}}q({pc:J,userId:b.userId,initiator:!0,restart:c}),f(b)})},onPeerLeft(Y){Y.forEach(({userId:b})=>{let A=B.get(b);if(!A)return;A.expirationTimeout=setTimeout(()=>$(b),_??0)})},onIceUrl(Y){S=Y},async onMessage(Y,b,A){let[C]=await R(A),J=C.pc;if(!J)return;if(Y==="offer"){q({pc:J,userId:A.userId,initiator:!1,restart(){C.pc=void 0}}),await J.setRemoteDescription(b);let c=await J.createAnswer();await J.setLocalDescription(c),A.receive("answer",J.localDescription?.toJSON()),await Z(C);return}if(Y==="answer"){await J.setRemoteDescription(b),await Z(C);return}if(Y==="ice"){let c=b;if(!J.remoteDescription){C.pendingRemoteIce.push(c);return}try{await J.addIceCandidate(c)}catch(P){h("⚠️ ERROR",{error:"add-ice-failed",userId:C.userId,detail:String(P)})}return}}});Q.set(`${V}/room/${D}`,{exitRoom:O,room:D,host:V})})}return{userId:N,enterRoom:M,exitRoom:W,leaveUser:$,end(){Q.forEach(({exitRoom:D})=>D()),Q.clear(),B.forEach(({userId:D})=>$(D)),B.clear()}}}function i({appId:H,logLine:q=console.debug,enterRoomFunction:_=k,peerlessUserExpiration:j,workerUrl:n,onRoomReady:h,onRoomClose:T,dataChannelOptions:x}){let E=[],z=new Set;function N(G,X,f,O){if(f){let Y=G.createDataChannel("data",x);B(X,Y,O),S.set(X,Y)}else{let Y=function(b){let A=b.channel;B(X,A,O),S.set(X,A)};return G.addEventListener("datachannel",Y),()=>{G.removeEventListener("datachannel",Y)}}}function B(G,X,f){X.onopen=()=>{q("\uD83D\uDCAC",{event:"dc-open",userId:G}),E.push(G),F.forEach((Y)=>Y(G,"join",E))};let O=({data:Y})=>{z.forEach((b)=>b(Y,G)),q("\uD83D\uDCAC",{event:"dc-message",userId:G,data:Y})};X.addEventListener("message",O),X.addEventListener("close",()=>{q("\uD83D\uDCAC",{event:"dc-close",userId:G}),E.splice(E.indexOf(G),1),F.forEach((Y)=>Y(G,"leave",E)),X.removeEventListener("message",O),f?.()}),X.onerror=()=>q("⚠️ ERROR",{error:"dc-error",userId:G})}let S=new Map,F=new Set,{userId:K,enterRoom:L,exitRoom:R,leaveUser:$,end:Z}=y({appId:H,enterRoomFunction:_,logLine:q,workerUrl:n,peerlessUserExpiration:j,onRoomReady:h,onRoomClose:T,onLeaveUser(G){let X=S.get(G);try{X?.close()}catch{}S.delete(G)},receivePeerConnection({pc:G,userId:X,initiator:f,restart:O}){N(G,X,f,O)}});function Q(G,X){S.forEach((f,O)=>{if(X&&O!==X)return;if(f.readyState==="open")f.send(G)})}function W(G){z.delete(G)}function M(G){return z.add(G),()=>{W(G)}}function D(G){F.delete(G)}function V(G){return F.add(G),()=>{D(G)}}return{userId:K,send:Q,enterRoom:L,exitRoom:R,leaveUser:$,getUsers:()=>E,addMessageListener:M,removeMessageListener:W,addUserListener:V,removeUserListener:D,end(){S.forEach((G)=>{try{G.close()}catch{}}),S.clear(),Z(),F.clear(),E.length=0}}}export{i as enterWorld,w as enterRoom,y as collectPeerConnections};
|
|
2
2
|
|
|
3
|
-
//# debugId=
|
|
3
|
+
//# debugId=8C1941BD41F4E71D64756E2164756E21
|
|
4
4
|
//# sourceMappingURL=index.js.map
|
package/dist/index.js.map
CHANGED
|
@@ -2,12 +2,12 @@
|
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../src/browser/impl/signal-room.ts", "../src/browser/signal-room.ts", "../src/browser/webrtc-peer-collector.ts", "../src/browser/enter-world.ts"],
|
|
4
4
|
"sourcesContent": [
|
|
5
|
-
"export interface IPeer<T extends string = string, P = any> {\n userId: string;\n receive(type: T, payload: P): boolean;\n}\n\n/**\n * enterRoom connects to the signaling room via WebSocket.\n */\nexport function enterRoom<T extends string, P = any>(params: {\n userId: string;\n appId: string;\n room: string;\n host: string;\n onOpen?: () => void;\n onClose?: (ev: Pick<CloseEvent, \"code\" | \"reason\" | \"wasClean\">) => void;\n onError?: () => void;\n logLine?: (direction: string, obj?: any) => void;\n onPeerJoined(users: IPeer<T, P>[]): void;\n onPeerLeft(users: { userId: string }[]): void;\n onIceUrl?(url: string): void;\n onMessage(type: T, payload: P, from: IPeer<T, P>): void;\n autoRejoin?: boolean;\n}): { exitRoom: () => void } {\n type Message = {\n type: \"peer-joined\" | \"peer-left\" | \"ice-server\" | T;\n userId: string;\n users: { userId: string }[];\n payload: P;\n url: string;\n };\n\n const { userId, appId, room, host, autoRejoin = true, logLine } = params;\n\n let exited = false;\n let retryCount = 0;\n let ws: WebSocket;\n let timeoutId: ReturnType<typeof setTimeout>;\n let initialConnection = true;\n\n const peers = new Map<string, IPeer<T, P>>();\n const wsUrl = `wss://${host}/room/${appId}/${room}?userId=${encodeURIComponent(\n userId\n )}`;\n\n // Helper for sending (uses the current ws instance)\n function send(type: T, to: string, payload: P) {\n if (!ws) return false;\n if (exited || ws.readyState !== WebSocket.OPEN) return false;\n const obj = { type, to, payload };\n ws.send(JSON.stringify(obj));\n logLine?.(\"👤 ➡️ 🖥️\", obj);\n return true;\n }\n\n function connect() {\n if (exited) return;\n\n ws = new WebSocket(wsUrl);\n\n ws.onopen = () => {\n if (initialConnection) {\n params.onOpen?.();\n initialConnection = false;\n }\n retryCount = 0; // Reset backoff on successful connection\n };\n\n ws.onmessage = (e: MessageEvent) => {\n try {\n const
|
|
5
|
+
"export interface IPeer<T extends string = string, P = any> {\n userId: string;\n receive(type: T, payload: P): boolean;\n}\n\n/**\n * enterRoom connects to the signaling room via WebSocket.\n */\nexport function enterRoom<T extends string, P = any>(params: {\n userId: string;\n appId: string;\n room: string;\n host: string;\n onOpen?: () => void;\n onClose?: (ev: Pick<CloseEvent, \"code\" | \"reason\" | \"wasClean\">) => void;\n onError?: () => void;\n logLine?: (direction: string, obj?: any) => void;\n onPeerJoined(users: IPeer<T, P>[]): void;\n onPeerLeft(users: { userId: string }[]): void;\n onIceUrl?(url: string): void;\n onMessage(type: T, payload: P, from: IPeer<T, P>): void;\n autoRejoin?: boolean;\n}): { exitRoom: () => void } {\n type Message = {\n type: \"peer-joined\" | \"peer-left\" | \"ice-server\" | T;\n userId: string;\n users: { userId: string }[];\n payload: P;\n url: string;\n };\n\n const { userId, appId, room, host, autoRejoin = true, logLine } = params;\n\n let exited = false;\n let retryCount = 0;\n let ws: WebSocket;\n let timeoutId: ReturnType<typeof setTimeout>;\n let initialConnection = true;\n\n const peers = new Map<string, IPeer<T, P>>();\n const wsUrl = `wss://${host}/room/${appId}/${room}?userId=${encodeURIComponent(\n userId\n )}`;\n\n // Helper for sending (uses the current ws instance)\n function send(type: T, to: string, payload: P) {\n if (!ws) return false;\n if (exited || ws.readyState !== WebSocket.OPEN) return false;\n const obj = { type, to, payload };\n ws.send(JSON.stringify(obj));\n logLine?.(\"👤 ➡️ 🖥️\", obj);\n return true;\n }\n\n function connect() {\n if (exited) return;\n\n ws = new WebSocket(wsUrl);\n\n ws.onopen = () => {\n if (initialConnection) {\n params.onOpen?.();\n initialConnection = false;\n }\n retryCount = 0; // Reset backoff on successful connection\n };\n\n ws.onmessage = (e: MessageEvent) => {\n try {\n const result = JSON.parse(e.data);\n console.log(\">>\", result);\n const msgs: Message[] = Array.isArray(result) ? result : [result];\n msgs.forEach((msg) => {\n logLine?.(\"🖥️ ➡️ 👤\", msg);\n if (msg.type === \"peer-joined\" || msg.type === \"peer-left\") {\n updatePeers(msg.users);\n } else if (msg.type === \"ice-server\") {\n params.onIceUrl?.(msg.url);\n } else if (msg.userId) {\n params.onMessage(msg.type, msg.payload, {\n userId: msg.userId,\n receive: (type: T, payload: P) => send(type, msg.userId, payload),\n });\n }\n });\n } catch {\n logLine?.(\"⚠️ ERROR\", { error: \"invalid-json\" });\n }\n };\n\n ws.onclose = (ev: CloseEvent) => {\n // 1. Check if we should even try to reconnect\n const recoverableCodes = [1001, 1006, 1011, 1012, 1013];\n const isRecoverable = recoverableCodes.includes(ev.code);\n\n if (autoRejoin && !exited && isRecoverable) {\n // 2. Exponential Backoff: 1s, 2s, 4s, 8s... capped at 30s\n const backoff = Math.min(Math.pow(2, retryCount) * 1000, 30000);\n // 3. Add Jitter: +/- 1000ms randomness\n const jitter = Math.random() * 1000;\n const delay = backoff + jitter;\n\n logLine?.(\"🔄 RECONNECTING\", {\n attempt: retryCount + 1,\n delayMs: Math.round(delay),\n });\n\n retryCount++;\n timeoutId = setTimeout(connect, delay);\n } else {\n params.onClose?.({\n code: ev.code,\n reason: ev.reason,\n wasClean: ev.wasClean,\n });\n }\n };\n\n ws.onerror = (ev) => {\n console.error(\"WS Error\", ev);\n params.onError?.();\n };\n }\n\n // Helper for peer tracking (logic from your original code)\n function updatePeers(updatedUsers: { userId: string }[]) {\n const joined: IPeer<T, P>[] = [];\n const left: { userId: string }[] = [];\n const updatedPeerSet = new Set<string>();\n\n updatedUsers.forEach(({ userId: pUserId }) => {\n if (pUserId === userId) return;\n if (!peers.has(userId)) {\n const newPeer = {\n userId: pUserId,\n receive: (t: T, p: P) => send(t, pUserId, p),\n };\n peers.set(userId, newPeer);\n joined.push(newPeer);\n }\n updatedPeerSet.add(userId);\n });\n\n for (const userId of peers.keys()) {\n if (!updatedPeerSet.has(userId)) {\n peers.delete(userId);\n left.push({ userId });\n }\n }\n // Notify peer joined first then peer left. (avoid disconnect in case the peer leaving / joining is on the same user).\n if (joined.length) params.onPeerJoined(joined);\n if (left.length) params.onPeerLeft(left);\n }\n\n // Start initial connection\n connect();\n\n return {\n exitRoom: () => {\n exited = true;\n clearTimeout(timeoutId);\n ws.close();\n },\n };\n}\n",
|
|
6
6
|
"import type { IPeer } from \"./impl/signal-room.js\";\nimport { enterRoom as baseEnterRoom } from \"./impl/signal-room.js\";\nimport { RoomEvent, WorkerCommand } from \"./signal-room.worker.js\";\n\nexport function enterRoom<T extends string, P = any>({\n userId,\n appId,\n room,\n host,\n autoRejoin = true,\n onOpen,\n onClose,\n onError,\n onPeerJoined,\n onPeerLeft,\n onIceUrl,\n onMessage,\n logLine,\n workerUrl,\n}: {\n userId: string;\n appId: string;\n room: string;\n host: string;\n autoRejoin?: boolean;\n onOpen?: () => void;\n onClose?: (ev: Pick<CloseEvent, \"code\" | \"reason\" | \"wasClean\">) => void;\n onError?: () => void;\n onPeerJoined: (users: IPeer<T, P>[]) => void;\n onPeerLeft: (users: { userId: string }[]) => void;\n onIceUrl?(url: string): void;\n onMessage: (type: T, payload: P, from: IPeer<T, P>) => void;\n logLine?: (direction: string, obj?: any) => void;\n\n // Pass the URL to your worker file (bundler will handle it)\n workerUrl?: URL;\n}): { exitRoom: () => void } {\n if (!workerUrl) {\n const CDN_WORKER_URL = `https://cdn.jsdelivr.net/npm/@dobuki/hello-worker/dist/signal-room.worker.min.js`;\n\n console.warn(\n \"Warning: enterRoom called without workerUrl; this may cause issues in some environments. You should pass workerUrl explicitly. Use:\",\n CDN_WORKER_URL\n );\n return baseEnterRoom<T, P>({\n userId,\n appId,\n room,\n host,\n autoRejoin,\n onOpen,\n onClose,\n onError,\n onPeerJoined,\n onPeerLeft,\n onIceUrl,\n onMessage,\n });\n }\n const worker = new Worker(workerUrl, { type: \"module\" });\n let exited = false;\n\n function makeUser({ userId }: { userId: string }): IPeer<T, P> {\n return {\n userId,\n receive: (type: T, payload: P) => {\n if (exited) return false;\n worker.postMessage({\n cmd: \"send\",\n toUserId: userId,\n host,\n room,\n type,\n payload,\n } as WorkerCommand);\n return true;\n },\n };\n }\n\n const onWorkerMessage = (e: MessageEvent<RoomEvent<T, P>>) => {\n const ev = e.data;\n\n if (ev.kind === \"open\") onOpen?.();\n else if (ev.kind === \"close\") {\n worker.terminate();\n onClose?.(ev.ev);\n } else if (ev.kind === \"error\") onError?.();\n else if (ev.kind === \"peer-joined\")\n onPeerJoined(ev.users.map((ev) => makeUser({ userId: ev.userId })));\n else if (ev.kind === \"peer-left\") onPeerLeft(ev.users);\n else if (ev.kind === \"ice-server\") onIceUrl?.(ev.url);\n else if (ev.kind === \"message\")\n onMessage(ev.type, ev.payload, makeUser({ userId: ev.fromUserId }));\n else if (ev.kind === \"log\") logLine?.(ev.direction, ev.obj);\n };\n\n worker.addEventListener(\"message\", onWorkerMessage);\n\n worker.postMessage({\n cmd: \"enter\",\n userId,\n appId,\n room,\n host,\n autoRejoin,\n } as WorkerCommand);\n\n return {\n exitRoom: () => {\n exited = true;\n worker.removeEventListener(\"message\", onWorkerMessage);\n worker.postMessage({ cmd: \"exit\" } as WorkerCommand);\n },\n };\n}\n\nexport type EnterRoom<T extends string, P> = typeof enterRoom<T, P>;\n",
|
|
7
|
-
"import { IPeer } from \"./impl/signal-room\";\nimport { EnterRoom, enterRoom } from \"./signal-room\";\n\nexport type SigType = \"offer\" | \"answer\" | \"ice\";\nexport type SigPayload = RTCSessionDescriptionInit | RTCIceCandidateInit;\n\ntype UserState = {\n userId: string;\n pc?: RTCPeerConnection;\n\n // ICE that arrived before we had remoteDescription\n pendingRemoteIce: RTCIceCandidateInit[];\n\n // the signaling \"user\" handle so we can send messages\n peer: IPeer<SigType, SigPayload>;\n\n expirationTimeout?: number;\n};\n\nconst DEFAULT_ENTER_ROOM = enterRoom;\n\nexport function collectPeerConnections({\n appId,\n receivePeerConnection,\n peerlessUserExpiration = 5000,\n fallbackRtcConfig = {\n iceServers: [{ urls: \"stun:stun.l.google.com:19302\" }],\n },\n enterRoomFunction: enterRoom = DEFAULT_ENTER_ROOM,\n logLine = console.debug,\n onLeaveUser,\n workerUrl,\n onRoomReady,\n onRoomClose,\n}: {\n appId: string;\n fallbackRtcConfig?: RTCConfiguration;\n enterRoomFunction?: EnterRoom<SigType, SigPayload>;\n onLeaveUser?: (userId: string) => void;\n logLine?: (direction: string, obj?: any) => void;\n workerUrl?: URL;\n peerlessUserExpiration?: number;\n receivePeerConnection(connection: {\n pc: RTCPeerConnection;\n userId: string;\n initiator: boolean;\n restart?: () => void;\n }): void;\n onRoomReady?(info: { host: string; room: string }): void;\n onRoomClose?(info: {\n host: string;\n room: string;\n ev: Pick<CloseEvent, \"reason\" | \"code\" | \"wasClean\">;\n }): void;\n}) {\n const userId = `user-${crypto.randomUUID()}`;\n const users: Map<string, UserState> = new Map();\n let iceUrl: string | undefined = undefined;\n let rtcConfig: RTCConfiguration | undefined;\n let rtcConfigExpiration: number = 0;\n\n async function getRtcConfig(): Promise<RTCConfiguration> {\n if (iceUrl) {\n try {\n const r = await fetch(iceUrl);\n if (!r.ok) throw new Error(`ICE endpoint failed: ${r.status}`);\n const result = (await r.json()) as RTCConfiguration & {\n expire: number;\n };\n rtcConfigExpiration = Date.now() + result.expire * 1000;\n rtcConfig = result;\n } catch (e) {\n console.warn(\"Using fallback rtcConfig:\", e);\n }\n }\n return fallbackRtcConfig;\n }\n\n async function setupPC(state: UserState) {\n state.pc = new RTCPeerConnection(\n rtcConfigExpiration - Date.now() < 10000\n ? await getRtcConfig()\n : rtcConfig\n );\n // Send local ICE candidates to this peer\n state.pc.onicecandidate = (ev) => {\n if (!ev.candidate) return;\n state.peer.receive(\"ice\", ev.candidate.toJSON());\n };\n\n state.pc.onconnectionstatechange = () => {\n logLine(\"💬\", {\n event: \"pc-state\",\n userId: state.userId,\n state: state.pc?.connectionState,\n });\n };\n return state.pc;\n }\n\n async function getPeer(\n peer: IPeer<SigType, SigPayload>\n ): Promise<[UserState, boolean]> {\n let state = users.get(peer.userId);\n let isNewPeer = false;\n if (!state) {\n const newState: UserState = {\n userId: peer.userId,\n pendingRemoteIce: [],\n peer,\n };\n users.set(peer.userId, newState);\n\n await setupPC(newState);\n state = newState;\n\n // New user\n users.set(state.userId, state);\n isNewPeer = true;\n } else if (state) {\n clearTimeout(state.expirationTimeout);\n state.expirationTimeout = 0;\n }\n if (!state.pc) {\n await setupPC(state);\n }\n state.peer = peer;\n return [state, isNewPeer];\n }\n\n function leaveUser(userId: string) {\n onLeaveUser?.(userId);\n const p = users.get(userId);\n if (!p) return;\n try {\n p.pc?.close();\n } catch {}\n users.delete(userId);\n }\n\n async function flushRemoteIce(state: UserState) {\n if (!state.pc?.remoteDescription) return;\n\n const queued = state.pendingRemoteIce;\n state.pendingRemoteIce = [];\n\n for (const ice of queued) {\n try {\n await state.pc.addIceCandidate(ice);\n } catch (e) {\n logLine(\"⚠️ ERROR\", {\n error: \"add-ice-failed\",\n userId: state.userId,\n detail: String(e),\n });\n }\n }\n }\n\n const roomsEntered = new Map<\n string,\n { room: string; host: string; exitRoom: () => void }\n >();\n\n function exit({ room, host }: { room: string; host: string }) {\n const key = `${host}/room/${room}`;\n const session = roomsEntered.get(key);\n if (session) {\n session.exitRoom();\n roomsEntered.delete(key);\n }\n }\n\n function enter({ room, host }: { room: string; host: string }) {\n return new Promise<void>(async (resolve, reject) => {\n async function makeOffer(user: IPeer) {\n // Offer flow: createOffer -> setLocalDescription -> send localDescription\n const [state] = await getPeer(user);\n const pc = state.pc;\n const offer = await pc?.createOffer();\n await pc?.setLocalDescription(offer);\n user.receive(\"offer\", pc?.localDescription?.toJSON()!);\n }\n\n const { exitRoom } = enterRoom({\n userId,\n appId,\n room,\n host,\n logLine,\n workerUrl,\n autoRejoin: true,\n\n onOpen() {\n onRoomReady?.({ room, host });\n resolve();\n },\n onError() {\n console.error(\"onError\");\n reject();\n },\n onClose(ev) {\n onRoomClose?.({ room, host, ev });\n },\n\n // Existing peers initiate to the newcomer\n onPeerJoined(joiningUsers: IPeer<SigType, SigPayload>[]) {\n joiningUsers.forEach(async (user) => {\n const [state, isNewPeer] = await getPeer(user);\n if (!isNewPeer) return;\n const pc = state.pc;\n if (!pc) return;\n\n async function restart() {\n const state = users.get(user.userId);\n if (state) {\n state.pc = undefined;\n const pc = await setupPC(state);\n receivePeerConnection({\n pc,\n userId: user.userId,\n initiator: true,\n restart,\n });\n await new Promise((resolve) => setTimeout(resolve, 3000));\n makeOffer(user);\n }\n }\n\n receivePeerConnection({\n pc,\n userId: user.userId,\n initiator: true,\n restart,\n });\n makeOffer(user);\n });\n },\n\n onPeerLeft(leavingUsers: { userId: string }[]) {\n leavingUsers.forEach(({ userId }) => {\n const state = users.get(userId);\n if (!state) return;\n state.expirationTimeout = setTimeout(\n () => leaveUser(userId),\n peerlessUserExpiration ?? 0\n );\n });\n },\n\n onIceUrl(url) {\n iceUrl = url;\n },\n\n async onMessage(type: SigType, payload: any, from: IPeer) {\n const [state] = await getPeer(from);\n const pc = state.pc;\n if (!pc) return;\n\n if (type === \"offer\") {\n receivePeerConnection({\n pc,\n userId: from.userId,\n initiator: false,\n restart() {\n // reset PC\n state.pc = undefined;\n },\n });\n // Responder: set remote offer\n await pc.setRemoteDescription(payload as RTCSessionDescriptionInit);\n\n // Create and send answer\n const answer = await pc.createAnswer();\n await pc.setLocalDescription(answer);\n\n from.receive(\"answer\", pc.localDescription?.toJSON()!);\n\n // Now safe to apply any queued ICE from this peer\n await flushRemoteIce(state);\n return;\n }\n\n if (type === \"answer\") {\n // Initiator: set remote answer\n await pc.setRemoteDescription(payload as RTCSessionDescriptionInit);\n await flushRemoteIce(state);\n return;\n }\n\n if (type === \"ice\") {\n const ice = payload as RTCIceCandidateInit;\n\n // If we don't have remoteDescription yet, queue it\n if (!pc.remoteDescription) {\n state.pendingRemoteIce.push(ice);\n return;\n }\n\n try {\n await pc.addIceCandidate(ice);\n } catch (e) {\n logLine(\"⚠️ ERROR\", {\n error: \"add-ice-failed\",\n userId: state.userId,\n detail: String(e),\n });\n }\n return;\n }\n },\n });\n roomsEntered.set(`${host}/room/${room}`, { exitRoom, room, host });\n });\n }\n\n return {\n userId,\n enterRoom: enter,\n exitRoom: exit,\n leaveUser,\n end() {\n roomsEntered.forEach(({ exitRoom }) => exitRoom());\n roomsEntered.clear();\n users.forEach(({ userId }) => leaveUser(userId));\n users.clear();\n },\n };\n}\n\n/*\nTurn Token ID\n<CF_TURN_TOKEN_ID>\n\nAPI Token\n<CF_RTC_API_TOKEN>\n\nCURL\ncurl \\\n\t-H \"Authorization: Bearer <CF_RTC_API_TOKEN>\" \\\n\t-H \"Content-Type: application/json\" -d '{\"ttl\": 86400}' \\\n\thttps://rtc.live.cloudflare.com/v1/turn/keys/<CF_TURN_TOKEN_ID>/credentials/generate-ice-servers\n\nJSON\n{\n\t\"iceServers\": [\n {\n \"urls\": [\n \"stun:stun.cloudflare.com:3478\",\n \"turn:turn.cloudflare.com:3478?transport=udp\",\n \"turn:turn.cloudflare.com:3478?transport=tcp\",\n \"turns:turn.cloudflare.com:5349?transport=tcp\"\n ],\n \"username\": \"xxxx\",\n \"credential\": \"yyyy\",\n }\n ]\n}\n\n*/\n",
|
|
8
|
-
"import { EnterRoom, enterRoom } from \"./signal-room\";\nimport {\n SigType,\n SigPayload,\n collectPeerConnections,\n} from \"./webrtc-peer-collector\";\n\ntype UserListener = (\n user: string,\n action: \"join\" | \"leave\",\n users: string[]\n) => void;\n\nexport function enterWorld<D extends string | Uint8Array>({\n appId,\n logLine = console.debug,\n enterRoomFunction = enterRoom,\n peerlessUserExpiration,\n workerUrl,\n onRoomReady,\n onRoomClose,\n dataChannelOptions,\n}: {\n appId: string;\n logLine?: (direction: string, obj?: any) => void;\n enterRoomFunction?: EnterRoom<SigType, SigPayload>;\n peerlessUserExpiration?: number;\n workerUrl?: URL;\n onRoomReady?(info: { host: string; room: string }): void;\n onRoomClose?(info: {\n host: string;\n room: string;\n ev: Pick<CloseEvent, \"reason\" | \"code\" | \"wasClean\">;\n }): void;\n dataChannelOptions?: RTCDataChannelInit;\n}) {\n const userIds: string[] = [];\n\n const messagesListeners = new Set<(data: any, from: string) => void>();\n\n function createDataChannel(\n pc: RTCPeerConnection,\n peerUserId: string,\n initiator: boolean,\n restart?: () => void\n ) {\n if (initiator) {\n const dc = pc.createDataChannel(\"data\", dataChannelOptions);\n wireDataChannel(peerUserId, dc, restart);\n dataChannels.set(peerUserId, dc);\n } else {\n function listener(ev: RTCDataChannelEvent) {\n const dc = ev.channel;\n wireDataChannel(peerUserId, dc, restart);\n dataChannels.set(peerUserId, dc);\n
|
|
7
|
+
"import { IPeer } from \"./impl/signal-room\";\nimport { EnterRoom, enterRoom } from \"./signal-room\";\n\nexport type SigType = \"offer\" | \"answer\" | \"ice\";\nexport type SigPayload = RTCSessionDescriptionInit | RTCIceCandidateInit;\n\ntype UserState = {\n userId: string;\n pc?: RTCPeerConnection;\n\n // ICE that arrived before we had remoteDescription\n pendingRemoteIce: RTCIceCandidateInit[];\n\n // the signaling \"user\" handle so we can send messages\n peer: IPeer<SigType, SigPayload>;\n\n expirationTimeout?: number;\n};\n\nconst DEFAULT_ENTER_ROOM = enterRoom;\n\nexport function collectPeerConnections({\n appId,\n receivePeerConnection,\n peerlessUserExpiration = 5000,\n fallbackRtcConfig = {\n iceServers: [{ urls: \"stun:stun.l.google.com:19302\" }],\n },\n enterRoomFunction: enterRoom = DEFAULT_ENTER_ROOM,\n logLine = console.debug,\n onLeaveUser,\n workerUrl,\n onRoomReady,\n onRoomClose,\n}: {\n appId: string;\n fallbackRtcConfig?: RTCConfiguration;\n enterRoomFunction?: EnterRoom<SigType, SigPayload>;\n onLeaveUser?: (userId: string) => void;\n logLine?: (direction: string, obj?: any) => void;\n workerUrl?: URL;\n peerlessUserExpiration?: number;\n receivePeerConnection(connection: {\n pc: RTCPeerConnection;\n userId: string;\n initiator: boolean;\n restart?: () => void;\n }): void;\n onRoomReady?(info: { host: string; room: string }): void;\n onRoomClose?(info: {\n host: string;\n room: string;\n ev: Pick<CloseEvent, \"reason\" | \"code\" | \"wasClean\">;\n }): void;\n}) {\n const userId = `user-${crypto.randomUUID()}`;\n const users: Map<string, UserState> = new Map();\n let iceUrl: string | undefined = undefined;\n let rtcConfig: (RTCConfiguration & { timestamp: number }) | undefined;\n\n async function getRtcConfig(): Promise<RTCConfiguration> {\n if (iceUrl) {\n try {\n const r = await fetch(iceUrl);\n if (!r.ok) throw new Error(`ICE endpoint failed: ${r.status}`);\n rtcConfig = (await r.json()) as RTCConfiguration & {\n timestamp: number;\n };\n } catch (e) {\n console.warn(\"Using fallback rtcConfig:\", e);\n }\n }\n return fallbackRtcConfig;\n }\n\n async function setupPC(state: UserState) {\n state.pc = new RTCPeerConnection(\n Date.now() - (rtcConfig?.timestamp ?? 0) < 10000\n ? rtcConfig\n : await getRtcConfig()\n );\n // Send local ICE candidates to this peer\n state.pc.onicecandidate = (ev) => {\n if (!ev.candidate) return;\n state.peer.receive(\"ice\", ev.candidate.toJSON());\n };\n\n state.pc.onconnectionstatechange = () => {\n logLine(\"💬\", {\n event: \"pc-state\",\n userId: state.userId,\n state: state.pc?.connectionState,\n });\n };\n return state.pc;\n }\n\n async function getPeer(\n peer: IPeer<SigType, SigPayload>\n ): Promise<[UserState, boolean]> {\n let state = users.get(peer.userId);\n let isNewPeer = false;\n if (!state) {\n const newState: UserState = {\n userId: peer.userId,\n pendingRemoteIce: [],\n peer,\n };\n users.set(peer.userId, newState);\n\n await setupPC(newState);\n state = newState;\n\n // New user\n users.set(state.userId, state);\n isNewPeer = true;\n } else if (state) {\n clearTimeout(state.expirationTimeout);\n state.expirationTimeout = 0;\n }\n if (!state.pc) {\n await setupPC(state);\n }\n state.peer = peer;\n return [state, isNewPeer];\n }\n\n function leaveUser(userId: string) {\n onLeaveUser?.(userId);\n const p = users.get(userId);\n if (!p) return;\n try {\n p.pc?.close();\n } catch {}\n users.delete(userId);\n }\n\n async function flushRemoteIce(state: UserState) {\n if (!state.pc?.remoteDescription) return;\n\n const queued = state.pendingRemoteIce;\n state.pendingRemoteIce = [];\n\n for (const ice of queued) {\n try {\n await state.pc.addIceCandidate(ice);\n } catch (e) {\n logLine(\"⚠️ ERROR\", {\n error: \"add-ice-failed\",\n userId: state.userId,\n detail: String(e),\n });\n }\n }\n }\n\n const roomsEntered = new Map<\n string,\n { room: string; host: string; exitRoom: () => void }\n >();\n\n function exit({ room, host }: { room: string; host: string }) {\n const key = `${host}/room/${room}`;\n const session = roomsEntered.get(key);\n if (session) {\n session.exitRoom();\n roomsEntered.delete(key);\n }\n }\n\n function enter({ room, host }: { room: string; host: string }) {\n return new Promise<void>(async (resolve, reject) => {\n async function makeOffer(user: IPeer) {\n // Offer flow: createOffer -> setLocalDescription -> send localDescription\n const [state] = await getPeer(user);\n const pc = state.pc;\n const offer = await pc?.createOffer();\n await pc?.setLocalDescription(offer);\n user.receive(\"offer\", pc?.localDescription?.toJSON()!);\n }\n\n const { exitRoom } = enterRoom({\n userId,\n appId,\n room,\n host,\n logLine,\n workerUrl,\n autoRejoin: true,\n\n onOpen() {\n onRoomReady?.({ room, host });\n resolve();\n },\n onError() {\n console.error(\"onError\");\n reject();\n },\n onClose(ev) {\n onRoomClose?.({ room, host, ev });\n },\n\n // Existing peers initiate to the newcomer\n onPeerJoined(joiningUsers: IPeer<SigType, SigPayload>[]) {\n joiningUsers.forEach(async (user) => {\n const [state, isNewPeer] = await getPeer(user);\n if (!isNewPeer) return;\n const pc = state.pc;\n if (!pc) return;\n\n async function restart() {\n const state = users.get(user.userId);\n if (state) {\n state.pc = undefined;\n const pc = await setupPC(state);\n receivePeerConnection({\n pc,\n userId: user.userId,\n initiator: true,\n restart,\n });\n await new Promise((resolve) => setTimeout(resolve, 3000));\n makeOffer(user);\n }\n }\n\n receivePeerConnection({\n pc,\n userId: user.userId,\n initiator: true,\n restart,\n });\n makeOffer(user);\n });\n },\n\n onPeerLeft(leavingUsers: { userId: string }[]) {\n leavingUsers.forEach(({ userId }) => {\n const state = users.get(userId);\n if (!state) return;\n state.expirationTimeout = setTimeout(\n () => leaveUser(userId),\n peerlessUserExpiration ?? 0\n );\n });\n },\n\n onIceUrl(url) {\n iceUrl = url;\n },\n\n async onMessage(type: SigType, payload: any, from: IPeer) {\n const [state] = await getPeer(from);\n const pc = state.pc;\n if (!pc) return;\n\n if (type === \"offer\") {\n receivePeerConnection({\n pc,\n userId: from.userId,\n initiator: false,\n restart() {\n // reset PC\n state.pc = undefined;\n },\n });\n // Responder: set remote offer\n await pc.setRemoteDescription(payload as RTCSessionDescriptionInit);\n\n // Create and send answer\n const answer = await pc.createAnswer();\n await pc.setLocalDescription(answer);\n\n from.receive(\"answer\", pc.localDescription?.toJSON()!);\n\n // Now safe to apply any queued ICE from this peer\n await flushRemoteIce(state);\n return;\n }\n\n if (type === \"answer\") {\n // Initiator: set remote answer\n await pc.setRemoteDescription(payload as RTCSessionDescriptionInit);\n await flushRemoteIce(state);\n return;\n }\n\n if (type === \"ice\") {\n const ice = payload as RTCIceCandidateInit;\n\n // If we don't have remoteDescription yet, queue it\n if (!pc.remoteDescription) {\n state.pendingRemoteIce.push(ice);\n return;\n }\n\n try {\n await pc.addIceCandidate(ice);\n } catch (e) {\n logLine(\"⚠️ ERROR\", {\n error: \"add-ice-failed\",\n userId: state.userId,\n detail: String(e),\n });\n }\n return;\n }\n },\n });\n roomsEntered.set(`${host}/room/${room}`, { exitRoom, room, host });\n });\n }\n\n return {\n userId,\n enterRoom: enter,\n exitRoom: exit,\n leaveUser,\n end() {\n roomsEntered.forEach(({ exitRoom }) => exitRoom());\n roomsEntered.clear();\n users.forEach(({ userId }) => leaveUser(userId));\n users.clear();\n },\n };\n}\n\n/*\nTurn Token ID\n<CF_TURN_TOKEN_ID>\n\nAPI Token\n<CF_RTC_API_TOKEN>\n\nCURL\ncurl \\\n\t-H \"Authorization: Bearer <CF_RTC_API_TOKEN>\" \\\n\t-H \"Content-Type: application/json\" -d '{\"ttl\": 86400}' \\\n\thttps://rtc.live.cloudflare.com/v1/turn/keys/<CF_TURN_TOKEN_ID>/credentials/generate-ice-servers\n\nJSON\n{\n\t\"iceServers\": [\n {\n \"urls\": [\n \"stun:stun.cloudflare.com:3478\",\n \"turn:turn.cloudflare.com:3478?transport=udp\",\n \"turn:turn.cloudflare.com:3478?transport=tcp\",\n \"turns:turn.cloudflare.com:5349?transport=tcp\"\n ],\n \"username\": \"xxxx\",\n \"credential\": \"yyyy\",\n }\n ]\n}\n\n*/\n",
|
|
8
|
+
"import { EnterRoom, enterRoom } from \"./signal-room\";\nimport {\n SigType,\n SigPayload,\n collectPeerConnections,\n} from \"./webrtc-peer-collector\";\n\ntype UserListener = (\n user: string,\n action: \"join\" | \"leave\",\n users: string[]\n) => void;\n\nexport function enterWorld<D extends string | Uint8Array>({\n appId,\n logLine = console.debug,\n enterRoomFunction = enterRoom,\n peerlessUserExpiration,\n workerUrl,\n onRoomReady,\n onRoomClose,\n dataChannelOptions,\n}: {\n appId: string;\n logLine?: (direction: string, obj?: any) => void;\n enterRoomFunction?: EnterRoom<SigType, SigPayload>;\n peerlessUserExpiration?: number;\n workerUrl?: URL;\n onRoomReady?(info: { host: string; room: string }): void;\n onRoomClose?(info: {\n host: string;\n room: string;\n ev: Pick<CloseEvent, \"reason\" | \"code\" | \"wasClean\">;\n }): void;\n dataChannelOptions?: RTCDataChannelInit;\n}) {\n const userIds: string[] = [];\n\n const messagesListeners = new Set<(data: any, from: string) => void>();\n\n function createDataChannel(\n pc: RTCPeerConnection,\n peerUserId: string,\n initiator: boolean,\n restart?: () => void\n ) {\n if (initiator) {\n const dc = pc.createDataChannel(\"data\", dataChannelOptions);\n wireDataChannel(peerUserId, dc, restart);\n dataChannels.set(peerUserId, dc);\n } else {\n function listener(ev: RTCDataChannelEvent) {\n const dc = ev.channel;\n wireDataChannel(peerUserId, dc, restart);\n dataChannels.set(peerUserId, dc);\n }\n pc.addEventListener(\"datachannel\", listener);\n return () => {\n pc.removeEventListener(\"datachannel\", listener);\n };\n }\n }\n\n function wireDataChannel(\n userId: string,\n dc: RTCDataChannel,\n restart?: () => void\n ) {\n dc.onopen = () => {\n logLine(\"💬\", { event: \"dc-open\", userId });\n userIds.push(userId);\n userListeners.forEach((listener) => listener(userId, \"join\", userIds));\n };\n const onmessage = ({ data }: MessageEvent) => {\n messagesListeners.forEach((listener) => listener(data as any, userId));\n logLine(\"💬\", { event: \"dc-message\", userId, data });\n };\n dc.addEventListener(\"message\", onmessage);\n dc.addEventListener(\"close\", () => {\n logLine(\"💬\", { event: \"dc-close\", userId });\n userIds.splice(userIds.indexOf(userId), 1);\n userListeners.forEach((listener) => listener(userId, \"leave\", userIds));\n dc.removeEventListener(\"message\", onmessage);\n restart?.();\n });\n dc.onerror = () => logLine(\"⚠️ ERROR\", { error: \"dc-error\", userId });\n }\n\n const dataChannels = new Map<string, RTCDataChannel>();\n const userListeners = new Set<UserListener>();\n\n const {\n userId,\n enterRoom,\n exitRoom,\n leaveUser,\n end: endPeerCollection,\n } = collectPeerConnections({\n appId,\n enterRoomFunction,\n logLine,\n workerUrl,\n peerlessUserExpiration,\n onRoomReady,\n onRoomClose,\n onLeaveUser(userId: string) {\n const dc = dataChannels.get(userId);\n try {\n dc?.close();\n } catch {}\n dataChannels.delete(userId);\n },\n receivePeerConnection({ pc, userId, initiator, restart }) {\n createDataChannel(pc, userId, initiator, restart);\n },\n });\n\n function send(data: D, userId?: string) {\n dataChannels.forEach((dataChannel, pUserId) => {\n if (userId && pUserId !== userId) return;\n if (dataChannel.readyState === \"open\") {\n dataChannel.send(data as any);\n }\n });\n }\n\n function removeMessageListener(listener: (data: D, from: string) => void) {\n messagesListeners.delete(listener);\n }\n\n function addMessageListener(listener: (data: D, from: string) => void) {\n messagesListeners.add(listener);\n return () => {\n removeMessageListener(listener);\n };\n }\n\n function removeUserListener(listener: UserListener) {\n userListeners.delete(listener);\n }\n\n function addUserListener(listener: UserListener) {\n userListeners.add(listener);\n return () => {\n removeUserListener(listener);\n };\n }\n\n return {\n userId,\n send,\n enterRoom,\n exitRoom,\n leaveUser,\n getUsers: () => userIds,\n addMessageListener,\n removeMessageListener,\n addUserListener,\n removeUserListener,\n end() {\n dataChannels.forEach((dataChannel) => {\n try {\n dataChannel.close();\n } catch {}\n });\n dataChannels.clear();\n endPeerCollection();\n userListeners.clear();\n userIds.length = 0;\n },\n };\n}\n"
|
|
9
9
|
],
|
|
10
|
-
"mappings": "AAQO,SAAS,CAAoC,CAAC,EAcxB,CAS3B,IAAQ,SAAQ,QAAO,OAAM,OAAM,aAAa,GAAM,WAAY,EAE9D,EAAS,GACT,EAAa,EACb,EACA,EACA,EAAoB,GAElB,EAAQ,IAAI,IACZ,EAAQ,SAAS,UAAa,KAAS,YAAe,mBAC1D,CACF,IAGA,SAAS,CAAI,CAAC,EAAS,EAAY,EAAY,CAC7C,GAAI,CAAC,EAAI,MAAO,GAChB,GAAI,GAAU,EAAG,aAAe,UAAU,KAAM,MAAO,GACvD,IAAM,EAAM,CAAE,OAAM,KAAI,SAAQ,EAGhC,OAFA,EAAG,KAAK,KAAK,UAAU,CAAG,CAAC,EAC3B,IAAU,gCAAY,CAAG,EAClB,GAGT,SAAS,CAAO,EAAG,CACjB,GAAI,EAAQ,OAEZ,EAAK,IAAI,UAAU,CAAK,EAExB,EAAG,OAAS,IAAM,CAChB,GAAI,EACF,EAAO,SAAS,EAChB,EAAoB,GAEtB,EAAa,GAGf,EAAG,UAAY,CAAC,IAAoB,CAClC,GAAI,CACF,IAAM,
|
|
11
|
-
"debugId": "
|
|
10
|
+
"mappings": "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",
|
|
11
|
+
"debugId": "8C1941BD41F4E71D64756E2164756E21",
|
|
12
12
|
"names": []
|
|
13
13
|
}
|
package/dist/signal-room.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
function b(F){let{userId:K,appId:
|
|
1
|
+
function b(F){let{userId:K,appId:Z,room:_,host:S,autoRejoin:J=!0,logLine:Q}=F,T=!1,V=0,D,$,N=!0,Y=new Map,O=`wss://${S}/room/${Z}/${_}?userId=${encodeURIComponent(K)}`;function H(B,A,q){if(!D)return!1;if(T||D.readyState!==WebSocket.OPEN)return!1;let z={type:B,to:A,payload:q};return D.send(JSON.stringify(z)),Q?.("\uD83D\uDC64 ➡️ \uD83D\uDDA5️",z),!0}function W(){if(T)return;D=new WebSocket(O),D.onopen=()=>{if(N)F.onOpen?.(),N=!1;V=0},D.onmessage=(B)=>{try{let A=JSON.parse(B.data);console.log(">>",A),(Array.isArray(A)?A:[A]).forEach((z)=>{if(Q?.("\uD83D\uDDA5️ ➡️ \uD83D\uDC64",z),z.type==="peer-joined"||z.type==="peer-left")E(z.users);else if(z.type==="ice-server")F.onIceUrl?.(z.url);else if(z.userId)F.onMessage(z.type,z.payload,{userId:z.userId,receive:(G,X)=>H(G,z.userId,X)})})}catch{Q?.("⚠️ ERROR",{error:"invalid-json"})}},D.onclose=(B)=>{let q=[1001,1006,1011,1012,1013].includes(B.code);if(J&&!T&&q){let z=Math.min(Math.pow(2,V)*1000,30000),G=Math.random()*1000,X=z+G;Q?.("\uD83D\uDD04 RECONNECTING",{attempt:V+1,delayMs:Math.round(X)}),V++,$=setTimeout(W,X)}else F.onClose?.({code:B.code,reason:B.reason,wasClean:B.wasClean})},D.onerror=(B)=>{console.error("WS Error",B),F.onError?.()}}function E(B){let A=[],q=[],z=new Set;B.forEach(({userId:G})=>{if(G===K)return;if(!Y.has(K)){let X={userId:G,receive:(x,M)=>H(x,G,M)};Y.set(K,X),A.push(X)}z.add(K)});for(let G of Y.keys())if(!z.has(G))Y.delete(G),q.push({userId:G});if(A.length)F.onPeerJoined(A);if(q.length)F.onPeerLeft(q)}return W(),{exitRoom:()=>{T=!0,clearTimeout($),D.close()}}}function L({userId:F,appId:K,room:Z,host:_,autoRejoin:S=!0,onOpen:J,onClose:Q,onError:T,onPeerJoined:V,onPeerLeft:D,onIceUrl:$,onMessage:N,logLine:Y,workerUrl:O}){if(!O)return console.warn("Warning: enterRoom called without workerUrl; this may cause issues in some environments. You should pass workerUrl explicitly. Use:","https://cdn.jsdelivr.net/npm/@dobuki/hello-worker/dist/signal-room.worker.min.js"),b({userId:F,appId:K,room:Z,host:_,autoRejoin:S,onOpen:J,onClose:Q,onError:T,onPeerJoined:V,onPeerLeft:D,onIceUrl:$,onMessage:N});let H=new Worker(O,{type:"module"}),W=!1;function E({userId:A}){return{userId:A,receive:(q,z)=>{if(W)return!1;return H.postMessage({cmd:"send",toUserId:A,host:_,room:Z,type:q,payload:z}),!0}}}let B=(A)=>{let q=A.data;if(q.kind==="open")J?.();else if(q.kind==="close")H.terminate(),Q?.(q.ev);else if(q.kind==="error")T?.();else if(q.kind==="peer-joined")V(q.users.map((z)=>E({userId:z.userId})));else if(q.kind==="peer-left")D(q.users);else if(q.kind==="ice-server")$?.(q.url);else if(q.kind==="message")N(q.type,q.payload,E({userId:q.fromUserId}));else if(q.kind==="log")Y?.(q.direction,q.obj)};return H.addEventListener("message",B),H.postMessage({cmd:"enter",userId:F,appId:K,room:Z,host:_,autoRejoin:S}),{exitRoom:()=>{W=!0,H.removeEventListener("message",B),H.postMessage({cmd:"exit"})}}}export{L as enterRoom};
|
|
2
2
|
|
|
3
|
-
//# debugId=
|
|
3
|
+
//# debugId=5FE4D77F3AF2CDFA64756E2164756E21
|
|
4
4
|
//# sourceMappingURL=signal-room.js.map
|
package/dist/signal-room.js.map
CHANGED
|
@@ -2,10 +2,10 @@
|
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../src/browser/impl/signal-room.ts", "../src/browser/signal-room.ts"],
|
|
4
4
|
"sourcesContent": [
|
|
5
|
-
"export interface IPeer<T extends string = string, P = any> {\n userId: string;\n receive(type: T, payload: P): boolean;\n}\n\n/**\n * enterRoom connects to the signaling room via WebSocket.\n */\nexport function enterRoom<T extends string, P = any>(params: {\n userId: string;\n appId: string;\n room: string;\n host: string;\n onOpen?: () => void;\n onClose?: (ev: Pick<CloseEvent, \"code\" | \"reason\" | \"wasClean\">) => void;\n onError?: () => void;\n logLine?: (direction: string, obj?: any) => void;\n onPeerJoined(users: IPeer<T, P>[]): void;\n onPeerLeft(users: { userId: string }[]): void;\n onIceUrl?(url: string): void;\n onMessage(type: T, payload: P, from: IPeer<T, P>): void;\n autoRejoin?: boolean;\n}): { exitRoom: () => void } {\n type Message = {\n type: \"peer-joined\" | \"peer-left\" | \"ice-server\" | T;\n userId: string;\n users: { userId: string }[];\n payload: P;\n url: string;\n };\n\n const { userId, appId, room, host, autoRejoin = true, logLine } = params;\n\n let exited = false;\n let retryCount = 0;\n let ws: WebSocket;\n let timeoutId: ReturnType<typeof setTimeout>;\n let initialConnection = true;\n\n const peers = new Map<string, IPeer<T, P>>();\n const wsUrl = `wss://${host}/room/${appId}/${room}?userId=${encodeURIComponent(\n userId\n )}`;\n\n // Helper for sending (uses the current ws instance)\n function send(type: T, to: string, payload: P) {\n if (!ws) return false;\n if (exited || ws.readyState !== WebSocket.OPEN) return false;\n const obj = { type, to, payload };\n ws.send(JSON.stringify(obj));\n logLine?.(\"👤 ➡️ 🖥️\", obj);\n return true;\n }\n\n function connect() {\n if (exited) return;\n\n ws = new WebSocket(wsUrl);\n\n ws.onopen = () => {\n if (initialConnection) {\n params.onOpen?.();\n initialConnection = false;\n }\n retryCount = 0; // Reset backoff on successful connection\n };\n\n ws.onmessage = (e: MessageEvent) => {\n try {\n const
|
|
5
|
+
"export interface IPeer<T extends string = string, P = any> {\n userId: string;\n receive(type: T, payload: P): boolean;\n}\n\n/**\n * enterRoom connects to the signaling room via WebSocket.\n */\nexport function enterRoom<T extends string, P = any>(params: {\n userId: string;\n appId: string;\n room: string;\n host: string;\n onOpen?: () => void;\n onClose?: (ev: Pick<CloseEvent, \"code\" | \"reason\" | \"wasClean\">) => void;\n onError?: () => void;\n logLine?: (direction: string, obj?: any) => void;\n onPeerJoined(users: IPeer<T, P>[]): void;\n onPeerLeft(users: { userId: string }[]): void;\n onIceUrl?(url: string): void;\n onMessage(type: T, payload: P, from: IPeer<T, P>): void;\n autoRejoin?: boolean;\n}): { exitRoom: () => void } {\n type Message = {\n type: \"peer-joined\" | \"peer-left\" | \"ice-server\" | T;\n userId: string;\n users: { userId: string }[];\n payload: P;\n url: string;\n };\n\n const { userId, appId, room, host, autoRejoin = true, logLine } = params;\n\n let exited = false;\n let retryCount = 0;\n let ws: WebSocket;\n let timeoutId: ReturnType<typeof setTimeout>;\n let initialConnection = true;\n\n const peers = new Map<string, IPeer<T, P>>();\n const wsUrl = `wss://${host}/room/${appId}/${room}?userId=${encodeURIComponent(\n userId\n )}`;\n\n // Helper for sending (uses the current ws instance)\n function send(type: T, to: string, payload: P) {\n if (!ws) return false;\n if (exited || ws.readyState !== WebSocket.OPEN) return false;\n const obj = { type, to, payload };\n ws.send(JSON.stringify(obj));\n logLine?.(\"👤 ➡️ 🖥️\", obj);\n return true;\n }\n\n function connect() {\n if (exited) return;\n\n ws = new WebSocket(wsUrl);\n\n ws.onopen = () => {\n if (initialConnection) {\n params.onOpen?.();\n initialConnection = false;\n }\n retryCount = 0; // Reset backoff on successful connection\n };\n\n ws.onmessage = (e: MessageEvent) => {\n try {\n const result = JSON.parse(e.data);\n console.log(\">>\", result);\n const msgs: Message[] = Array.isArray(result) ? result : [result];\n msgs.forEach((msg) => {\n logLine?.(\"🖥️ ➡️ 👤\", msg);\n if (msg.type === \"peer-joined\" || msg.type === \"peer-left\") {\n updatePeers(msg.users);\n } else if (msg.type === \"ice-server\") {\n params.onIceUrl?.(msg.url);\n } else if (msg.userId) {\n params.onMessage(msg.type, msg.payload, {\n userId: msg.userId,\n receive: (type: T, payload: P) => send(type, msg.userId, payload),\n });\n }\n });\n } catch {\n logLine?.(\"⚠️ ERROR\", { error: \"invalid-json\" });\n }\n };\n\n ws.onclose = (ev: CloseEvent) => {\n // 1. Check if we should even try to reconnect\n const recoverableCodes = [1001, 1006, 1011, 1012, 1013];\n const isRecoverable = recoverableCodes.includes(ev.code);\n\n if (autoRejoin && !exited && isRecoverable) {\n // 2. Exponential Backoff: 1s, 2s, 4s, 8s... capped at 30s\n const backoff = Math.min(Math.pow(2, retryCount) * 1000, 30000);\n // 3. Add Jitter: +/- 1000ms randomness\n const jitter = Math.random() * 1000;\n const delay = backoff + jitter;\n\n logLine?.(\"🔄 RECONNECTING\", {\n attempt: retryCount + 1,\n delayMs: Math.round(delay),\n });\n\n retryCount++;\n timeoutId = setTimeout(connect, delay);\n } else {\n params.onClose?.({\n code: ev.code,\n reason: ev.reason,\n wasClean: ev.wasClean,\n });\n }\n };\n\n ws.onerror = (ev) => {\n console.error(\"WS Error\", ev);\n params.onError?.();\n };\n }\n\n // Helper for peer tracking (logic from your original code)\n function updatePeers(updatedUsers: { userId: string }[]) {\n const joined: IPeer<T, P>[] = [];\n const left: { userId: string }[] = [];\n const updatedPeerSet = new Set<string>();\n\n updatedUsers.forEach(({ userId: pUserId }) => {\n if (pUserId === userId) return;\n if (!peers.has(userId)) {\n const newPeer = {\n userId: pUserId,\n receive: (t: T, p: P) => send(t, pUserId, p),\n };\n peers.set(userId, newPeer);\n joined.push(newPeer);\n }\n updatedPeerSet.add(userId);\n });\n\n for (const userId of peers.keys()) {\n if (!updatedPeerSet.has(userId)) {\n peers.delete(userId);\n left.push({ userId });\n }\n }\n // Notify peer joined first then peer left. (avoid disconnect in case the peer leaving / joining is on the same user).\n if (joined.length) params.onPeerJoined(joined);\n if (left.length) params.onPeerLeft(left);\n }\n\n // Start initial connection\n connect();\n\n return {\n exitRoom: () => {\n exited = true;\n clearTimeout(timeoutId);\n ws.close();\n },\n };\n}\n",
|
|
6
6
|
"import type { IPeer } from \"./impl/signal-room.js\";\nimport { enterRoom as baseEnterRoom } from \"./impl/signal-room.js\";\nimport { RoomEvent, WorkerCommand } from \"./signal-room.worker.js\";\n\nexport function enterRoom<T extends string, P = any>({\n userId,\n appId,\n room,\n host,\n autoRejoin = true,\n onOpen,\n onClose,\n onError,\n onPeerJoined,\n onPeerLeft,\n onIceUrl,\n onMessage,\n logLine,\n workerUrl,\n}: {\n userId: string;\n appId: string;\n room: string;\n host: string;\n autoRejoin?: boolean;\n onOpen?: () => void;\n onClose?: (ev: Pick<CloseEvent, \"code\" | \"reason\" | \"wasClean\">) => void;\n onError?: () => void;\n onPeerJoined: (users: IPeer<T, P>[]) => void;\n onPeerLeft: (users: { userId: string }[]) => void;\n onIceUrl?(url: string): void;\n onMessage: (type: T, payload: P, from: IPeer<T, P>) => void;\n logLine?: (direction: string, obj?: any) => void;\n\n // Pass the URL to your worker file (bundler will handle it)\n workerUrl?: URL;\n}): { exitRoom: () => void } {\n if (!workerUrl) {\n const CDN_WORKER_URL = `https://cdn.jsdelivr.net/npm/@dobuki/hello-worker/dist/signal-room.worker.min.js`;\n\n console.warn(\n \"Warning: enterRoom called without workerUrl; this may cause issues in some environments. You should pass workerUrl explicitly. Use:\",\n CDN_WORKER_URL\n );\n return baseEnterRoom<T, P>({\n userId,\n appId,\n room,\n host,\n autoRejoin,\n onOpen,\n onClose,\n onError,\n onPeerJoined,\n onPeerLeft,\n onIceUrl,\n onMessage,\n });\n }\n const worker = new Worker(workerUrl, { type: \"module\" });\n let exited = false;\n\n function makeUser({ userId }: { userId: string }): IPeer<T, P> {\n return {\n userId,\n receive: (type: T, payload: P) => {\n if (exited) return false;\n worker.postMessage({\n cmd: \"send\",\n toUserId: userId,\n host,\n room,\n type,\n payload,\n } as WorkerCommand);\n return true;\n },\n };\n }\n\n const onWorkerMessage = (e: MessageEvent<RoomEvent<T, P>>) => {\n const ev = e.data;\n\n if (ev.kind === \"open\") onOpen?.();\n else if (ev.kind === \"close\") {\n worker.terminate();\n onClose?.(ev.ev);\n } else if (ev.kind === \"error\") onError?.();\n else if (ev.kind === \"peer-joined\")\n onPeerJoined(ev.users.map((ev) => makeUser({ userId: ev.userId })));\n else if (ev.kind === \"peer-left\") onPeerLeft(ev.users);\n else if (ev.kind === \"ice-server\") onIceUrl?.(ev.url);\n else if (ev.kind === \"message\")\n onMessage(ev.type, ev.payload, makeUser({ userId: ev.fromUserId }));\n else if (ev.kind === \"log\") logLine?.(ev.direction, ev.obj);\n };\n\n worker.addEventListener(\"message\", onWorkerMessage);\n\n worker.postMessage({\n cmd: \"enter\",\n userId,\n appId,\n room,\n host,\n autoRejoin,\n } as WorkerCommand);\n\n return {\n exitRoom: () => {\n exited = true;\n worker.removeEventListener(\"message\", onWorkerMessage);\n worker.postMessage({ cmd: \"exit\" } as WorkerCommand);\n },\n };\n}\n\nexport type EnterRoom<T extends string, P> = typeof enterRoom<T, P>;\n"
|
|
7
7
|
],
|
|
8
|
-
"mappings": "AAQO,SAAS,CAAoC,CAAC,EAcxB,CAS3B,IAAQ,SAAQ,QAAO,OAAM,OAAM,aAAa,GAAM,WAAY,EAE9D,EAAS,GACT,EAAa,EACb,EACA,EACA,EAAoB,GAElB,EAAQ,IAAI,IACZ,EAAQ,SAAS,UAAa,KAAS,YAAe,mBAC1D,CACF,IAGA,SAAS,CAAI,CAAC,EAAS,EAAY,EAAY,CAC7C,GAAI,CAAC,EAAI,MAAO,GAChB,GAAI,GAAU,EAAG,aAAe,UAAU,KAAM,MAAO,GACvD,IAAM,EAAM,CAAE,OAAM,KAAI,SAAQ,EAGhC,OAFA,EAAG,KAAK,KAAK,UAAU,CAAG,CAAC,EAC3B,IAAU,gCAAY,CAAG,EAClB,GAGT,SAAS,CAAO,EAAG,CACjB,GAAI,EAAQ,OAEZ,EAAK,IAAI,UAAU,CAAK,EAExB,EAAG,OAAS,IAAM,CAChB,GAAI,EACF,EAAO,SAAS,EAChB,EAAoB,GAEtB,EAAa,GAGf,EAAG,UAAY,CAAC,IAAoB,CAClC,GAAI,CACF,IAAM,
|
|
9
|
-
"debugId": "
|
|
8
|
+
"mappings": "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",
|
|
9
|
+
"debugId": "5FE4D77F3AF2CDFA64756E2164756E21",
|
|
10
10
|
"names": []
|
|
11
11
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
function h(D){let{userId:q,appId:
|
|
1
|
+
function h(D){let{userId:q,appId:V,room:A,host:B,autoRejoin:J=!0,logLine:Y}=D,Z=!1,_=0,K,M,N=!0,$=new Map,L=`wss://${B}/room/${V}/${A}?userId=${encodeURIComponent(q)}`;function W(E,G,O){if(!K)return!1;if(Z||K.readyState!==WebSocket.OPEN)return!1;let z={type:E,to:G,payload:O};return K.send(JSON.stringify(z)),Y?.("\uD83D\uDC64 ➡️ \uD83D\uDDA5️",z),!0}function F(){if(Z)return;K=new WebSocket(L),K.onopen=()=>{if(N)D.onOpen?.(),N=!1;_=0},K.onmessage=(E)=>{try{let G=JSON.parse(E.data);console.log(">>",G),(Array.isArray(G)?G:[G]).forEach((z)=>{if(Y?.("\uD83D\uDDA5️ ➡️ \uD83D\uDC64",z),z.type==="peer-joined"||z.type==="peer-left")P(z.users);else if(z.type==="ice-server")D.onIceUrl?.(z.url);else if(z.userId)D.onMessage(z.type,z.payload,{userId:z.userId,receive:(H,T)=>W(H,z.userId,T)})})}catch{Y?.("⚠️ ERROR",{error:"invalid-json"})}},K.onclose=(E)=>{let O=[1001,1006,1011,1012,1013].includes(E.code);if(J&&!Z&&O){let z=Math.min(Math.pow(2,_)*1000,30000),H=Math.random()*1000,T=z+H;Y?.("\uD83D\uDD04 RECONNECTING",{attempt:_+1,delayMs:Math.round(T)}),_++,M=setTimeout(F,T)}else D.onClose?.({code:E.code,reason:E.reason,wasClean:E.wasClean})},K.onerror=(E)=>{console.error("WS Error",E),D.onError?.()}}function P(E){let G=[],O=[],z=new Set;E.forEach(({userId:H})=>{if(H===q)return;if(!$.has(q)){let T={userId:H,receive:(R,b)=>W(R,H,b)};$.set(q,T),G.push(T)}z.add(q)});for(let H of $.keys())if(!z.has(H))$.delete(H),O.push({userId:H});if(G.length)D.onPeerJoined(G);if(O.length)D.onPeerLeft(O)}return F(),{exitRoom:()=>{Z=!0,clearTimeout(M),K.close()}}}var k=null,X=new Map;function Q(D){self.postMessage(D)}self.addEventListener("message",(D)=>{let q=D.data;if(console.debug("[signal-room.worker] received command",q),q.cmd==="enter"){k?.(),k=null,X.clear(),k=h({userId:q.userId,appId:q.appId,room:q.room,host:q.host,autoRejoin:q.autoRejoin,onOpen:()=>Q({kind:"open"}),onClose:({code:A,reason:B,wasClean:J})=>Q({kind:"close",ev:{code:A,reason:B,wasClean:J}}),onError:()=>Q({kind:"error"}),logLine:(A,B)=>{console.debug(`[signal-room.worker] ${A}`,B),Q({kind:"log",direction:A,obj:B})},onPeerJoined:(A)=>{A.forEach(({userId:B,receive:J})=>X.set(`${q.host}/${q.room}/${B}`,J)),Q({kind:"peer-joined",users:A.map(({userId:B})=>({userId:B}))})},onPeerLeft:(A)=>{A.forEach(({userId:B})=>X.delete(`${q.host}/${q.room}/${B}`)),Q({kind:"peer-left",users:A})},onIceUrl(A){Q({kind:"ice-server",url:A})},onMessage:(A,B,J)=>{X.set(`${q.host}/${q.room}/${J.userId}`,J.receive),Q({kind:"message",type:A,payload:B,fromUserId:J.userId})}}).exitRoom;return}if(q.cmd==="send"){let V=X.get(`${q.host}/${q.room}/${q.toUserId}`);if(V)V(q.type,q.payload);return}if(q.cmd==="exit"){k?.(),self.close();return}});
|
|
2
2
|
|
|
3
|
-
//# debugId=
|
|
3
|
+
//# debugId=F93B044ACAC6056164756E2164756E21
|
|
4
4
|
//# sourceMappingURL=signal-room.worker.js.map
|
|
@@ -2,10 +2,10 @@
|
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../src/browser/impl/signal-room.ts", "../src/browser/signal-room.worker.ts"],
|
|
4
4
|
"sourcesContent": [
|
|
5
|
-
"export interface IPeer<T extends string = string, P = any> {\n userId: string;\n receive(type: T, payload: P): boolean;\n}\n\n/**\n * enterRoom connects to the signaling room via WebSocket.\n */\nexport function enterRoom<T extends string, P = any>(params: {\n userId: string;\n appId: string;\n room: string;\n host: string;\n onOpen?: () => void;\n onClose?: (ev: Pick<CloseEvent, \"code\" | \"reason\" | \"wasClean\">) => void;\n onError?: () => void;\n logLine?: (direction: string, obj?: any) => void;\n onPeerJoined(users: IPeer<T, P>[]): void;\n onPeerLeft(users: { userId: string }[]): void;\n onIceUrl?(url: string): void;\n onMessage(type: T, payload: P, from: IPeer<T, P>): void;\n autoRejoin?: boolean;\n}): { exitRoom: () => void } {\n type Message = {\n type: \"peer-joined\" | \"peer-left\" | \"ice-server\" | T;\n userId: string;\n users: { userId: string }[];\n payload: P;\n url: string;\n };\n\n const { userId, appId, room, host, autoRejoin = true, logLine } = params;\n\n let exited = false;\n let retryCount = 0;\n let ws: WebSocket;\n let timeoutId: ReturnType<typeof setTimeout>;\n let initialConnection = true;\n\n const peers = new Map<string, IPeer<T, P>>();\n const wsUrl = `wss://${host}/room/${appId}/${room}?userId=${encodeURIComponent(\n userId\n )}`;\n\n // Helper for sending (uses the current ws instance)\n function send(type: T, to: string, payload: P) {\n if (!ws) return false;\n if (exited || ws.readyState !== WebSocket.OPEN) return false;\n const obj = { type, to, payload };\n ws.send(JSON.stringify(obj));\n logLine?.(\"👤 ➡️ 🖥️\", obj);\n return true;\n }\n\n function connect() {\n if (exited) return;\n\n ws = new WebSocket(wsUrl);\n\n ws.onopen = () => {\n if (initialConnection) {\n params.onOpen?.();\n initialConnection = false;\n }\n retryCount = 0; // Reset backoff on successful connection\n };\n\n ws.onmessage = (e: MessageEvent) => {\n try {\n const
|
|
5
|
+
"export interface IPeer<T extends string = string, P = any> {\n userId: string;\n receive(type: T, payload: P): boolean;\n}\n\n/**\n * enterRoom connects to the signaling room via WebSocket.\n */\nexport function enterRoom<T extends string, P = any>(params: {\n userId: string;\n appId: string;\n room: string;\n host: string;\n onOpen?: () => void;\n onClose?: (ev: Pick<CloseEvent, \"code\" | \"reason\" | \"wasClean\">) => void;\n onError?: () => void;\n logLine?: (direction: string, obj?: any) => void;\n onPeerJoined(users: IPeer<T, P>[]): void;\n onPeerLeft(users: { userId: string }[]): void;\n onIceUrl?(url: string): void;\n onMessage(type: T, payload: P, from: IPeer<T, P>): void;\n autoRejoin?: boolean;\n}): { exitRoom: () => void } {\n type Message = {\n type: \"peer-joined\" | \"peer-left\" | \"ice-server\" | T;\n userId: string;\n users: { userId: string }[];\n payload: P;\n url: string;\n };\n\n const { userId, appId, room, host, autoRejoin = true, logLine } = params;\n\n let exited = false;\n let retryCount = 0;\n let ws: WebSocket;\n let timeoutId: ReturnType<typeof setTimeout>;\n let initialConnection = true;\n\n const peers = new Map<string, IPeer<T, P>>();\n const wsUrl = `wss://${host}/room/${appId}/${room}?userId=${encodeURIComponent(\n userId\n )}`;\n\n // Helper for sending (uses the current ws instance)\n function send(type: T, to: string, payload: P) {\n if (!ws) return false;\n if (exited || ws.readyState !== WebSocket.OPEN) return false;\n const obj = { type, to, payload };\n ws.send(JSON.stringify(obj));\n logLine?.(\"👤 ➡️ 🖥️\", obj);\n return true;\n }\n\n function connect() {\n if (exited) return;\n\n ws = new WebSocket(wsUrl);\n\n ws.onopen = () => {\n if (initialConnection) {\n params.onOpen?.();\n initialConnection = false;\n }\n retryCount = 0; // Reset backoff on successful connection\n };\n\n ws.onmessage = (e: MessageEvent) => {\n try {\n const result = JSON.parse(e.data);\n console.log(\">>\", result);\n const msgs: Message[] = Array.isArray(result) ? result : [result];\n msgs.forEach((msg) => {\n logLine?.(\"🖥️ ➡️ 👤\", msg);\n if (msg.type === \"peer-joined\" || msg.type === \"peer-left\") {\n updatePeers(msg.users);\n } else if (msg.type === \"ice-server\") {\n params.onIceUrl?.(msg.url);\n } else if (msg.userId) {\n params.onMessage(msg.type, msg.payload, {\n userId: msg.userId,\n receive: (type: T, payload: P) => send(type, msg.userId, payload),\n });\n }\n });\n } catch {\n logLine?.(\"⚠️ ERROR\", { error: \"invalid-json\" });\n }\n };\n\n ws.onclose = (ev: CloseEvent) => {\n // 1. Check if we should even try to reconnect\n const recoverableCodes = [1001, 1006, 1011, 1012, 1013];\n const isRecoverable = recoverableCodes.includes(ev.code);\n\n if (autoRejoin && !exited && isRecoverable) {\n // 2. Exponential Backoff: 1s, 2s, 4s, 8s... capped at 30s\n const backoff = Math.min(Math.pow(2, retryCount) * 1000, 30000);\n // 3. Add Jitter: +/- 1000ms randomness\n const jitter = Math.random() * 1000;\n const delay = backoff + jitter;\n\n logLine?.(\"🔄 RECONNECTING\", {\n attempt: retryCount + 1,\n delayMs: Math.round(delay),\n });\n\n retryCount++;\n timeoutId = setTimeout(connect, delay);\n } else {\n params.onClose?.({\n code: ev.code,\n reason: ev.reason,\n wasClean: ev.wasClean,\n });\n }\n };\n\n ws.onerror = (ev) => {\n console.error(\"WS Error\", ev);\n params.onError?.();\n };\n }\n\n // Helper for peer tracking (logic from your original code)\n function updatePeers(updatedUsers: { userId: string }[]) {\n const joined: IPeer<T, P>[] = [];\n const left: { userId: string }[] = [];\n const updatedPeerSet = new Set<string>();\n\n updatedUsers.forEach(({ userId: pUserId }) => {\n if (pUserId === userId) return;\n if (!peers.has(userId)) {\n const newPeer = {\n userId: pUserId,\n receive: (t: T, p: P) => send(t, pUserId, p),\n };\n peers.set(userId, newPeer);\n joined.push(newPeer);\n }\n updatedPeerSet.add(userId);\n });\n\n for (const userId of peers.keys()) {\n if (!updatedPeerSet.has(userId)) {\n peers.delete(userId);\n left.push({ userId });\n }\n }\n // Notify peer joined first then peer left. (avoid disconnect in case the peer leaving / joining is on the same user).\n if (joined.length) params.onPeerJoined(joined);\n if (left.length) params.onPeerLeft(left);\n }\n\n // Start initial connection\n connect();\n\n return {\n exitRoom: () => {\n exited = true;\n clearTimeout(timeoutId);\n ws.close();\n },\n };\n}\n",
|
|
6
6
|
"/// <reference lib=\"webworker\" />\n\nimport { enterRoom, type IPeer } from \"./impl/signal-room.js\";\n\nexport type RoomEvent<T extends string = string, P = any> =\n | { kind: \"open\" }\n | { kind: \"close\"; ev: Pick<CloseEvent, \"code\" | \"reason\" | \"wasClean\"> }\n | { kind: \"error\" }\n | { kind: \"peer-joined\"; users: { userId: string }[] }\n | { kind: \"peer-left\"; users: { userId: string }[] }\n | { kind: \"ice-server\"; url: string }\n | {\n kind: \"message\";\n type: T;\n payload: P;\n fromUserId: string;\n }\n | { kind: \"log\"; direction: string; obj?: any };\n\nexport type WorkerCommand<T extends string = string, P = any> =\n | {\n cmd: \"enter\";\n userId: string;\n appId: string;\n room: string;\n host: string;\n autoRejoin: boolean;\n }\n | { cmd: \"exit\" }\n | {\n cmd: \"send\";\n host: string;\n room: string;\n toUserId: string;\n type: T;\n payload: P;\n };\n\nlet exitRoom: (() => void) | null = null;\n\n// Map from userId -> a function to send to that peer (comes from IUser.receive)\nconst peerSend = new Map<string, (type: any, payload: any) => boolean>();\n\nfunction emit<T extends string, P>(ev: RoomEvent<T, P>) {\n (self as DedicatedWorkerGlobalScope).postMessage(ev);\n}\n\nself.addEventListener(\"message\", (e: MessageEvent<WorkerCommand>) => {\n const msg = e.data;\n console.debug(\"[signal-room.worker] received command\", msg);\n\n if (msg.cmd === \"enter\") {\n // If re-entering, clean up first\n exitRoom?.();\n exitRoom = null;\n peerSend.clear();\n\n const result = enterRoom({\n userId: msg.userId,\n appId: msg.appId,\n room: msg.room,\n host: msg.host,\n autoRejoin: msg.autoRejoin,\n onOpen: () => emit({ kind: \"open\" }),\n onClose: ({\n code,\n reason,\n wasClean,\n }: Pick<CloseEvent, \"code\" | \"reason\" | \"wasClean\">) =>\n emit({ kind: \"close\", ev: { code, reason, wasClean } }),\n onError: () => emit({ kind: \"error\" }),\n logLine: (direction: string, obj?: any) => {\n console.debug(`[signal-room.worker] ${direction}`, obj);\n emit({ kind: \"log\", direction, obj });\n },\n onPeerJoined: (users: IPeer[]) => {\n // Save the ability to send to this peer\n users.forEach(({ userId, receive }) =>\n peerSend.set(`${msg.host}/${msg.room}/${userId}`, receive)\n );\n emit({\n kind: \"peer-joined\",\n users: users.map(({ userId }) => ({ userId })),\n });\n },\n onPeerLeft: (users: { userId: string }[]) => {\n users.forEach(({ userId }) =>\n peerSend.delete(`${msg.host}/${msg.room}/${userId}`)\n );\n emit({ kind: \"peer-left\", users });\n },\n onIceUrl(url: string) {\n emit({ kind: \"ice-server\", url });\n },\n onMessage: (type: any, payload: any, from: IPeer) => {\n // We can also learn peerSend via onMessage in case join events vary\n peerSend.set(`${msg.host}/${msg.room}/${from.userId}`, from.receive);\n emit({\n kind: \"message\",\n type,\n payload,\n fromUserId: from.userId,\n });\n },\n });\n\n exitRoom = result.exitRoom;\n return;\n }\n\n if (msg.cmd === \"send\") {\n const sendFn = peerSend.get(`${msg.host}/${msg.room}/${msg.toUserId}`);\n if (sendFn) sendFn(msg.type, msg.payload);\n return;\n }\n\n if (msg.cmd === \"exit\") {\n exitRoom?.();\n self.close();\n return;\n }\n});\n"
|
|
7
7
|
],
|
|
8
|
-
"mappings": "AAQO,SAAS,CAAoC,CAAC,EAcxB,CAS3B,IAAQ,SAAQ,QAAO,OAAM,OAAM,aAAa,GAAM,WAAY,EAE9D,EAAS,GACT,EAAa,EACb,EACA,EACA,EAAoB,GAElB,EAAQ,IAAI,IACZ,EAAQ,SAAS,UAAa,KAAS,YAAe,mBAC1D,CACF,IAGA,SAAS,CAAI,CAAC,EAAS,EAAY,EAAY,CAC7C,GAAI,CAAC,EAAI,MAAO,GAChB,GAAI,GAAU,EAAG,aAAe,UAAU,KAAM,MAAO,GACvD,IAAM,EAAM,CAAE,OAAM,KAAI,SAAQ,EAGhC,OAFA,EAAG,KAAK,KAAK,UAAU,CAAG,CAAC,EAC3B,IAAU,gCAAY,CAAG,EAClB,GAGT,SAAS,CAAO,EAAG,CACjB,GAAI,EAAQ,OAEZ,EAAK,IAAI,UAAU,CAAK,EAExB,EAAG,OAAS,IAAM,CAChB,GAAI,EACF,EAAO,SAAS,EAChB,EAAoB,GAEtB,EAAa,GAGf,EAAG,UAAY,CAAC,IAAoB,CAClC,GAAI,CACF,IAAM,
|
|
9
|
-
"debugId": "
|
|
8
|
+
"mappings": "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",
|
|
9
|
+
"debugId": "F93B044ACAC6056164756E2164756E21",
|
|
10
10
|
"names": []
|
|
11
11
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"webrtc-peer-collector.d.ts","sourceRoot":"","sources":["../src/browser/webrtc-peer-collector.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,SAAS,EAAa,MAAM,eAAe,CAAC;AAErD,MAAM,MAAM,OAAO,GAAG,OAAO,GAAG,QAAQ,GAAG,KAAK,CAAC;AACjD,MAAM,MAAM,UAAU,GAAG,yBAAyB,GAAG,mBAAmB,CAAC;AAiBzE,wBAAgB,sBAAsB,CAAC,EACrC,KAAK,EACL,qBAAqB,EACrB,sBAA6B,EAC7B,iBAEC,EACD,iBAAiB,EAAE,SAA8B,EACjD,OAAuB,EACvB,WAAW,EACX,SAAS,EACT,WAAW,EACX,WAAW,GACZ,EAAE;IACD,KAAK,EAAE,MAAM,CAAC;IACd,iBAAiB,CAAC,EAAE,gBAAgB,CAAC;IACrC,iBAAiB,CAAC,EAAE,SAAS,CAAC,OAAO,EAAE,UAAU,CAAC,CAAC;IACnD,WAAW,CAAC,EAAE,CAAC,MAAM,EAAE,MAAM,KAAK,IAAI,CAAC;IACvC,OAAO,CAAC,EAAE,CAAC,SAAS,EAAE,MAAM,EAAE,GAAG,CAAC,EAAE,GAAG,KAAK,IAAI,CAAC;IACjD,SAAS,CAAC,EAAE,GAAG,CAAC;IAChB,sBAAsB,CAAC,EAAE,MAAM,CAAC;IAChC,qBAAqB,CAAC,UAAU,EAAE;QAChC,EAAE,EAAE,iBAAiB,CAAC;QACtB,MAAM,EAAE,MAAM,CAAC;QACf,SAAS,EAAE,OAAO,CAAC;QACnB,OAAO,CAAC,EAAE,MAAM,IAAI,CAAC;KACtB,GAAG,IAAI,CAAC;IACT,WAAW,CAAC,CAAC,IAAI,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAA;KAAE,GAAG,IAAI,CAAC;IACzD,WAAW,CAAC,CAAC,IAAI,EAAE;QACjB,IAAI,EAAE,MAAM,CAAC;QACb,IAAI,EAAE,MAAM,CAAC;QACb,EAAE,EAAE,IAAI,CAAC,UAAU,EAAE,QAAQ,GAAG,MAAM,GAAG,UAAU,CAAC,CAAC;KACtD,GAAG,IAAI,CAAC;CACV;;
|
|
1
|
+
{"version":3,"file":"webrtc-peer-collector.d.ts","sourceRoot":"","sources":["../src/browser/webrtc-peer-collector.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,SAAS,EAAa,MAAM,eAAe,CAAC;AAErD,MAAM,MAAM,OAAO,GAAG,OAAO,GAAG,QAAQ,GAAG,KAAK,CAAC;AACjD,MAAM,MAAM,UAAU,GAAG,yBAAyB,GAAG,mBAAmB,CAAC;AAiBzE,wBAAgB,sBAAsB,CAAC,EACrC,KAAK,EACL,qBAAqB,EACrB,sBAA6B,EAC7B,iBAEC,EACD,iBAAiB,EAAE,SAA8B,EACjD,OAAuB,EACvB,WAAW,EACX,SAAS,EACT,WAAW,EACX,WAAW,GACZ,EAAE;IACD,KAAK,EAAE,MAAM,CAAC;IACd,iBAAiB,CAAC,EAAE,gBAAgB,CAAC;IACrC,iBAAiB,CAAC,EAAE,SAAS,CAAC,OAAO,EAAE,UAAU,CAAC,CAAC;IACnD,WAAW,CAAC,EAAE,CAAC,MAAM,EAAE,MAAM,KAAK,IAAI,CAAC;IACvC,OAAO,CAAC,EAAE,CAAC,SAAS,EAAE,MAAM,EAAE,GAAG,CAAC,EAAE,GAAG,KAAK,IAAI,CAAC;IACjD,SAAS,CAAC,EAAE,GAAG,CAAC;IAChB,sBAAsB,CAAC,EAAE,MAAM,CAAC;IAChC,qBAAqB,CAAC,UAAU,EAAE;QAChC,EAAE,EAAE,iBAAiB,CAAC;QACtB,MAAM,EAAE,MAAM,CAAC;QACf,SAAS,EAAE,OAAO,CAAC;QACnB,OAAO,CAAC,EAAE,MAAM,IAAI,CAAC;KACtB,GAAG,IAAI,CAAC;IACT,WAAW,CAAC,CAAC,IAAI,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAA;KAAE,GAAG,IAAI,CAAC;IACzD,WAAW,CAAC,CAAC,IAAI,EAAE;QACjB,IAAI,EAAE,MAAM,CAAC;QACb,IAAI,EAAE,MAAM,CAAC;QACb,EAAE,EAAE,IAAI,CAAC,UAAU,EAAE,QAAQ,GAAG,MAAM,GAAG,UAAU,CAAC,CAAC;KACtD,GAAG,IAAI,CAAC;CACV;;gCAoHgC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAA;KAAE;+BAT/B;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAA;KAAE;wBAlCjC,MAAM;;EAsMlC"}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
function f(H){let{userId:
|
|
1
|
+
function f(H){let{userId:D,appId:h,room:k,host:R,autoRejoin:N=!0,logLine:O}=H,T=!1,_=0,z,E,$=!0,M=new Map,j=`wss://${R}/room/${h}/${k}?userId=${encodeURIComponent(D)}`;function F(Z,Y,Q){if(!z)return!1;if(T||z.readyState!==WebSocket.OPEN)return!1;let X={type:Z,to:Y,payload:Q};return z.send(JSON.stringify(X)),O?.("\uD83D\uDC64 ➡️ \uD83D\uDDA5️",X),!0}function x(){if(T)return;z=new WebSocket(j),z.onopen=()=>{if($)H.onOpen?.(),$=!1;_=0},z.onmessage=(Z)=>{try{let Y=JSON.parse(Z.data);console.log(">>",Y),(Array.isArray(Y)?Y:[Y]).forEach((X)=>{if(O?.("\uD83D\uDDA5️ ➡️ \uD83D\uDC64",X),X.type==="peer-joined"||X.type==="peer-left")L(X.users);else if(X.type==="ice-server")H.onIceUrl?.(X.url);else if(X.userId)H.onMessage(X.type,X.payload,{userId:X.userId,receive:(W,G)=>F(W,X.userId,G)})})}catch{O?.("⚠️ ERROR",{error:"invalid-json"})}},z.onclose=(Z)=>{let Q=[1001,1006,1011,1012,1013].includes(Z.code);if(N&&!T&&Q){let X=Math.min(Math.pow(2,_)*1000,30000),W=Math.random()*1000,G=X+W;O?.("\uD83D\uDD04 RECONNECTING",{attempt:_+1,delayMs:Math.round(G)}),_++,E=setTimeout(x,G)}else H.onClose?.({code:Z.code,reason:Z.reason,wasClean:Z.wasClean})},z.onerror=(Z)=>{console.error("WS Error",Z),H.onError?.()}}function L(Z){let Y=[],Q=[],X=new Set;Z.forEach(({userId:W})=>{if(W===D)return;if(!M.has(D)){let G={userId:W,receive:(V,J)=>F(V,W,J)};M.set(D,G),Y.push(G)}X.add(D)});for(let W of M.keys())if(!X.has(W))M.delete(W),Q.push({userId:W});if(Y.length)H.onPeerJoined(Y);if(Q.length)H.onPeerLeft(Q)}return x(),{exitRoom:()=>{T=!0,clearTimeout(E),z.close()}}}function U({userId:H,appId:D,room:h,host:k,autoRejoin:R=!0,onOpen:N,onClose:O,onError:T,onPeerJoined:_,onPeerLeft:z,onIceUrl:E,onMessage:$,logLine:M,workerUrl:j}){if(!j)return console.warn("Warning: enterRoom called without workerUrl; this may cause issues in some environments. You should pass workerUrl explicitly. Use:","https://cdn.jsdelivr.net/npm/@dobuki/hello-worker/dist/signal-room.worker.min.js"),f({userId:H,appId:D,room:h,host:k,autoRejoin:R,onOpen:N,onClose:O,onError:T,onPeerJoined:_,onPeerLeft:z,onIceUrl:E,onMessage:$});let F=new Worker(j,{type:"module"}),x=!1;function L({userId:Y}){return{userId:Y,receive:(Q,X)=>{if(x)return!1;return F.postMessage({cmd:"send",toUserId:Y,host:k,room:h,type:Q,payload:X}),!0}}}let Z=(Y)=>{let Q=Y.data;if(Q.kind==="open")N?.();else if(Q.kind==="close")F.terminate(),O?.(Q.ev);else if(Q.kind==="error")T?.();else if(Q.kind==="peer-joined")_(Q.users.map((X)=>L({userId:X.userId})));else if(Q.kind==="peer-left")z(Q.users);else if(Q.kind==="ice-server")E?.(Q.url);else if(Q.kind==="message")$(Q.type,Q.payload,L({userId:Q.fromUserId}));else if(Q.kind==="log")M?.(Q.direction,Q.obj)};return F.addEventListener("message",Z),F.postMessage({cmd:"enter",userId:H,appId:D,room:h,host:k,autoRejoin:R}),{exitRoom:()=>{x=!0,F.removeEventListener("message",Z),F.postMessage({cmd:"exit"})}}}var g=U;function i({appId:H,receivePeerConnection:D,peerlessUserExpiration:h=5000,fallbackRtcConfig:k={iceServers:[{urls:"stun:stun.l.google.com:19302"}]},enterRoomFunction:R=g,logLine:N=console.debug,onLeaveUser:O,workerUrl:T,onRoomReady:_,onRoomClose:z}){let E=`user-${crypto.randomUUID()}`,$=new Map,M=void 0,j;async function F(){if(M)try{let G=await fetch(M);if(!G.ok)throw Error(`ICE endpoint failed: ${G.status}`);j=await G.json()}catch(G){console.warn("Using fallback rtcConfig:",G)}return k}async function x(G){return G.pc=new RTCPeerConnection(Date.now()-(j?.timestamp??0)<1e4?j:await F()),G.pc.onicecandidate=(V)=>{if(!V.candidate)return;G.peer.receive("ice",V.candidate.toJSON())},G.pc.onconnectionstatechange=()=>{N("\uD83D\uDCAC",{event:"pc-state",userId:G.userId,state:G.pc?.connectionState})},G.pc}async function L(G){let V=$.get(G.userId),J=!1;if(!V){let b={userId:G.userId,pendingRemoteIce:[],peer:G};$.set(G.userId,b),await x(b),V=b,$.set(V.userId,V),J=!0}else if(V)clearTimeout(V.expirationTimeout),V.expirationTimeout=0;if(!V.pc)await x(V);return V.peer=G,[V,J]}function Z(G){O?.(G);let V=$.get(G);if(!V)return;try{V.pc?.close()}catch{}$.delete(G)}async function Y(G){if(!G.pc?.remoteDescription)return;let V=G.pendingRemoteIce;G.pendingRemoteIce=[];for(let J of V)try{await G.pc.addIceCandidate(J)}catch(b){N("⚠️ ERROR",{error:"add-ice-failed",userId:G.userId,detail:String(b)})}}let Q=new Map;function X({room:G,host:V}){let J=`${V}/room/${G}`,b=Q.get(J);if(b)b.exitRoom(),Q.delete(J)}function W({room:G,host:V}){return new Promise(async(J,b)=>{async function C(B){let[q]=await L(B),K=q.pc,S=await K?.createOffer();await K?.setLocalDescription(S),B.receive("offer",K?.localDescription?.toJSON())}let{exitRoom:y}=R({userId:E,appId:H,room:G,host:V,logLine:N,workerUrl:T,autoRejoin:!0,onOpen(){_?.({room:G,host:V}),J()},onError(){console.error("onError"),b()},onClose(B){z?.({room:G,host:V,ev:B})},onPeerJoined(B){B.forEach(async(q)=>{let[K,S]=await L(q);if(!S)return;let A=K.pc;if(!A)return;async function P(){let w=$.get(q.userId);if(w){w.pc=void 0;let I=await x(w);D({pc:I,userId:q.userId,initiator:!0,restart:P}),await new Promise((v)=>setTimeout(v,3000)),C(q)}}D({pc:A,userId:q.userId,initiator:!0,restart:P}),C(q)})},onPeerLeft(B){B.forEach(({userId:q})=>{let K=$.get(q);if(!K)return;K.expirationTimeout=setTimeout(()=>Z(q),h??0)})},onIceUrl(B){M=B},async onMessage(B,q,K){let[S]=await L(K),A=S.pc;if(!A)return;if(B==="offer"){D({pc:A,userId:K.userId,initiator:!1,restart(){S.pc=void 0}}),await A.setRemoteDescription(q);let P=await A.createAnswer();await A.setLocalDescription(P),K.receive("answer",A.localDescription?.toJSON()),await Y(S);return}if(B==="answer"){await A.setRemoteDescription(q),await Y(S);return}if(B==="ice"){let P=q;if(!A.remoteDescription){S.pendingRemoteIce.push(P);return}try{await A.addIceCandidate(P)}catch(w){N("⚠️ ERROR",{error:"add-ice-failed",userId:S.userId,detail:String(w)})}return}}});Q.set(`${V}/room/${G}`,{exitRoom:y,room:G,host:V})})}return{userId:E,enterRoom:W,exitRoom:X,leaveUser:Z,end(){Q.forEach(({exitRoom:G})=>G()),Q.clear(),$.forEach(({userId:G})=>Z(G)),$.clear()}}}export{i as collectPeerConnections};
|
|
2
2
|
|
|
3
|
-
//# debugId=
|
|
3
|
+
//# debugId=BD170F7BAB4156B264756E2164756E21
|
|
4
4
|
//# sourceMappingURL=webrtc-peer-collector.js.map
|
|
@@ -2,11 +2,11 @@
|
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../src/browser/impl/signal-room.ts", "../src/browser/signal-room.ts", "../src/browser/webrtc-peer-collector.ts"],
|
|
4
4
|
"sourcesContent": [
|
|
5
|
-
"export interface IPeer<T extends string = string, P = any> {\n userId: string;\n receive(type: T, payload: P): boolean;\n}\n\n/**\n * enterRoom connects to the signaling room via WebSocket.\n */\nexport function enterRoom<T extends string, P = any>(params: {\n userId: string;\n appId: string;\n room: string;\n host: string;\n onOpen?: () => void;\n onClose?: (ev: Pick<CloseEvent, \"code\" | \"reason\" | \"wasClean\">) => void;\n onError?: () => void;\n logLine?: (direction: string, obj?: any) => void;\n onPeerJoined(users: IPeer<T, P>[]): void;\n onPeerLeft(users: { userId: string }[]): void;\n onIceUrl?(url: string): void;\n onMessage(type: T, payload: P, from: IPeer<T, P>): void;\n autoRejoin?: boolean;\n}): { exitRoom: () => void } {\n type Message = {\n type: \"peer-joined\" | \"peer-left\" | \"ice-server\" | T;\n userId: string;\n users: { userId: string }[];\n payload: P;\n url: string;\n };\n\n const { userId, appId, room, host, autoRejoin = true, logLine } = params;\n\n let exited = false;\n let retryCount = 0;\n let ws: WebSocket;\n let timeoutId: ReturnType<typeof setTimeout>;\n let initialConnection = true;\n\n const peers = new Map<string, IPeer<T, P>>();\n const wsUrl = `wss://${host}/room/${appId}/${room}?userId=${encodeURIComponent(\n userId\n )}`;\n\n // Helper for sending (uses the current ws instance)\n function send(type: T, to: string, payload: P) {\n if (!ws) return false;\n if (exited || ws.readyState !== WebSocket.OPEN) return false;\n const obj = { type, to, payload };\n ws.send(JSON.stringify(obj));\n logLine?.(\"👤 ➡️ 🖥️\", obj);\n return true;\n }\n\n function connect() {\n if (exited) return;\n\n ws = new WebSocket(wsUrl);\n\n ws.onopen = () => {\n if (initialConnection) {\n params.onOpen?.();\n initialConnection = false;\n }\n retryCount = 0; // Reset backoff on successful connection\n };\n\n ws.onmessage = (e: MessageEvent) => {\n try {\n const
|
|
5
|
+
"export interface IPeer<T extends string = string, P = any> {\n userId: string;\n receive(type: T, payload: P): boolean;\n}\n\n/**\n * enterRoom connects to the signaling room via WebSocket.\n */\nexport function enterRoom<T extends string, P = any>(params: {\n userId: string;\n appId: string;\n room: string;\n host: string;\n onOpen?: () => void;\n onClose?: (ev: Pick<CloseEvent, \"code\" | \"reason\" | \"wasClean\">) => void;\n onError?: () => void;\n logLine?: (direction: string, obj?: any) => void;\n onPeerJoined(users: IPeer<T, P>[]): void;\n onPeerLeft(users: { userId: string }[]): void;\n onIceUrl?(url: string): void;\n onMessage(type: T, payload: P, from: IPeer<T, P>): void;\n autoRejoin?: boolean;\n}): { exitRoom: () => void } {\n type Message = {\n type: \"peer-joined\" | \"peer-left\" | \"ice-server\" | T;\n userId: string;\n users: { userId: string }[];\n payload: P;\n url: string;\n };\n\n const { userId, appId, room, host, autoRejoin = true, logLine } = params;\n\n let exited = false;\n let retryCount = 0;\n let ws: WebSocket;\n let timeoutId: ReturnType<typeof setTimeout>;\n let initialConnection = true;\n\n const peers = new Map<string, IPeer<T, P>>();\n const wsUrl = `wss://${host}/room/${appId}/${room}?userId=${encodeURIComponent(\n userId\n )}`;\n\n // Helper for sending (uses the current ws instance)\n function send(type: T, to: string, payload: P) {\n if (!ws) return false;\n if (exited || ws.readyState !== WebSocket.OPEN) return false;\n const obj = { type, to, payload };\n ws.send(JSON.stringify(obj));\n logLine?.(\"👤 ➡️ 🖥️\", obj);\n return true;\n }\n\n function connect() {\n if (exited) return;\n\n ws = new WebSocket(wsUrl);\n\n ws.onopen = () => {\n if (initialConnection) {\n params.onOpen?.();\n initialConnection = false;\n }\n retryCount = 0; // Reset backoff on successful connection\n };\n\n ws.onmessage = (e: MessageEvent) => {\n try {\n const result = JSON.parse(e.data);\n console.log(\">>\", result);\n const msgs: Message[] = Array.isArray(result) ? result : [result];\n msgs.forEach((msg) => {\n logLine?.(\"🖥️ ➡️ 👤\", msg);\n if (msg.type === \"peer-joined\" || msg.type === \"peer-left\") {\n updatePeers(msg.users);\n } else if (msg.type === \"ice-server\") {\n params.onIceUrl?.(msg.url);\n } else if (msg.userId) {\n params.onMessage(msg.type, msg.payload, {\n userId: msg.userId,\n receive: (type: T, payload: P) => send(type, msg.userId, payload),\n });\n }\n });\n } catch {\n logLine?.(\"⚠️ ERROR\", { error: \"invalid-json\" });\n }\n };\n\n ws.onclose = (ev: CloseEvent) => {\n // 1. Check if we should even try to reconnect\n const recoverableCodes = [1001, 1006, 1011, 1012, 1013];\n const isRecoverable = recoverableCodes.includes(ev.code);\n\n if (autoRejoin && !exited && isRecoverable) {\n // 2. Exponential Backoff: 1s, 2s, 4s, 8s... capped at 30s\n const backoff = Math.min(Math.pow(2, retryCount) * 1000, 30000);\n // 3. Add Jitter: +/- 1000ms randomness\n const jitter = Math.random() * 1000;\n const delay = backoff + jitter;\n\n logLine?.(\"🔄 RECONNECTING\", {\n attempt: retryCount + 1,\n delayMs: Math.round(delay),\n });\n\n retryCount++;\n timeoutId = setTimeout(connect, delay);\n } else {\n params.onClose?.({\n code: ev.code,\n reason: ev.reason,\n wasClean: ev.wasClean,\n });\n }\n };\n\n ws.onerror = (ev) => {\n console.error(\"WS Error\", ev);\n params.onError?.();\n };\n }\n\n // Helper for peer tracking (logic from your original code)\n function updatePeers(updatedUsers: { userId: string }[]) {\n const joined: IPeer<T, P>[] = [];\n const left: { userId: string }[] = [];\n const updatedPeerSet = new Set<string>();\n\n updatedUsers.forEach(({ userId: pUserId }) => {\n if (pUserId === userId) return;\n if (!peers.has(userId)) {\n const newPeer = {\n userId: pUserId,\n receive: (t: T, p: P) => send(t, pUserId, p),\n };\n peers.set(userId, newPeer);\n joined.push(newPeer);\n }\n updatedPeerSet.add(userId);\n });\n\n for (const userId of peers.keys()) {\n if (!updatedPeerSet.has(userId)) {\n peers.delete(userId);\n left.push({ userId });\n }\n }\n // Notify peer joined first then peer left. (avoid disconnect in case the peer leaving / joining is on the same user).\n if (joined.length) params.onPeerJoined(joined);\n if (left.length) params.onPeerLeft(left);\n }\n\n // Start initial connection\n connect();\n\n return {\n exitRoom: () => {\n exited = true;\n clearTimeout(timeoutId);\n ws.close();\n },\n };\n}\n",
|
|
6
6
|
"import type { IPeer } from \"./impl/signal-room.js\";\nimport { enterRoom as baseEnterRoom } from \"./impl/signal-room.js\";\nimport { RoomEvent, WorkerCommand } from \"./signal-room.worker.js\";\n\nexport function enterRoom<T extends string, P = any>({\n userId,\n appId,\n room,\n host,\n autoRejoin = true,\n onOpen,\n onClose,\n onError,\n onPeerJoined,\n onPeerLeft,\n onIceUrl,\n onMessage,\n logLine,\n workerUrl,\n}: {\n userId: string;\n appId: string;\n room: string;\n host: string;\n autoRejoin?: boolean;\n onOpen?: () => void;\n onClose?: (ev: Pick<CloseEvent, \"code\" | \"reason\" | \"wasClean\">) => void;\n onError?: () => void;\n onPeerJoined: (users: IPeer<T, P>[]) => void;\n onPeerLeft: (users: { userId: string }[]) => void;\n onIceUrl?(url: string): void;\n onMessage: (type: T, payload: P, from: IPeer<T, P>) => void;\n logLine?: (direction: string, obj?: any) => void;\n\n // Pass the URL to your worker file (bundler will handle it)\n workerUrl?: URL;\n}): { exitRoom: () => void } {\n if (!workerUrl) {\n const CDN_WORKER_URL = `https://cdn.jsdelivr.net/npm/@dobuki/hello-worker/dist/signal-room.worker.min.js`;\n\n console.warn(\n \"Warning: enterRoom called without workerUrl; this may cause issues in some environments. You should pass workerUrl explicitly. Use:\",\n CDN_WORKER_URL\n );\n return baseEnterRoom<T, P>({\n userId,\n appId,\n room,\n host,\n autoRejoin,\n onOpen,\n onClose,\n onError,\n onPeerJoined,\n onPeerLeft,\n onIceUrl,\n onMessage,\n });\n }\n const worker = new Worker(workerUrl, { type: \"module\" });\n let exited = false;\n\n function makeUser({ userId }: { userId: string }): IPeer<T, P> {\n return {\n userId,\n receive: (type: T, payload: P) => {\n if (exited) return false;\n worker.postMessage({\n cmd: \"send\",\n toUserId: userId,\n host,\n room,\n type,\n payload,\n } as WorkerCommand);\n return true;\n },\n };\n }\n\n const onWorkerMessage = (e: MessageEvent<RoomEvent<T, P>>) => {\n const ev = e.data;\n\n if (ev.kind === \"open\") onOpen?.();\n else if (ev.kind === \"close\") {\n worker.terminate();\n onClose?.(ev.ev);\n } else if (ev.kind === \"error\") onError?.();\n else if (ev.kind === \"peer-joined\")\n onPeerJoined(ev.users.map((ev) => makeUser({ userId: ev.userId })));\n else if (ev.kind === \"peer-left\") onPeerLeft(ev.users);\n else if (ev.kind === \"ice-server\") onIceUrl?.(ev.url);\n else if (ev.kind === \"message\")\n onMessage(ev.type, ev.payload, makeUser({ userId: ev.fromUserId }));\n else if (ev.kind === \"log\") logLine?.(ev.direction, ev.obj);\n };\n\n worker.addEventListener(\"message\", onWorkerMessage);\n\n worker.postMessage({\n cmd: \"enter\",\n userId,\n appId,\n room,\n host,\n autoRejoin,\n } as WorkerCommand);\n\n return {\n exitRoom: () => {\n exited = true;\n worker.removeEventListener(\"message\", onWorkerMessage);\n worker.postMessage({ cmd: \"exit\" } as WorkerCommand);\n },\n };\n}\n\nexport type EnterRoom<T extends string, P> = typeof enterRoom<T, P>;\n",
|
|
7
|
-
"import { IPeer } from \"./impl/signal-room\";\nimport { EnterRoom, enterRoom } from \"./signal-room\";\n\nexport type SigType = \"offer\" | \"answer\" | \"ice\";\nexport type SigPayload = RTCSessionDescriptionInit | RTCIceCandidateInit;\n\ntype UserState = {\n userId: string;\n pc?: RTCPeerConnection;\n\n // ICE that arrived before we had remoteDescription\n pendingRemoteIce: RTCIceCandidateInit[];\n\n // the signaling \"user\" handle so we can send messages\n peer: IPeer<SigType, SigPayload>;\n\n expirationTimeout?: number;\n};\n\nconst DEFAULT_ENTER_ROOM = enterRoom;\n\nexport function collectPeerConnections({\n appId,\n receivePeerConnection,\n peerlessUserExpiration = 5000,\n fallbackRtcConfig = {\n iceServers: [{ urls: \"stun:stun.l.google.com:19302\" }],\n },\n enterRoomFunction: enterRoom = DEFAULT_ENTER_ROOM,\n logLine = console.debug,\n onLeaveUser,\n workerUrl,\n onRoomReady,\n onRoomClose,\n}: {\n appId: string;\n fallbackRtcConfig?: RTCConfiguration;\n enterRoomFunction?: EnterRoom<SigType, SigPayload>;\n onLeaveUser?: (userId: string) => void;\n logLine?: (direction: string, obj?: any) => void;\n workerUrl?: URL;\n peerlessUserExpiration?: number;\n receivePeerConnection(connection: {\n pc: RTCPeerConnection;\n userId: string;\n initiator: boolean;\n restart?: () => void;\n }): void;\n onRoomReady?(info: { host: string; room: string }): void;\n onRoomClose?(info: {\n host: string;\n room: string;\n ev: Pick<CloseEvent, \"reason\" | \"code\" | \"wasClean\">;\n }): void;\n}) {\n const userId = `user-${crypto.randomUUID()}`;\n const users: Map<string, UserState> = new Map();\n let iceUrl: string | undefined = undefined;\n let rtcConfig: RTCConfiguration | undefined;\n let rtcConfigExpiration: number = 0;\n\n async function getRtcConfig(): Promise<RTCConfiguration> {\n if (iceUrl) {\n try {\n const r = await fetch(iceUrl);\n if (!r.ok) throw new Error(`ICE endpoint failed: ${r.status}`);\n const result = (await r.json()) as RTCConfiguration & {\n expire: number;\n };\n rtcConfigExpiration = Date.now() + result.expire * 1000;\n rtcConfig = result;\n } catch (e) {\n console.warn(\"Using fallback rtcConfig:\", e);\n }\n }\n return fallbackRtcConfig;\n }\n\n async function setupPC(state: UserState) {\n state.pc = new RTCPeerConnection(\n rtcConfigExpiration - Date.now() < 10000\n ? await getRtcConfig()\n : rtcConfig\n );\n // Send local ICE candidates to this peer\n state.pc.onicecandidate = (ev) => {\n if (!ev.candidate) return;\n state.peer.receive(\"ice\", ev.candidate.toJSON());\n };\n\n state.pc.onconnectionstatechange = () => {\n logLine(\"💬\", {\n event: \"pc-state\",\n userId: state.userId,\n state: state.pc?.connectionState,\n });\n };\n return state.pc;\n }\n\n async function getPeer(\n peer: IPeer<SigType, SigPayload>\n ): Promise<[UserState, boolean]> {\n let state = users.get(peer.userId);\n let isNewPeer = false;\n if (!state) {\n const newState: UserState = {\n userId: peer.userId,\n pendingRemoteIce: [],\n peer,\n };\n users.set(peer.userId, newState);\n\n await setupPC(newState);\n state = newState;\n\n // New user\n users.set(state.userId, state);\n isNewPeer = true;\n } else if (state) {\n clearTimeout(state.expirationTimeout);\n state.expirationTimeout = 0;\n }\n if (!state.pc) {\n await setupPC(state);\n }\n state.peer = peer;\n return [state, isNewPeer];\n }\n\n function leaveUser(userId: string) {\n onLeaveUser?.(userId);\n const p = users.get(userId);\n if (!p) return;\n try {\n p.pc?.close();\n } catch {}\n users.delete(userId);\n }\n\n async function flushRemoteIce(state: UserState) {\n if (!state.pc?.remoteDescription) return;\n\n const queued = state.pendingRemoteIce;\n state.pendingRemoteIce = [];\n\n for (const ice of queued) {\n try {\n await state.pc.addIceCandidate(ice);\n } catch (e) {\n logLine(\"⚠️ ERROR\", {\n error: \"add-ice-failed\",\n userId: state.userId,\n detail: String(e),\n });\n }\n }\n }\n\n const roomsEntered = new Map<\n string,\n { room: string; host: string; exitRoom: () => void }\n >();\n\n function exit({ room, host }: { room: string; host: string }) {\n const key = `${host}/room/${room}`;\n const session = roomsEntered.get(key);\n if (session) {\n session.exitRoom();\n roomsEntered.delete(key);\n }\n }\n\n function enter({ room, host }: { room: string; host: string }) {\n return new Promise<void>(async (resolve, reject) => {\n async function makeOffer(user: IPeer) {\n // Offer flow: createOffer -> setLocalDescription -> send localDescription\n const [state] = await getPeer(user);\n const pc = state.pc;\n const offer = await pc?.createOffer();\n await pc?.setLocalDescription(offer);\n user.receive(\"offer\", pc?.localDescription?.toJSON()!);\n }\n\n const { exitRoom } = enterRoom({\n userId,\n appId,\n room,\n host,\n logLine,\n workerUrl,\n autoRejoin: true,\n\n onOpen() {\n onRoomReady?.({ room, host });\n resolve();\n },\n onError() {\n console.error(\"onError\");\n reject();\n },\n onClose(ev) {\n onRoomClose?.({ room, host, ev });\n },\n\n // Existing peers initiate to the newcomer\n onPeerJoined(joiningUsers: IPeer<SigType, SigPayload>[]) {\n joiningUsers.forEach(async (user) => {\n const [state, isNewPeer] = await getPeer(user);\n if (!isNewPeer) return;\n const pc = state.pc;\n if (!pc) return;\n\n async function restart() {\n const state = users.get(user.userId);\n if (state) {\n state.pc = undefined;\n const pc = await setupPC(state);\n receivePeerConnection({\n pc,\n userId: user.userId,\n initiator: true,\n restart,\n });\n await new Promise((resolve) => setTimeout(resolve, 3000));\n makeOffer(user);\n }\n }\n\n receivePeerConnection({\n pc,\n userId: user.userId,\n initiator: true,\n restart,\n });\n makeOffer(user);\n });\n },\n\n onPeerLeft(leavingUsers: { userId: string }[]) {\n leavingUsers.forEach(({ userId }) => {\n const state = users.get(userId);\n if (!state) return;\n state.expirationTimeout = setTimeout(\n () => leaveUser(userId),\n peerlessUserExpiration ?? 0\n );\n });\n },\n\n onIceUrl(url) {\n iceUrl = url;\n },\n\n async onMessage(type: SigType, payload: any, from: IPeer) {\n const [state] = await getPeer(from);\n const pc = state.pc;\n if (!pc) return;\n\n if (type === \"offer\") {\n receivePeerConnection({\n pc,\n userId: from.userId,\n initiator: false,\n restart() {\n // reset PC\n state.pc = undefined;\n },\n });\n // Responder: set remote offer\n await pc.setRemoteDescription(payload as RTCSessionDescriptionInit);\n\n // Create and send answer\n const answer = await pc.createAnswer();\n await pc.setLocalDescription(answer);\n\n from.receive(\"answer\", pc.localDescription?.toJSON()!);\n\n // Now safe to apply any queued ICE from this peer\n await flushRemoteIce(state);\n return;\n }\n\n if (type === \"answer\") {\n // Initiator: set remote answer\n await pc.setRemoteDescription(payload as RTCSessionDescriptionInit);\n await flushRemoteIce(state);\n return;\n }\n\n if (type === \"ice\") {\n const ice = payload as RTCIceCandidateInit;\n\n // If we don't have remoteDescription yet, queue it\n if (!pc.remoteDescription) {\n state.pendingRemoteIce.push(ice);\n return;\n }\n\n try {\n await pc.addIceCandidate(ice);\n } catch (e) {\n logLine(\"⚠️ ERROR\", {\n error: \"add-ice-failed\",\n userId: state.userId,\n detail: String(e),\n });\n }\n return;\n }\n },\n });\n roomsEntered.set(`${host}/room/${room}`, { exitRoom, room, host });\n });\n }\n\n return {\n userId,\n enterRoom: enter,\n exitRoom: exit,\n leaveUser,\n end() {\n roomsEntered.forEach(({ exitRoom }) => exitRoom());\n roomsEntered.clear();\n users.forEach(({ userId }) => leaveUser(userId));\n users.clear();\n },\n };\n}\n\n/*\nTurn Token ID\n<CF_TURN_TOKEN_ID>\n\nAPI Token\n<CF_RTC_API_TOKEN>\n\nCURL\ncurl \\\n\t-H \"Authorization: Bearer <CF_RTC_API_TOKEN>\" \\\n\t-H \"Content-Type: application/json\" -d '{\"ttl\": 86400}' \\\n\thttps://rtc.live.cloudflare.com/v1/turn/keys/<CF_TURN_TOKEN_ID>/credentials/generate-ice-servers\n\nJSON\n{\n\t\"iceServers\": [\n {\n \"urls\": [\n \"stun:stun.cloudflare.com:3478\",\n \"turn:turn.cloudflare.com:3478?transport=udp\",\n \"turn:turn.cloudflare.com:3478?transport=tcp\",\n \"turns:turn.cloudflare.com:5349?transport=tcp\"\n ],\n \"username\": \"xxxx\",\n \"credential\": \"yyyy\",\n }\n ]\n}\n\n*/\n"
|
|
7
|
+
"import { IPeer } from \"./impl/signal-room\";\nimport { EnterRoom, enterRoom } from \"./signal-room\";\n\nexport type SigType = \"offer\" | \"answer\" | \"ice\";\nexport type SigPayload = RTCSessionDescriptionInit | RTCIceCandidateInit;\n\ntype UserState = {\n userId: string;\n pc?: RTCPeerConnection;\n\n // ICE that arrived before we had remoteDescription\n pendingRemoteIce: RTCIceCandidateInit[];\n\n // the signaling \"user\" handle so we can send messages\n peer: IPeer<SigType, SigPayload>;\n\n expirationTimeout?: number;\n};\n\nconst DEFAULT_ENTER_ROOM = enterRoom;\n\nexport function collectPeerConnections({\n appId,\n receivePeerConnection,\n peerlessUserExpiration = 5000,\n fallbackRtcConfig = {\n iceServers: [{ urls: \"stun:stun.l.google.com:19302\" }],\n },\n enterRoomFunction: enterRoom = DEFAULT_ENTER_ROOM,\n logLine = console.debug,\n onLeaveUser,\n workerUrl,\n onRoomReady,\n onRoomClose,\n}: {\n appId: string;\n fallbackRtcConfig?: RTCConfiguration;\n enterRoomFunction?: EnterRoom<SigType, SigPayload>;\n onLeaveUser?: (userId: string) => void;\n logLine?: (direction: string, obj?: any) => void;\n workerUrl?: URL;\n peerlessUserExpiration?: number;\n receivePeerConnection(connection: {\n pc: RTCPeerConnection;\n userId: string;\n initiator: boolean;\n restart?: () => void;\n }): void;\n onRoomReady?(info: { host: string; room: string }): void;\n onRoomClose?(info: {\n host: string;\n room: string;\n ev: Pick<CloseEvent, \"reason\" | \"code\" | \"wasClean\">;\n }): void;\n}) {\n const userId = `user-${crypto.randomUUID()}`;\n const users: Map<string, UserState> = new Map();\n let iceUrl: string | undefined = undefined;\n let rtcConfig: (RTCConfiguration & { timestamp: number }) | undefined;\n\n async function getRtcConfig(): Promise<RTCConfiguration> {\n if (iceUrl) {\n try {\n const r = await fetch(iceUrl);\n if (!r.ok) throw new Error(`ICE endpoint failed: ${r.status}`);\n rtcConfig = (await r.json()) as RTCConfiguration & {\n timestamp: number;\n };\n } catch (e) {\n console.warn(\"Using fallback rtcConfig:\", e);\n }\n }\n return fallbackRtcConfig;\n }\n\n async function setupPC(state: UserState) {\n state.pc = new RTCPeerConnection(\n Date.now() - (rtcConfig?.timestamp ?? 0) < 10000\n ? rtcConfig\n : await getRtcConfig()\n );\n // Send local ICE candidates to this peer\n state.pc.onicecandidate = (ev) => {\n if (!ev.candidate) return;\n state.peer.receive(\"ice\", ev.candidate.toJSON());\n };\n\n state.pc.onconnectionstatechange = () => {\n logLine(\"💬\", {\n event: \"pc-state\",\n userId: state.userId,\n state: state.pc?.connectionState,\n });\n };\n return state.pc;\n }\n\n async function getPeer(\n peer: IPeer<SigType, SigPayload>\n ): Promise<[UserState, boolean]> {\n let state = users.get(peer.userId);\n let isNewPeer = false;\n if (!state) {\n const newState: UserState = {\n userId: peer.userId,\n pendingRemoteIce: [],\n peer,\n };\n users.set(peer.userId, newState);\n\n await setupPC(newState);\n state = newState;\n\n // New user\n users.set(state.userId, state);\n isNewPeer = true;\n } else if (state) {\n clearTimeout(state.expirationTimeout);\n state.expirationTimeout = 0;\n }\n if (!state.pc) {\n await setupPC(state);\n }\n state.peer = peer;\n return [state, isNewPeer];\n }\n\n function leaveUser(userId: string) {\n onLeaveUser?.(userId);\n const p = users.get(userId);\n if (!p) return;\n try {\n p.pc?.close();\n } catch {}\n users.delete(userId);\n }\n\n async function flushRemoteIce(state: UserState) {\n if (!state.pc?.remoteDescription) return;\n\n const queued = state.pendingRemoteIce;\n state.pendingRemoteIce = [];\n\n for (const ice of queued) {\n try {\n await state.pc.addIceCandidate(ice);\n } catch (e) {\n logLine(\"⚠️ ERROR\", {\n error: \"add-ice-failed\",\n userId: state.userId,\n detail: String(e),\n });\n }\n }\n }\n\n const roomsEntered = new Map<\n string,\n { room: string; host: string; exitRoom: () => void }\n >();\n\n function exit({ room, host }: { room: string; host: string }) {\n const key = `${host}/room/${room}`;\n const session = roomsEntered.get(key);\n if (session) {\n session.exitRoom();\n roomsEntered.delete(key);\n }\n }\n\n function enter({ room, host }: { room: string; host: string }) {\n return new Promise<void>(async (resolve, reject) => {\n async function makeOffer(user: IPeer) {\n // Offer flow: createOffer -> setLocalDescription -> send localDescription\n const [state] = await getPeer(user);\n const pc = state.pc;\n const offer = await pc?.createOffer();\n await pc?.setLocalDescription(offer);\n user.receive(\"offer\", pc?.localDescription?.toJSON()!);\n }\n\n const { exitRoom } = enterRoom({\n userId,\n appId,\n room,\n host,\n logLine,\n workerUrl,\n autoRejoin: true,\n\n onOpen() {\n onRoomReady?.({ room, host });\n resolve();\n },\n onError() {\n console.error(\"onError\");\n reject();\n },\n onClose(ev) {\n onRoomClose?.({ room, host, ev });\n },\n\n // Existing peers initiate to the newcomer\n onPeerJoined(joiningUsers: IPeer<SigType, SigPayload>[]) {\n joiningUsers.forEach(async (user) => {\n const [state, isNewPeer] = await getPeer(user);\n if (!isNewPeer) return;\n const pc = state.pc;\n if (!pc) return;\n\n async function restart() {\n const state = users.get(user.userId);\n if (state) {\n state.pc = undefined;\n const pc = await setupPC(state);\n receivePeerConnection({\n pc,\n userId: user.userId,\n initiator: true,\n restart,\n });\n await new Promise((resolve) => setTimeout(resolve, 3000));\n makeOffer(user);\n }\n }\n\n receivePeerConnection({\n pc,\n userId: user.userId,\n initiator: true,\n restart,\n });\n makeOffer(user);\n });\n },\n\n onPeerLeft(leavingUsers: { userId: string }[]) {\n leavingUsers.forEach(({ userId }) => {\n const state = users.get(userId);\n if (!state) return;\n state.expirationTimeout = setTimeout(\n () => leaveUser(userId),\n peerlessUserExpiration ?? 0\n );\n });\n },\n\n onIceUrl(url) {\n iceUrl = url;\n },\n\n async onMessage(type: SigType, payload: any, from: IPeer) {\n const [state] = await getPeer(from);\n const pc = state.pc;\n if (!pc) return;\n\n if (type === \"offer\") {\n receivePeerConnection({\n pc,\n userId: from.userId,\n initiator: false,\n restart() {\n // reset PC\n state.pc = undefined;\n },\n });\n // Responder: set remote offer\n await pc.setRemoteDescription(payload as RTCSessionDescriptionInit);\n\n // Create and send answer\n const answer = await pc.createAnswer();\n await pc.setLocalDescription(answer);\n\n from.receive(\"answer\", pc.localDescription?.toJSON()!);\n\n // Now safe to apply any queued ICE from this peer\n await flushRemoteIce(state);\n return;\n }\n\n if (type === \"answer\") {\n // Initiator: set remote answer\n await pc.setRemoteDescription(payload as RTCSessionDescriptionInit);\n await flushRemoteIce(state);\n return;\n }\n\n if (type === \"ice\") {\n const ice = payload as RTCIceCandidateInit;\n\n // If we don't have remoteDescription yet, queue it\n if (!pc.remoteDescription) {\n state.pendingRemoteIce.push(ice);\n return;\n }\n\n try {\n await pc.addIceCandidate(ice);\n } catch (e) {\n logLine(\"⚠️ ERROR\", {\n error: \"add-ice-failed\",\n userId: state.userId,\n detail: String(e),\n });\n }\n return;\n }\n },\n });\n roomsEntered.set(`${host}/room/${room}`, { exitRoom, room, host });\n });\n }\n\n return {\n userId,\n enterRoom: enter,\n exitRoom: exit,\n leaveUser,\n end() {\n roomsEntered.forEach(({ exitRoom }) => exitRoom());\n roomsEntered.clear();\n users.forEach(({ userId }) => leaveUser(userId));\n users.clear();\n },\n };\n}\n\n/*\nTurn Token ID\n<CF_TURN_TOKEN_ID>\n\nAPI Token\n<CF_RTC_API_TOKEN>\n\nCURL\ncurl \\\n\t-H \"Authorization: Bearer <CF_RTC_API_TOKEN>\" \\\n\t-H \"Content-Type: application/json\" -d '{\"ttl\": 86400}' \\\n\thttps://rtc.live.cloudflare.com/v1/turn/keys/<CF_TURN_TOKEN_ID>/credentials/generate-ice-servers\n\nJSON\n{\n\t\"iceServers\": [\n {\n \"urls\": [\n \"stun:stun.cloudflare.com:3478\",\n \"turn:turn.cloudflare.com:3478?transport=udp\",\n \"turn:turn.cloudflare.com:3478?transport=tcp\",\n \"turns:turn.cloudflare.com:5349?transport=tcp\"\n ],\n \"username\": \"xxxx\",\n \"credential\": \"yyyy\",\n }\n ]\n}\n\n*/\n"
|
|
8
8
|
],
|
|
9
|
-
"mappings": "AAQO,SAAS,CAAoC,CAAC,EAcxB,CAS3B,IAAQ,SAAQ,QAAO,OAAM,OAAM,aAAa,GAAM,WAAY,EAE9D,EAAS,GACT,EAAa,EACb,EACA,EACA,EAAoB,GAElB,EAAQ,IAAI,IACZ,EAAQ,SAAS,UAAa,KAAS,YAAe,mBAC1D,CACF,IAGA,SAAS,CAAI,CAAC,EAAS,EAAY,EAAY,CAC7C,GAAI,CAAC,EAAI,MAAO,GAChB,GAAI,GAAU,EAAG,aAAe,UAAU,KAAM,MAAO,GACvD,IAAM,EAAM,CAAE,OAAM,KAAI,SAAQ,EAGhC,OAFA,EAAG,KAAK,KAAK,UAAU,CAAG,CAAC,EAC3B,IAAU,gCAAY,CAAG,EAClB,GAGT,SAAS,CAAO,EAAG,CACjB,GAAI,EAAQ,OAEZ,EAAK,IAAI,UAAU,CAAK,EAExB,EAAG,OAAS,IAAM,CAChB,GAAI,EACF,EAAO,SAAS,EAChB,EAAoB,GAEtB,EAAa,GAGf,EAAG,UAAY,CAAC,IAAoB,CAClC,GAAI,CACF,IAAM,
|
|
10
|
-
"debugId": "
|
|
9
|
+
"mappings": "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",
|
|
10
|
+
"debugId": "BD170F7BAB4156B264756E2164756E21",
|
|
11
11
|
"names": []
|
|
12
12
|
}
|