@remix-gg/sdk 0.9.0 → 0.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.min.js CHANGED
@@ -32,15 +32,813 @@
32
32
  __export(exports_src, {
33
33
  sdk: () => sdk,
34
34
  ZERO_SAFE_AREA_INSET: () => ZERO_SAFE_AREA_INSET,
35
- RemixSDK: () => RemixSDK
35
+ RemixSDK: () => RemixSDK,
36
+ RealtimeRoomError: () => RealtimeRoomError
36
37
  });
38
+
39
+ // src/mesh.ts
40
+ function isPolitePeer(selfId, peerId) {
41
+ return selfId < peerId;
42
+ }
43
+ var EVENTS_CHANNEL_ID = 1;
44
+ var STATE_CHANNEL_ID = 2;
45
+ var MAX_STATE_BUFFERED_BYTES = 64 * 1024;
46
+ var DEFAULT_TUNING = {
47
+ connectTimeoutMs: 12000,
48
+ disconnectedGraceMs: 4000,
49
+ maxRebuilds: 3,
50
+ giveUpRetryMs: 15000,
51
+ maxQueuedEventMessages: 64
52
+ };
53
+ function wireDescription(description, epoch) {
54
+ return epoch === undefined ? { type: description.type, sdp: description.sdp } : { type: description.type, sdp: description.sdp, epoch };
55
+ }
56
+ function parseWireDescription(payload) {
57
+ const value = parseJson(payload);
58
+ if (!isRecord(value))
59
+ return null;
60
+ const { type, sdp, epoch } = value;
61
+ if (type !== "offer" && type !== "answer")
62
+ return null;
63
+ if (typeof sdp !== "string")
64
+ return null;
65
+ if (epoch === undefined)
66
+ return { type, sdp };
67
+ if (typeof epoch !== "number" || !Number.isFinite(epoch))
68
+ return null;
69
+ return { type, sdp, epoch };
70
+ }
71
+ function parseCandidate(payload) {
72
+ const value = parseJson(payload);
73
+ return isRecord(value) ? value : null;
74
+ }
75
+ function byeIsDeparture(payload) {
76
+ const value = parseJson(payload);
77
+ if (!isRecord(value))
78
+ return true;
79
+ return value.reason === undefined;
80
+ }
81
+ function parseJson(payload) {
82
+ try {
83
+ return JSON.parse(payload);
84
+ } catch {
85
+ return null;
86
+ }
87
+ }
88
+ function isRecord(value) {
89
+ return typeof value === "object" && value !== null;
90
+ }
91
+ function createDataMesh(opts) {
92
+ const { selfId, host } = opts;
93
+ const timing = { ...DEFAULT_TUNING, ...opts.tuning };
94
+ const slots = new Map;
95
+ const backlogs = new Map;
96
+ const rebuildCounts = new Map;
97
+ const reportedFailures = new Set;
98
+ const wanted = new Set;
99
+ const giveUpRetries = new Map;
100
+ const clearGiveUpRetry = (peerId) => {
101
+ const timer = giveUpRetries.get(peerId);
102
+ if (timer) {
103
+ clearTimeout(timer);
104
+ giveUpRetries.delete(peerId);
105
+ }
106
+ };
107
+ let iceServers = opts.iceServers;
108
+ let running = true;
109
+ let epochCounter = 0;
110
+ let signalChain = Promise.resolve();
111
+ const clearSlotTimers = (slot) => {
112
+ if (slot.connectTimer) {
113
+ clearTimeout(slot.connectTimer);
114
+ slot.connectTimer = null;
115
+ }
116
+ if (slot.disconnectedTimer) {
117
+ clearTimeout(slot.disconnectedTimer);
118
+ slot.disconnectedTimer = null;
119
+ }
120
+ };
121
+ const teardownSlot = (peerId, options = {}) => {
122
+ if (options.departed)
123
+ backlogs.delete(peerId);
124
+ const slot = slots.get(peerId);
125
+ if (!slot) {
126
+ if (options.departed)
127
+ host.onPeerGone(peerId);
128
+ return;
129
+ }
130
+ slots.delete(peerId);
131
+ clearSlotTimers(slot);
132
+ slot.events.onopen = null;
133
+ slot.events.onmessage = null;
134
+ slot.state.onmessage = null;
135
+ slot.pc.close();
136
+ if (options.departed)
137
+ host.onPeerGone(peerId);
138
+ };
139
+ const scheduleGiveUpRetry = (peerId) => {
140
+ clearGiveUpRetry(peerId);
141
+ giveUpRetries.set(peerId, setTimeout(() => {
142
+ giveUpRetries.delete(peerId);
143
+ if (!running || !wanted.has(peerId) || slots.has(peerId))
144
+ return;
145
+ host.onDiagnostic?.(`mesh: retrying peer ${peerId} after the give-up rest`);
146
+ ensureSlot(peerId);
147
+ }, timing.giveUpRetryMs));
148
+ };
149
+ const rebuildSlot = (peerId, why) => {
150
+ if (!running || !slots.has(peerId))
151
+ return;
152
+ const attempts = (rebuildCounts.get(peerId) ?? 0) + 1;
153
+ teardownSlot(peerId);
154
+ if (attempts > timing.maxRebuilds) {
155
+ rebuildCounts.delete(peerId);
156
+ host.onDiagnostic?.(`mesh: gave up connecting peer ${peerId} after ${timing.maxRebuilds} rebuilds`);
157
+ scheduleGiveUpRetry(peerId);
158
+ host.onRelayRefreshNeeded?.();
159
+ if (!reportedFailures.has(peerId)) {
160
+ reportedFailures.add(peerId);
161
+ host.onError("Could not connect to a player. One of you may be on a network that blocks direct connections.");
162
+ }
163
+ return;
164
+ }
165
+ rebuildCounts.set(peerId, attempts);
166
+ host.post({ toUserId: peerId, kind: "bye", payload: JSON.stringify({ reason: why }) });
167
+ ensureSlot(peerId);
168
+ };
169
+ const armConnectWatchdog = (peerId, slot) => {
170
+ if (slot.connectTimer)
171
+ clearTimeout(slot.connectTimer);
172
+ slot.connectTimer = setTimeout(() => {
173
+ slot.connectTimer = null;
174
+ if (slots.get(peerId) !== slot)
175
+ return;
176
+ if (slot.pc.connectionState !== "connected")
177
+ rebuildSlot(peerId, "connect-timeout");
178
+ }, timing.connectTimeoutMs);
179
+ };
180
+ const reportConnectedPairType = (peerId, pc) => {
181
+ const diagnostic = host.onDiagnostic;
182
+ if (!diagnostic || typeof pc.getStats !== "function")
183
+ return;
184
+ pc.getStats().then((stats) => {
185
+ const candidateTypes = new Map;
186
+ const nominatedPairs = [];
187
+ for (const report of stats.values()) {
188
+ const row = report;
189
+ if (row.type === "local-candidate" || row.type === "remote-candidate") {
190
+ if (row.id && row.candidateType)
191
+ candidateTypes.set(row.id, row.candidateType);
192
+ } else if (row.type === "candidate-pair" && row.nominated && row.state === "succeeded") {
193
+ nominatedPairs.push(row);
194
+ }
195
+ }
196
+ const pair = nominatedPairs[0];
197
+ if (!pair)
198
+ return;
199
+ const local = candidateTypes.get(pair.localCandidateId ?? "") ?? "unknown";
200
+ const remote = candidateTypes.get(pair.remoteCandidateId ?? "") ?? "unknown";
201
+ diagnostic(`mesh: peer ${peerId} connected via ${local}/${remote}`);
202
+ }).catch(() => {});
203
+ };
204
+ const flushQueued = (peerId, slot) => {
205
+ const backlog = backlogs.get(peerId) ?? [];
206
+ backlogs.delete(peerId);
207
+ for (const data of backlog) {
208
+ try {
209
+ slot.events.send(data);
210
+ } catch (error) {
211
+ host.onDiagnostic?.(`mesh: flush to ${peerId} failed: ${error instanceof Error ? error.message : String(error)}`);
212
+ }
213
+ }
214
+ };
215
+ const ensureSlot = (peerId) => {
216
+ const existing = slots.get(peerId);
217
+ if (existing)
218
+ return existing;
219
+ const pc = new RTCPeerConnection({ iceServers });
220
+ const events = pc.createDataChannel("events", { negotiated: true, id: EVENTS_CHANNEL_ID });
221
+ const state = pc.createDataChannel("state", {
222
+ negotiated: true,
223
+ id: STATE_CHANNEL_ID,
224
+ ordered: false,
225
+ maxRetransmits: 0
226
+ });
227
+ events.binaryType = "arraybuffer";
228
+ state.binaryType = "arraybuffer";
229
+ const slot = {
230
+ pc,
231
+ events,
232
+ state,
233
+ makingOffer: false,
234
+ opened: false,
235
+ connectTimer: null,
236
+ disconnectedTimer: null,
237
+ pendingCandidates: [],
238
+ offerEpoch: 0,
239
+ answeringEpoch: null
240
+ };
241
+ slots.set(peerId, slot);
242
+ const isCurrent = () => slots.get(peerId)?.pc === pc;
243
+ events.onopen = () => {
244
+ if (!isCurrent())
245
+ return;
246
+ flushQueued(peerId, slot);
247
+ if (!slot.opened) {
248
+ slot.opened = true;
249
+ host.onPeerOpen(peerId);
250
+ }
251
+ };
252
+ events.onmessage = (event) => {
253
+ if (!isCurrent())
254
+ return;
255
+ host.onMessage(peerId, "events", event.data);
256
+ };
257
+ state.onmessage = (event) => {
258
+ if (!isCurrent())
259
+ return;
260
+ host.onMessage(peerId, "state", event.data);
261
+ };
262
+ pc.onicecandidate = (event) => {
263
+ if (!isCurrent())
264
+ return;
265
+ if (!event.candidate)
266
+ return;
267
+ host.post({ toUserId: peerId, kind: "ice", payload: JSON.stringify(event.candidate) });
268
+ };
269
+ pc.onnegotiationneeded = async () => {
270
+ if (!isCurrent())
271
+ return;
272
+ try {
273
+ slot.makingOffer = true;
274
+ await pc.setLocalDescription();
275
+ if (!isCurrent())
276
+ return;
277
+ if (pc.localDescription) {
278
+ epochCounter += 1;
279
+ slot.offerEpoch = epochCounter;
280
+ host.post({
281
+ toUserId: peerId,
282
+ kind: "offer",
283
+ payload: JSON.stringify(wireDescription(pc.localDescription, slot.offerEpoch))
284
+ });
285
+ }
286
+ } catch (error) {
287
+ if (isCurrent()) {
288
+ host.onError(error instanceof Error ? error.message : "Mesh negotiation failed");
289
+ }
290
+ } finally {
291
+ slot.makingOffer = false;
292
+ }
293
+ };
294
+ pc.onconnectionstatechange = () => {
295
+ if (!isCurrent())
296
+ return;
297
+ const connection = pc.connectionState;
298
+ if (connection === "connected") {
299
+ clearSlotTimers(slot);
300
+ rebuildCounts.delete(peerId);
301
+ reportConnectedPairType(peerId, pc);
302
+ return;
303
+ }
304
+ if (connection === "failed") {
305
+ slot.pc.restartIce();
306
+ armConnectWatchdog(peerId, slot);
307
+ return;
308
+ }
309
+ if (connection === "disconnected") {
310
+ if (slot.disconnectedTimer)
311
+ return;
312
+ slot.disconnectedTimer = setTimeout(() => {
313
+ slot.disconnectedTimer = null;
314
+ if (slot.pc.connectionState === "disconnected") {
315
+ slot.pc.restartIce();
316
+ armConnectWatchdog(peerId, slot);
317
+ }
318
+ }, timing.disconnectedGraceMs);
319
+ return;
320
+ }
321
+ if (connection === "closed")
322
+ teardownSlot(peerId, { departed: true });
323
+ };
324
+ armConnectWatchdog(peerId, slot);
325
+ return slot;
326
+ };
327
+ const applySignal = async (fromUserId, kind, payload) => {
328
+ if (!running)
329
+ return;
330
+ if (kind === "bye") {
331
+ rebuildCounts.delete(fromUserId);
332
+ teardownSlot(fromUserId, { departed: byeIsDeparture(payload) });
333
+ return;
334
+ }
335
+ const slot = ensureSlot(fromUserId);
336
+ const polite = isPolitePeer(selfId, fromUserId);
337
+ const stale = () => slots.get(fromUserId) !== slot;
338
+ if (kind === "ice") {
339
+ const candidate = parseCandidate(payload);
340
+ if (!candidate)
341
+ return;
342
+ if (!slot.pc.remoteDescription) {
343
+ slot.pendingCandidates.push(candidate);
344
+ return;
345
+ }
346
+ try {
347
+ await slot.pc.addIceCandidate(candidate);
348
+ } catch {}
349
+ return;
350
+ }
351
+ if (kind !== "offer" && kind !== "answer")
352
+ return;
353
+ const description = parseWireDescription(payload);
354
+ if (!description)
355
+ return;
356
+ const flushPending = async () => {
357
+ const queued = slot.pendingCandidates;
358
+ slot.pendingCandidates = [];
359
+ for (const candidate of queued) {
360
+ if (stale())
361
+ return;
362
+ try {
363
+ await slot.pc.addIceCandidate(candidate);
364
+ } catch {}
365
+ }
366
+ };
367
+ const offerCollision = description.type === "offer" && (slot.makingOffer || slot.pc.signalingState !== "stable");
368
+ if (!polite && offerCollision)
369
+ return;
370
+ if (description.type === "answer" && slot.pc.signalingState !== "have-local-offer")
371
+ return;
372
+ if (description.type === "answer" && description.epoch !== undefined && description.epoch !== slot.offerEpoch) {
373
+ return;
374
+ }
375
+ if (description.type === "offer")
376
+ slot.answeringEpoch = description.epoch ?? null;
377
+ try {
378
+ await slot.pc.setRemoteDescription(description);
379
+ } catch (error) {
380
+ if (stale())
381
+ return;
382
+ throw error;
383
+ }
384
+ if (stale())
385
+ return;
386
+ await flushPending();
387
+ if (description.type === "answer" || stale())
388
+ return;
389
+ await slot.pc.setLocalDescription();
390
+ if (stale())
391
+ return;
392
+ if (slot.pc.localDescription) {
393
+ host.post({
394
+ toUserId: fromUserId,
395
+ kind: "answer",
396
+ payload: JSON.stringify(wireDescription(slot.pc.localDescription, slot.answeringEpoch ?? undefined))
397
+ });
398
+ }
399
+ };
400
+ return {
401
+ setIceServers(next) {
402
+ iceServers = next;
403
+ },
404
+ setPeers(peers) {
405
+ if (!running)
406
+ return;
407
+ wanted.clear();
408
+ for (const peer of peers) {
409
+ if (peer.userId !== selfId)
410
+ wanted.add(peer.userId);
411
+ }
412
+ for (const peerId of [...slots.keys()]) {
413
+ if (!wanted.has(peerId)) {
414
+ rebuildCounts.delete(peerId);
415
+ teardownSlot(peerId, { departed: true });
416
+ }
417
+ }
418
+ for (const peerId of [...giveUpRetries.keys()]) {
419
+ if (!wanted.has(peerId))
420
+ clearGiveUpRetry(peerId);
421
+ }
422
+ for (const peerId of backlogs.keys()) {
423
+ if (!wanted.has(peerId))
424
+ backlogs.delete(peerId);
425
+ }
426
+ for (const peerId of wanted) {
427
+ if (!giveUpRetries.has(peerId))
428
+ ensureSlot(peerId);
429
+ }
430
+ },
431
+ handleSignal(fromUserId, kind, payload) {
432
+ const chained = signalChain.then(() => applySignal(fromUserId, kind, payload).catch((error) => {
433
+ if (running) {
434
+ host.onError(error instanceof Error ? error.message : "Mesh signal failed");
435
+ }
436
+ }));
437
+ signalChain = chained;
438
+ return chained;
439
+ },
440
+ send(toUserId, channel, data) {
441
+ if (!running)
442
+ return false;
443
+ const slot = slots.get(toUserId);
444
+ if (!slot && !wanted.has(toUserId))
445
+ return false;
446
+ const target = channel === "events" ? slot?.events : slot?.state;
447
+ if (!target || target.readyState !== "open") {
448
+ if (channel !== "events")
449
+ return false;
450
+ const queued = backlogs.get(toUserId) ?? [];
451
+ if (queued.length >= timing.maxQueuedEventMessages) {
452
+ queued.shift();
453
+ host.onDiagnostic?.(`mesh: event backlog for ${toUserId} overflowed; dropped oldest`);
454
+ }
455
+ queued.push(data);
456
+ backlogs.set(toUserId, queued);
457
+ return true;
458
+ }
459
+ if (channel === "state" && target.bufferedAmount >= MAX_STATE_BUFFERED_BYTES)
460
+ return false;
461
+ try {
462
+ target.send(data);
463
+ return true;
464
+ } catch (error) {
465
+ host.onDiagnostic?.(`mesh: send to ${toUserId} failed: ${error instanceof Error ? error.message : String(error)}`);
466
+ return false;
467
+ }
468
+ },
469
+ broadcast(channel, data) {
470
+ for (const peerId of slots.keys())
471
+ this.send(peerId, channel, data);
472
+ },
473
+ stop() {
474
+ running = false;
475
+ for (const peerId of [...slots.keys()])
476
+ teardownSlot(peerId);
477
+ for (const peerId of [...giveUpRetries.keys()])
478
+ clearGiveUpRetry(peerId);
479
+ wanted.clear();
480
+ rebuildCounts.clear();
481
+ reportedFailures.clear();
482
+ backlogs.clear();
483
+ }
484
+ };
485
+ }
486
+
487
+ // src/rooms.ts
488
+ class RealtimeRoomError extends Error {
489
+ code;
490
+ constructor(code, message) {
491
+ super(message);
492
+ this.name = "RealtimeRoomError";
493
+ this.code = code;
494
+ }
495
+ }
496
+ var subscribe = (set, callback) => {
497
+ set.add(callback);
498
+ return () => set.delete(callback);
499
+ };
500
+ var textDecoder = new TextDecoder;
501
+ var DEFAULT_REQUEST_TIMEOUT_MS = 45000;
502
+ var MAX_JOIN_CODE_LENGTH = 32;
503
+ function isPeers(value) {
504
+ return Array.isArray(value) && value.every((peer) => {
505
+ if (typeof peer !== "object" || peer === null)
506
+ return false;
507
+ const row = peer;
508
+ return typeof row.userId === "string" && typeof row.username === "string" && (row.pfp === null || typeof row.pfp === "string") && (row.joinedAt === null || typeof row.joinedAt === "string");
509
+ });
510
+ }
511
+ function isIceServers(value) {
512
+ return Array.isArray(value) && value.every((server) => {
513
+ if (typeof server !== "object" || server === null)
514
+ return false;
515
+ const row = server;
516
+ return (typeof row.urls === "string" || Array.isArray(row.urls) && row.urls.every((url) => typeof url === "string")) && (row.username === undefined || typeof row.username === "string") && (row.credential === undefined || typeof row.credential === "string");
517
+ });
518
+ }
519
+ function createRealtimeController(wire, options = {}) {
520
+ const requestTimeoutMs = options.requestTimeoutMs ?? DEFAULT_REQUEST_TIMEOUT_MS;
521
+ let live = null;
522
+ let pending = null;
523
+ const roomListeners = new Set;
524
+ const roomErrorListeners = new Set;
525
+ let unheardRoomError = null;
526
+ const reportRoomError = (failure) => {
527
+ if (roomErrorListeners.size === 0)
528
+ unheardRoomError = failure;
529
+ else
530
+ for (const callback of roomErrorListeners)
531
+ callback(failure);
532
+ };
533
+ const endRoom = (room, reason) => {
534
+ if (room.ended)
535
+ return;
536
+ room.ended = true;
537
+ if (live === room)
538
+ live = null;
539
+ room.mesh.stop();
540
+ for (const callback of room.callbacks.ended)
541
+ callback(reason);
542
+ };
543
+ const requestRoom = (post) => {
544
+ if (pending) {
545
+ return Promise.reject(new RealtimeRoomError("join_failed", "A room request is already in flight."));
546
+ }
547
+ if (live) {
548
+ const previous = live;
549
+ endRoom(previous, "closed");
550
+ wire.post("multiplayer_leave_room");
551
+ }
552
+ return new Promise((resolve, reject) => {
553
+ const timer = setTimeout(() => {
554
+ if (pending !== mine)
555
+ return;
556
+ pending = null;
557
+ reject(new RealtimeRoomError("join_failed", "The platform did not answer. Realtime rooms are available on Remix Desktop only."));
558
+ }, requestTimeoutMs);
559
+ const mine = {
560
+ resolve: (room) => {
561
+ clearTimeout(timer);
562
+ resolve(room);
563
+ },
564
+ reject: (error) => {
565
+ clearTimeout(timer);
566
+ reject(error);
567
+ }
568
+ };
569
+ pending = mine;
570
+ post();
571
+ });
572
+ };
573
+ const channelFor = (reliable) => reliable ? "events" : "state";
574
+ const encodePayload = (data) => JSON.stringify(data === undefined ? null : data);
575
+ const withoutSelf = (peers, selfId) => peers.filter((peer) => peer.userId !== selfId);
576
+ const startRoom = (session) => {
577
+ const peers = withoutSelf(session.peers, session.selfId);
578
+ const roster = new Map(peers.map((peer) => [peer.userId, peer]));
579
+ const callbacks = {
580
+ message: new Set,
581
+ join: new Set,
582
+ leave: new Set,
583
+ ended: new Set,
584
+ error: new Set
585
+ };
586
+ const mesh = createDataMesh({
587
+ selfId: session.selfId,
588
+ iceServers: session.iceServers,
589
+ host: {
590
+ post: (signal) => wire.post("multiplayer_signal", {
591
+ toUserId: signal.toUserId,
592
+ kind: signal.kind,
593
+ payload: signal.payload
594
+ }),
595
+ onMessage: (userId, _channel, data) => {
596
+ if (room.ended)
597
+ return;
598
+ let parsed;
599
+ try {
600
+ parsed = JSON.parse(typeof data === "string" ? data : textDecoder.decode(data));
601
+ } catch {
602
+ return;
603
+ }
604
+ for (const callback of room.callbacks.message)
605
+ callback(userId, parsed);
606
+ },
607
+ onPeerOpen: () => {},
608
+ onPeerGone: () => {},
609
+ onError: (message) => {
610
+ if (room.ended)
611
+ return;
612
+ for (const callback of room.callbacks.error)
613
+ callback(message);
614
+ },
615
+ onRelayRefreshNeeded: () => {
616
+ if (!room.ended)
617
+ wire.post("multiplayer_refresh_ice");
618
+ }
619
+ }
620
+ });
621
+ const surface = {
622
+ roomId: session.roomId,
623
+ code: session.code,
624
+ selfId: session.selfId,
625
+ hostUserId: session.hostUserId,
626
+ isHost: session.hostUserId === session.selfId,
627
+ get peers() {
628
+ return room.peerList;
629
+ },
630
+ get ended() {
631
+ return room.ended;
632
+ },
633
+ send: (data, options2) => {
634
+ if (room.ended)
635
+ return;
636
+ const channel = channelFor(options2?.reliable !== false);
637
+ const encoded = encodePayload(data);
638
+ for (const userId of room.roster.keys())
639
+ room.mesh.send(userId, channel, encoded);
640
+ },
641
+ sendTo: (userId, data, options2) => {
642
+ if (room.ended)
643
+ return;
644
+ room.mesh.send(userId, channelFor(options2?.reliable !== false), encodePayload(data));
645
+ },
646
+ onMessage: (callback) => subscribe(room.callbacks.message, callback),
647
+ onPeerJoin: (callback) => subscribe(room.callbacks.join, callback),
648
+ onPeerLeave: (callback) => subscribe(room.callbacks.leave, callback),
649
+ onEnded: (callback) => subscribe(room.callbacks.ended, callback),
650
+ onError: (callback) => subscribe(room.callbacks.error, callback),
651
+ invite: () => {
652
+ if (!room.ended)
653
+ wire.post("multiplayer_request_invite");
654
+ },
655
+ leave: () => {
656
+ if (room.ended)
657
+ return;
658
+ endRoom(room, "left");
659
+ wire.post("multiplayer_leave_room");
660
+ }
661
+ };
662
+ const room = {
663
+ session,
664
+ roster,
665
+ peerList: peers,
666
+ ended: false,
667
+ mesh,
668
+ surface,
669
+ callbacks
670
+ };
671
+ try {
672
+ mesh.setPeers(peers);
673
+ } catch (error) {
674
+ mesh.stop();
675
+ throw error;
676
+ }
677
+ return room;
678
+ };
679
+ const readSession = (data) => {
680
+ if (typeof data !== "object" || data === null)
681
+ return null;
682
+ const session = data;
683
+ if (typeof session.roomId !== "string" || typeof session.selfId !== "string" || typeof session.code !== "string" || typeof session.gameId !== "string" || typeof session.hostUserId !== "string" || !isPeers(session.peers) || !isIceServers(session.iceServers)) {
684
+ return null;
685
+ }
686
+ return {
687
+ roomId: session.roomId,
688
+ code: session.code,
689
+ gameId: session.gameId,
690
+ selfId: session.selfId,
691
+ hostUserId: session.hostUserId,
692
+ peers: session.peers,
693
+ iceServers: session.iceServers
694
+ };
695
+ };
696
+ return {
697
+ createRoom() {
698
+ return requestRoom(() => wire.post("multiplayer_create_room"));
699
+ },
700
+ joinRoom(code) {
701
+ const trimmed = code.trim();
702
+ if (trimmed.length === 0 || trimmed.length > MAX_JOIN_CODE_LENGTH) {
703
+ return Promise.reject(new RealtimeRoomError("room_not_found", "That is not an invite code."));
704
+ }
705
+ return requestRoom(() => wire.post("multiplayer_join_room", { code: trimmed }));
706
+ },
707
+ get room() {
708
+ return live?.surface ?? null;
709
+ },
710
+ onRoom(callback) {
711
+ const off = subscribe(roomListeners, callback);
712
+ if (live && !live.ended)
713
+ callback(live.surface);
714
+ return off;
715
+ },
716
+ onRoomError(callback) {
717
+ const off = subscribe(roomErrorListeners, callback);
718
+ if (unheardRoomError) {
719
+ const failure = unheardRoomError;
720
+ unheardRoomError = null;
721
+ callback(failure);
722
+ }
723
+ return off;
724
+ },
725
+ handleHostEvent(type, data) {
726
+ switch (type) {
727
+ case "multiplayer_session": {
728
+ const session = readSession(data);
729
+ if (!session) {
730
+ const waiting2 = pending;
731
+ pending = null;
732
+ waiting2?.reject(new RealtimeRoomError("join_failed", "The platform sent a malformed session."));
733
+ return;
734
+ }
735
+ if (live)
736
+ endRoom(live, "closed");
737
+ unheardRoomError = null;
738
+ const waiting = pending;
739
+ pending = null;
740
+ let room;
741
+ try {
742
+ room = startRoom(session);
743
+ } catch (error) {
744
+ const failure = new RealtimeRoomError("join_failed", error instanceof Error ? error.message : "Could not start the room transport.");
745
+ wire.post("multiplayer_leave_room");
746
+ if (waiting)
747
+ waiting.reject(failure);
748
+ else
749
+ reportRoomError(failure);
750
+ return;
751
+ }
752
+ live = room;
753
+ waiting?.resolve(room.surface);
754
+ for (const callback of roomListeners)
755
+ callback(room.surface);
756
+ return;
757
+ }
758
+ case "multiplayer_peers": {
759
+ const room = live;
760
+ if (!room)
761
+ return;
762
+ const raw = data?.peers;
763
+ if (!isPeers(raw))
764
+ return;
765
+ const peers = withoutSelf(raw, room.session.selfId);
766
+ const next = new Map(peers.map((peer) => [peer.userId, peer]));
767
+ const joined = [];
768
+ const left = [];
769
+ for (const [userId, peer] of next) {
770
+ if (!room.roster.has(userId))
771
+ joined.push(peer);
772
+ }
773
+ for (const [userId, peer] of room.roster) {
774
+ if (!next.has(userId))
775
+ left.push(peer);
776
+ }
777
+ room.roster = next;
778
+ room.peerList = peers;
779
+ room.mesh.setPeers(peers);
780
+ for (const peer of joined)
781
+ for (const callback of room.callbacks.join)
782
+ callback(peer);
783
+ for (const peer of left)
784
+ for (const callback of room.callbacks.leave)
785
+ callback(peer);
786
+ return;
787
+ }
788
+ case "multiplayer_signals": {
789
+ const room = live;
790
+ if (!room)
791
+ return;
792
+ const signals = data?.signals;
793
+ if (!Array.isArray(signals))
794
+ return;
795
+ for (const signal of signals) {
796
+ if (typeof signal?.fromUserId !== "string" || typeof signal.kind !== "string" || typeof signal.payload !== "string") {
797
+ continue;
798
+ }
799
+ room.mesh.handleSignal(signal.fromUserId, signal.kind, signal.payload);
800
+ }
801
+ return;
802
+ }
803
+ case "multiplayer_ice_servers": {
804
+ const iceServers = data?.iceServers;
805
+ if (live && isIceServers(iceServers))
806
+ live.mesh.setIceServers(iceServers);
807
+ return;
808
+ }
809
+ case "multiplayer_session_ended": {
810
+ const reason = data?.reason ?? "closed";
811
+ if (live)
812
+ endRoom(live, reason);
813
+ return;
814
+ }
815
+ case "multiplayer_error": {
816
+ const payload = data;
817
+ const failure = new RealtimeRoomError(payload?.code ?? "join_failed", payload?.message ?? "The room request failed.");
818
+ const waiting = pending;
819
+ pending = null;
820
+ if (waiting) {
821
+ waiting.reject(failure);
822
+ } else {
823
+ reportRoomError(failure);
824
+ }
825
+ return;
826
+ }
827
+ default:
828
+ return;
829
+ }
830
+ }
831
+ };
832
+ }
833
+
834
+ // src/index.ts
37
835
  var ZERO_SAFE_AREA_INSET = {
38
836
  top: 0,
39
837
  right: 0,
40
838
  bottom: 0,
41
839
  left: 0
42
840
  };
43
- var SDK_VERSION = "0.9.0";
841
+ var SDK_VERSION = "0.10.0";
44
842
 
45
843
  class RemixSDK {
46
844
  version = SDK_VERSION;
@@ -207,7 +1005,12 @@
207
1005
  }
208
1006
  }
209
1007
  };
1008
+ realtimeController = createRealtimeController({
1009
+ post: (type, data) => this.sendMessage(type, data)
1010
+ });
1011
+ realtime = this.realtimeController;
210
1012
  emit(eventType, data) {
1013
+ this.realtimeController.handleHostEvent(eventType, data);
211
1014
  if (eventType === "game_info") {
212
1015
  const eventData = data;
213
1016
  this._gameInfo = eventData;
@@ -290,4 +1093,4 @@
290
1093
  }
291
1094
  })();
292
1095
 
293
- //# debugId=3963BA8C33858F2A64756E2164756E21
1096
+ //# debugId=69C4A9F79EEEBD5964756E2164756E21