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 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.1.0/dist/onelobby.global.min.js"></script>
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.1.0/dist/onelobby.bundle.mjs"`.
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
 
@@ -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 & {
@@ -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) {
@@ -1 +1 @@
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 } 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}\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 };\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"]}
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;