onelobby 0.1.0 → 0.2.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/README.md +2 -2
- package/dist/direct/index.d.ts +3 -1
- package/dist/direct/index.js +1 -1
- package/dist/direct/index.js.map +1 -1
- package/dist/index.d.ts +4 -1
- package/dist/index.js +3 -0
- package/dist/index.js.map +1 -1
- package/dist/lobby.d.ts +33 -0
- package/dist/lobby.js +62 -0
- package/dist/lobby.js.map +1 -1
- package/dist/onelobby.bundle.mjs +5 -5
- package/dist/onelobby.bundle.mjs.map +4 -4
- package/dist/onelobby.d.ts +12 -3
- package/dist/onelobby.global.js +186 -19
- package/dist/onelobby.global.js.map +4 -4
- package/dist/onelobby.global.min.js +5 -5
- package/dist/onelobby.global.min.js.map +4 -4
- package/dist/onelobby.js +7 -4
- package/dist/onelobby.js.map +1 -1
- package/dist/socket.d.ts +63 -0
- package/dist/socket.js +142 -0
- package/dist/socket.js.map +1 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -9,7 +9,7 @@ You need a OneLobby lobby server (the WebSocket URL, like `wss://lobby.example.c
|
|
|
9
9
|
## Use it from a `<script>` tag (no build tools)
|
|
10
10
|
|
|
11
11
|
```html
|
|
12
|
-
<script src="https://cdn.jsdelivr.net/npm/onelobby@0.
|
|
12
|
+
<script src="https://cdn.jsdelivr.net/npm/onelobby@0.2.0/dist/onelobby.global.min.js"></script>
|
|
13
13
|
<script>
|
|
14
14
|
const kit = new OneLobby.OneLobby({
|
|
15
15
|
gameId: "com.you.yourgame", // any unique name for your game
|
|
@@ -51,7 +51,7 @@ const lobby = await kit.host({ name: "My game", maxPlayers: 4 });
|
|
|
51
51
|
console.log("Tell your friend this code:", formatCode(lobby.code));
|
|
52
52
|
```
|
|
53
53
|
|
|
54
|
-
Without a bundler: `import { OneLobby } from "https://cdn.jsdelivr.net/npm/onelobby@0.
|
|
54
|
+
Without a bundler: `import { OneLobby } from "https://cdn.jsdelivr.net/npm/onelobby@0.2.0/dist/onelobby.bundle.mjs"`.
|
|
55
55
|
|
|
56
56
|
## Made for players
|
|
57
57
|
|
package/dist/direct/index.d.ts
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
* `import { RTCPeerConnection } from "node-datachannel/polyfill"`. Its libdatachannel honours the port pin.
|
|
10
10
|
*/
|
|
11
11
|
import type { OneLobby } from "../onelobby.js";
|
|
12
|
-
import type { Lobby } from "../lobby.js";
|
|
12
|
+
import type { Lobby, RateLimit } from "../lobby.js";
|
|
13
13
|
import { DirectHubHost, type HttpHandler } from "./hub-host.js";
|
|
14
14
|
import { type PortMapperOptions } from "./portmap.js";
|
|
15
15
|
export { DirectHubHost, LocalHubSocket, PortInUseError } from "./hub-host.js";
|
|
@@ -38,6 +38,8 @@ export interface DirectHostOptions {
|
|
|
38
38
|
* `kit.joinDirect(location.host)`; the lobby's `joinUrl` is then the link to share.
|
|
39
39
|
*/
|
|
40
40
|
http?: HttpHandler;
|
|
41
|
+
/** Per-joiner cap on incoming raw messages, e.g. `{ messages: 60, perMs: 1000 }`; the excess is dropped. */
|
|
42
|
+
rateLimit?: RateLimit;
|
|
41
43
|
}
|
|
42
44
|
/** A direct host lobby: the normal Lobby, plus the hub behind it. Leaving or ending the lobby stops the hub. */
|
|
43
45
|
export type DirectLobby = Lobby & {
|
package/dist/direct/index.js
CHANGED
|
@@ -40,7 +40,7 @@ export async function hostDirect(kit, o = {}) {
|
|
|
40
40
|
let lobby;
|
|
41
41
|
try {
|
|
42
42
|
const name = o.name ?? `${kit.options.displayName || "Player"}'s game`;
|
|
43
|
-
const h = { name, maxPlayers, lateJoin: o.lateJoin ?? true, meta: o.meta };
|
|
43
|
+
const h = { name, maxPlayers, lateJoin: o.lateJoin ?? true, meta: o.meta, rateLimit: o.rateLimit };
|
|
44
44
|
lobby = await kit.hostDirectWith(h, { socketFactory: () => hub.connectLocal(), wrapLinks });
|
|
45
45
|
}
|
|
46
46
|
catch (e) {
|
package/dist/direct/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/direct/index.ts"],"names":[],"mappings":"AAcA,OAAO,EAAE,cAAc,EAAE,MAAM,gBAAgB,CAAC;AAChD,OAAO,EAAE,mBAAmB,EAAE,MAAM,sBAAsB,CAAC;AAC3D,OAAO,EAAE,aAAa,EAAE,cAAc,EAAoB,MAAM,eAAe,CAAC;AAChF,OAAO,EAAE,gBAAgB,EAAE,MAAM,UAAU,CAAC;AAC5C,OAAO,EAAE,YAAY,EAAE,OAAO,EAAE,cAAc,EAA8C,MAAM,cAAc,CAAC;AACjH,OAAO,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAC;AAEhD,OAAO,EAAE,aAAa,EAAE,cAAc,EAAE,cAAc,EAAE,MAAM,eAAe,CAAC;AAE9E,OAAO,EAAE,YAAY,EAAE,OAAO,EAAE,UAAU,EAAE,QAAQ,EAAE,iBAAiB,EAAE,gBAAgB,EAAE,KAAK,EAAE,MAAM,UAAU,CAAC;AAEnH,OAAO,EAAE,UAAU,EAAE,YAAY,EAAE,OAAO,EAAE,cAAc,EAAE,MAAM,cAAc,CAAC;AAEjF,OAAO,EAAE,oBAAoB,EAAE,MAAM,cAAc,CAAC;
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/direct/index.ts"],"names":[],"mappings":"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","sourcesContent":["/// <reference types=\"node\" />\n/**\n * `onelobby/direct` (Node only): host a direct-IP lobby from a Node process, with no server of anyone's\n * (docs/direct-ip.md). The process runs the signaling hub, listens on TCP `port`, binds its peer links to UDP `port`,\n * asks the router to forward both (UPnP, then NAT-PMP) and hands out a join code. Joiners use `kit.joinDirect(code)`\n * from `onelobby` (Node or browser) or `JoinDirectAsync` in C#; the wire protocol is the same, so they mix.\n *\n * WebRTC in Node needs an RTCPeerConnection: `npm install node-datachannel`, then\n * `advanced: { linkFactory: webRtcLinkFactoryFor(RTCPeerConnection) }` with\n * `import { RTCPeerConnection } from \"node-datachannel/polyfill\"`. Its libdatachannel honours the port pin.\n */\nimport type { HostOptions, OneLobby } from \"../onelobby.js\";\nimport type { Lobby, RateLimit } from \"../lobby.js\";\nimport type { LinkFactory } from \"../transport.js\";\nimport { encodeJoinCode } from \"../joincode.js\";\nimport { DEFAULT_DIRECT_PORT } from \"../direct-address.js\";\nimport { DirectHubHost, PortInUseError, type HttpHandler } from \"./hub-host.js\";\nimport { directHubOptions } from \"./hub.js\";\nimport { isPublicIPv4, lanIPv4, localAddresses, type PortMapResult, type PortMapperOptions } from \"./portmap.js\";\nimport { OneLobbyError } from \"../signaling.js\";\n\nexport { DirectHubHost, LocalHubSocket, PortInUseError } from \"./hub-host.js\";\nexport type { HttpHandler, HttpReply } from \"./hub-host.js\";\nexport { SignalingHub, Session, RateWindow, sanitize, defaultHubOptions, directHubOptions, PROTO } from \"./hub.js\";\nexport type { HubOptions, HubDeps } from \"./hub.js\";\nexport { PortMapper, isPublicIPv4, lanIPv4, localAddresses } from \"./portmap.js\";\nexport type { PortMapResult, PortMapperOptions, PortMapMethod } from \"./portmap.js\";\nexport { webRtcLinkFactoryFor } from \"../webrtc.js\";\n\nexport interface DirectHostOptions {\n /** TCP and UDP port to listen on (default 47810). */\n port?: number;\n /** Optional; joiners must send the same one or get `bad_password`. */\n password?: string;\n maxPlayers?: number;\n /** Lobby name; default \"<displayName>'s game\". */\n name?: string;\n lateJoin?: boolean;\n meta?: Record<string, string>;\n /** Ask the router to forward the port so the code works from the internet (s.2a). The host-menu toggle; default on. */\n openPort?: boolean;\n /** Test seam and knobs for the router request (description defaults to \"OneLobby <gameId>\"). */\n portMapping?: PortMapperOptions;\n /**\n * Serves plain-HTTP `GET`s on the same port, typically a join page for browsers (recipe s.4b). The page joins with\n * `kit.joinDirect(location.host)`; the lobby's `joinUrl` is then the link to share.\n */\n http?: HttpHandler;\n /** Per-joiner cap on incoming raw messages, e.g. `{ messages: 60, perMs: 1000 }`; the excess is dropped. */\n rateLimit?: RateLimit;\n}\n\n/** A direct host lobby: the normal Lobby, plus the hub behind it. Leaving or ending the lobby stops the hub. */\nexport type DirectLobby = Lobby & {\n directHub: DirectHubHost;\n /** With the `http` option: `http://<the code's address>:<port>/`, the link a browser opens to join. Else null. */\n joinUrl: string | null;\n};\n\n/**\n * Hosts a direct-IP lobby (docs/direct-ip.md s.3): starts the hub and listener on `port`, opens the router port\n * (unless `openPort: false`), connects `kit` to the hub in memory and creates the lobby. Resolves with the lobby;\n * show `lobby.joinCode` with `lobby.joinCodeKind` (\"internet\" or \"same_network\"). Rejects with `port_in_use`.\n * When the lobby ends (leave, or the process exits) the listener closes and the router mapping is removed; joiners\n * get `sessionEnded(\"host_left\")`.\n */\nexport async function hostDirect(kit: OneLobby, o: DirectHostOptions = {}): Promise<DirectLobby> {\n const port = o.port ?? DEFAULT_DIRECT_PORT;\n const maxPlayers = o.maxPlayers ?? 4;\n let hub: DirectHubHost;\n try {\n hub = await DirectHubHost.listen(port, directHubOptions(maxPlayers, o.password));\n hub.http = o.http ?? null;\n } catch (e) {\n if (e instanceof PortInUseError) throw new OneLobbyError(\"port_in_use\", e.message.replace(/^port_in_use: /, \"\"));\n throw e;\n }\n // The router request runs alongside lobby creation; it takes up to a few seconds when nothing answers.\n const mapping: Promise<PortMapResult | null> =\n o.openPort === false\n ? Promise.resolve(null)\n : hub.openPort({ description: `OneLobby ${kit.options.gameId}`, ...o.portMapping }).catch((e: Error) => ({\n mapped: false, method: \"none\" as const, externalIPv4: null, localIPv4: null, detail: e.message,\n }));\n const wrapLinks = (inner: LinkFactory): LinkFactory => ({\n create: (lo) => inner.create({ ...lo, iceServers: [], portRange: { begin: hub.port, end: hub.port } }),\n });\n let lobby: Lobby;\n try {\n const name = o.name ?? `${kit.options.displayName || \"Player\"}'s game`;\n const h: HostOptions = { name, maxPlayers, lateJoin: o.lateJoin ?? true, meta: o.meta, rateLimit: o.rateLimit };\n lobby = await kit.hostDirectWith(h, { socketFactory: () => hub.connectLocal(), wrapLinks });\n } catch (e) {\n await mapping;\n await hub.close();\n throw e;\n }\n lobby.on(\"sessionEnded\", () => void hub.close());\n const map = await mapping;\n lobby.directAddresses = localAddresses(hub.port);\n lobby.portMapping = map ? map.detail : \"off\";\n let codeIp: string | null = null;\n if (map?.mapped && isPublicIPv4(map.externalIPv4)) {\n codeIp = map.externalIPv4!;\n lobby.joinCode = encodeJoinCode(codeIp, hub.port);\n lobby.joinCodeKind = \"internet\";\n } else {\n codeIp = map?.localIPv4 ?? (await lanIPv4());\n if (codeIp) {\n lobby.joinCode = encodeJoinCode(codeIp, hub.port);\n lobby.joinCodeKind = \"same_network\";\n }\n }\n const joinUrl = o.http && codeIp ? `http://${codeIp}:${hub.port}/` : null;\n return Object.assign(lobby, { directHub: hub, joinUrl });\n}\n"]}
|
package/dist/index.d.ts
CHANGED
|
@@ -8,7 +8,10 @@ export type { OneLobbyOptions, HostOptions, ListOptions, OneLobbyEvents, JoinDir
|
|
|
8
8
|
export { DEFAULT_READY_TIMEOUT_MS } from "./onelobby.js";
|
|
9
9
|
export type { AdvancedOptions, SeatStorage } from "./onelobby.js";
|
|
10
10
|
export { Lobby } from "./lobby.js";
|
|
11
|
-
export type { LobbyEvents, LobbyConfig, Peer, Phase } from "./lobby.js";
|
|
11
|
+
export type { LobbyEvents, LobbyConfig, Peer, Phase, RateLimit } from "./lobby.js";
|
|
12
|
+
export { MIN_PEER_TIMEOUT_MS } from "./lobby.js";
|
|
13
|
+
export { closeCodeFor } from "./socket.js";
|
|
14
|
+
export type { LobbySocket, LobbySocketOptions } from "./socket.js";
|
|
12
15
|
export { Lockstep } from "./lockstep.js";
|
|
13
16
|
export type { Frame, DesyncInfo, LockstepEvents } from "./lockstep.js";
|
|
14
17
|
export { Chat, CHAT_MAX_BYTES } from "./chat.js";
|
package/dist/index.js
CHANGED
|
@@ -6,6 +6,9 @@
|
|
|
6
6
|
export { OneLobby, LIB_NAME, LIB_VERSION } from "./onelobby.js";
|
|
7
7
|
export { DEFAULT_READY_TIMEOUT_MS } from "./onelobby.js";
|
|
8
8
|
export { Lobby } from "./lobby.js";
|
|
9
|
+
export { MIN_PEER_TIMEOUT_MS } from "./lobby.js";
|
|
10
|
+
// WebSocket-shaped sockets over a lobby: lobby.asWebSocket() (joiner), lobby.onConnection(conn => ...) (host).
|
|
11
|
+
export { closeCodeFor } from "./socket.js";
|
|
9
12
|
export { Lockstep } from "./lockstep.js";
|
|
10
13
|
export { Chat, CHAT_MAX_BYTES } from "./chat.js";
|
|
11
14
|
export { Voice } from "./voice.js";
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AACH,OAAO,EAAE,QAAQ,EAAE,QAAQ,EAAE,WAAW,EAAE,MAAM,eAAe,CAAC;AAEhE,OAAO,EAAE,wBAAwB,EAAE,MAAM,eAAe,CAAC;AAEzD,OAAO,EAAE,KAAK,EAAE,MAAM,YAAY,CAAC;AAEnC,OAAO,EAAE,QAAQ,EAAE,MAAM,eAAe,CAAC;AAEzC,OAAO,EAAE,IAAI,EAAE,cAAc,EAAE,MAAM,WAAW,CAAC;AAEjD,OAAO,EAAE,KAAK,EAAE,MAAM,YAAY,CAAC;AAInC,OAAO,EAAE,sBAAsB,EAAE,MAAM,oBAAoB,CAAC;AAC5D,OAAO,EAAE,aAAa,EAAE,UAAU,EAAE,MAAM,gBAAgB,CAAC;AAC3D,iHAAiH;AACjH,8GAA8G;AAC9G,OAAO,EAAE,cAAc,EAAE,iBAAiB,EAAE,MAAM,eAAe,CAAC;AAElE,OAAO,EAAE,oBAAoB,EAAE,MAAM,aAAa,CAAC;AACnD,OAAO,EAAE,aAAa,EAAE,MAAM,gBAAgB,CAAC;AAK/C,OAAO,EAAE,aAAa,EAAE,QAAQ,EAAE,WAAW,EAAE,MAAM,eAAe,CAAC;AAErE,yEAAyE;AACzE,OAAO,EAAE,SAAS,EAAE,cAAc,EAAE,MAAM,iBAAiB,CAAC","sourcesContent":["/**\n * Public API of `onelobby`: create a `OneLobby`, `host()` / `join(code)` / `list()` lobbies, then use the\n * `Lobby`'s lockstep, raw messages, chat and voice. Lower-level pieces (codec, chunking, migration, jitter buffer,\n * WebRTC/signaling classes, clocks, the gridpaint demo game) live in `onelobby/internals`.\n */\nexport { OneLobby, LIB_NAME, LIB_VERSION } from \"./onelobby.js\";\nexport type { OneLobbyOptions, HostOptions, ListOptions, OneLobbyEvents, JoinDirectOptions } from \"./onelobby.js\";\nexport { DEFAULT_READY_TIMEOUT_MS } from \"./onelobby.js\";\nexport type { AdvancedOptions, SeatStorage } from \"./onelobby.js\";\nexport { Lobby } from \"./lobby.js\";\nexport type { LobbyEvents, LobbyConfig, Peer, Phase } from \"./lobby.js\";\nexport { Lockstep } from \"./lockstep.js\";\nexport type { Frame, DesyncInfo, LockstepEvents } from \"./lockstep.js\";\nexport { Chat, CHAT_MAX_BYTES } from \"./chat.js\";\nexport type { ChatMessage, ChatEvents } from \"./chat.js\";\nexport { Voice } from \"./voice.js\";\nexport type { VoiceEvents, VoiceDeps } from \"./voice.js\";\nexport type { VoiceMode } from \"./voice-core.js\";\nexport type { VoiceCodec, VoiceEncoderHandle, VoiceDecoderHandle, FrameSource, AudioSink, VoiceError } from \"./voice-types.js\";\nexport { webCodecsOpusAvailable } from \"./voice-browser.js\";\nexport { normalizeCode, formatCode } from \"./crockford.js\";\n// Direct IP (docs/direct-ip.md): kit.joinDirect(code); the code is the host's address in disguise. Hosting is in\n// onelobby/direct (Node). webRtcLinkFactoryFor gives Node joiners a WebRTC stack (node-datachannel/polyfill).\nexport { decodeJoinCode, looksLikeJoinCode } from \"./joincode.js\";\nexport type { DecodedJoinCode, JoinCodeError } from \"./joincode.js\";\nexport { webRtcLinkFactoryFor } from \"./webrtc.js\";\nexport { OneLobbyError } from \"./signaling.js\";\nexport type { LobbySummary, LobbyState, PeerInfo } from \"./signaling.js\";\n// Types for the seams in OneLobbyOptions (ChannelName also tags raw messages).\nexport type { PeerLink, LinkFactory, SocketFactory, SocketLike, IceServer, ChannelName } from \"./transport.js\";\nexport type { Clock } from \"./clock.js\";\nexport { describeError, shareUrl, codeFromUrl } from \"./friendly.js\";\nexport type { FriendlyError } from \"./friendly.js\";\n// Pre-rename names, kept for one version so existing games keep working.\nexport { Keepalive, KeepaliveError } from \"./deprecated.js\";\nexport type { KeepaliveOptions, KeepaliveEvents } from \"./deprecated.js\";\n"]}
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AACH,OAAO,EAAE,QAAQ,EAAE,QAAQ,EAAE,WAAW,EAAE,MAAM,eAAe,CAAC;AAEhE,OAAO,EAAE,wBAAwB,EAAE,MAAM,eAAe,CAAC;AAEzD,OAAO,EAAE,KAAK,EAAE,MAAM,YAAY,CAAC;AAEnC,OAAO,EAAE,mBAAmB,EAAE,MAAM,YAAY,CAAC;AACjD,+GAA+G;AAC/G,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAE3C,OAAO,EAAE,QAAQ,EAAE,MAAM,eAAe,CAAC;AAEzC,OAAO,EAAE,IAAI,EAAE,cAAc,EAAE,MAAM,WAAW,CAAC;AAEjD,OAAO,EAAE,KAAK,EAAE,MAAM,YAAY,CAAC;AAInC,OAAO,EAAE,sBAAsB,EAAE,MAAM,oBAAoB,CAAC;AAC5D,OAAO,EAAE,aAAa,EAAE,UAAU,EAAE,MAAM,gBAAgB,CAAC;AAC3D,iHAAiH;AACjH,8GAA8G;AAC9G,OAAO,EAAE,cAAc,EAAE,iBAAiB,EAAE,MAAM,eAAe,CAAC;AAElE,OAAO,EAAE,oBAAoB,EAAE,MAAM,aAAa,CAAC;AACnD,OAAO,EAAE,aAAa,EAAE,MAAM,gBAAgB,CAAC;AAK/C,OAAO,EAAE,aAAa,EAAE,QAAQ,EAAE,WAAW,EAAE,MAAM,eAAe,CAAC;AAErE,yEAAyE;AACzE,OAAO,EAAE,SAAS,EAAE,cAAc,EAAE,MAAM,iBAAiB,CAAC","sourcesContent":["/**\n * Public API of `onelobby`: create a `OneLobby`, `host()` / `join(code)` / `list()` lobbies, then use the\n * `Lobby`'s lockstep, raw messages, chat and voice. Lower-level pieces (codec, chunking, migration, jitter buffer,\n * WebRTC/signaling classes, clocks, the gridpaint demo game) live in `onelobby/internals`.\n */\nexport { OneLobby, LIB_NAME, LIB_VERSION } from \"./onelobby.js\";\nexport type { OneLobbyOptions, HostOptions, ListOptions, OneLobbyEvents, JoinDirectOptions } from \"./onelobby.js\";\nexport { DEFAULT_READY_TIMEOUT_MS } from \"./onelobby.js\";\nexport type { AdvancedOptions, SeatStorage } from \"./onelobby.js\";\nexport { Lobby } from \"./lobby.js\";\nexport type { LobbyEvents, LobbyConfig, Peer, Phase, RateLimit } from \"./lobby.js\";\nexport { MIN_PEER_TIMEOUT_MS } from \"./lobby.js\";\n// WebSocket-shaped sockets over a lobby: lobby.asWebSocket() (joiner), lobby.onConnection(conn => ...) (host).\nexport { closeCodeFor } from \"./socket.js\";\nexport type { LobbySocket, LobbySocketOptions } from \"./socket.js\";\nexport { Lockstep } from \"./lockstep.js\";\nexport type { Frame, DesyncInfo, LockstepEvents } from \"./lockstep.js\";\nexport { Chat, CHAT_MAX_BYTES } from \"./chat.js\";\nexport type { ChatMessage, ChatEvents } from \"./chat.js\";\nexport { Voice } from \"./voice.js\";\nexport type { VoiceEvents, VoiceDeps } from \"./voice.js\";\nexport type { VoiceMode } from \"./voice-core.js\";\nexport type { VoiceCodec, VoiceEncoderHandle, VoiceDecoderHandle, FrameSource, AudioSink, VoiceError } from \"./voice-types.js\";\nexport { webCodecsOpusAvailable } from \"./voice-browser.js\";\nexport { normalizeCode, formatCode } from \"./crockford.js\";\n// Direct IP (docs/direct-ip.md): kit.joinDirect(code); the code is the host's address in disguise. Hosting is in\n// onelobby/direct (Node). webRtcLinkFactoryFor gives Node joiners a WebRTC stack (node-datachannel/polyfill).\nexport { decodeJoinCode, looksLikeJoinCode } from \"./joincode.js\";\nexport type { DecodedJoinCode, JoinCodeError } from \"./joincode.js\";\nexport { webRtcLinkFactoryFor } from \"./webrtc.js\";\nexport { OneLobbyError } from \"./signaling.js\";\nexport type { LobbySummary, LobbyState, PeerInfo } from \"./signaling.js\";\n// Types for the seams in OneLobbyOptions (ChannelName also tags raw messages).\nexport type { PeerLink, LinkFactory, SocketFactory, SocketLike, IceServer, ChannelName } from \"./transport.js\";\nexport type { Clock } from \"./clock.js\";\nexport { describeError, shareUrl, codeFromUrl } from \"./friendly.js\";\nexport type { FriendlyError } from \"./friendly.js\";\n// Pre-rename names, kept for one version so existing games keep working.\nexport { Keepalive, KeepaliveError } from \"./deprecated.js\";\nexport type { KeepaliveOptions, KeepaliveEvents } from \"./deprecated.js\";\n"]}
|
package/dist/lobby.d.ts
CHANGED
|
@@ -6,6 +6,7 @@ import { Lockstep } from "./lockstep.js";
|
|
|
6
6
|
import { type LobbyState, type PeerInfo, type SignalingClient } from "./signaling.js";
|
|
7
7
|
import type { ChannelName, IceServer, LinkFactory, PeerLink } from "./transport.js";
|
|
8
8
|
import { Voice, type VoiceDeps } from "./voice.js";
|
|
9
|
+
import { type LobbySocket, type LobbySocketOptions } from "./socket.js";
|
|
9
10
|
export type Phase = "lobby" | "in_game";
|
|
10
11
|
export type Peer = PeerInfo;
|
|
11
12
|
export type LobbyEvents = {
|
|
@@ -46,7 +47,23 @@ export interface LobbyConfig {
|
|
|
46
47
|
* (link, peer_left, a host_changed from the hub as it shuts down, or the signaling socket) ends with "host_left".
|
|
47
48
|
*/
|
|
48
49
|
direct?: boolean;
|
|
50
|
+
/**
|
|
51
|
+
* Treat a peer as gone after this many ms without hearing anything from it (0 or absent: off, and a silent peer
|
|
52
|
+
* surfaces only when its link or signaling socket times out, about 30 s). Joiners ping the host every 2 s and the
|
|
53
|
+
* host answers, so the host drops a silent joiner (`peerLeft`, "timeout") and a joiner handles a silent host as
|
|
54
|
+
* lost (a direct lobby ends with "host_left"). Values under 6000 are raised to 6000.
|
|
55
|
+
*/
|
|
56
|
+
peerTimeoutMs?: number;
|
|
57
|
+
/** Host: per-peer cap on incoming raw messages; the excess is dropped before `message` fires or is relayed. */
|
|
58
|
+
rateLimit?: RateLimit;
|
|
59
|
+
}
|
|
60
|
+
/** At most `messages` raw messages per peer in any `perMs` window (a fixed window, reset every `perMs`). */
|
|
61
|
+
export interface RateLimit {
|
|
62
|
+
messages: number;
|
|
63
|
+
perMs: number;
|
|
49
64
|
}
|
|
65
|
+
/** The smallest `peerTimeoutMs` honoured: three missed pings. */
|
|
66
|
+
export declare const MIN_PEER_TIMEOUT_MS = 6000;
|
|
50
67
|
/** The name shown for a peer that chose none: "Player N", N = its seat (peer id, join order). */
|
|
51
68
|
export declare function seatName(p: PeerInfo): PeerInfo;
|
|
52
69
|
/** One joined lobby: roster, star of peer links, and the lockstep/chat/voice engines riding on them. */
|
|
@@ -96,6 +113,10 @@ export declare class Lobby extends Emitter<LobbyEvents> {
|
|
|
96
113
|
private isReady;
|
|
97
114
|
private cancelReadyTimer;
|
|
98
115
|
private relayRetry;
|
|
116
|
+
/** clock.now() of the last frame from each peer (peerTimeoutMs). */
|
|
117
|
+
private lastHeard;
|
|
118
|
+
/** Host: per-peer fixed window for rateLimit. */
|
|
119
|
+
private rate;
|
|
99
120
|
/**
|
|
100
121
|
* Resolves when this peer can exchange data: immediately for the host, once the link to the host is open for a
|
|
101
122
|
* joiner. Rejects with OneLobbyError("ice_failed") when the joiner cannot reach the host in `readyTimeoutMs`
|
|
@@ -106,6 +127,15 @@ export declare class Lobby extends Emitter<LobbyEvents> {
|
|
|
106
127
|
get isHost(): boolean;
|
|
107
128
|
get peers(): Peer[];
|
|
108
129
|
peer(peerId: number): Peer | undefined;
|
|
130
|
+
/** Peers whose link is open and handshake done (host: the ready joiners; joiner: the host once ready). */
|
|
131
|
+
get connectedPeers(): number[];
|
|
132
|
+
/**
|
|
133
|
+
* Joiner: the link to the host as a WebSocket-shaped object (text by default), so WebSocket client code runs
|
|
134
|
+
* unchanged. It opens when `ready` resolves and closes when the session ends (`host_left` gives code 1001).
|
|
135
|
+
*/
|
|
136
|
+
asWebSocket(opts?: LobbySocketOptions): LobbySocket;
|
|
137
|
+
/** Host: `handler(conn)` once per joiner (a WebSocket-shaped connection), the server-side mirror of asWebSocket(). */
|
|
138
|
+
onConnection(handler: (conn: LobbySocket) => void, opts?: LobbySocketOptions): () => void;
|
|
109
139
|
set snapshotChunkSize(n: number);
|
|
110
140
|
/** Current migration epoch (0 until the first host change). */
|
|
111
141
|
get hostEpoch(): number;
|
|
@@ -150,6 +180,7 @@ export declare class Lobby extends Emitter<LobbyEvents> {
|
|
|
150
180
|
private onHi;
|
|
151
181
|
private flush;
|
|
152
182
|
private onRaw;
|
|
183
|
+
private allowRaw;
|
|
153
184
|
private setHost;
|
|
154
185
|
/** The host is gone (link closed, no frames for 3 s, or the server said so): run the s.9 rule. */
|
|
155
186
|
private handleHostLost;
|
|
@@ -162,6 +193,8 @@ export declare class Lobby extends Emitter<LobbyEvents> {
|
|
|
162
193
|
private setPhase;
|
|
163
194
|
private onServerClosed;
|
|
164
195
|
private watchdog;
|
|
196
|
+
/** peerTimeoutMs: the host drops joiners it has not heard from; a joiner handles a silent host as lost. */
|
|
197
|
+
private dropSilent;
|
|
165
198
|
private pingHost;
|
|
166
199
|
private teardown;
|
|
167
200
|
/** Total in-flight links (diagnostics and tests). */
|
package/dist/lobby.js
CHANGED
|
@@ -6,6 +6,9 @@ import { Lockstep } from "./lockstep.js";
|
|
|
6
6
|
import { claimWinsWrapped, pickNewHost } from "./migration.js";
|
|
7
7
|
import { OneLobbyError } from "./signaling.js";
|
|
8
8
|
import { Voice } from "./voice.js";
|
|
9
|
+
import { hostConnections, joinerSocket } from "./socket.js";
|
|
10
|
+
/** The smallest `peerTimeoutMs` honoured: three missed pings. */
|
|
11
|
+
export const MIN_PEER_TIMEOUT_MS = 6000;
|
|
9
12
|
/** The name shown for a peer that chose none: "Player N", N = its seat (peer id, join order). */
|
|
10
13
|
export function seatName(p) {
|
|
11
14
|
const n = typeof p.name === "string" ? p.name.trim() : "";
|
|
@@ -66,6 +69,10 @@ export class Lobby extends Emitter {
|
|
|
66
69
|
isReady = false;
|
|
67
70
|
cancelReadyTimer = null;
|
|
68
71
|
relayRetry = false;
|
|
72
|
+
/** clock.now() of the last frame from each peer (peerTimeoutMs). */
|
|
73
|
+
lastHeard = new Map();
|
|
74
|
+
/** Host: per-peer fixed window for rateLimit. */
|
|
75
|
+
rate = new Map();
|
|
69
76
|
/**
|
|
70
77
|
* Resolves when this peer can exchange data: immediately for the host, once the link to the host is open for a
|
|
71
78
|
* joiner. Rejects with OneLobbyError("ice_failed") when the joiner cannot reach the host in `readyTimeoutMs`
|
|
@@ -145,6 +152,21 @@ export class Lobby extends Emitter {
|
|
|
145
152
|
peer(peerId) {
|
|
146
153
|
return this.roster.get(peerId);
|
|
147
154
|
}
|
|
155
|
+
/** Peers whose link is open and handshake done (host: the ready joiners; joiner: the host once ready). */
|
|
156
|
+
get connectedPeers() {
|
|
157
|
+
return this.connectedPeerIds();
|
|
158
|
+
}
|
|
159
|
+
/**
|
|
160
|
+
* Joiner: the link to the host as a WebSocket-shaped object (text by default), so WebSocket client code runs
|
|
161
|
+
* unchanged. It opens when `ready` resolves and closes when the session ends (`host_left` gives code 1001).
|
|
162
|
+
*/
|
|
163
|
+
asWebSocket(opts) {
|
|
164
|
+
return joinerSocket(this, opts);
|
|
165
|
+
}
|
|
166
|
+
/** Host: `handler(conn)` once per joiner (a WebSocket-shaped connection), the server-side mirror of asWebSocket(). */
|
|
167
|
+
onConnection(handler, opts) {
|
|
168
|
+
return hostConnections(this, handler, opts);
|
|
169
|
+
}
|
|
148
170
|
set snapshotChunkSize(n) {
|
|
149
171
|
this.lockstep.chunkSize = n;
|
|
150
172
|
}
|
|
@@ -400,6 +422,8 @@ export class Lobby extends Emitter {
|
|
|
400
422
|
}
|
|
401
423
|
if (!d)
|
|
402
424
|
return;
|
|
425
|
+
if (!reassembled)
|
|
426
|
+
this.lastHeard.set(from, this.cfg.clock.now());
|
|
403
427
|
const { msg, route } = d;
|
|
404
428
|
switch (msg.type) {
|
|
405
429
|
case "hi":
|
|
@@ -497,6 +521,8 @@ export class Lobby extends Emitter {
|
|
|
497
521
|
this.emit("message", route, bytes, channel);
|
|
498
522
|
return;
|
|
499
523
|
}
|
|
524
|
+
if (!this.allowRaw(from))
|
|
525
|
+
return;
|
|
500
526
|
if (route === 0 || route === this.localPeerId) {
|
|
501
527
|
this.emit("message", from, bytes, channel);
|
|
502
528
|
}
|
|
@@ -509,6 +535,16 @@ export class Lobby extends Emitter {
|
|
|
509
535
|
this.sendTo(route, { type: "raw", bytes }, from, channel);
|
|
510
536
|
}
|
|
511
537
|
}
|
|
538
|
+
allowRaw(from) {
|
|
539
|
+
const lim = this.cfg.rateLimit;
|
|
540
|
+
if (!lim || !(lim.messages > 0) || !(lim.perMs > 0))
|
|
541
|
+
return true;
|
|
542
|
+
const now = this.cfg.clock.now();
|
|
543
|
+
let w = this.rate.get(from);
|
|
544
|
+
if (!w || now - w.start >= lim.perMs)
|
|
545
|
+
this.rate.set(from, (w = { start: now, n: 0 }));
|
|
546
|
+
return ++w.n <= lim.messages;
|
|
547
|
+
}
|
|
512
548
|
// ---- migration -------------------------------------------------------------------------------------------------
|
|
513
549
|
setHost(hostId) {
|
|
514
550
|
if (hostId === this.hostPeerId)
|
|
@@ -549,6 +585,7 @@ export class Lobby extends Emitter {
|
|
|
549
585
|
this.markReady();
|
|
550
586
|
this.connected.clear();
|
|
551
587
|
this.hiSentTo.clear();
|
|
588
|
+
this.lastHeard.clear(); // joiners spoke to the old host; their clocks start when they say hi to us
|
|
552
589
|
this.lockstep.becomeHost();
|
|
553
590
|
this.emit("hostChanged", this.localPeerId);
|
|
554
591
|
if (this.serverUp)
|
|
@@ -694,6 +731,8 @@ export class Lobby extends Emitter {
|
|
|
694
731
|
}
|
|
695
732
|
this.connected.delete(peerId);
|
|
696
733
|
this.outbox.delete(peerId);
|
|
734
|
+
this.lastHeard.delete(peerId);
|
|
735
|
+
this.rate.delete(peerId);
|
|
697
736
|
this.lockstep.onPeerLeft(peerId);
|
|
698
737
|
this.chat.onPeerLeft(peerId);
|
|
699
738
|
this.voice.onPeerLeft(peerId);
|
|
@@ -732,6 +771,11 @@ export class Lobby extends Emitter {
|
|
|
732
771
|
}
|
|
733
772
|
// ---- housekeeping ----------------------------------------------------------------------------------------------
|
|
734
773
|
watchdog() {
|
|
774
|
+
if (this.ended)
|
|
775
|
+
return;
|
|
776
|
+
const timeout = this.cfg.peerTimeoutMs ?? 0;
|
|
777
|
+
if (timeout > 0)
|
|
778
|
+
this.dropSilent(Math.max(timeout, MIN_PEER_TIMEOUT_MS));
|
|
735
779
|
if (this.ended || this.isHost)
|
|
736
780
|
return;
|
|
737
781
|
if (this.mode === "lockstep" && this.lockstep.hostSilent()) {
|
|
@@ -740,6 +784,24 @@ export class Lobby extends Emitter {
|
|
|
740
784
|
this.handleHostLost("timeout");
|
|
741
785
|
}
|
|
742
786
|
}
|
|
787
|
+
/** peerTimeoutMs: the host drops joiners it has not heard from; a joiner handles a silent host as lost. */
|
|
788
|
+
dropSilent(timeout) {
|
|
789
|
+
const now = this.cfg.clock.now();
|
|
790
|
+
if (this.isHost) {
|
|
791
|
+
for (const id of [...this.connected]) {
|
|
792
|
+
const last = this.lastHeard.get(id);
|
|
793
|
+
if (last !== undefined && now - last > timeout)
|
|
794
|
+
this.removePeer(id, "timeout");
|
|
795
|
+
}
|
|
796
|
+
}
|
|
797
|
+
else if (this.isReady) {
|
|
798
|
+
const last = this.lastHeard.get(this.hostPeerId);
|
|
799
|
+
if (last !== undefined && now - last > timeout) {
|
|
800
|
+
this.lastHeard.delete(this.hostPeerId);
|
|
801
|
+
this.handleHostLost("timeout");
|
|
802
|
+
}
|
|
803
|
+
}
|
|
804
|
+
}
|
|
743
805
|
pingHost() {
|
|
744
806
|
if (this.ended || this.isHost)
|
|
745
807
|
return;
|