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