@dobuki/hello-worker 1.0.84 → 1.0.86

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.
@@ -1,4 +1,634 @@
1
- class v{sendToServerFunctions=[];icePromiseResolve;icePromise;receiveIce(V,W){this.icePromiseResolve?.({url:V,expiration:W}),this.icePromiseResolve=void 0,this.icePromise=void 0}addRequester(V){return this.sendToServerFunctions.push(V),()=>{this.removeRequester(V)}}removeRequester(V){this.sendToServerFunctions.splice(this.sendToServerFunctions.indexOf(V),1)}sendToServer(V){this.sendToServerFunctions[Math.floor(this.sendToServerFunctions.length*Math.random())](V)}async requestIce(){if(!this.icePromise)this.icePromise=new Promise((V)=>{this.icePromiseResolve=V,this.sendToServer("request-ice")});return await this.icePromise}}function U(V){let{userId:W,worldId:K,room:S,host:M,autoRejoin:z=!0,logLine:H}=V,x=!1,O=0,D,j,_=!0,A=new Map,C=`wss://${M}/room/${K}/${S}?userId=${encodeURIComponent(W)}`,T=[],k=0;function E(Z,F,X){if(!D)return H?.("\uD83D\uDC64 ➡️ ❌","no ws available"),!1;let Q={type:Z,to:F,payload:X};if(T.push(Q),H?.("\uD83D\uDC64 ➡️ \uD83D\uDDA5️",Q),clearTimeout(k),x||D.readyState!==WebSocket.OPEN)return H?.("\uD83D\uDC64 ➡️ ❌","Not in opened state: "+D.readyState),!1;return k=setTimeout(()=>{D.send(JSON.stringify(T)),T.length=0}),!0}function P(){if(x)return;D=new WebSocket(C),D.onopen=()=>{if(_)V.onOpen?.(),_=!1;O=0},D.onmessage=(Z)=>{try{let F=JSON.parse(Z.data);(Array.isArray(F)?F:[F]).forEach((Q)=>{if(H?.("\uD83D\uDDA5️ ➡️ \uD83D\uDC64",Q),Q.type==="peer-joined"||Q.type==="peer-left")Y(Q.users,Q);else if(Q.type==="ice-server")V.onIceUrl?.(Q.url,Q.expiration);else if(Q.userId)V.onMessage(Q.type,Q.payload,Q.userId)})}catch{H?.("⚠️ ERROR",{error:"invalid-json"})}},D.onclose=(Z)=>{let X=[1001,1006,1011,1012,1013].includes(Z.code);if(z&&!x&&X){let Q=Math.min(Math.pow(2,O)*1000,15000),N=Math.random()*1000,J=Q+N;H?.("\uD83D\uDD04 RECONNECTING",{attempt:O+1,delayMs:Math.round(J)}),O++,j=setTimeout(P,J)}else V.onClose?.({code:Z.code,reason:Z.reason,wasClean:Z.wasClean})},D.onerror=(Z)=>{console.error("WS Error",Z),V.onError?.()}}function Y(Z,F){let X=[],Q=[],N=new Set,J=Z.filter((B)=>B.userId===W)[0];if(!J){H?.("⚠️","Cannot find self in updated users");return}let f=J.joined;Z.forEach(({userId:B,joined:w})=>{if(B===W)return;if(!A.has(B)||F.type==="peer-joined"&&B===F.userId){let R={userId:B,joined:w};A.set(B,R),X.push(R)}N.add(B)});for(let B of A.keys())if(!N.has(B)||F.type==="peer-left"&&B===F.userId)A.delete(B),Q.push({userId:B});if(X.length)V.onPeerJoined(X,f);if(Q.length)V.onPeerLeft(Q)}return P(),{send(Z,F,X){E(Z,F,X)},exitRoom:()=>{x=!0,clearTimeout(j),D.close()}}}function c({userId:V,worldId:W,room:K,host:S,autoRejoin:M=!0,onOpen:z,onClose:H,onError:x,onPeerJoined:O,onPeerLeft:D,onIceUrl:j,onMessage:_,logLine:A,workerUrl:C}){if(!C)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"),U({userId:V,worldId:W,room:K,host:S,autoRejoin:M,onOpen:z,onClose:H,onError:x,onPeerJoined:O,onPeerLeft:D,onIceUrl:j,onMessage:_});let T=new Worker(C,{type:"module"}),k=!1,E=(P)=>{let Y=P.data;if(Y.kind==="open")z?.();else if(Y.kind==="close")T.terminate(),H?.(Y.ev);else if(Y.kind==="error")x?.();else if(Y.kind==="peer-joined")O(Y.users.map((Z)=>({userId:Z.userId,joined:Z.joined})),Y.joined);else if(Y.kind==="peer-left")D(Y.users);else if(Y.kind==="ice-server")j?.(Y.url,Y.expiration);else if(Y.kind==="message")_(Y.type,Y.payload,Y.fromUserId);else if(Y.kind==="log")A?.(Y.direction,Y.obj)};return T.addEventListener("message",E),T.postMessage({cmd:"enter",userId:V,worldId:W,room:K,host:S,autoRejoin:M}),{exitRoom:()=>{k=!0,T.removeEventListener("message",E),T.postMessage({cmd:"exit"})},send:(P,Y,Z)=>{T.postMessage({cmd:"send",toUserId:Y,host:S,room:K,type:P,payload:Z})}}}var y={iceServers:[{urls:"stun:stun.l.google.com:19302"}]};class i{iceUrlProvider;constructor(V){this.iceUrlProvider=V}rtcConfig={...y,timestamp:Date.now()};rtcConfigPromise;async getRtcConfig(){if(Date.now()-(this.rtcConfig?.timestamp??0)<1e4)return this.rtcConfig;if(!this.rtcConfigPromise)this.rtcConfigPromise=new Promise(async(W)=>{let K=3;for(let S=0;S<K;S++)try{let M=(await this.iceUrlProvider.requestIce()).url,z=await fetch(M);if(!z.ok)throw Error(`ICE endpoint failed: ${z.status}`);let H=await z.json();W(H);return}catch(M){console.warn("Failed fetching iceUrl")}}),this.rtcConfig=await this.rtcConfigPromise,this.rtcConfigPromise=void 0;return this.rtcConfig}}var g=c;function s({userId:V,worldId:W,receivePeerConnection:K,peerlessUserExpiration:S=5000,enterRoomFunction:M=g,logLine:z,onLeaveUser:H,workerUrl:x,onRoomReady:O,onRoomClose:D,onBroadcastMessage:j}){let _=V??`user-${crypto.randomUUID()}`,A=new Map,C=new Map;function T(F){H?.(F);let X=A.get(F);if(!X)return;A.delete(F);try{X.close()}catch{}}async function k(F){if(!F.connection?.pc?.remoteDescription)return;let X=F.connection.pendingRemoteIce;F.connection.pendingRemoteIce=[];for(let Q of X)try{await F.connection.pc.addIceCandidate(Q)}catch(N){z?.("⚠️ ERROR",{error:"add-ice-failed",userId:F.peer,detail:String(N)})}}let E=new v,P=new i(E);function Y({room:F,host:X}){let Q=`${X}/room/${F}`,N=C.get(Q);if(N)N.exitRoom(),C.delete(Q)}function Z({room:F,host:X}){return new Promise(async(Q,N)=>{async function J(q){if(q.connectionPromise)return q.connectionPromise;let $=new Promise(async(b)=>{q.connection={id:`conn-${crypto.randomUUID()}`,pc:new RTCPeerConnection(await P.getRtcConfig()),pendingRemoteIce:[]},q.connection.pc.onicecandidate=(h)=>{if(!h.candidate)return;R("ice",q.peer,{connectionId:q.connection?.id,ice:h.candidate.toJSON()})},q.connection.pc.onconnectionstatechange=async()=>{if(z?.("\uD83D\uDCAC",{event:"pc-state",userId:q.peer,state:q.connection?.pc?.connectionState}),q.connection?.pc?.connectionState==="failed"){q.close();let h=await f(q.peer,!0);if(h.connection?.pc)K({pc:h.connection?.pc,userId:h.peer,restart:()=>h.close()});else z?.("\uD83D\uDC64ℹ️","no pc: "+h.peer);return}},b(q.connection)});return q.connectionPromise=$,await $,q.connectionPromise=void 0,$}async function f(q,$){let b=A.get(q);if(!b||$){let h={peer:q,close(){if(this.connection)this.connection.pc.close(),this.connection=void 0;A.delete(q)},async reset(){h.close(),setTimeout(async()=>{let G=await f(q,!0);if(!G.connection?.pc){z?.("⚠️","no pc");return}K({pc:G.connection?.pc,userId:G.peer,restart:()=>G.close()}),await B(G.peer)},500)}};b=h,await J(h),A.set(b.peer,b)}else if(b){if(clearTimeout(b.expirationTimeout),b.expirationTimeout=0,!b.connection?.pc||b.connection?.pc.signalingState==="closed")await J(b)}return b.peer=q,b}async function B(q){let $=await f(q),b=$.connection?.pc,h=await b?.createOffer();await b?.setLocalDescription(h),R("offer",q,{connectionId:$.connection?.id,offer:b?.localDescription?.toJSON()})}let{exitRoom:w,send:R}=M({userId:_,worldId:W,room:F,host:X,logLine:z,workerUrl:x,autoRejoin:!0,onOpen(){O?.({room:F,host:X}),Q()},onError(){console.error("onError"),N()},onClose(q){D?.({room:F,host:X,ev:q})},onPeerJoined(q,$){q.forEach(async(b)=>{let h=await f(b.userId,!0);h.joined=b.joined;let G=h.connection?.pc;if(!G){z?.("\uD83D\uDC64ℹ️","no pc: "+b.userId);return}if(K({pc:G,userId:b.userId,restart:()=>h.close()}),b.joined>$||b.joined===$&&b.userId.localeCompare(_)>0)await B(b.userId)})},onPeerLeft(q){q.forEach(({userId:$})=>{let b=A.get($);if(!b)return;b.expirationTimeout=setTimeout(()=>T($),S??0)})},onIceUrl(q,$){E.receiveIce(q,$)},async onMessage(q,$,b){if(q==="offer"&&$.offer){let h=await f(b,!1),G=!h.connection||h.connection.pc.signalingState==="stable"?await J(h):h.connection;z?.("\uD83D\uDCAC",{type:q,signalingState:G.pc.signalingState}),G.peerConnectionId=$.connectionId,K({pc:G.pc,userId:b,restart:()=>h.close()}),await G.pc.setRemoteDescription($.offer);let L=await G.pc.createAnswer();await G.pc.setLocalDescription(L),R("answer",b,{connectionId:G.id,answer:G.pc.localDescription?.toJSON()}),await k(h);return}if(q==="answer"&&$.answer){let h=await f(b,!1),G=h.connection&&h.connection.pc.signalingState!=="closed"?h.connection:await J(h);z?.("\uD83D\uDCAC",{type:q,signalingState:G.pc.signalingState}),await G.pc.setRemoteDescription($.answer),G.peerConnectionId=$.connectionId,await k(h);return}if(q==="ice"&&$.ice){let h=await f(b,!1),G=h.connection??await h.connectionPromise;if(!G){z?.("⚠️","No connection");return}if(z?.("\uD83D\uDCAC",{type:q,signalingState:G.pc.signalingState}),G.peerConnectionId&&$.connectionId!==G.peerConnectionId){z?.("⚠️","Mismatch peerConnectionID"+$.connectionId+"vs"+G.peerConnectionId);return}if(!G.pc.remoteDescription||!G.peerConnectionId){G.peerConnectionId=$.connectionId,G.pendingRemoteIce.push($.ice);return}try{await G.pc.addIceCandidate($.ice)}catch(L){z?.("⚠️ ERROR",{error:"add-ice-failed",userId:h.peer,detail:String(L)})}return}if(q==="broadcast")j?.($,b)}}),I=E.addRequester((q)=>R(q,"server"));C.set(`${X}/room/${F}`,{exitRoom:()=>{w(),I()},room:F,host:X,broadcast:(q)=>R("broadcast","server",q)})})}return{userId:_,enterRoom:Z,exitRoom:Y,leaveUser:T,async reset(F){A.get(F)?.reset()},broadcast(F){C.forEach((X)=>X.broadcast(F))},end(){C.forEach(({exitRoom:F})=>F()),C.clear(),A.forEach(({peer:F})=>T(F)),A.clear()}}}export{s as collectPeerConnections};
1
+ // src/browser/utils/ice-url-provider.ts
2
+ class IceUrlProvider {
3
+ sendToServerFunctions = new Array;
4
+ icePromiseResolve;
5
+ icePromise;
6
+ receiveIce(url, expiration) {
7
+ this.icePromiseResolve?.({ url, expiration });
8
+ this.icePromiseResolve = undefined;
9
+ this.icePromise = undefined;
10
+ }
11
+ addRequester(requester) {
12
+ this.sendToServerFunctions.push(requester);
13
+ return () => {
14
+ this.removeRequester(requester);
15
+ };
16
+ }
17
+ removeRequester(requester) {
18
+ this.sendToServerFunctions.splice(this.sendToServerFunctions.indexOf(requester), 1);
19
+ }
20
+ sendToServer(command) {
21
+ this.sendToServerFunctions[Math.floor(this.sendToServerFunctions.length * Math.random())](command);
22
+ }
23
+ async requestIce() {
24
+ if (!this.icePromise) {
25
+ this.icePromise = new Promise((resolve) => {
26
+ this.icePromiseResolve = resolve;
27
+ this.sendToServer("request-ice");
28
+ });
29
+ }
30
+ return await this.icePromise;
31
+ }
32
+ }
2
33
 
3
- //# debugId=676E5AB7574F379664756E2164756E21
34
+ // src/browser/signal/impl/signal-room.ts
35
+ function enterRoom(params) {
36
+ const {
37
+ userId,
38
+ worldId,
39
+ room,
40
+ host,
41
+ protocol,
42
+ autoRejoin = true,
43
+ logLine
44
+ } = params;
45
+ let exited = false;
46
+ let retryCount = 0;
47
+ let ws;
48
+ let timeoutId;
49
+ let initialConnection = true;
50
+ const peers = new Map;
51
+ const wsUrl = `${protocol ?? "wss"}://${host}/room/${worldId}/${room}?userId=${encodeURIComponent(userId)}`;
52
+ const accumulatedMessages = [];
53
+ let timeout = 0;
54
+ function send(type, to, payload) {
55
+ if (!ws) {
56
+ logLine?.("\uD83D\uDC64 ➡️ ❌", "no ws available");
57
+ return false;
58
+ }
59
+ const obj = { type, to, payload };
60
+ accumulatedMessages.push(obj);
61
+ logLine?.("\uD83D\uDC64 ➡️ \uD83D\uDDA5️", obj);
62
+ clearTimeout(timeout);
63
+ if (exited || ws.readyState !== WebSocket.OPEN) {
64
+ logLine?.("\uD83D\uDC64 ➡️ ❌", "Not in opened state: " + ws.readyState);
65
+ return false;
66
+ }
67
+ timeout = setTimeout(() => {
68
+ ws.send(JSON.stringify(accumulatedMessages));
69
+ accumulatedMessages.length = 0;
70
+ });
71
+ return true;
72
+ }
73
+ function connect() {
74
+ if (exited)
75
+ return;
76
+ ws = new WebSocket(wsUrl);
77
+ ws.onopen = () => {
78
+ if (initialConnection) {
79
+ params.onOpen?.();
80
+ initialConnection = false;
81
+ }
82
+ retryCount = 0;
83
+ };
84
+ ws.onmessage = (e) => {
85
+ try {
86
+ const result = JSON.parse(e.data);
87
+ const msgs = Array.isArray(result) ? result : [result];
88
+ msgs.forEach((msg) => {
89
+ logLine?.("\uD83D\uDDA5️ ➡️ \uD83D\uDC64", msg);
90
+ if (msg.type === "peer-joined" || msg.type === "peer-left") {
91
+ updatePeers(msg.users, msg);
92
+ } else if (msg.type === "ice-server") {
93
+ params.onIceUrl?.(msg.url, msg.expiration);
94
+ } else if (msg.userId) {
95
+ params.onMessage(msg.type, msg.payload, msg.userId);
96
+ }
97
+ });
98
+ } catch {
99
+ logLine?.("⚠️ ERROR", { error: "invalid-json" });
100
+ }
101
+ };
102
+ ws.onclose = (ev) => {
103
+ const recoverableCodes = [1001, 1006, 1011, 1012, 1013];
104
+ const isRecoverable = recoverableCodes.includes(ev.code);
105
+ if (autoRejoin && !exited && isRecoverable) {
106
+ const backoff = Math.min(Math.pow(2, retryCount) * 1000, 15000);
107
+ const jitter = Math.random() * 1000;
108
+ const delay = backoff + jitter;
109
+ logLine?.("\uD83D\uDD04 RECONNECTING", {
110
+ attempt: retryCount + 1,
111
+ delayMs: Math.round(delay)
112
+ });
113
+ retryCount++;
114
+ timeoutId = setTimeout(connect, delay);
115
+ } else {
116
+ params.onClose?.({
117
+ code: ev.code,
118
+ reason: ev.reason,
119
+ wasClean: ev.wasClean
120
+ });
121
+ }
122
+ };
123
+ ws.onerror = (ev) => {
124
+ console.error("WS Error", ev);
125
+ params.onError?.();
126
+ };
127
+ }
128
+ function updatePeers(updatedUsers, msg) {
129
+ const joined = [];
130
+ const left = [];
131
+ const updatedPeerSet = new Set;
132
+ const selfPeer = updatedUsers.filter((peer) => peer.userId === userId)[0];
133
+ if (!selfPeer) {
134
+ logLine?.("⚠️", "Cannot find self in updated users");
135
+ return;
136
+ }
137
+ const selfJoined = selfPeer.joined;
138
+ updatedUsers.forEach(({ userId: pUserId, joined: joinedTime }) => {
139
+ if (pUserId === userId)
140
+ return;
141
+ if (!peers.has(pUserId) || msg.type === "peer-joined" && pUserId === msg.userId) {
142
+ const newPeer = {
143
+ userId: pUserId,
144
+ joined: joinedTime
145
+ };
146
+ peers.set(pUserId, newPeer);
147
+ joined.push(newPeer);
148
+ }
149
+ updatedPeerSet.add(pUserId);
150
+ });
151
+ for (const pUserId of peers.keys()) {
152
+ if (!updatedPeerSet.has(pUserId) || msg.type === "peer-left" && pUserId === msg.userId) {
153
+ peers.delete(pUserId);
154
+ left.push({ userId: pUserId });
155
+ }
156
+ }
157
+ if (joined.length)
158
+ params.onPeerJoined(joined, selfJoined);
159
+ if (left.length)
160
+ params.onPeerLeft(left);
161
+ }
162
+ connect();
163
+ return {
164
+ send(type, userId2, payload) {
165
+ send(type, userId2, payload);
166
+ },
167
+ exitRoom: () => {
168
+ exited = true;
169
+ clearTimeout(timeoutId);
170
+ ws.close();
171
+ }
172
+ };
173
+ }
174
+
175
+ // src/browser/signal/signal-room.ts
176
+ function enterRoom2({
177
+ userId,
178
+ worldId,
179
+ room,
180
+ protocol = "wss",
181
+ host,
182
+ autoRejoin = true,
183
+ onOpen,
184
+ onClose,
185
+ onError,
186
+ onPeerJoined,
187
+ onPeerLeft,
188
+ onIceUrl,
189
+ onMessage,
190
+ logLine,
191
+ workerUrl
192
+ }) {
193
+ if (!workerUrl) {
194
+ const CDN_WORKER_URL = `https://cdn.jsdelivr.net/npm/@dobuki/hello-worker/dist/signal-room.worker.min.js`;
195
+ console.warn("Warning: enterRoom called without workerUrl; this may cause issues in some environments. You should pass workerUrl explicitly. Use:", CDN_WORKER_URL);
196
+ return enterRoom({
197
+ userId,
198
+ worldId,
199
+ room,
200
+ protocol,
201
+ host,
202
+ autoRejoin,
203
+ onOpen,
204
+ onClose,
205
+ onError,
206
+ onPeerJoined,
207
+ onPeerLeft,
208
+ onIceUrl,
209
+ onMessage
210
+ });
211
+ }
212
+ let worker;
213
+ const res = fetch(workerUrl).then(async (res2) => {
214
+ if (!res2.ok) {
215
+ throw new Error(`Failed to load worker script: ${res2.status}`);
216
+ }
217
+ const source = await res2.text();
218
+ const blob = new Blob([source], { type: "text/javascript" });
219
+ const blobUrl = URL.createObjectURL(blob);
220
+ worker = new Worker(blobUrl, { type: "module" });
221
+ worker.addEventListener("message", onWorkerMessage);
222
+ worker.postMessage({
223
+ cmd: "enter",
224
+ userId,
225
+ worldId,
226
+ room,
227
+ protocol,
228
+ host,
229
+ autoRejoin
230
+ });
231
+ });
232
+ let exited = false;
233
+ const onWorkerMessage = (e) => {
234
+ const ev = e.data;
235
+ if (ev.kind === "open")
236
+ onOpen?.();
237
+ else if (ev.kind === "close") {
238
+ worker?.terminate();
239
+ onClose?.(ev.ev);
240
+ } else if (ev.kind === "error")
241
+ onError?.();
242
+ else if (ev.kind === "peer-joined")
243
+ onPeerJoined(ev.users.map((ev2) => ({ userId: ev2.userId, joined: ev2.joined })), ev.joined);
244
+ else if (ev.kind === "peer-left")
245
+ onPeerLeft(ev.users);
246
+ else if (ev.kind === "ice-server")
247
+ onIceUrl?.(ev.url, ev.expiration);
248
+ else if (ev.kind === "message")
249
+ onMessage(ev.type, ev.payload, ev.fromUserId);
250
+ else if (ev.kind === "log")
251
+ logLine?.(ev.direction, ev.obj);
252
+ };
253
+ return {
254
+ exitRoom: () => {
255
+ exited = true;
256
+ worker?.removeEventListener("message", onWorkerMessage);
257
+ worker?.postMessage({ cmd: "exit" });
258
+ },
259
+ send: (type, toUserId, payload) => {
260
+ worker?.postMessage({
261
+ cmd: "send",
262
+ toUserId,
263
+ host,
264
+ room,
265
+ type,
266
+ payload
267
+ });
268
+ }
269
+ };
270
+ }
271
+
272
+ // src/browser/utils/rtc-config.ts
273
+ var FALLBACK_RTC_CONFIG = {
274
+ iceServers: [{ urls: "stun:stun.l.google.com:19302" }]
275
+ };
276
+
277
+ class RTCConfigProvider {
278
+ iceUrlProvider;
279
+ constructor(iceUrlProvider) {
280
+ this.iceUrlProvider = iceUrlProvider;
281
+ }
282
+ rtcConfig = {
283
+ ...FALLBACK_RTC_CONFIG,
284
+ timestamp: Date.now()
285
+ };
286
+ rtcConfigPromise;
287
+ async getRtcConfig() {
288
+ const now = Date.now();
289
+ if (now - (this.rtcConfig?.timestamp ?? 0) < 1e4) {
290
+ return this.rtcConfig;
291
+ }
292
+ if (!this.rtcConfigPromise) {
293
+ this.rtcConfigPromise = new Promise(async (resolve) => {
294
+ let retries = 3;
295
+ for (let r = 0;r < retries; r++) {
296
+ try {
297
+ const iceUrl = (await this.iceUrlProvider.requestIce()).url;
298
+ const r2 = await fetch(iceUrl);
299
+ if (!r2.ok)
300
+ throw new Error(`ICE endpoint failed: ${r2.status}`);
301
+ const rtcConfig = await r2.json();
302
+ resolve(rtcConfig);
303
+ return;
304
+ } catch (e) {
305
+ console.warn("Failed fetching iceUrl");
306
+ }
307
+ }
308
+ });
309
+ this.rtcConfig = await this.rtcConfigPromise;
310
+ this.rtcConfigPromise = undefined;
311
+ }
312
+ return this.rtcConfig;
313
+ }
314
+ }
315
+
316
+ // src/browser/webrtc-peer-collector.ts
317
+ var DEFAULT_ENTER_ROOM = enterRoom2;
318
+ function collectPeerConnections({
319
+ userId: passedUserId,
320
+ worldId,
321
+ receivePeerConnection,
322
+ peerlessUserExpiration = 5000,
323
+ enterRoomFunction: enterRoom3 = DEFAULT_ENTER_ROOM,
324
+ logLine,
325
+ onLeaveUser,
326
+ workerUrl,
327
+ onRoomReady,
328
+ onRoomClose,
329
+ onBroadcastMessage
330
+ }) {
331
+ const userId = passedUserId ?? `user-${crypto.randomUUID()}`;
332
+ const users = new Map;
333
+ const roomsEntered = new Map;
334
+ function leaveUser(userId2) {
335
+ onLeaveUser?.(userId2);
336
+ const p = users.get(userId2);
337
+ if (!p)
338
+ return;
339
+ users.delete(userId2);
340
+ try {
341
+ p.close();
342
+ } catch {}
343
+ }
344
+ async function flushRemoteIce(state) {
345
+ if (!state.connection?.pc?.remoteDescription)
346
+ return;
347
+ const queued = state.connection.pendingRemoteIce;
348
+ state.connection.pendingRemoteIce = [];
349
+ for (const ice of queued) {
350
+ try {
351
+ await state.connection.pc.addIceCandidate(ice);
352
+ } catch (e) {
353
+ logLine?.("⚠️ ERROR", {
354
+ error: "add-ice-failed",
355
+ userId: state.peer,
356
+ detail: String(e)
357
+ });
358
+ }
359
+ }
360
+ }
361
+ const iceUrlProvider = new IceUrlProvider;
362
+ const rtcConfigProvider = new RTCConfigProvider(iceUrlProvider);
363
+ function exit({ room, host }) {
364
+ const key = `${host}/room/${room}`;
365
+ const session = roomsEntered.get(key);
366
+ if (session) {
367
+ session.exitRoom();
368
+ roomsEntered.delete(key);
369
+ }
370
+ }
371
+ function enter({
372
+ room,
373
+ host,
374
+ protocol
375
+ }) {
376
+ return new Promise(async (resolve, reject) => {
377
+ async function setupConnection(state) {
378
+ if (state.connectionPromise) {
379
+ return state.connectionPromise;
380
+ }
381
+ const promise = new Promise(async (resolve2) => {
382
+ state.connection = {
383
+ id: `conn-${crypto.randomUUID()}`,
384
+ pc: new RTCPeerConnection(await rtcConfigProvider.getRtcConfig()),
385
+ pendingRemoteIce: []
386
+ };
387
+ state.connection.pc.onicecandidate = (ev) => {
388
+ if (!ev.candidate)
389
+ return;
390
+ send("ice", state.peer, {
391
+ connectionId: state.connection?.id,
392
+ ice: ev.candidate.toJSON()
393
+ });
394
+ };
395
+ state.connection.pc.onconnectionstatechange = async () => {
396
+ logLine?.("\uD83D\uDCAC", {
397
+ event: "pc-state",
398
+ userId: state.peer,
399
+ state: state.connection?.pc?.connectionState
400
+ });
401
+ if (state.connection?.pc?.connectionState === "failed") {
402
+ state.close();
403
+ const userState = await getPeer(state.peer, true);
404
+ if (userState.connection?.pc) {
405
+ receivePeerConnection({
406
+ pc: userState.connection?.pc,
407
+ userId: userState.peer,
408
+ restart: () => userState.close()
409
+ });
410
+ } else {
411
+ logLine?.("\uD83D\uDC64ℹ️", "no pc: " + userState.peer);
412
+ }
413
+ return;
414
+ }
415
+ };
416
+ resolve2(state.connection);
417
+ });
418
+ state.connectionPromise = promise;
419
+ await promise;
420
+ state.connectionPromise = undefined;
421
+ return promise;
422
+ }
423
+ async function getPeer(peer, forceReset) {
424
+ let state = users.get(peer);
425
+ if (!state || forceReset) {
426
+ const newState = {
427
+ peer,
428
+ close() {
429
+ if (this.connection) {
430
+ this.connection.pc.close();
431
+ this.connection = undefined;
432
+ }
433
+ users.delete(peer);
434
+ },
435
+ async reset() {
436
+ newState.close();
437
+ setTimeout(async () => {
438
+ const userState = await getPeer(peer, true);
439
+ if (!userState.connection?.pc) {
440
+ logLine?.("⚠️", "no pc");
441
+ return;
442
+ }
443
+ receivePeerConnection({
444
+ pc: userState.connection?.pc,
445
+ userId: userState.peer,
446
+ restart: () => userState.close()
447
+ });
448
+ await makeOffer(userState.peer);
449
+ }, 500);
450
+ }
451
+ };
452
+ state = newState;
453
+ await setupConnection(newState);
454
+ users.set(state.peer, state);
455
+ } else if (state) {
456
+ clearTimeout(state.expirationTimeout);
457
+ state.expirationTimeout = 0;
458
+ if (!state.connection?.pc || state.connection?.pc.signalingState === "closed") {
459
+ await setupConnection(state);
460
+ }
461
+ }
462
+ state.peer = peer;
463
+ return state;
464
+ }
465
+ async function makeOffer(userId2) {
466
+ const state = await getPeer(userId2);
467
+ const pc = state.connection?.pc;
468
+ const offer = await pc?.createOffer();
469
+ await pc?.setLocalDescription(offer);
470
+ send("offer", userId2, {
471
+ connectionId: state.connection?.id,
472
+ offer: pc?.localDescription?.toJSON()
473
+ });
474
+ }
475
+ const { exitRoom, send } = enterRoom3({
476
+ userId,
477
+ worldId,
478
+ room,
479
+ protocol,
480
+ host,
481
+ logLine,
482
+ workerUrl,
483
+ autoRejoin: true,
484
+ onOpen() {
485
+ onRoomReady?.({ room, host });
486
+ resolve();
487
+ },
488
+ onError() {
489
+ console.error("onError");
490
+ reject();
491
+ },
492
+ onClose(ev) {
493
+ onRoomClose?.({ room, host, ev });
494
+ },
495
+ onPeerJoined(joiningUsers, selfJoined) {
496
+ joiningUsers.forEach(async (user) => {
497
+ const state = await getPeer(user.userId, true);
498
+ state.joined = user.joined;
499
+ const pc = state.connection?.pc;
500
+ if (!pc) {
501
+ logLine?.("\uD83D\uDC64ℹ️", "no pc: " + user.userId);
502
+ return;
503
+ }
504
+ receivePeerConnection({
505
+ pc,
506
+ userId: user.userId,
507
+ restart: () => state.close()
508
+ });
509
+ if (user.joined > selfJoined || user.joined === selfJoined && user.userId.localeCompare(userId) > 0) {
510
+ await makeOffer(user.userId);
511
+ }
512
+ });
513
+ },
514
+ onPeerLeft(leavingUsers) {
515
+ leavingUsers.forEach(({ userId: userId2 }) => {
516
+ const state = users.get(userId2);
517
+ if (!state)
518
+ return;
519
+ state.expirationTimeout = setTimeout(() => leaveUser(userId2), peerlessUserExpiration ?? 0);
520
+ });
521
+ },
522
+ onIceUrl(url, expiration) {
523
+ iceUrlProvider.receiveIce(url, expiration);
524
+ },
525
+ async onMessage(type, payload, from) {
526
+ if (type === "offer" && payload.offer) {
527
+ const state = await getPeer(from, false);
528
+ const connection = !state.connection || state.connection.pc.signalingState === "stable" ? await setupConnection(state) : state.connection;
529
+ logLine?.("\uD83D\uDCAC", {
530
+ type,
531
+ signalingState: connection.pc.signalingState
532
+ });
533
+ connection.peerConnectionId = payload.connectionId;
534
+ receivePeerConnection({
535
+ pc: connection.pc,
536
+ userId: from,
537
+ restart: () => state.close()
538
+ });
539
+ await connection.pc.setRemoteDescription(payload.offer);
540
+ const answer = await connection.pc.createAnswer();
541
+ await connection.pc.setLocalDescription(answer);
542
+ send("answer", from, {
543
+ connectionId: connection.id,
544
+ answer: connection.pc.localDescription?.toJSON()
545
+ });
546
+ await flushRemoteIce(state);
547
+ return;
548
+ }
549
+ if (type === "answer" && payload.answer) {
550
+ const state = await getPeer(from, false);
551
+ const connection = state.connection && state.connection.pc.signalingState !== "closed" ? state.connection : await setupConnection(state);
552
+ logLine?.("\uD83D\uDCAC", {
553
+ type,
554
+ signalingState: connection.pc.signalingState
555
+ });
556
+ await connection.pc.setRemoteDescription(payload.answer);
557
+ connection.peerConnectionId = payload.connectionId;
558
+ await flushRemoteIce(state);
559
+ return;
560
+ }
561
+ if (type === "ice" && payload.ice) {
562
+ const state = await getPeer(from, false);
563
+ const connection = state.connection ?? await state.connectionPromise;
564
+ if (!connection) {
565
+ logLine?.("⚠️", "No connection");
566
+ return;
567
+ }
568
+ logLine?.("\uD83D\uDCAC", {
569
+ type,
570
+ signalingState: connection.pc.signalingState
571
+ });
572
+ if (connection.peerConnectionId && payload.connectionId !== connection.peerConnectionId) {
573
+ logLine?.("⚠️", "Mismatch peerConnectionID" + payload.connectionId + "vs" + connection.peerConnectionId);
574
+ return;
575
+ }
576
+ if (!connection.pc.remoteDescription || !connection.peerConnectionId) {
577
+ connection.peerConnectionId = payload.connectionId;
578
+ connection.pendingRemoteIce.push(payload.ice);
579
+ return;
580
+ }
581
+ try {
582
+ await connection.pc.addIceCandidate(payload.ice);
583
+ } catch (e) {
584
+ logLine?.("⚠️ ERROR", {
585
+ error: "add-ice-failed",
586
+ userId: state.peer,
587
+ detail: String(e)
588
+ });
589
+ }
590
+ return;
591
+ }
592
+ if (type === "broadcast") {
593
+ onBroadcastMessage?.(payload, from);
594
+ }
595
+ }
596
+ });
597
+ const removeRequester = iceUrlProvider.addRequester((command) => send(command, "server"));
598
+ roomsEntered.set(`${host}/room/${room}`, {
599
+ exitRoom: () => {
600
+ exitRoom();
601
+ removeRequester();
602
+ },
603
+ room,
604
+ host,
605
+ broadcast: (payload) => send("broadcast", "server", payload)
606
+ });
607
+ });
608
+ }
609
+ return {
610
+ userId,
611
+ enterRoom: enter,
612
+ exitRoom: exit,
613
+ leaveUser,
614
+ async reset(userId2) {
615
+ const userState = users.get(userId2);
616
+ userState?.reset();
617
+ },
618
+ broadcast(payload) {
619
+ roomsEntered.forEach((room) => room.broadcast(payload));
620
+ },
621
+ end() {
622
+ roomsEntered.forEach(({ exitRoom }) => exitRoom());
623
+ roomsEntered.clear();
624
+ users.forEach(({ peer }) => leaveUser(peer));
625
+ users.clear();
626
+ }
627
+ };
628
+ }
629
+ export {
630
+ collectPeerConnections
631
+ };
632
+
633
+ //# debugId=FD438A7CC3C2722364756E2164756E21
4
634
  //# sourceMappingURL=webrtc-peer-collector.js.map