@carverjs/multiplayer 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (43) hide show
  1. package/dist/NetworkManager-DrKM2tEx.d.mts +369 -0
  2. package/dist/NetworkManager-nvVAOr1O.d.ts +369 -0
  3. package/dist/chunk-3KT73N2S.mjs +655 -0
  4. package/dist/chunk-3KT73N2S.mjs.map +1 -0
  5. package/dist/chunk-EO3YNPRQ.mjs +817 -0
  6. package/dist/chunk-EO3YNPRQ.mjs.map +1 -0
  7. package/dist/chunk-UD6FDZMX.mjs +581 -0
  8. package/dist/chunk-UD6FDZMX.mjs.map +1 -0
  9. package/dist/firebase-CPu87KA0.d.ts +100 -0
  10. package/dist/firebase-PE6MxGdJ.d.mts +100 -0
  11. package/dist/index.d.mts +316 -0
  12. package/dist/index.d.ts +316 -0
  13. package/dist/index.js +3817 -0
  14. package/dist/index.js.map +1 -0
  15. package/dist/index.mjs +1743 -0
  16. package/dist/index.mjs.map +1 -0
  17. package/dist/strategy.d.mts +7 -0
  18. package/dist/strategy.d.ts +7 -0
  19. package/dist/strategy.js +619 -0
  20. package/dist/strategy.js.map +1 -0
  21. package/dist/strategy.mjs +11 -0
  22. package/dist/strategy.mjs.map +1 -0
  23. package/dist/sync.d.mts +212 -0
  24. package/dist/sync.d.ts +212 -0
  25. package/dist/sync.js +845 -0
  26. package/dist/sync.js.map +1 -0
  27. package/dist/sync.mjs +11 -0
  28. package/dist/sync.mjs.map +1 -0
  29. package/dist/transport.d.mts +159 -0
  30. package/dist/transport.d.ts +159 -0
  31. package/dist/transport.js +1274 -0
  32. package/dist/transport.js.map +1 -0
  33. package/dist/transport.mjs +19 -0
  34. package/dist/transport.mjs.map +1 -0
  35. package/dist/types-5LHBOW08.d.mts +74 -0
  36. package/dist/types-5LHBOW08.d.ts +74 -0
  37. package/dist/types.d.mts +2 -0
  38. package/dist/types.d.ts +2 -0
  39. package/dist/types.js +19 -0
  40. package/dist/types.js.map +1 -0
  41. package/dist/types.mjs +1 -0
  42. package/dist/types.mjs.map +1 -0
  43. package/package.json +73 -0
@@ -0,0 +1,655 @@
1
+ // src/transport/webrtc/ice.ts
2
+ var DEFAULT_STUN_SERVERS = [
3
+ { urls: "stun:stun.l.google.com:19302" },
4
+ { urls: "stun:stun1.l.google.com:19302" },
5
+ { urls: "stun:stun2.l.google.com:19302" },
6
+ { urls: "stun:stun.cloudflare.com:3478" }
7
+ ];
8
+ function buildICEConfig(options) {
9
+ const servers = options?.iceServers && options.iceServers.length > 0 ? options.iceServers : DEFAULT_STUN_SERVERS;
10
+ return {
11
+ iceServers: servers,
12
+ iceCandidatePoolSize: 10,
13
+ iceTransportPolicy: options?.iceTransportPolicy ?? "all"
14
+ };
15
+ }
16
+
17
+ // src/transport/webrtc/peer.ts
18
+ var PeerConnection = class {
19
+ constructor(peerId, config, events) {
20
+ this._channels = /* @__PURE__ */ new Map();
21
+ this._state = "connecting";
22
+ this._remoteDescriptionSet = false;
23
+ this._pendingCandidates = [];
24
+ this.peerId = peerId;
25
+ this._events = events;
26
+ this._connection = new RTCPeerConnection(config);
27
+ this._connection.onicecandidate = (e) => {
28
+ if (e.candidate) {
29
+ this._events.onIceCandidate(e.candidate);
30
+ }
31
+ };
32
+ this._connection.oniceconnectionstatechange = () => {
33
+ this._updateState();
34
+ };
35
+ this._connection.onconnectionstatechange = () => {
36
+ this._updateState();
37
+ };
38
+ this._connection.ondatachannel = (e) => {
39
+ const channel = e.channel;
40
+ this._channels.set(channel.label, channel);
41
+ this._events.onDataChannel(channel);
42
+ };
43
+ }
44
+ get state() {
45
+ return this._state;
46
+ }
47
+ get connection() {
48
+ return this._connection;
49
+ }
50
+ _updateState() {
51
+ const iceState = this._connection.iceConnectionState;
52
+ const connState = this._connection.connectionState;
53
+ let newState;
54
+ if (connState === "connected" || iceState === "connected") {
55
+ newState = "connected";
56
+ } else if (connState === "failed" || iceState === "failed") {
57
+ newState = "failed";
58
+ } else if (connState === "closed" || iceState === "closed" || iceState === "disconnected") {
59
+ newState = "disconnected";
60
+ } else {
61
+ newState = "connecting";
62
+ }
63
+ if (newState !== this._state) {
64
+ this._state = newState;
65
+ this._events.onStateChange(newState);
66
+ }
67
+ }
68
+ async createOffer() {
69
+ const offer = await this._connection.createOffer();
70
+ await this._connection.setLocalDescription(offer);
71
+ return offer;
72
+ }
73
+ async handleOffer(offer) {
74
+ await this._connection.setRemoteDescription(new RTCSessionDescription(offer));
75
+ this._remoteDescriptionSet = true;
76
+ await this._flushPendingCandidates();
77
+ const answer = await this._connection.createAnswer();
78
+ await this._connection.setLocalDescription(answer);
79
+ return answer;
80
+ }
81
+ async handleAnswer(answer) {
82
+ await this._connection.setRemoteDescription(new RTCSessionDescription(answer));
83
+ this._remoteDescriptionSet = true;
84
+ await this._flushPendingCandidates();
85
+ }
86
+ async addIceCandidate(candidate) {
87
+ if (!this._remoteDescriptionSet) {
88
+ this._pendingCandidates.push(candidate);
89
+ return;
90
+ }
91
+ try {
92
+ await this._connection.addIceCandidate(new RTCIceCandidate(candidate));
93
+ } catch {
94
+ }
95
+ }
96
+ async _flushPendingCandidates() {
97
+ const candidates = this._pendingCandidates;
98
+ this._pendingCandidates = [];
99
+ for (const c of candidates) {
100
+ try {
101
+ await this._connection.addIceCandidate(new RTCIceCandidate(c));
102
+ } catch {
103
+ }
104
+ }
105
+ }
106
+ createDataChannel(name, options) {
107
+ const existing = this._channels.get(name);
108
+ if (existing && existing.readyState !== "closed") {
109
+ return existing;
110
+ }
111
+ const dcOptions = {};
112
+ if (options?.reliable === false) {
113
+ dcOptions.ordered = options?.ordered ?? false;
114
+ dcOptions.maxRetransmits = options?.maxRetransmits ?? 0;
115
+ } else {
116
+ dcOptions.ordered = options?.ordered ?? true;
117
+ }
118
+ const channel = this._connection.createDataChannel(name, dcOptions);
119
+ this._channels.set(name, channel);
120
+ return channel;
121
+ }
122
+ getDataChannel(name) {
123
+ return this._channels.get(name);
124
+ }
125
+ close() {
126
+ for (const channel of this._channels.values()) {
127
+ try {
128
+ channel.close();
129
+ } catch {
130
+ }
131
+ }
132
+ this._channels.clear();
133
+ this._pendingCandidates = [];
134
+ this._remoteDescriptionSet = false;
135
+ try {
136
+ this._connection.close();
137
+ } catch {
138
+ }
139
+ this._state = "disconnected";
140
+ }
141
+ };
142
+
143
+ // src/transport/webrtc/WebRTCTransport.ts
144
+ var ROOM_CONTROL_CHANNEL = "carver:room-control";
145
+ function electHost(peerIds) {
146
+ return [...peerIds].sort()[0];
147
+ }
148
+ var WebRTCTransport = class {
149
+ /**
150
+ * @param strategy Shared SignalingStrategy instance (managed by MultiplayerProvider)
151
+ * @param iceServers Optional ICE servers (STUN + TURN). Defaults to public STUN.
152
+ * @param iceTransportPolicy 'all' (default) or 'relay' (force TURN only).
153
+ */
154
+ constructor(strategy, iceServers, iceTransportPolicy) {
155
+ this._peers = /* @__PURE__ */ new Map();
156
+ this._peerSet = /* @__PURE__ */ new Set();
157
+ this._hostId = "";
158
+ this._isHost = false;
159
+ this._callbacks = {
160
+ onPeerJoin: [],
161
+ onPeerLeave: [],
162
+ onPeerUpdated: [],
163
+ onHostChanged: []
164
+ };
165
+ this._roomUpdatedCallbacks = [];
166
+ this._channels = /* @__PURE__ */ new Map();
167
+ this._rateLimitConfig = { maxMessagesPerSecond: 60, windowMs: 1e3 };
168
+ this._rateLimitCounters = /* @__PURE__ */ new Map();
169
+ this._connected = false;
170
+ this._room = null;
171
+ this._playerMap = /* @__PURE__ */ new Map();
172
+ this._initialPeers = [];
173
+ this._strategyUnsubs = [];
174
+ this._strategy = strategy;
175
+ this._peerId = strategy.selfId;
176
+ this._iceConfig = buildICEConfig({ iceServers, iceTransportPolicy });
177
+ }
178
+ // ── CarverTransport getters ──
179
+ get peerId() {
180
+ return this._peerId;
181
+ }
182
+ get peers() {
183
+ return this._peerSet;
184
+ }
185
+ get hostId() {
186
+ return this._hostId;
187
+ }
188
+ get isHost() {
189
+ return this._isHost;
190
+ }
191
+ get room() {
192
+ return this._room ?? void 0;
193
+ }
194
+ get initialPlayers() {
195
+ return this._initialPeers;
196
+ }
197
+ // ── Event registration ──
198
+ onPeerJoin(cb) {
199
+ this._callbacks.onPeerJoin.push(cb);
200
+ }
201
+ onPeerLeave(cb) {
202
+ this._callbacks.onPeerLeave.push(cb);
203
+ }
204
+ onPeerUpdated(cb) {
205
+ this._callbacks.onPeerUpdated.push(cb);
206
+ }
207
+ onRoomUpdated(cb) {
208
+ this._roomUpdatedCallbacks.push(cb);
209
+ }
210
+ onHostChanged(cb) {
211
+ this._callbacks.onHostChanged.push(cb);
212
+ }
213
+ // ── Channel management ──
214
+ createChannel(name, options) {
215
+ const existing = this._channels.get(name);
216
+ if (existing) {
217
+ return {
218
+ send: (data, target) => this._sendOnChannel(name, data, target),
219
+ onReceive: (cb) => {
220
+ existing.receivers.push(cb);
221
+ },
222
+ close: () => {
223
+ this._channels.delete(name);
224
+ }
225
+ };
226
+ }
227
+ const state = {
228
+ name,
229
+ options: options ?? { reliable: true, ordered: true },
230
+ receivers: []
231
+ };
232
+ this._channels.set(name, state);
233
+ if (this._connected) {
234
+ for (const peer of this._peers.values()) {
235
+ this._createDataChannelOnPeer(peer, name, state.options);
236
+ }
237
+ }
238
+ return {
239
+ send: (data, target) => this._sendOnChannel(name, data, target),
240
+ onReceive: (cb) => {
241
+ state.receivers.push(cb);
242
+ },
243
+ close: () => {
244
+ this._channels.delete(name);
245
+ }
246
+ };
247
+ }
248
+ // ── Connect / Disconnect ──
249
+ async connect(roomId, config) {
250
+ if (config?.iceServers) {
251
+ this._iceConfig = buildICEConfig({
252
+ iceServers: config.iceServers,
253
+ iceTransportPolicy: config.iceTransportPolicy
254
+ });
255
+ }
256
+ this._setupRoomControlChannel();
257
+ this._preRegisterChannel("carver:events", { reliable: true, ordered: true });
258
+ this._preRegisterChannel("carver:snapshots", { reliable: false, ordered: false });
259
+ this._preRegisterChannel("carver:acks", { reliable: true, ordered: true });
260
+ this._preRegisterChannel("carver:inputs", { reliable: true, ordered: true });
261
+ this._preRegisterChannel("carver:network-state", { reliable: true, ordered: true });
262
+ this._strategyUnsubs.push(
263
+ this._strategy.onPeerDiscovered((peerId, meta) => {
264
+ this._onStrategyPeerDiscovered(peerId, meta);
265
+ })
266
+ );
267
+ this._strategyUnsubs.push(
268
+ this._strategy.onPeerLeft((peerId) => {
269
+ this._removePeer(peerId);
270
+ this._playerMap.delete(peerId);
271
+ this._electAndSetHost();
272
+ for (const cb of this._callbacks.onPeerLeave) cb(peerId);
273
+ })
274
+ );
275
+ this._strategyUnsubs.push(
276
+ this._strategy.onSignal((fromPeerId, data) => {
277
+ this._handleSignal(fromPeerId, data);
278
+ })
279
+ );
280
+ await this._strategy.joinRoom(roomId, {
281
+ displayName: config?.displayName,
282
+ ...config?.playerMetadata ?? {}
283
+ });
284
+ const selfPlayer = {
285
+ peerId: this._peerId,
286
+ displayName: config?.displayName ?? `Player-${this._peerId.slice(0, 4)}`,
287
+ isHost: false,
288
+ isSelf: true,
289
+ isReady: false,
290
+ isConnected: true,
291
+ metadata: config?.playerMetadata ?? {},
292
+ latencyMs: 0,
293
+ joinedAt: Date.now()
294
+ };
295
+ this._playerMap.set(this._peerId, selfPlayer);
296
+ this._electAndSetHost();
297
+ this._room = {
298
+ id: roomId,
299
+ name: roomId,
300
+ hostId: this._hostId,
301
+ playerCount: this._playerMap.size,
302
+ maxPlayers: config?.maxPlayers ?? 8,
303
+ isPrivate: false,
304
+ metadata: {},
305
+ createdAt: Date.now(),
306
+ state: "lobby"
307
+ };
308
+ this._initialPeers = Array.from(this._playerMap.values());
309
+ this._connected = true;
310
+ }
311
+ disconnect() {
312
+ this._connected = false;
313
+ for (const unsub of this._strategyUnsubs) unsub();
314
+ this._strategyUnsubs = [];
315
+ for (const peer of this._peers.values()) peer.close();
316
+ this._peers.clear();
317
+ this._peerSet.clear();
318
+ this._channels.clear();
319
+ this._rateLimitCounters.clear();
320
+ this._playerMap.clear();
321
+ this._strategy.leaveRoom().catch(() => {
322
+ });
323
+ this._hostId = "";
324
+ this._isHost = false;
325
+ this._room = null;
326
+ }
327
+ /** Expose strategy for lobby hooks */
328
+ get strategy() {
329
+ return this._strategy;
330
+ }
331
+ // ── Channel pre-registration ──
332
+ /**
333
+ * Register a channel name and options without creating data channels yet.
334
+ * When _connectToPeer runs, it iterates this._channels and creates data
335
+ * channels for every registered name in the initial WebRTC offer.
336
+ * Later, when EventSync/SnapshotSync call createChannel(), the idempotent
337
+ * check returns the pre-registered entry and they just attach receivers.
338
+ */
339
+ _preRegisterChannel(name, options) {
340
+ if (this._channels.has(name)) return;
341
+ this._channels.set(name, { name, options, receivers: [] });
342
+ }
343
+ // ── Room management (over WebRTC data channels) ──
344
+ setReady(ready) {
345
+ this._sendControlMessage({ type: "request-ready", ready });
346
+ }
347
+ setMetadata(metadata) {
348
+ this._sendControlMessage({ type: "request-metadata", metadata });
349
+ }
350
+ setRoomMetadata(metadata) {
351
+ if (!this._isHost) return;
352
+ this._sendControlMessage({ type: "request-room-metadata", metadata });
353
+ }
354
+ kick(peerId, reason) {
355
+ if (!this._isHost) return;
356
+ this._broadcastControlMessage({ type: "kick", peerId, reason });
357
+ }
358
+ transferHost(peerId) {
359
+ if (!this._isHost) return;
360
+ this._sendControlMessage({ type: "request-transfer-host", peerId });
361
+ }
362
+ setRoomState(state) {
363
+ if (!this._isHost) return;
364
+ this._sendControlMessage({ type: "request-room-state", state });
365
+ }
366
+ setMaxPlayers(n) {
367
+ if (!this._isHost) return;
368
+ this._sendControlMessage({ type: "request-max-players", maxPlayers: n });
369
+ }
370
+ lockRoom() {
371
+ if (!this._isHost) return;
372
+ this._sendControlMessage({ type: "request-lock" });
373
+ }
374
+ unlockRoom() {
375
+ if (!this._isHost) return;
376
+ this._sendControlMessage({ type: "request-unlock" });
377
+ }
378
+ /** No-op: lobby uses strategy.subscribeToLobby() directly */
379
+ requestRoomList() {
380
+ }
381
+ // ── Private: Strategy callbacks ──
382
+ _onStrategyPeerDiscovered(peerId, meta) {
383
+ this._connectToPeer(peerId);
384
+ this._peerSet.add(peerId);
385
+ const player = {
386
+ peerId,
387
+ displayName: meta.displayName ?? `Player-${peerId.slice(0, 4)}`,
388
+ isHost: false,
389
+ isSelf: false,
390
+ isReady: false,
391
+ isConnected: true,
392
+ metadata: meta,
393
+ latencyMs: 0,
394
+ joinedAt: Date.now()
395
+ };
396
+ this._playerMap.set(peerId, player);
397
+ this._electAndSetHost();
398
+ for (const cb of this._callbacks.onPeerJoin) cb(peerId);
399
+ for (const cb of this._callbacks.onPeerUpdated) cb(player);
400
+ }
401
+ // ── Private: Room control channel ──
402
+ _setupRoomControlChannel() {
403
+ const ch = this.createChannel(ROOM_CONTROL_CHANNEL, {
404
+ reliable: true,
405
+ ordered: true
406
+ });
407
+ ch.onReceive((msg, peerId) => {
408
+ this._handleControlMessage(msg, peerId);
409
+ });
410
+ }
411
+ _handleControlMessage(msg, fromPeerId) {
412
+ switch (msg.type) {
413
+ case "player-updated": {
414
+ this._playerMap.set(msg.player.peerId, msg.player);
415
+ for (const cb of this._callbacks.onPeerUpdated) cb(msg.player);
416
+ break;
417
+ }
418
+ case "room-updated": {
419
+ if (this._room) {
420
+ Object.assign(this._room, msg.room);
421
+ for (const cb of this._roomUpdatedCallbacks) cb(this._room);
422
+ }
423
+ break;
424
+ }
425
+ case "kick": {
426
+ if (msg.peerId === this._peerId) {
427
+ this.disconnect();
428
+ }
429
+ break;
430
+ }
431
+ case "host-changed": {
432
+ this._hostId = msg.newHostId;
433
+ this._isHost = msg.newHostId === this._peerId;
434
+ for (const cb of this._callbacks.onHostChanged) cb(msg.newHostId);
435
+ break;
436
+ }
437
+ case "sync-state": {
438
+ this._room = msg.room;
439
+ for (const p of msg.players) {
440
+ this._playerMap.set(p.peerId, { ...p, isSelf: p.peerId === this._peerId });
441
+ for (const cb of this._callbacks.onPeerUpdated) cb(p);
442
+ }
443
+ for (const cb of this._roomUpdatedCallbacks) cb(msg.room);
444
+ break;
445
+ }
446
+ // Host processes requests from peers
447
+ case "request-ready": {
448
+ if (!this._isHost) break;
449
+ const p = this._playerMap.get(fromPeerId);
450
+ if (p) {
451
+ p.isReady = msg.ready;
452
+ this._broadcastControlMessage({ type: "player-updated", player: p });
453
+ }
454
+ break;
455
+ }
456
+ case "request-metadata": {
457
+ if (!this._isHost) break;
458
+ const pm = this._playerMap.get(fromPeerId);
459
+ if (pm) {
460
+ pm.metadata = { ...pm.metadata, ...msg.metadata };
461
+ this._broadcastControlMessage({ type: "player-updated", player: pm });
462
+ }
463
+ break;
464
+ }
465
+ case "request-room-metadata": {
466
+ if (!this._isHost || !this._room) break;
467
+ this._room.metadata = { ...this._room.metadata, ...msg.metadata };
468
+ this._broadcastControlMessage({ type: "room-updated", room: this._room });
469
+ break;
470
+ }
471
+ case "request-room-state": {
472
+ if (!this._isHost || !this._room) break;
473
+ this._room.state = msg.state;
474
+ this._broadcastControlMessage({ type: "room-updated", room: this._room });
475
+ break;
476
+ }
477
+ case "request-max-players": {
478
+ if (!this._isHost || !this._room) break;
479
+ this._room.maxPlayers = msg.maxPlayers;
480
+ this._broadcastControlMessage({ type: "room-updated", room: this._room });
481
+ break;
482
+ }
483
+ case "request-lock": {
484
+ if (!this._isHost || !this._room) break;
485
+ this._room.locked = true;
486
+ this._broadcastControlMessage({ type: "room-updated", room: this._room });
487
+ break;
488
+ }
489
+ case "request-unlock": {
490
+ if (!this._isHost || !this._room) break;
491
+ this._room.locked = false;
492
+ this._broadcastControlMessage({ type: "room-updated", room: this._room });
493
+ break;
494
+ }
495
+ case "request-transfer-host": {
496
+ if (!this._isHost) break;
497
+ this._hostId = msg.peerId;
498
+ this._isHost = false;
499
+ this._broadcastControlMessage({ type: "host-changed", newHostId: msg.peerId });
500
+ break;
501
+ }
502
+ }
503
+ }
504
+ _sendControlMessage(msg) {
505
+ if (this._isHost && msg.type.startsWith("request-")) {
506
+ this._handleControlMessage(msg, this._peerId);
507
+ return;
508
+ }
509
+ if (this._hostId && this._hostId !== this._peerId) {
510
+ this._sendOnChannel(ROOM_CONTROL_CHANNEL, msg, this._hostId);
511
+ }
512
+ }
513
+ _broadcastControlMessage(msg) {
514
+ this._sendOnChannel(ROOM_CONTROL_CHANNEL, msg);
515
+ this._handleControlMessage(msg, this._peerId);
516
+ }
517
+ // ── Private: Host election ──
518
+ _electAndSetHost() {
519
+ const allIds = [this._peerId, ...this._peerSet];
520
+ const newHostId = electHost(allIds);
521
+ const changed = newHostId !== this._hostId;
522
+ this._hostId = newHostId;
523
+ this._isHost = newHostId === this._peerId;
524
+ for (const [id, p] of this._playerMap) {
525
+ p.isHost = id === newHostId;
526
+ }
527
+ if (this._room) {
528
+ this._room.hostId = newHostId;
529
+ this._room.playerCount = this._playerMap.size;
530
+ }
531
+ if (changed) {
532
+ for (const cb of this._callbacks.onHostChanged) cb(newHostId);
533
+ }
534
+ }
535
+ // ── Private: WebRTC peer management ──
536
+ _connectToPeer(peerId) {
537
+ if (this._peers.has(peerId)) return;
538
+ const peer = new PeerConnection(peerId, this._iceConfig, {
539
+ onStateChange: (state) => {
540
+ if (state === "connected" && this._isHost && this._room) {
541
+ const syncMsg = {
542
+ type: "sync-state",
543
+ room: this._room,
544
+ players: Array.from(this._playerMap.values())
545
+ };
546
+ setTimeout(() => {
547
+ this._sendOnChannel(ROOM_CONTROL_CHANNEL, syncMsg, peerId);
548
+ }, 100);
549
+ }
550
+ if (state === "failed" || state === "disconnected") {
551
+ this._removePeer(peerId);
552
+ this._playerMap.delete(peerId);
553
+ this._electAndSetHost();
554
+ for (const cb of this._callbacks.onPeerLeave) cb(peerId);
555
+ }
556
+ },
557
+ onDataChannel: (channel) => {
558
+ this._setupDataChannelReceiver(channel, peerId);
559
+ },
560
+ onIceCandidate: (candidate) => {
561
+ this._strategy.signal(peerId, { type: "ice-candidate", candidate: candidate.toJSON() });
562
+ }
563
+ });
564
+ this._peers.set(peerId, peer);
565
+ this._peerSet.add(peerId);
566
+ if (this._peerId < peerId) {
567
+ for (const [name, state] of this._channels) {
568
+ this._createDataChannelOnPeer(peer, name, state.options);
569
+ }
570
+ peer.createOffer().then((offer) => {
571
+ this._strategy.signal(peerId, { type: "offer", sdp: offer });
572
+ });
573
+ }
574
+ }
575
+ async _handleSignal(peerId, data) {
576
+ try {
577
+ const signal = data;
578
+ let peer = this._peers.get(peerId);
579
+ if (signal.type === "offer") {
580
+ if (!peer) {
581
+ this._connectToPeer(peerId);
582
+ peer = this._peers.get(peerId);
583
+ }
584
+ const answer = await peer.handleOffer(signal.sdp);
585
+ this._strategy.signal(peerId, { type: "answer", sdp: answer });
586
+ } else if (signal.type === "answer" && peer) {
587
+ await peer.handleAnswer(signal.sdp);
588
+ } else if (signal.type === "ice-candidate" && peer) {
589
+ await peer.addIceCandidate(signal.candidate);
590
+ }
591
+ } catch (err) {
592
+ if (typeof console !== "undefined") {
593
+ console.error("[CarverJS] Signal handling failed:", err);
594
+ }
595
+ }
596
+ }
597
+ // ── Private: Data channel helpers ──
598
+ _createDataChannelOnPeer(peer, name, options) {
599
+ const channel = peer.createDataChannel(name, options);
600
+ this._setupDataChannelReceiver(channel, peer.peerId);
601
+ }
602
+ _setupDataChannelReceiver(dataChannel, peerId) {
603
+ const channelName = dataChannel.label;
604
+ dataChannel.onmessage = (event) => {
605
+ if (!this._checkRateLimit(peerId)) return;
606
+ const channelState = this._channels.get(channelName);
607
+ if (!channelState) return;
608
+ try {
609
+ const data = typeof event.data === "string" ? JSON.parse(event.data) : event.data;
610
+ for (const receiver of channelState.receivers) receiver(data, peerId);
611
+ } catch {
612
+ }
613
+ };
614
+ }
615
+ _sendOnChannel(channelName, data, target) {
616
+ const serialized = typeof data === "object" && data !== null && !(data instanceof ArrayBuffer) && !(data instanceof Uint8Array) ? JSON.stringify(data) : data;
617
+ const targets = target ? Array.isArray(target) ? target : [target] : Array.from(this._peers.keys());
618
+ for (const pid of targets) {
619
+ const peer = this._peers.get(pid);
620
+ const ch = peer?.getDataChannel(channelName);
621
+ if (ch?.readyState === "open") {
622
+ try {
623
+ ch.send(serialized);
624
+ } catch {
625
+ }
626
+ }
627
+ }
628
+ }
629
+ _removePeer(peerId) {
630
+ const peer = this._peers.get(peerId);
631
+ if (peer) {
632
+ peer.close();
633
+ this._peers.delete(peerId);
634
+ }
635
+ this._peerSet.delete(peerId);
636
+ this._rateLimitCounters.delete(peerId);
637
+ }
638
+ _checkRateLimit(peerId) {
639
+ const now = Date.now();
640
+ let c = this._rateLimitCounters.get(peerId);
641
+ if (!c || now >= c.resetAt) {
642
+ c = { count: 0, resetAt: now + this._rateLimitConfig.windowMs };
643
+ this._rateLimitCounters.set(peerId, c);
644
+ }
645
+ c.count++;
646
+ return c.count <= this._rateLimitConfig.maxMessagesPerSecond;
647
+ }
648
+ };
649
+
650
+ export {
651
+ buildICEConfig,
652
+ PeerConnection,
653
+ WebRTCTransport
654
+ };
655
+ //# sourceMappingURL=chunk-3KT73N2S.mjs.map