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