onelobby 0.2.0 → 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -2
- 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/lobby.d.ts +41 -4
- package/dist/lobby.js +73 -15
- 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 +129 -25
- 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/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/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.1";
|
|
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.1 | MIT license */
|
|
2
2
|
"use strict";
|
|
3
3
|
var OneLobby = (() => {
|
|
4
4
|
var __defProp = Object.defineProperty;
|
|
@@ -1626,11 +1626,12 @@ var OneLobby = (() => {
|
|
|
1626
1626
|
|
|
1627
1627
|
// src/socket.ts
|
|
1628
1628
|
function closeCodeFor(reason) {
|
|
1629
|
-
if (reason === "left") return 1e3;
|
|
1629
|
+
if (reason === "left" || reason === "disconnected") return 1e3;
|
|
1630
1630
|
if (reason === "host_left" || reason === "closed") return 1001;
|
|
1631
1631
|
if (reason === "kicked") return 4e3;
|
|
1632
1632
|
return 1006;
|
|
1633
1633
|
}
|
|
1634
|
+
var nextTask = globalThis.setImmediate ?? ((f) => void setTimeout(f, 0));
|
|
1634
1635
|
var enc2 = new TextEncoder();
|
|
1635
1636
|
var dec2 = new TextDecoder();
|
|
1636
1637
|
var PeerSocket = class {
|
|
@@ -1646,7 +1647,9 @@ var OneLobby = (() => {
|
|
|
1646
1647
|
onLocalClose;
|
|
1647
1648
|
readyState = 0;
|
|
1648
1649
|
bufferedAmount = 0;
|
|
1649
|
-
|
|
1650
|
+
openHandler = null;
|
|
1651
|
+
/** OPEN, but `onopen` has not run yet for any handler. */
|
|
1652
|
+
openPending = false;
|
|
1650
1653
|
onmessage = null;
|
|
1651
1654
|
onclose = null;
|
|
1652
1655
|
onerror = null;
|
|
@@ -1662,12 +1665,28 @@ var OneLobby = (() => {
|
|
|
1662
1665
|
this.closed(code, reason);
|
|
1663
1666
|
this.onLocalClose();
|
|
1664
1667
|
}
|
|
1665
|
-
|
|
1668
|
+
get onopen() {
|
|
1669
|
+
return this.openHandler;
|
|
1670
|
+
}
|
|
1671
|
+
set onopen(h) {
|
|
1672
|
+
this.openHandler = h;
|
|
1673
|
+
if (h && this.openPending) nextTask(() => this.fireOpen());
|
|
1674
|
+
}
|
|
1675
|
+
/**
|
|
1676
|
+
* @internal OPEN now; `onopen` now, or on a later task (like a real WebSocket) so a handler assigned
|
|
1677
|
+
* after any number of awaits in the same task still runs.
|
|
1678
|
+
*/
|
|
1666
1679
|
open(later = false) {
|
|
1667
1680
|
if (this.readyState !== 0) return;
|
|
1668
1681
|
this.readyState = 1;
|
|
1669
|
-
|
|
1670
|
-
|
|
1682
|
+
this.openPending = true;
|
|
1683
|
+
if (!later) this.fireOpen();
|
|
1684
|
+
else nextTask(() => this.fireOpen());
|
|
1685
|
+
}
|
|
1686
|
+
fireOpen() {
|
|
1687
|
+
if (this.readyState !== 1 || !this.openPending || !this.openHandler) return;
|
|
1688
|
+
this.openPending = false;
|
|
1689
|
+
this.openHandler({ type: "open" });
|
|
1671
1690
|
}
|
|
1672
1691
|
/** @internal */
|
|
1673
1692
|
deliver(bytes) {
|
|
@@ -1689,7 +1708,7 @@ var OneLobby = (() => {
|
|
|
1689
1708
|
lobby.on("message", (from, bytes) => {
|
|
1690
1709
|
if (from === lobby.hostPeerId) s.deliver(bytes);
|
|
1691
1710
|
}),
|
|
1692
|
-
lobby.on("sessionEnded", (reason) => s.closed(closeCodeFor(reason), reason)),
|
|
1711
|
+
lobby.on("sessionEnded", (reason, close) => s.closed(close?.code ?? closeCodeFor(reason), close?.reason ?? reason)),
|
|
1693
1712
|
// The socket's far end is the host it opened to; after a migration that server is gone (direct lobbies end instead).
|
|
1694
1713
|
lobby.on("hostChanged", () => s.closed(1001, "host_changed"))
|
|
1695
1714
|
];
|
|
@@ -1722,7 +1741,10 @@ var OneLobby = (() => {
|
|
|
1722
1741
|
const offs = [
|
|
1723
1742
|
lobby.on("peerReady", (p) => void ensure(p.peerId)),
|
|
1724
1743
|
lobby.on("message", (from, bytes) => ensure(from)?.deliver(bytes)),
|
|
1725
|
-
lobby.on(
|
|
1744
|
+
lobby.on(
|
|
1745
|
+
"peerLeft",
|
|
1746
|
+
(p, reason, close) => conns.get(p.peerId)?.closed(close?.code ?? (reason === "left" ? 1001 : closeCodeFor(reason)), close?.reason ?? reason)
|
|
1747
|
+
),
|
|
1726
1748
|
lobby.on("sessionEnded", (reason) => {
|
|
1727
1749
|
for (const c of [...conns.values()]) c.closed(reason === "left" ? 1001 : closeCodeFor(reason), reason);
|
|
1728
1750
|
offs.forEach((f) => f());
|
|
@@ -1736,6 +1758,7 @@ var OneLobby = (() => {
|
|
|
1736
1758
|
|
|
1737
1759
|
// src/lobby.ts
|
|
1738
1760
|
var MIN_PEER_TIMEOUT_MS = 6e3;
|
|
1761
|
+
var KICK_LINGER_MS = 3e3;
|
|
1739
1762
|
function seatName(p) {
|
|
1740
1763
|
const n = typeof p.name === "string" ? p.name.trim() : "";
|
|
1741
1764
|
if (n && n !== "Player") return p;
|
|
@@ -1797,12 +1820,14 @@ var OneLobby = (() => {
|
|
|
1797
1820
|
relayRetry = false;
|
|
1798
1821
|
/** clock.now() of the last frame from each peer (peerTimeoutMs). */
|
|
1799
1822
|
lastHeard = /* @__PURE__ */ new Map();
|
|
1823
|
+
/** Host: links of kicked / disconnected peers, kept open briefly (removePeer `linger`). */
|
|
1824
|
+
lingering = /* @__PURE__ */ new Set();
|
|
1800
1825
|
/** Host: per-peer fixed window for rateLimit. */
|
|
1801
1826
|
rate = /* @__PURE__ */ new Map();
|
|
1802
1827
|
/**
|
|
1803
1828
|
* Resolves when this peer can exchange data: immediately for the host, once the link to the host is open for a
|
|
1804
1829
|
* 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).
|
|
1830
|
+
* (or with "closed"/"kicked" when the lobby ends first; "closed" also when the host disconnected it).
|
|
1806
1831
|
*/
|
|
1807
1832
|
ready = new Promise((res, rej) => {
|
|
1808
1833
|
this.markReady = () => {
|
|
@@ -1930,10 +1955,32 @@ var OneLobby = (() => {
|
|
|
1930
1955
|
this.signaling.send({ t: "meta", meta });
|
|
1931
1956
|
this.emit("metaChanged", this.meta);
|
|
1932
1957
|
}
|
|
1933
|
-
|
|
1958
|
+
/**
|
|
1959
|
+
* Host: removes a player. By default it is a ban: the hub refuses that player's address for the rest of this lobby
|
|
1960
|
+
* (a new tab or a reload included). `{ ban: false }` is `disconnect()`. `code` / `reason` reach the player's
|
|
1961
|
+
* `asWebSocket()` onclose (and `sessionEnded`'s second argument); without them its socket closes with 4000 "kicked".
|
|
1962
|
+
*/
|
|
1963
|
+
async kick(peerId, opts = {}) {
|
|
1934
1964
|
if (!this.isHost) throw new Error("only the host can kick");
|
|
1935
|
-
|
|
1936
|
-
|
|
1965
|
+
const { ban = true, code, reason } = opts;
|
|
1966
|
+
if (code !== void 0 && !(code === 1e3 || Number.isInteger(code) && code >= 3e3 && code <= 4999))
|
|
1967
|
+
throw new RangeError("close code must be 1000 or 3000-4999");
|
|
1968
|
+
if (reason !== void 0 && reason.length > 120) throw new RangeError("close reason must be at most 120 characters");
|
|
1969
|
+
const msg = { t: "kick", peerId };
|
|
1970
|
+
if (!ban) msg.ban = false;
|
|
1971
|
+
if (code !== void 0) msg.closeCode = code;
|
|
1972
|
+
if (reason !== void 0) msg.closeReason = reason;
|
|
1973
|
+
this.signaling.send(msg);
|
|
1974
|
+
const close = code !== void 0 || reason !== void 0 ? { code, reason } : void 0;
|
|
1975
|
+
this.removePeer(peerId, ban ? "kicked" : "disconnected", close, true);
|
|
1976
|
+
}
|
|
1977
|
+
/**
|
|
1978
|
+
* Host: ends one player's connection without a ban, like a server closing one WebSocket. The player's
|
|
1979
|
+
* `asWebSocket()` gets onclose with `code` (default 1000) and `reason` (default "disconnected"), its lobby ends with
|
|
1980
|
+
* `sessionEnded("disconnected")`, and it may join again (a new tab, a reload). Same as `kick(peerId, { ban: false, code, reason })`.
|
|
1981
|
+
*/
|
|
1982
|
+
disconnect(peerId, code, reason) {
|
|
1983
|
+
return this.kick(peerId, { ban: false, code, reason });
|
|
1937
1984
|
}
|
|
1938
1985
|
async approve(peerId, ok) {
|
|
1939
1986
|
if (!this.isHost) throw new Error("only the host can approve joins");
|
|
@@ -2334,8 +2381,13 @@ var OneLobby = (() => {
|
|
|
2334
2381
|
void this.onSignal(Number(m.from), m);
|
|
2335
2382
|
break;
|
|
2336
2383
|
case "error":
|
|
2337
|
-
if (m.code === "kicked")
|
|
2338
|
-
|
|
2384
|
+
if (m.code === "kicked") {
|
|
2385
|
+
const d = m.data ?? {};
|
|
2386
|
+
const close = {};
|
|
2387
|
+
if (typeof d.closeCode === "number") close.code = d.closeCode;
|
|
2388
|
+
if (typeof d.closeReason === "string") close.reason = d.closeReason;
|
|
2389
|
+
this.teardown(d.ban === false ? "disconnected" : "kicked", close.code !== void 0 || close.reason !== void 0 ? close : void 0);
|
|
2390
|
+
} else this.reportError(new OneLobbyError(String(m.code), String(m.msg ?? ""), m.data));
|
|
2339
2391
|
break;
|
|
2340
2392
|
}
|
|
2341
2393
|
}
|
|
@@ -2350,7 +2402,12 @@ var OneLobby = (() => {
|
|
|
2350
2402
|
if (s.ice) payload.ice = s.ice;
|
|
2351
2403
|
await link.handleSignal(payload);
|
|
2352
2404
|
}
|
|
2353
|
-
|
|
2405
|
+
/**
|
|
2406
|
+
* `linger` (host kick / disconnect): stop using the peer's link at once but leave it open until that peer closes it
|
|
2407
|
+
* or KICK_LINGER_MS pass. The hub tells the peer over its WebSocket; closing the data link first would make it look
|
|
2408
|
+
* like the host vanished (a migration and a 1001 instead of the host's close code).
|
|
2409
|
+
*/
|
|
2410
|
+
removePeer(peerId, reason, close, linger = false) {
|
|
2354
2411
|
const peer = this.roster.get(peerId);
|
|
2355
2412
|
if (peerId === this.localPeerId) {
|
|
2356
2413
|
this.teardown(reason === "kicked" ? "kicked" : reason);
|
|
@@ -2359,11 +2416,23 @@ var OneLobby = (() => {
|
|
|
2359
2416
|
if (peer) this.roster.delete(peerId);
|
|
2360
2417
|
const link = this.links.get(peerId);
|
|
2361
2418
|
if (link && peerId !== this.hostPeerId) {
|
|
2362
|
-
link.onClose = null;
|
|
2363
2419
|
link.onMessage = null;
|
|
2364
|
-
link.close();
|
|
2365
2420
|
this.links.delete(peerId);
|
|
2366
2421
|
this.hiSentTo.delete(link);
|
|
2422
|
+
if (linger) {
|
|
2423
|
+
this.lingering.add(link);
|
|
2424
|
+
const done = () => {
|
|
2425
|
+
if (!this.lingering.delete(link)) return;
|
|
2426
|
+
cancel();
|
|
2427
|
+
link.onClose = null;
|
|
2428
|
+
link.close();
|
|
2429
|
+
};
|
|
2430
|
+
const cancel = this.cfg.clock.setTimeout(done, KICK_LINGER_MS);
|
|
2431
|
+
link.onClose = done;
|
|
2432
|
+
} else {
|
|
2433
|
+
link.onClose = null;
|
|
2434
|
+
link.close();
|
|
2435
|
+
}
|
|
2367
2436
|
}
|
|
2368
2437
|
this.connected.delete(peerId);
|
|
2369
2438
|
this.outbox.delete(peerId);
|
|
@@ -2372,14 +2441,15 @@ var OneLobby = (() => {
|
|
|
2372
2441
|
this.lockstep.onPeerLeft(peerId);
|
|
2373
2442
|
this.chat.onPeerLeft(peerId);
|
|
2374
2443
|
this.voice.onPeerLeft(peerId);
|
|
2375
|
-
if (peer) this.emit("peerLeft", peer, reason);
|
|
2444
|
+
if (peer) this.emit("peerLeft", peer, reason, close);
|
|
2376
2445
|
if (peerId === this.hostPeerId && !this.isHost) {
|
|
2377
2446
|
this.handleHostLost(reason);
|
|
2378
2447
|
}
|
|
2379
2448
|
}
|
|
2449
|
+
/** Lockstep drops (stalled, desynced): removed without a ban, so the player can come back (a rejoin resyncs). */
|
|
2380
2450
|
dropPeer(peerId, reason) {
|
|
2381
|
-
this.signaling.send({ t: "kick", peerId });
|
|
2382
|
-
this.removePeer(peerId, reason);
|
|
2451
|
+
this.signaling.send({ t: "kick", peerId, ban: false, closeReason: reason });
|
|
2452
|
+
this.removePeer(peerId, reason, void 0, true);
|
|
2383
2453
|
}
|
|
2384
2454
|
setPhase(p) {
|
|
2385
2455
|
if (p !== "lobby" && p !== "in_game") return;
|
|
@@ -2430,7 +2500,7 @@ var OneLobby = (() => {
|
|
|
2430
2500
|
if (this.ended || this.isHost) return;
|
|
2431
2501
|
this.sendTo(this.hostPeerId, { type: "ping", n: ++this.pingN, t: this.cfg.clock.now() >>> 0 }, 0, "fast");
|
|
2432
2502
|
}
|
|
2433
|
-
teardown(reason) {
|
|
2503
|
+
teardown(reason, close) {
|
|
2434
2504
|
if (this.ended) return;
|
|
2435
2505
|
this.ended = true;
|
|
2436
2506
|
this.cancelReadyTimer?.();
|
|
@@ -2442,16 +2512,17 @@ var OneLobby = (() => {
|
|
|
2442
2512
|
this.cancelPing?.();
|
|
2443
2513
|
this.lockstep.stop();
|
|
2444
2514
|
this.voice.close();
|
|
2445
|
-
for (const l of this.links.values()) {
|
|
2515
|
+
for (const l of [...this.links.values(), ...this.lingering]) {
|
|
2446
2516
|
l.onClose = null;
|
|
2447
2517
|
l.onMessage = null;
|
|
2448
2518
|
l.close();
|
|
2449
2519
|
}
|
|
2450
2520
|
this.links.clear();
|
|
2521
|
+
this.lingering.clear();
|
|
2451
2522
|
this.signaling.onClose = null;
|
|
2452
2523
|
this.signaling.onMessage = null;
|
|
2453
2524
|
this.signaling.close();
|
|
2454
|
-
this.emit("sessionEnded", reason);
|
|
2525
|
+
this.emit("sessionEnded", reason, close);
|
|
2455
2526
|
}
|
|
2456
2527
|
/** Total in-flight links (diagnostics and tests). */
|
|
2457
2528
|
get linkCount() {
|
|
@@ -2761,6 +2832,29 @@ registerProcessor('onelobby-capture', OneLobbyCapture);
|
|
|
2761
2832
|
}
|
|
2762
2833
|
return crc;
|
|
2763
2834
|
}
|
|
2835
|
+
function parseIPv4(ip) {
|
|
2836
|
+
const parts = ip.split(".");
|
|
2837
|
+
if (parts.length !== 4) return null;
|
|
2838
|
+
let n = 0;
|
|
2839
|
+
for (const p of parts) {
|
|
2840
|
+
if (!/^\d{1,3}$/.test(p) || Number(p) > 255) return null;
|
|
2841
|
+
n = n * 256 + Number(p);
|
|
2842
|
+
}
|
|
2843
|
+
return n;
|
|
2844
|
+
}
|
|
2845
|
+
function encodeJoinCode(ipv4, port) {
|
|
2846
|
+
const ip = parseIPv4(ipv4);
|
|
2847
|
+
if (ip === null) throw new Error(`not an IPv4 address: ${ipv4}`);
|
|
2848
|
+
if (!Number.isInteger(port) || port < 1 || port > 65535) throw new Error(`bad port ${port}`);
|
|
2849
|
+
const data = BigInt(ip) << 16n | BigInt(port);
|
|
2850
|
+
let v = data << 5n | BigInt(crc5(data, 50));
|
|
2851
|
+
let s = "";
|
|
2852
|
+
for (let i = 0; i < JOIN_CODE_LENGTH; i++) {
|
|
2853
|
+
s = CROCKFORD_ALPHABET[Number(v & 31n)] + s;
|
|
2854
|
+
v >>= 5n;
|
|
2855
|
+
}
|
|
2856
|
+
return `${s.slice(0, 4)}-${s.slice(4, 8)}-${s.slice(8)}`;
|
|
2857
|
+
}
|
|
2764
2858
|
function normalizeJoinCode(input) {
|
|
2765
2859
|
const s = input.replace(/[ \-]/g, "").replace(/[a-z]/g, (c) => c.toUpperCase()).replace(/[IL]/g, "1").replace(/O/g, "0");
|
|
2766
2860
|
if (s.length !== JOIN_CODE_LENGTH) return null;
|
|
@@ -2836,7 +2930,7 @@ registerProcessor('onelobby-capture', OneLobbyCapture);
|
|
|
2836
2930
|
|
|
2837
2931
|
// src/onelobby.ts
|
|
2838
2932
|
var LIB_NAME = "onelobby";
|
|
2839
|
-
var LIB_VERSION = "0.2.
|
|
2933
|
+
var LIB_VERSION = "0.2.1";
|
|
2840
2934
|
var DEFAULT_READY_TIMEOUT_MS = 15e3;
|
|
2841
2935
|
function hasMic() {
|
|
2842
2936
|
return typeof navigator !== "undefined" && !!navigator.mediaDevices?.getUserMedia && typeof AudioWorkletNode !== "undefined";
|
|
@@ -3080,7 +3174,9 @@ registerProcessor('onelobby-capture', OneLobbyCapture);
|
|
|
3080
3174
|
const { sig, welcome } = await this.connect(addr.url, this.socketFactory, opts.connectTimeoutMs ?? 5e3);
|
|
3081
3175
|
try {
|
|
3082
3176
|
const state = await sig.request({ t: "join", ...opts.password ? { password: opts.password } : {} }, "lobby");
|
|
3083
|
-
|
|
3177
|
+
const lobby = this.makeLobby(sig, welcome, state, { links });
|
|
3178
|
+
lobby.joinCode = joinedWith(addr);
|
|
3179
|
+
return lobby;
|
|
3084
3180
|
} catch (e) {
|
|
3085
3181
|
sig.close();
|
|
3086
3182
|
throw e;
|
|
@@ -3139,6 +3235,14 @@ registerProcessor('onelobby-capture', OneLobbyCapture);
|
|
|
3139
3235
|
}
|
|
3140
3236
|
}
|
|
3141
3237
|
};
|
|
3238
|
+
function joinedWith(addr) {
|
|
3239
|
+
if (addr.isIPv6) return addr.display;
|
|
3240
|
+
try {
|
|
3241
|
+
return encodeJoinCode(addr.host, addr.port);
|
|
3242
|
+
} catch {
|
|
3243
|
+
return addr.display;
|
|
3244
|
+
}
|
|
3245
|
+
}
|
|
3142
3246
|
|
|
3143
3247
|
// src/friendly.ts
|
|
3144
3248
|
var TEXT = {
|