onelobby 0.2.0 → 0.2.2
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/README.md +2 -2
- package/dist/direct/hub-host.js +23 -0
- package/dist/direct/hub-host.js.map +1 -1
- package/dist/direct/hub.js +23 -5
- package/dist/direct/hub.js.map +1 -1
- package/dist/direct/index.d.ts +8 -1
- package/dist/direct/index.js +17 -8
- package/dist/direct/index.js.map +1 -1
- package/dist/direct/portmap.d.ts +12 -1
- package/dist/direct/portmap.js +61 -14
- package/dist/direct/portmap.js.map +1 -1
- package/dist/lobby.d.ts +43 -4
- package/dist/lobby.js +98 -21
- package/dist/lobby.js.map +1 -1
- package/dist/onelobby.bundle.mjs +5 -5
- package/dist/onelobby.bundle.mjs.map +3 -3
- package/dist/onelobby.d.ts +1 -1
- package/dist/onelobby.global.js +186 -32
- package/dist/onelobby.global.js.map +2 -2
- package/dist/onelobby.global.min.js +5 -5
- package/dist/onelobby.global.min.js.map +3 -3
- package/dist/onelobby.js +17 -3
- package/dist/onelobby.js.map +1 -1
- package/dist/signaling.d.ts +5 -1
- package/dist/signaling.js +17 -2
- package/dist/signaling.js.map +1 -1
- package/dist/socket.d.ts +11 -5
- package/dist/socket.js +34 -10
- package/dist/socket.js.map +1 -1
- package/dist/testing.js +13 -3
- package/dist/testing.js.map +1 -1
- package/dist/webrtc.d.ts +12 -0
- package/dist/webrtc.js +30 -1
- package/dist/webrtc.js.map +1 -1
- package/package.json +1 -1
package/dist/onelobby.d.ts
CHANGED
|
@@ -5,7 +5,7 @@ import { type LobbySummary } from "./signaling.js";
|
|
|
5
5
|
import { type LinkFactory, type SocketFactory } from "./transport.js";
|
|
6
6
|
import type { VoiceCodec, FrameSource, AudioSink } from "./voice-types.js";
|
|
7
7
|
export declare const LIB_NAME = "onelobby";
|
|
8
|
-
export declare const LIB_VERSION = "0.2.
|
|
8
|
+
export declare const LIB_VERSION = "0.2.2";
|
|
9
9
|
/** Where `rememberSeat` keeps the resume token: the subset of the Web Storage API it uses. */
|
|
10
10
|
export interface SeatStorage {
|
|
11
11
|
getItem(key: string): string | null;
|
package/dist/onelobby.global.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
/*! onelobby 0.2.
|
|
1
|
+
/*! onelobby 0.2.2 | MIT license */
|
|
2
2
|
"use strict";
|
|
3
3
|
var OneLobby = (() => {
|
|
4
4
|
var __defProp = Object.defineProperty;
|
|
@@ -1146,8 +1146,19 @@ var OneLobby = (() => {
|
|
|
1146
1146
|
cancelPing = null;
|
|
1147
1147
|
pingN = 0;
|
|
1148
1148
|
closed = false;
|
|
1149
|
-
|
|
1149
|
+
handler = null;
|
|
1150
|
+
// Messages with no waiter and no handler yet. The reply that resolves join/create can arrive in the same socket
|
|
1151
|
+
// read as the host's first offer; both are dispatched before the awaiting code builds its Lobby and sets a handler.
|
|
1152
|
+
backlog = [];
|
|
1150
1153
|
onClose = null;
|
|
1154
|
+
get onMessage() {
|
|
1155
|
+
return this.handler;
|
|
1156
|
+
}
|
|
1157
|
+
/** Setting a handler first delivers, in order, what arrived while there was none. */
|
|
1158
|
+
set onMessage(h) {
|
|
1159
|
+
this.handler = h;
|
|
1160
|
+
while (h && this.handler === h && this.backlog.length) h(this.backlog.shift());
|
|
1161
|
+
}
|
|
1151
1162
|
connect() {
|
|
1152
1163
|
return new Promise((resolve, reject) => {
|
|
1153
1164
|
let s;
|
|
@@ -1187,7 +1198,8 @@ var OneLobby = (() => {
|
|
|
1187
1198
|
else w.resolve(m);
|
|
1188
1199
|
return;
|
|
1189
1200
|
}
|
|
1190
|
-
this.
|
|
1201
|
+
if (this.handler) this.handler(m);
|
|
1202
|
+
else if (this.backlog.length < 256) this.backlog.push(m);
|
|
1191
1203
|
};
|
|
1192
1204
|
s.onclose = () => {
|
|
1193
1205
|
if (!this.lastRx) reject(new OneLobbyError("connect_failed", `cannot reach ${this.url}`));
|
|
@@ -1626,11 +1638,12 @@ var OneLobby = (() => {
|
|
|
1626
1638
|
|
|
1627
1639
|
// src/socket.ts
|
|
1628
1640
|
function closeCodeFor(reason) {
|
|
1629
|
-
if (reason === "left") return 1e3;
|
|
1641
|
+
if (reason === "left" || reason === "disconnected") return 1e3;
|
|
1630
1642
|
if (reason === "host_left" || reason === "closed") return 1001;
|
|
1631
1643
|
if (reason === "kicked") return 4e3;
|
|
1632
1644
|
return 1006;
|
|
1633
1645
|
}
|
|
1646
|
+
var nextTask = globalThis.setImmediate ?? ((f) => void setTimeout(f, 0));
|
|
1634
1647
|
var enc2 = new TextEncoder();
|
|
1635
1648
|
var dec2 = new TextDecoder();
|
|
1636
1649
|
var PeerSocket = class {
|
|
@@ -1646,7 +1659,9 @@ var OneLobby = (() => {
|
|
|
1646
1659
|
onLocalClose;
|
|
1647
1660
|
readyState = 0;
|
|
1648
1661
|
bufferedAmount = 0;
|
|
1649
|
-
|
|
1662
|
+
openHandler = null;
|
|
1663
|
+
/** OPEN, but `onopen` has not run yet for any handler. */
|
|
1664
|
+
openPending = false;
|
|
1650
1665
|
onmessage = null;
|
|
1651
1666
|
onclose = null;
|
|
1652
1667
|
onerror = null;
|
|
@@ -1662,12 +1677,28 @@ var OneLobby = (() => {
|
|
|
1662
1677
|
this.closed(code, reason);
|
|
1663
1678
|
this.onLocalClose();
|
|
1664
1679
|
}
|
|
1665
|
-
|
|
1680
|
+
get onopen() {
|
|
1681
|
+
return this.openHandler;
|
|
1682
|
+
}
|
|
1683
|
+
set onopen(h) {
|
|
1684
|
+
this.openHandler = h;
|
|
1685
|
+
if (h && this.openPending) nextTask(() => this.fireOpen());
|
|
1686
|
+
}
|
|
1687
|
+
/**
|
|
1688
|
+
* @internal OPEN now; `onopen` now, or on a later task (like a real WebSocket) so a handler assigned
|
|
1689
|
+
* after any number of awaits in the same task still runs.
|
|
1690
|
+
*/
|
|
1666
1691
|
open(later = false) {
|
|
1667
1692
|
if (this.readyState !== 0) return;
|
|
1668
1693
|
this.readyState = 1;
|
|
1669
|
-
|
|
1670
|
-
|
|
1694
|
+
this.openPending = true;
|
|
1695
|
+
if (!later) this.fireOpen();
|
|
1696
|
+
else nextTask(() => this.fireOpen());
|
|
1697
|
+
}
|
|
1698
|
+
fireOpen() {
|
|
1699
|
+
if (this.readyState !== 1 || !this.openPending || !this.openHandler) return;
|
|
1700
|
+
this.openPending = false;
|
|
1701
|
+
this.openHandler({ type: "open" });
|
|
1671
1702
|
}
|
|
1672
1703
|
/** @internal */
|
|
1673
1704
|
deliver(bytes) {
|
|
@@ -1689,7 +1720,7 @@ var OneLobby = (() => {
|
|
|
1689
1720
|
lobby.on("message", (from, bytes) => {
|
|
1690
1721
|
if (from === lobby.hostPeerId) s.deliver(bytes);
|
|
1691
1722
|
}),
|
|
1692
|
-
lobby.on("sessionEnded", (reason) => s.closed(closeCodeFor(reason), reason)),
|
|
1723
|
+
lobby.on("sessionEnded", (reason, close) => s.closed(close?.code ?? closeCodeFor(reason), close?.reason ?? reason)),
|
|
1693
1724
|
// The socket's far end is the host it opened to; after a migration that server is gone (direct lobbies end instead).
|
|
1694
1725
|
lobby.on("hostChanged", () => s.closed(1001, "host_changed"))
|
|
1695
1726
|
];
|
|
@@ -1722,7 +1753,10 @@ var OneLobby = (() => {
|
|
|
1722
1753
|
const offs = [
|
|
1723
1754
|
lobby.on("peerReady", (p) => void ensure(p.peerId)),
|
|
1724
1755
|
lobby.on("message", (from, bytes) => ensure(from)?.deliver(bytes)),
|
|
1725
|
-
lobby.on(
|
|
1756
|
+
lobby.on(
|
|
1757
|
+
"peerLeft",
|
|
1758
|
+
(p, reason, close) => conns.get(p.peerId)?.closed(close?.code ?? (reason === "left" ? 1001 : closeCodeFor(reason)), close?.reason ?? reason)
|
|
1759
|
+
),
|
|
1726
1760
|
lobby.on("sessionEnded", (reason) => {
|
|
1727
1761
|
for (const c of [...conns.values()]) c.closed(reason === "left" ? 1001 : closeCodeFor(reason), reason);
|
|
1728
1762
|
offs.forEach((f) => f());
|
|
@@ -1736,6 +1770,7 @@ var OneLobby = (() => {
|
|
|
1736
1770
|
|
|
1737
1771
|
// src/lobby.ts
|
|
1738
1772
|
var MIN_PEER_TIMEOUT_MS = 6e3;
|
|
1773
|
+
var KICK_LINGER_MS = 3e3;
|
|
1739
1774
|
function seatName(p) {
|
|
1740
1775
|
const n = typeof p.name === "string" ? p.name.trim() : "";
|
|
1741
1776
|
if (n && n !== "Player") return p;
|
|
@@ -1775,6 +1810,8 @@ var OneLobby = (() => {
|
|
|
1775
1810
|
cfg;
|
|
1776
1811
|
roster = /* @__PURE__ */ new Map();
|
|
1777
1812
|
links = /* @__PURE__ */ new Map();
|
|
1813
|
+
/** ICE from a peer that has no link yet: its candidates can overtake its offer (node-datachannel). */
|
|
1814
|
+
earlyIce = /* @__PURE__ */ new Map();
|
|
1778
1815
|
connected = /* @__PURE__ */ new Set();
|
|
1779
1816
|
// host: peers that said hi
|
|
1780
1817
|
hiSentTo = /* @__PURE__ */ new Set();
|
|
@@ -1797,12 +1834,14 @@ var OneLobby = (() => {
|
|
|
1797
1834
|
relayRetry = false;
|
|
1798
1835
|
/** clock.now() of the last frame from each peer (peerTimeoutMs). */
|
|
1799
1836
|
lastHeard = /* @__PURE__ */ new Map();
|
|
1837
|
+
/** Host: links of kicked / disconnected peers, kept open briefly (removePeer `linger`). */
|
|
1838
|
+
lingering = /* @__PURE__ */ new Set();
|
|
1800
1839
|
/** Host: per-peer fixed window for rateLimit. */
|
|
1801
1840
|
rate = /* @__PURE__ */ new Map();
|
|
1802
1841
|
/**
|
|
1803
1842
|
* Resolves when this peer can exchange data: immediately for the host, once the link to the host is open for a
|
|
1804
1843
|
* joiner. Rejects with OneLobbyError("ice_failed") when the joiner cannot reach the host in `readyTimeoutMs`
|
|
1805
|
-
* (or with "closed"/"kicked" when the lobby ends first).
|
|
1844
|
+
* (or with "closed"/"kicked" when the lobby ends first; "closed" also when the host disconnected it).
|
|
1806
1845
|
*/
|
|
1807
1846
|
ready = new Promise((res, rej) => {
|
|
1808
1847
|
this.markReady = () => {
|
|
@@ -1930,10 +1969,32 @@ var OneLobby = (() => {
|
|
|
1930
1969
|
this.signaling.send({ t: "meta", meta });
|
|
1931
1970
|
this.emit("metaChanged", this.meta);
|
|
1932
1971
|
}
|
|
1933
|
-
|
|
1972
|
+
/**
|
|
1973
|
+
* Host: removes a player. By default it is a ban: the hub refuses that player's address for the rest of this lobby
|
|
1974
|
+
* (a new tab or a reload included). `{ ban: false }` is `disconnect()`. `code` / `reason` reach the player's
|
|
1975
|
+
* `asWebSocket()` onclose (and `sessionEnded`'s second argument); without them its socket closes with 4000 "kicked".
|
|
1976
|
+
*/
|
|
1977
|
+
async kick(peerId, opts = {}) {
|
|
1934
1978
|
if (!this.isHost) throw new Error("only the host can kick");
|
|
1935
|
-
|
|
1936
|
-
|
|
1979
|
+
const { ban = true, code, reason } = opts;
|
|
1980
|
+
if (code !== void 0 && !(code === 1e3 || Number.isInteger(code) && code >= 3e3 && code <= 4999))
|
|
1981
|
+
throw new RangeError("close code must be 1000 or 3000-4999");
|
|
1982
|
+
if (reason !== void 0 && reason.length > 120) throw new RangeError("close reason must be at most 120 characters");
|
|
1983
|
+
const msg = { t: "kick", peerId };
|
|
1984
|
+
if (!ban) msg.ban = false;
|
|
1985
|
+
if (code !== void 0) msg.closeCode = code;
|
|
1986
|
+
if (reason !== void 0) msg.closeReason = reason;
|
|
1987
|
+
this.signaling.send(msg);
|
|
1988
|
+
const close = code !== void 0 || reason !== void 0 ? { code, reason } : void 0;
|
|
1989
|
+
this.removePeer(peerId, ban ? "kicked" : "disconnected", close, true);
|
|
1990
|
+
}
|
|
1991
|
+
/**
|
|
1992
|
+
* Host: ends one player's connection without a ban, like a server closing one WebSocket. The player's
|
|
1993
|
+
* `asWebSocket()` gets onclose with `code` (default 1000) and `reason` (default "disconnected"), its lobby ends with
|
|
1994
|
+
* `sessionEnded("disconnected")`, and it may join again (a new tab, a reload). Same as `kick(peerId, { ban: false, code, reason })`.
|
|
1995
|
+
*/
|
|
1996
|
+
disconnect(peerId, code, reason) {
|
|
1997
|
+
return this.kick(peerId, { ban: false, code, reason });
|
|
1937
1998
|
}
|
|
1938
1999
|
async approve(peerId, ok) {
|
|
1939
2000
|
if (!this.isHost) throw new Error("only the host can approve joins");
|
|
@@ -2334,36 +2395,71 @@ var OneLobby = (() => {
|
|
|
2334
2395
|
void this.onSignal(Number(m.from), m);
|
|
2335
2396
|
break;
|
|
2336
2397
|
case "error":
|
|
2337
|
-
if (m.code === "kicked")
|
|
2338
|
-
|
|
2398
|
+
if (m.code === "kicked") {
|
|
2399
|
+
const d = m.data ?? {};
|
|
2400
|
+
const close = {};
|
|
2401
|
+
if (typeof d.closeCode === "number") close.code = d.closeCode;
|
|
2402
|
+
if (typeof d.closeReason === "string") close.reason = d.closeReason;
|
|
2403
|
+
this.teardown(d.ban === false ? "disconnected" : "kicked", close.code !== void 0 || close.reason !== void 0 ? close : void 0);
|
|
2404
|
+
} else this.reportError(new OneLobbyError(String(m.code), String(m.msg ?? ""), m.data));
|
|
2339
2405
|
break;
|
|
2340
2406
|
}
|
|
2341
2407
|
}
|
|
2342
2408
|
async onSignal(from, s) {
|
|
2343
2409
|
let link = this.links.get(from);
|
|
2344
|
-
if (s.sdp?.type === "offer") {
|
|
2345
|
-
link = this.makeLink(from);
|
|
2346
|
-
}
|
|
2347
|
-
if (!link) return;
|
|
2348
2410
|
const payload = {};
|
|
2349
2411
|
if (s.sdp) payload.sdp = s.sdp;
|
|
2350
2412
|
if (s.ice) payload.ice = s.ice;
|
|
2413
|
+
if (s.sdp?.type === "offer") {
|
|
2414
|
+
link = this.makeLink(from);
|
|
2415
|
+
const early = this.earlyIce.get(from) ?? [];
|
|
2416
|
+
this.earlyIce.delete(from);
|
|
2417
|
+
const done = link.handleSignal(payload);
|
|
2418
|
+
for (const e of early) void link.handleSignal(e);
|
|
2419
|
+
await done;
|
|
2420
|
+
return;
|
|
2421
|
+
}
|
|
2422
|
+
if (!link) {
|
|
2423
|
+
if (!payload.ice || this.ended || !this.roster.has(from)) return;
|
|
2424
|
+
const q = this.earlyIce.get(from) ?? [];
|
|
2425
|
+
if (q.length < 64) q.push(payload);
|
|
2426
|
+
this.earlyIce.set(from, q);
|
|
2427
|
+
return;
|
|
2428
|
+
}
|
|
2351
2429
|
await link.handleSignal(payload);
|
|
2352
2430
|
}
|
|
2353
|
-
|
|
2431
|
+
/**
|
|
2432
|
+
* `linger` (host kick / disconnect): stop using the peer's link at once but leave it open until that peer closes it
|
|
2433
|
+
* or KICK_LINGER_MS pass. The hub tells the peer over its WebSocket; closing the data link first would make it look
|
|
2434
|
+
* like the host vanished (a migration and a 1001 instead of the host's close code).
|
|
2435
|
+
*/
|
|
2436
|
+
removePeer(peerId, reason, close, linger = false) {
|
|
2354
2437
|
const peer = this.roster.get(peerId);
|
|
2355
2438
|
if (peerId === this.localPeerId) {
|
|
2356
2439
|
this.teardown(reason === "kicked" ? "kicked" : reason);
|
|
2357
2440
|
return;
|
|
2358
2441
|
}
|
|
2359
2442
|
if (peer) this.roster.delete(peerId);
|
|
2443
|
+
this.earlyIce.delete(peerId);
|
|
2360
2444
|
const link = this.links.get(peerId);
|
|
2361
2445
|
if (link && peerId !== this.hostPeerId) {
|
|
2362
|
-
link.onClose = null;
|
|
2363
2446
|
link.onMessage = null;
|
|
2364
|
-
link.close();
|
|
2365
2447
|
this.links.delete(peerId);
|
|
2366
2448
|
this.hiSentTo.delete(link);
|
|
2449
|
+
if (linger) {
|
|
2450
|
+
this.lingering.add(link);
|
|
2451
|
+
const done = () => {
|
|
2452
|
+
if (!this.lingering.delete(link)) return;
|
|
2453
|
+
cancel();
|
|
2454
|
+
link.onClose = null;
|
|
2455
|
+
link.close();
|
|
2456
|
+
};
|
|
2457
|
+
const cancel = this.cfg.clock.setTimeout(done, KICK_LINGER_MS);
|
|
2458
|
+
link.onClose = done;
|
|
2459
|
+
} else {
|
|
2460
|
+
link.onClose = null;
|
|
2461
|
+
link.close();
|
|
2462
|
+
}
|
|
2367
2463
|
}
|
|
2368
2464
|
this.connected.delete(peerId);
|
|
2369
2465
|
this.outbox.delete(peerId);
|
|
@@ -2372,14 +2468,15 @@ var OneLobby = (() => {
|
|
|
2372
2468
|
this.lockstep.onPeerLeft(peerId);
|
|
2373
2469
|
this.chat.onPeerLeft(peerId);
|
|
2374
2470
|
this.voice.onPeerLeft(peerId);
|
|
2375
|
-
if (peer) this.emit("peerLeft", peer, reason);
|
|
2471
|
+
if (peer) this.emit("peerLeft", peer, reason, close);
|
|
2376
2472
|
if (peerId === this.hostPeerId && !this.isHost) {
|
|
2377
2473
|
this.handleHostLost(reason);
|
|
2378
2474
|
}
|
|
2379
2475
|
}
|
|
2476
|
+
/** Lockstep drops (stalled, desynced): removed without a ban, so the player can come back (a rejoin resyncs). */
|
|
2380
2477
|
dropPeer(peerId, reason) {
|
|
2381
|
-
this.signaling.send({ t: "kick", peerId });
|
|
2382
|
-
this.removePeer(peerId, reason);
|
|
2478
|
+
this.signaling.send({ t: "kick", peerId, ban: false, closeReason: reason });
|
|
2479
|
+
this.removePeer(peerId, reason, void 0, true);
|
|
2383
2480
|
}
|
|
2384
2481
|
setPhase(p) {
|
|
2385
2482
|
if (p !== "lobby" && p !== "in_game") return;
|
|
@@ -2430,7 +2527,7 @@ var OneLobby = (() => {
|
|
|
2430
2527
|
if (this.ended || this.isHost) return;
|
|
2431
2528
|
this.sendTo(this.hostPeerId, { type: "ping", n: ++this.pingN, t: this.cfg.clock.now() >>> 0 }, 0, "fast");
|
|
2432
2529
|
}
|
|
2433
|
-
teardown(reason) {
|
|
2530
|
+
teardown(reason, close) {
|
|
2434
2531
|
if (this.ended) return;
|
|
2435
2532
|
this.ended = true;
|
|
2436
2533
|
this.cancelReadyTimer?.();
|
|
@@ -2442,16 +2539,18 @@ var OneLobby = (() => {
|
|
|
2442
2539
|
this.cancelPing?.();
|
|
2443
2540
|
this.lockstep.stop();
|
|
2444
2541
|
this.voice.close();
|
|
2445
|
-
for (const l of this.links.values()) {
|
|
2542
|
+
for (const l of [...this.links.values(), ...this.lingering]) {
|
|
2446
2543
|
l.onClose = null;
|
|
2447
2544
|
l.onMessage = null;
|
|
2448
2545
|
l.close();
|
|
2449
2546
|
}
|
|
2450
2547
|
this.links.clear();
|
|
2548
|
+
this.earlyIce.clear();
|
|
2549
|
+
this.lingering.clear();
|
|
2451
2550
|
this.signaling.onClose = null;
|
|
2452
2551
|
this.signaling.onMessage = null;
|
|
2453
2552
|
this.signaling.close();
|
|
2454
|
-
this.emit("sessionEnded", reason);
|
|
2553
|
+
this.emit("sessionEnded", reason, close);
|
|
2455
2554
|
}
|
|
2456
2555
|
/** Total in-flight links (diagnostics and tests). */
|
|
2457
2556
|
get linkCount() {
|
|
@@ -2620,8 +2719,14 @@ registerProcessor('onelobby-capture', OneLobbyCapture);
|
|
|
2620
2719
|
pc;
|
|
2621
2720
|
channels = {};
|
|
2622
2721
|
pendingIce = [];
|
|
2722
|
+
/**
|
|
2723
|
+
* Set once setRemoteDescription resolves. Not `pc.remoteDescription`: node-datachannel's polyfill returns an object
|
|
2724
|
+
* there before any is set, and its addIceCandidate throws until one is.
|
|
2725
|
+
*/
|
|
2726
|
+
haveRemote = false;
|
|
2623
2727
|
closed = false;
|
|
2624
2728
|
opened = 0;
|
|
2729
|
+
early = [];
|
|
2625
2730
|
extra;
|
|
2626
2731
|
/** `rtc`: the RTCPeerConnection class to use (default the global one; in Node, node-datachannel's polyfill). */
|
|
2627
2732
|
constructor(opts, rtc = globalThis.RTCPeerConnection) {
|
|
@@ -2647,6 +2752,14 @@ registerProcessor('onelobby-capture', OneLobbyCapture);
|
|
|
2647
2752
|
else this.fail(s);
|
|
2648
2753
|
}
|
|
2649
2754
|
};
|
|
2755
|
+
}
|
|
2756
|
+
/**
|
|
2757
|
+
* The offerer creates the channels just before its offer, the answerer once the remote offer is set. Creating them
|
|
2758
|
+
* any earlier makes libdatachannel (node-datachannel) start an offer of its own on the answering side, which then
|
|
2759
|
+
* collides with the real one (both ICE agents controlling, the link never opens).
|
|
2760
|
+
*/
|
|
2761
|
+
openChannels() {
|
|
2762
|
+
if (this.channels.reliable) return;
|
|
2650
2763
|
for (const name of ["reliable", "fast", "voice"]) {
|
|
2651
2764
|
const ch = this.pc.createDataChannel(name, CHANNEL_INIT[name]);
|
|
2652
2765
|
ch.binaryType = "arraybuffer";
|
|
@@ -2654,6 +2767,7 @@ registerProcessor('onelobby-capture', OneLobbyCapture);
|
|
|
2654
2767
|
if (++this.opened === 3 && !this.closed) {
|
|
2655
2768
|
this.isOpen = true;
|
|
2656
2769
|
this.onOpen?.();
|
|
2770
|
+
for (const [n, data] of this.early.splice(0)) if (!this.closed) this.onMessage?.(n, data);
|
|
2657
2771
|
void this.reportPath();
|
|
2658
2772
|
}
|
|
2659
2773
|
};
|
|
@@ -2661,6 +2775,10 @@ registerProcessor('onelobby-capture', OneLobbyCapture);
|
|
|
2661
2775
|
ch.onmessage = (e) => {
|
|
2662
2776
|
const d = e.data;
|
|
2663
2777
|
const data = d instanceof ArrayBuffer ? new Uint8Array(d) : ArrayBuffer.isView(d) ? new Uint8Array(d.buffer, d.byteOffset, d.byteLength) : new Uint8Array(d);
|
|
2778
|
+
if (!this.isOpen) {
|
|
2779
|
+
if (this.early.length < 1e3) this.early.push([name, data]);
|
|
2780
|
+
return;
|
|
2781
|
+
}
|
|
2664
2782
|
this.onMessage?.(name, data);
|
|
2665
2783
|
};
|
|
2666
2784
|
this.channels[name] = ch;
|
|
@@ -2669,6 +2787,7 @@ registerProcessor('onelobby-capture', OneLobbyCapture);
|
|
|
2669
2787
|
startOffer() {
|
|
2670
2788
|
void (async () => {
|
|
2671
2789
|
try {
|
|
2790
|
+
this.openChannels();
|
|
2672
2791
|
const offer = await this.pc.createOffer();
|
|
2673
2792
|
await this.pc.setLocalDescription(offer);
|
|
2674
2793
|
this.onSignal?.({ sdp: { type: "offer", sdp: offer.sdp ?? "" } });
|
|
@@ -2681,19 +2800,21 @@ registerProcessor('onelobby-capture', OneLobbyCapture);
|
|
|
2681
2800
|
try {
|
|
2682
2801
|
if (s.sdp) {
|
|
2683
2802
|
await this.pc.setRemoteDescription({ type: s.sdp.type, sdp: s.sdp.sdp });
|
|
2803
|
+
this.haveRemote = true;
|
|
2684
2804
|
for (const c of this.pendingIce.splice(0)) await this.pc.addIceCandidate(c);
|
|
2685
2805
|
if (this.extra.length) {
|
|
2686
2806
|
const mid = /^a=mid:(\S+)/m.exec(s.sdp.sdp)?.[1] ?? "0";
|
|
2687
2807
|
for (const candidate of this.extra.splice(0)) await this.pc.addIceCandidate({ candidate, sdpMid: mid, sdpMLineIndex: 0 });
|
|
2688
2808
|
}
|
|
2689
2809
|
if (s.sdp.type === "offer") {
|
|
2810
|
+
this.openChannels();
|
|
2690
2811
|
const answer = await this.pc.createAnswer();
|
|
2691
2812
|
await this.pc.setLocalDescription(answer);
|
|
2692
2813
|
this.onSignal?.({ sdp: { type: "answer", sdp: answer.sdp ?? "" } });
|
|
2693
2814
|
}
|
|
2694
2815
|
}
|
|
2695
2816
|
if (s.ice) {
|
|
2696
|
-
if (this.
|
|
2817
|
+
if (this.haveRemote) await this.pc.addIceCandidate(s.ice);
|
|
2697
2818
|
else this.pendingIce.push(s.ice);
|
|
2698
2819
|
}
|
|
2699
2820
|
} catch (e) {
|
|
@@ -2761,6 +2882,29 @@ registerProcessor('onelobby-capture', OneLobbyCapture);
|
|
|
2761
2882
|
}
|
|
2762
2883
|
return crc;
|
|
2763
2884
|
}
|
|
2885
|
+
function parseIPv4(ip) {
|
|
2886
|
+
const parts = ip.split(".");
|
|
2887
|
+
if (parts.length !== 4) return null;
|
|
2888
|
+
let n = 0;
|
|
2889
|
+
for (const p of parts) {
|
|
2890
|
+
if (!/^\d{1,3}$/.test(p) || Number(p) > 255) return null;
|
|
2891
|
+
n = n * 256 + Number(p);
|
|
2892
|
+
}
|
|
2893
|
+
return n;
|
|
2894
|
+
}
|
|
2895
|
+
function encodeJoinCode(ipv4, port) {
|
|
2896
|
+
const ip = parseIPv4(ipv4);
|
|
2897
|
+
if (ip === null) throw new Error(`not an IPv4 address: ${ipv4}`);
|
|
2898
|
+
if (!Number.isInteger(port) || port < 1 || port > 65535) throw new Error(`bad port ${port}`);
|
|
2899
|
+
const data = BigInt(ip) << 16n | BigInt(port);
|
|
2900
|
+
let v = data << 5n | BigInt(crc5(data, 50));
|
|
2901
|
+
let s = "";
|
|
2902
|
+
for (let i = 0; i < JOIN_CODE_LENGTH; i++) {
|
|
2903
|
+
s = CROCKFORD_ALPHABET[Number(v & 31n)] + s;
|
|
2904
|
+
v >>= 5n;
|
|
2905
|
+
}
|
|
2906
|
+
return `${s.slice(0, 4)}-${s.slice(4, 8)}-${s.slice(8)}`;
|
|
2907
|
+
}
|
|
2764
2908
|
function normalizeJoinCode(input) {
|
|
2765
2909
|
const s = input.replace(/[ \-]/g, "").replace(/[a-z]/g, (c) => c.toUpperCase()).replace(/[IL]/g, "1").replace(/O/g, "0");
|
|
2766
2910
|
if (s.length !== JOIN_CODE_LENGTH) return null;
|
|
@@ -2836,7 +2980,7 @@ registerProcessor('onelobby-capture', OneLobbyCapture);
|
|
|
2836
2980
|
|
|
2837
2981
|
// src/onelobby.ts
|
|
2838
2982
|
var LIB_NAME = "onelobby";
|
|
2839
|
-
var LIB_VERSION = "0.2.
|
|
2983
|
+
var LIB_VERSION = "0.2.2";
|
|
2840
2984
|
var DEFAULT_READY_TIMEOUT_MS = 15e3;
|
|
2841
2985
|
function hasMic() {
|
|
2842
2986
|
return typeof navigator !== "undefined" && !!navigator.mediaDevices?.getUserMedia && typeof AudioWorkletNode !== "undefined";
|
|
@@ -3080,7 +3224,9 @@ registerProcessor('onelobby-capture', OneLobbyCapture);
|
|
|
3080
3224
|
const { sig, welcome } = await this.connect(addr.url, this.socketFactory, opts.connectTimeoutMs ?? 5e3);
|
|
3081
3225
|
try {
|
|
3082
3226
|
const state = await sig.request({ t: "join", ...opts.password ? { password: opts.password } : {} }, "lobby");
|
|
3083
|
-
|
|
3227
|
+
const lobby = this.makeLobby(sig, welcome, state, { links });
|
|
3228
|
+
lobby.joinCode = joinedWith(addr);
|
|
3229
|
+
return lobby;
|
|
3084
3230
|
} catch (e) {
|
|
3085
3231
|
sig.close();
|
|
3086
3232
|
throw e;
|
|
@@ -3139,6 +3285,14 @@ registerProcessor('onelobby-capture', OneLobbyCapture);
|
|
|
3139
3285
|
}
|
|
3140
3286
|
}
|
|
3141
3287
|
};
|
|
3288
|
+
function joinedWith(addr) {
|
|
3289
|
+
if (addr.isIPv6) return addr.display;
|
|
3290
|
+
try {
|
|
3291
|
+
return encodeJoinCode(addr.host, addr.port);
|
|
3292
|
+
} catch {
|
|
3293
|
+
return addr.display;
|
|
3294
|
+
}
|
|
3295
|
+
}
|
|
3142
3296
|
|
|
3143
3297
|
// src/friendly.ts
|
|
3144
3298
|
var TEXT = {
|