onelobby 0.3.0 → 0.4.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/dist/friendly.js CHANGED
@@ -3,6 +3,38 @@
3
3
  * Nothing here talks to the network; it is safe to call from any UI code.
4
4
  */
5
5
  import { formatCode, normalizeCode } from "./crockford.js";
6
+ /**
7
+ * Every `code` a `OneLobbyError` can carry. Compare against these instead of string literals; C# has the shared ones in
8
+ * `LobbyErrorCodes`. `kicked` with `data.reason === "denied"` is a refused join request; `rate_limited` with
9
+ * `data.reason === "max_lobbies"` is the per-address lobby limit.
10
+ */
11
+ export const ErrorCodes = {
12
+ // from the lobby server or a direct host's hub
13
+ UnknownCode: "unknown_code",
14
+ BadCode: "bad_code",
15
+ LobbyFull: "lobby_full",
16
+ VersionMismatch: "version_mismatch",
17
+ InGame: "in_game",
18
+ Kicked: "kicked",
19
+ BadPassword: "bad_password",
20
+ NotHost: "not_host",
21
+ RateLimited: "rate_limited",
22
+ ServerFull: "server_full",
23
+ GameNotAllowed: "game_not_allowed",
24
+ BadRequest: "bad_request",
25
+ // from this client
26
+ ServerClosed: "server_closed",
27
+ IceFailed: "ice_failed",
28
+ Timeout: "timeout",
29
+ DirectUnreachable: "direct_unreachable",
30
+ BadAddress: "bad_address",
31
+ PortInUse: "port_in_use",
32
+ ConnectFailed: "connect_failed",
33
+ Closed: "closed",
34
+ ServerLost: "server_lost",
35
+ InvalidToken: "invalid_token",
36
+ NoServer: "no_server",
37
+ };
6
38
  const TEXT = {
7
39
  // server errors (spec/signaling.md)
8
40
  unknown_code: { message: "We couldn't find a lobby with that code.", hint: "Check the code with the host. Codes stop working once everyone leaves." },
@@ -1 +1 @@
1
- {"version":3,"file":"friendly.js","sourceRoot":"","sources":["../src/friendly.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Player-facing helpers: plain-language error text and shareable join links.\n * Nothing here talks to the network; it is safe to call from any UI code.\n */\nimport { formatCode, normalizeCode } from \"./crockford.js\";\n\nexport interface FriendlyError {\n /** Stable machine code (the OneLobbyError code, or \"unknown\"). */\n code: string;\n /** One short sentence a player can read. */\n message: string;\n /** What the player can do about it, when there is something. */\n hint?: string;\n}\n\ntype Entry = { message: string; hint?: string };\n\nconst TEXT: Record<string, Entry> = {\n // server errors (spec/signaling.md)\n unknown_code: { message: \"We couldn't find a lobby with that code.\", hint: \"Check the code with the host. Codes stop working once everyone leaves.\" },\n bad_code: { message: \"That doesn't look like a lobby code.\", hint: \"Codes are 6 letters and numbers, like 7HQ 4K2.\" },\n lobby_full: { message: \"That lobby is full.\", hint: \"Ask the host to make room, or start your own lobby.\" },\n version_mismatch: { message: \"You and the host are running different versions of the game.\", hint: \"Reload the page (or update the game) on both sides, then try again.\" },\n in_game: { message: \"That game has already started.\", hint: \"Ask the host to let people join mid-game, or wait for the next round.\" },\n kicked: { message: \"The host removed you from the lobby.\" },\n denied: { message: \"The host didn't let you in this time.\", hint: \"Ask the host to approve you, then try again.\" },\n not_host: { message: \"Only the host can do that.\" },\n rate_limited: { message: \"Slow down a little: too many tries in a short time.\", hint: \"Wait a moment, then try again.\" },\n max_lobbies: { message: \"You already have the most lobbies open that this server allows.\", hint: \"Close one of your other lobbies, then try again.\" },\n server_full: { message: \"The game server is full right now.\", hint: \"Try again in a few minutes.\" },\n game_not_allowed: { message: \"This game server doesn't host this game.\", hint: \"The game's server setting may be wrong; let the game's owner know.\" },\n bad_request: { message: \"The game sent something the server didn't understand.\", hint: \"Reload the page and try again. If it keeps happening, the game may need an update.\" },\n // client errors\n connect_failed: { message: \"Couldn't reach the game server.\", hint: \"Check your internet connection, then try again.\" },\n closed: { message: \"Lost the connection to the game server.\", hint: \"Check your internet connection, then try again.\" },\n server_lost: { message: \"Lost the connection to the game server.\", hint: \"Your game may keep going; new players can't join until it's back.\" },\n invalid_token: { message: \"Your old seat in that lobby is no longer available.\", hint: \"Join again with the lobby code.\" },\n ice_failed: { message: \"Couldn't connect to the other players.\", hint: \"Some networks (school, work, public Wi-Fi) block game connections. Try another network or a phone hotspot.\" },\n timeout: { message: \"The connection took too long.\", hint: \"Check your internet connection, then try again.\" },\n left: { message: \"You left the lobby.\" },\n unknown: { message: \"Something went wrong.\", hint: \"Try again. If it keeps happening, reload the page.\" },\n};\n\nconst CODE_RE = /^[a-z][a-z0-9_]*$/;\n\nfunction codeOf(err: unknown): { code: string | null; data?: Record<string, unknown> } {\n if (typeof err === \"string\") {\n const m = /^([a-z][a-z0-9_]*)(?::|$)/.exec(err.trim());\n return { code: m ? m[1] : null };\n }\n if (err && typeof err === \"object\") {\n const o = err as { code?: unknown; data?: unknown; message?: unknown; name?: unknown };\n const data = o.data && typeof o.data === \"object\" ? (o.data as Record<string, unknown>) : undefined;\n if (typeof o.code === \"string\" && CODE_RE.test(o.code)) return { code: o.code, data };\n if (typeof o.message === \"string\") {\n const m = /^([a-z][a-z0-9_]*):/.exec(o.message);\n if (m && TEXT[m[1]]) return { code: m[1], data };\n // browser network failures (fetch/WebSocket) surface as TypeError / NetworkError\n if (o.name === \"NetworkError\" || /network|failed to fetch|websocket/i.test(o.message)) return { code: \"connect_failed\" };\n if (o.name === \"TimeoutError\" || /timed? ?out/i.test(o.message)) return { code: \"timeout\" };\n }\n }\n return { code: null };\n}\n\n/**\n * Plain-language, player-facing text for any error the kit (or its server) can produce.\n * Never returns a stack trace or raw technical message; unknown errors get a generic friendly line.\n */\nexport function describeError(err: unknown): FriendlyError {\n const { code, data } = codeOf(err);\n if (!code) return { code: \"unknown\", ...TEXT.unknown };\n let key = code;\n if (code === \"kicked\" && data?.reason === \"denied\") key = \"denied\";\n if (code === \"rate_limited\" && data?.reason === \"max_lobbies\") key = \"max_lobbies\";\n const e = TEXT[key];\n if (!e) return { code, ...TEXT.unknown };\n const out: FriendlyError = { code, message: e.message };\n if (e.hint) out.hint = e.hint;\n if (code === \"rate_limited\" && typeof data?.retryAfterMs === \"number\" && key === \"rate_limited\") {\n const s = Math.max(1, Math.ceil(data.retryAfterMs / 1000));\n out.hint = `Wait ${s} second${s === 1 ? \"\" : \"s\"}, then try again.`;\n }\n return out;\n}\n\nfunction currentHref(): string | undefined {\n try {\n return typeof location !== \"undefined\" ? location.href : undefined;\n } catch {\n return undefined;\n }\n}\n\n/**\n * A link that opens the game straight into lobby `code`, e.g. `https://game.example/?join=7HQ-4K2`.\n * `base` defaults to the current page; any existing `param` is replaced. Throws on an invalid code.\n */\nexport function shareUrl(code: string, base?: string, param = \"join\"): string {\n const norm = normalizeCode(code);\n if (!norm) throw new Error(`\"${code}\" is not a valid lobby code`);\n const b = base ?? currentHref();\n if (!b) throw new Error(\"shareUrl needs a base URL outside the browser\");\n const u = new URL(b);\n u.searchParams.delete(param);\n // URLSearchParams would encode the display space as \"+\": use a hyphen, which normalizeCode also accepts\n u.searchParams.set(param, formatCode(norm).replace(\" \", \"-\"));\n return u.toString();\n}\n\n/** The lobby code carried by a share link (see shareUrl), or null when there is none or it is not a valid code. */\nexport function codeFromUrl(url?: string, param = \"join\"): string | null {\n const href = url ?? currentHref();\n if (!href) return null;\n let u: URL;\n try {\n u = new URL(href, \"http://localhost/\");\n } catch {\n return null;\n }\n const v = u.searchParams.get(param) ?? new URLSearchParams(u.hash.replace(/^#/, \"\")).get(param);\n return v ? normalizeCode(v) : null;\n}\n"]}
1
+ {"version":3,"file":"friendly.js","sourceRoot":"","sources":["../src/friendly.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Player-facing helpers: plain-language error text and shareable join links.\n * Nothing here talks to the network; it is safe to call from any UI code.\n */\nimport { formatCode, normalizeCode } from \"./crockford.js\";\n\nexport interface FriendlyError {\n /** Stable machine code (the OneLobbyError code, or \"unknown\"). */\n code: string;\n /** One short sentence a player can read. */\n message: string;\n /** What the player can do about it, when there is something. */\n hint?: string;\n}\n\ntype Entry = { message: string; hint?: string };\n\n/**\n * Every `code` a `OneLobbyError` can carry. Compare against these instead of string literals; C# has the shared ones in\n * `LobbyErrorCodes`. `kicked` with `data.reason === \"denied\"` is a refused join request; `rate_limited` with\n * `data.reason === \"max_lobbies\"` is the per-address lobby limit.\n */\nexport const ErrorCodes = {\n // from the lobby server or a direct host's hub\n UnknownCode: \"unknown_code\",\n BadCode: \"bad_code\",\n LobbyFull: \"lobby_full\",\n VersionMismatch: \"version_mismatch\",\n InGame: \"in_game\",\n Kicked: \"kicked\",\n BadPassword: \"bad_password\",\n NotHost: \"not_host\",\n RateLimited: \"rate_limited\",\n ServerFull: \"server_full\",\n GameNotAllowed: \"game_not_allowed\",\n BadRequest: \"bad_request\",\n // from this client\n ServerClosed: \"server_closed\",\n IceFailed: \"ice_failed\",\n Timeout: \"timeout\",\n DirectUnreachable: \"direct_unreachable\",\n BadAddress: \"bad_address\",\n PortInUse: \"port_in_use\",\n ConnectFailed: \"connect_failed\",\n Closed: \"closed\",\n ServerLost: \"server_lost\",\n InvalidToken: \"invalid_token\",\n NoServer: \"no_server\",\n} as const;\nexport type ErrorCode = (typeof ErrorCodes)[keyof typeof ErrorCodes];\n\nconst TEXT: Record<string, Entry> = {\n // server errors (spec/signaling.md)\n unknown_code: { message: \"We couldn't find a lobby with that code.\", hint: \"Check the code with the host. Codes stop working once everyone leaves.\" },\n bad_code: { message: \"That doesn't look like a lobby code.\", hint: \"Codes are 6 letters and numbers, like 7HQ 4K2.\" },\n lobby_full: { message: \"That lobby is full.\", hint: \"Ask the host to make room, or start your own lobby.\" },\n version_mismatch: { message: \"You and the host are running different versions of the game.\", hint: \"Reload the page (or update the game) on both sides, then try again.\" },\n in_game: { message: \"That game has already started.\", hint: \"Ask the host to let people join mid-game, or wait for the next round.\" },\n kicked: { message: \"The host removed you from the lobby.\" },\n denied: { message: \"The host didn't let you in this time.\", hint: \"Ask the host to approve you, then try again.\" },\n not_host: { message: \"Only the host can do that.\" },\n rate_limited: { message: \"Slow down a little: too many tries in a short time.\", hint: \"Wait a moment, then try again.\" },\n max_lobbies: { message: \"You already have the most lobbies open that this server allows.\", hint: \"Close one of your other lobbies, then try again.\" },\n server_full: { message: \"The game server is full right now.\", hint: \"Try again in a few minutes.\" },\n game_not_allowed: { message: \"This game server doesn't host this game.\", hint: \"The game's server setting may be wrong; let the game's owner know.\" },\n bad_request: { message: \"The game sent something the server didn't understand.\", hint: \"Reload the page and try again. If it keeps happening, the game may need an update.\" },\n // client errors\n connect_failed: { message: \"Couldn't reach the game server.\", hint: \"Check your internet connection, then try again.\" },\n closed: { message: \"Lost the connection to the game server.\", hint: \"Check your internet connection, then try again.\" },\n server_lost: { message: \"Lost the connection to the game server.\", hint: \"Your game may keep going; new players can't join until it's back.\" },\n invalid_token: { message: \"Your old seat in that lobby is no longer available.\", hint: \"Join again with the lobby code.\" },\n ice_failed: { message: \"Couldn't connect to the other players.\", hint: \"Some networks (school, work, public Wi-Fi) block game connections. Try another network or a phone hotspot.\" },\n timeout: { message: \"The connection took too long.\", hint: \"Check your internet connection, then try again.\" },\n left: { message: \"You left the lobby.\" },\n unknown: { message: \"Something went wrong.\", hint: \"Try again. If it keeps happening, reload the page.\" },\n};\n\nconst CODE_RE = /^[a-z][a-z0-9_]*$/;\n\nfunction codeOf(err: unknown): { code: string | null; data?: Record<string, unknown> } {\n if (typeof err === \"string\") {\n const m = /^([a-z][a-z0-9_]*)(?::|$)/.exec(err.trim());\n return { code: m ? m[1] : null };\n }\n if (err && typeof err === \"object\") {\n const o = err as { code?: unknown; data?: unknown; message?: unknown; name?: unknown };\n const data = o.data && typeof o.data === \"object\" ? (o.data as Record<string, unknown>) : undefined;\n if (typeof o.code === \"string\" && CODE_RE.test(o.code)) return { code: o.code, data };\n if (typeof o.message === \"string\") {\n const m = /^([a-z][a-z0-9_]*):/.exec(o.message);\n if (m && TEXT[m[1]]) return { code: m[1], data };\n // browser network failures (fetch/WebSocket) surface as TypeError / NetworkError\n if (o.name === \"NetworkError\" || /network|failed to fetch|websocket/i.test(o.message)) return { code: \"connect_failed\" };\n if (o.name === \"TimeoutError\" || /timed? ?out/i.test(o.message)) return { code: \"timeout\" };\n }\n }\n return { code: null };\n}\n\n/**\n * Plain-language, player-facing text for any error the kit (or its server) can produce.\n * Never returns a stack trace or raw technical message; unknown errors get a generic friendly line.\n */\nexport function describeError(err: unknown): FriendlyError {\n const { code, data } = codeOf(err);\n if (!code) return { code: \"unknown\", ...TEXT.unknown };\n let key = code;\n if (code === \"kicked\" && data?.reason === \"denied\") key = \"denied\";\n if (code === \"rate_limited\" && data?.reason === \"max_lobbies\") key = \"max_lobbies\";\n const e = TEXT[key];\n if (!e) return { code, ...TEXT.unknown };\n const out: FriendlyError = { code, message: e.message };\n if (e.hint) out.hint = e.hint;\n if (code === \"rate_limited\" && typeof data?.retryAfterMs === \"number\" && key === \"rate_limited\") {\n const s = Math.max(1, Math.ceil(data.retryAfterMs / 1000));\n out.hint = `Wait ${s} second${s === 1 ? \"\" : \"s\"}, then try again.`;\n }\n return out;\n}\n\nfunction currentHref(): string | undefined {\n try {\n return typeof location !== \"undefined\" ? location.href : undefined;\n } catch {\n return undefined;\n }\n}\n\n/**\n * A link that opens the game straight into lobby `code`, e.g. `https://game.example/?join=7HQ-4K2`.\n * `base` defaults to the current page; any existing `param` is replaced. Throws on an invalid code.\n */\nexport function shareUrl(code: string, base?: string, param = \"join\"): string {\n const norm = normalizeCode(code);\n if (!norm) throw new Error(`\"${code}\" is not a valid lobby code`);\n const b = base ?? currentHref();\n if (!b) throw new Error(\"shareUrl needs a base URL outside the browser\");\n const u = new URL(b);\n u.searchParams.delete(param);\n // URLSearchParams would encode the display space as \"+\": use a hyphen, which normalizeCode also accepts\n u.searchParams.set(param, formatCode(norm).replace(\" \", \"-\"));\n return u.toString();\n}\n\n/** The lobby code carried by a share link (see shareUrl), or null when there is none or it is not a valid code. */\nexport function codeFromUrl(url?: string, param = \"join\"): string | null {\n const href = url ?? currentHref();\n if (!href) return null;\n let u: URL;\n try {\n u = new URL(href, \"http://localhost/\");\n } catch {\n return null;\n }\n const v = u.searchParams.get(param) ?? new URLSearchParams(u.hash.replace(/^#/, \"\")).get(param);\n return v ? normalizeCode(v) : null;\n}\n"]}
package/dist/index.d.ts CHANGED
@@ -29,7 +29,7 @@ export { OneLobbyError } from "./signaling.js";
29
29
  export type { LobbySummary, LobbyState, PeerInfo } from "./signaling.js";
30
30
  export type { PeerLink, LinkFactory, SocketFactory, SocketLike, IceServer, ChannelName } from "./transport.js";
31
31
  export type { Clock } from "./clock.js";
32
- export { describeError, shareUrl, codeFromUrl } from "./friendly.js";
33
- export type { FriendlyError } from "./friendly.js";
32
+ export { describeError, shareUrl, codeFromUrl, ErrorCodes } from "./friendly.js";
33
+ export type { FriendlyError, ErrorCode } from "./friendly.js";
34
34
  export { Keepalive, KeepaliveError } from "./deprecated.js";
35
35
  export type { KeepaliveOptions, KeepaliveEvents } from "./deprecated.js";
package/dist/index.js CHANGED
@@ -19,7 +19,7 @@ export { normalizeCode, formatCode } from "./crockford.js";
19
19
  export { decodeJoinCode, looksLikeJoinCode } from "./joincode.js";
20
20
  export { webRtcLinkFactoryFor } from "./webrtc.js";
21
21
  export { OneLobbyError } from "./signaling.js";
22
- export { describeError, shareUrl, codeFromUrl } from "./friendly.js";
22
+ export { describeError, shareUrl, codeFromUrl, ErrorCodes } from "./friendly.js";
23
23
  // Pre-rename names, kept for one version so existing games keep working.
24
24
  export { Keepalive, KeepaliveError } from "./deprecated.js";
25
25
  //# sourceMappingURL=index.js.map
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,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"]}
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,UAAU,EAAE,MAAM,eAAe,CAAC;AAEjF,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, ErrorCodes } from \"./friendly.js\";\nexport type { FriendlyError, ErrorCode } 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"]}
@@ -22,6 +22,8 @@ export declare const MAX_DESYNCS = 3;
22
22
  export declare const MAX_RESTORE_RETRIES = 3;
23
23
  export declare const HOST_SILENCE_MS = 3000;
24
24
  export declare const PEER_HASH_SILENCE_MS = 10000;
25
+ /** Commands one remote peer may have waiting for the host's next frame; the excess is dropped. */
26
+ export declare const MAX_PENDING_PER_PEER = 256;
25
27
  export interface LockstepOptions {
26
28
  hashEveryTicks: number;
27
29
  /** Frames kept for replays (late join, migration). Default 30 s of ticks. */
@@ -58,6 +60,7 @@ export declare class Lockstep extends Emitter<LockstepEvents> {
58
60
  private hosting;
59
61
  private _started;
60
62
  private pending;
63
+ private _droppedCommands;
61
64
  private ready;
62
65
  private snapshotting;
63
66
  private cancelTicker;
@@ -71,6 +74,8 @@ export declare class Lockstep extends Emitter<LockstepEvents> {
71
74
  private staleUpTo;
72
75
  private pendingSnapshot;
73
76
  constructor(hub: Hub, opts: LockstepOptions);
77
+ /** Remote commands the host dropped because that peer already had MAX_PENDING_PER_PEER waiting. */
78
+ get droppedCommands(): number;
74
79
  get started(): boolean;
75
80
  get framesBuffered(): number;
76
81
  /** True when the game should run ticks without rendering (> 2 frames behind). */
package/dist/lockstep.js CHANGED
@@ -6,6 +6,8 @@ export const MAX_DESYNCS = 3;
6
6
  export const MAX_RESTORE_RETRIES = 3;
7
7
  export const HOST_SILENCE_MS = 3000;
8
8
  export const PEER_HASH_SILENCE_MS = 10_000;
9
+ /** Commands one remote peer may have waiting for the host's next frame; the excess is dropped. */
10
+ export const MAX_PENDING_PER_PEER = 256;
9
11
  const SNAPSHOT_COMPRESS_MIN = 1024;
10
12
  /**
11
13
  * Host-ordered lockstep (spec/binary.md, DESIGN s.8). One class for both roles: the host emits `frame(T)` on a fixed
@@ -39,6 +41,7 @@ export class Lockstep extends Emitter {
39
41
  hosting = false;
40
42
  _started = false;
41
43
  pending = [];
44
+ _droppedCommands = 0;
42
45
  ready = new Set();
43
46
  snapshotting = new Set();
44
47
  cancelTicker = null;
@@ -60,6 +63,10 @@ export class Lockstep extends Emitter {
60
63
  this.lastFrameRx = hub.clock.now();
61
64
  }
62
65
  // ---- public game-facing surface -------------------------------------------------------------------------------
66
+ /** Remote commands the host dropped because that peer already had MAX_PENDING_PER_PEER waiting. */
67
+ get droppedCommands() {
68
+ return this._droppedCommands;
69
+ }
63
70
  get started() {
64
71
  return this._started;
65
72
  }
@@ -222,8 +229,16 @@ export class Lockstep extends Emitter {
222
229
  handle(from, msg) {
223
230
  switch (msg.type) {
224
231
  case "cmd":
225
- if (this.hosting)
226
- this.pending.push({ peerId: from, bytes: msg.bytes });
232
+ if (this.hosting) {
233
+ let waiting = 0;
234
+ for (const c of this.pending)
235
+ if (c.peerId === from)
236
+ waiting++;
237
+ if (waiting >= MAX_PENDING_PER_PEER)
238
+ this._droppedCommands++;
239
+ else
240
+ this.pending.push({ peerId: from, bytes: msg.bytes });
241
+ }
227
242
  break;
228
243
  case "frame":
229
244
  if (!this.hosting)
@@ -1 +1 @@
1
- {"version":3,"file":"lockstep.js","sourceRoot":"","sources":["../src/lockstep.ts"],"names":[],"mappings":"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","sourcesContent":["import { CHUNK_PAYLOAD, splitChunks } from \"./chunking.js\";\nimport { PROTO, type FrameCommand, type Message } from \"./codec.js\";\nimport { deflate, deflateAvailable, inflate } from \"./deflate.js\";\nimport { Emitter } from \"./emitter.js\";\nimport type { Hub } from \"./hub.js\";\n\nexport interface Frame {\n tick: number;\n commands: FrameCommand[];\n}\n\nexport interface DesyncInfo {\n tick: number;\n hostHash: bigint;\n yourHash: bigint;\n}\n\nexport type LockstepEvents = {\n desync: [DesyncInfo];\n /** A snapshot was applied (late join, desync, falling too far behind). */\n restored: [tick: number];\n /** First frame (or snapshot) of the game reached this peer: the session is in game. */\n started: [];\n error: [Error];\n};\n\nexport const MAX_DESYNCS = 3;\nexport const MAX_RESTORE_RETRIES = 3;\nexport const HOST_SILENCE_MS = 3000;\nexport const PEER_HASH_SILENCE_MS = 10_000;\nconst SNAPSHOT_COMPRESS_MIN = 1024;\n\nexport interface LockstepOptions {\n hashEveryTicks: number;\n /** Frames kept for replays (late join, migration). Default 30 s of ticks. */\n historyFrames?: number;\n}\n\n/**\n * Host-ordered lockstep (spec/binary.md, DESIGN s.8). One class for both roles: the host emits `frame(T)` on a fixed\n * cadence and never waits; joiners queue frames and hand them out through `nextFrame()`.\n *\n * Ticks: the initial state is tick 0 and has no frame; `frame(T)` produces tick T. `reportHash(tick, h)` is called\n * after applying `frame.tick` (the library only sends when `tick % hashEveryTicks == 0`, tick > 0). `snapshot()` must\n * return the state after the last frame returned by `nextFrame()`; `restore(tick, bytes)` receives that tick, and the\n * next frame applied is `tick + 1`.\n */\nexport class Lockstep extends Emitter<LockstepEvents> {\n snapshot: (() => Uint8Array) | null = null;\n restore: ((tick: number, bytes: Uint8Array) => void) | null = null;\n\n readonly hashEveryTicks: number;\n /** Chunk body size used when serving snapshots. */\n chunkSize = CHUNK_PAYLOAD;\n private historyMax: number;\n\n private queue: Frame[] = [];\n private history: Frame[] = [];\n /** Highest frame tick received/emitted; -1 = no state (a fresh joiner). */\n private lastFrameTick = -1;\n /** Tick of the state the game holds (last frame returned by nextFrame, or a restored snapshot). */\n private consumedTick = 0;\n private cmdSeq = 1;\n private awaitingSnapshot = false;\n private lastFrameRx = 0;\n private lastSnapshotReq = -Infinity;\n private restoreFailures = 0;\n\n // host side\n private hosting = false;\n private _started = false;\n private pending: FrameCommand[] = [];\n private ready = new Set<number>();\n private snapshotting = new Set<number>();\n private cancelTicker: (() => void) | null = null;\n private baseTime = 0;\n private emitted = 0;\n private ownHashes = new Map<number, bigint>();\n private peerHashes = new Map<number, Map<number, bigint>>();\n private desyncs = new Map<number, number>();\n private lastHashRx = new Map<number, number>();\n /** Hashes a peer sent for ticks up to this were computed before it restored a snapshot: stale, not evidence. */\n private staleUpTo = new Map<number, { from: number; to: number }>();\n\n // joiner side\n private pendingSnapshot: { tick: number; total: number; flags: number } | null = null;\n\n constructor(private hub: Hub, opts: LockstepOptions) {\n super();\n this.hashEveryTicks = Math.max(1, opts.hashEveryTicks);\n this.historyMax = opts.historyFrames ?? Math.max(60, hub.tickRate * 30);\n this.lastFrameRx = hub.clock.now();\n }\n\n // ---- public game-facing surface -------------------------------------------------------------------------------\n\n get started(): boolean {\n return this._started;\n }\n get framesBuffered(): number {\n return this.queue.length;\n }\n /** True when the game should run ticks without rendering (> 2 frames behind). */\n get catchingUp(): boolean {\n return this.queue.length > 2;\n }\n /** Tick of the game's current state. */\n get currentTick(): number {\n return this.consumedTick;\n }\n /** Highest frame tick known to this peer (0 when none yet; the host's `welcome_dc.tick`). */\n get lastTick(): number {\n return Math.max(0, this.lastFrameTick);\n }\n /** What `hi.lastTick` carries: -1 until the first frame/snapshot. */\n get hiTick(): number {\n return this.lastFrameTick;\n }\n\n /** Queue a command. Host: into its own next frame. Joiner: to the host (reliable). */\n submit(bytes: Uint8Array): void {\n if (this.hosting) {\n this.pending.push({ peerId: this.hub.localPeerId, bytes: bytes.slice() });\n } else {\n this.hub.sendToHost({ type: \"cmd\", seq: this.cmdSeq++ >>> 0, bytes });\n }\n }\n\n nextFrame(): Frame | null {\n const f = this.queue.shift() ?? null;\n if (f) this.consumedTick = f.tick;\n return f;\n }\n\n reportHash(tick: number, hash: bigint): void {\n if (this.awaitingSnapshot || tick <= 0 || tick % this.hashEveryTicks !== 0) return;\n if (this.hosting) {\n this.ownHashes.set(tick, hash);\n this.prune(this.ownHashes);\n const theirs = this.peerHashes.get(tick);\n if (theirs) {\n this.peerHashes.delete(tick);\n for (const [peer, h] of [...theirs].sort((a, b) => a[0] - b[0])) this.compare(peer, tick, hash, h);\n }\n } else {\n this.hub.sendToHost({ type: \"hash\", tick, hash });\n }\n }\n\n // ---- role control (called by Lobby) ---------------------------------------------------------------------------\n\n /** Host role without emitting yet (lobby phase): peers may already say hi. */\n hostIdle(): void {\n this.hosting = true;\n if (this.lastFrameTick < 0) this.lastFrameTick = 0;\n }\n\n /** Host begins emitting frames (game start, or after winning a migration), continuing after the last known tick. */\n hostStart(): void {\n this.hostIdle();\n this._started = true;\n this.emitted = 0;\n this.baseTime = this.hub.clock.now();\n this.cancelTicker?.();\n const interval = 1000 / this.hub.tickRate;\n this.cancelTicker = this.hub.clock.setInterval(() => this.pump(interval), Math.max(1, Math.floor(interval / 2)));\n }\n\n /** Winning a migration: continue from our last received frame. Pending commands are lost by design. */\n becomeHost(): void {\n this.pending = [];\n this.ready.clear();\n this.snapshotting.clear();\n this.ownHashes.clear();\n this.peerHashes.clear();\n this.desyncs.clear();\n this.awaitingSnapshot = false;\n this.hostIdle();\n if (this._started) this.hostStart();\n }\n\n /** Losing a claim, or a new host appeared: stop emitting. Keep history; the new host decides replay vs snapshot. */\n becomeJoiner(): void {\n this.hosting = false;\n this.cancelTicker?.();\n this.cancelTicker = null;\n this.pending = [];\n this.ready.clear();\n this.lastFrameRx = this.hub.clock.now();\n }\n\n /** A joiner just learned the session is in game (first frame, snapshot, or phase from the server). */\n markStarted(): void {\n if (this._started) return;\n this._started = true;\n this.emit(\"started\");\n }\n\n stop(): void {\n this.cancelTicker?.();\n this.cancelTicker = null;\n }\n\n onPeerLeft(peerId: number): void {\n this.ready.delete(peerId);\n this.snapshotting.delete(peerId);\n this.desyncs.delete(peerId);\n this.lastHashRx.delete(peerId);\n this.staleUpTo.delete(peerId);\n for (const m of this.peerHashes.values()) m.delete(peerId);\n this.pending = this.pending.filter((c) => c.peerId !== peerId);\n }\n\n // ---- host: frame emission ---------------------------------------------------------------------------------------\n\n private pump(interval: number): void {\n if (!this.hosting) return;\n const now = this.hub.clock.now();\n let due = Math.floor((now - this.baseTime + 1) / interval) - this.emitted;\n if (due > 10) {\n // Starved (e.g. a throttled tab): do not flood. Rebase rather than emit a burst of empty frames.\n this.baseTime = now - this.emitted * interval;\n due = 1;\n }\n for (let i = 0; i < due; i++) this.emitFrame();\n }\n\n private emitFrame(): void {\n // sorted by peerId, stable: one peer's commands keep their arrival order\n const commands = this.pending.splice(0).sort((a, b) => a.peerId - b.peerId);\n const frame: Frame = { tick: this.lastFrameTick + 1, commands };\n this.emitted++;\n this.lastFrameTick = frame.tick;\n this.record(frame);\n this.queue.push(frame);\n const msg: Message = { type: \"frame\", tick: frame.tick, commands };\n for (const p of this.ready) this.hub.sendTo(p, msg, 0);\n this.checkPeerSilence();\n }\n\n private record(f: Frame): void {\n this.history.push(f);\n if (this.history.length > this.historyMax) this.history.splice(0, this.history.length - this.historyMax);\n }\n\n /** The host drops a peer it has had no `hash` from for 10 s (scaled up when hashes are rare). */\n private checkPeerSilence(): void {\n if (this.ownHashes.size === 0) return;\n const now = this.hub.clock.now();\n const limit = Math.max(PEER_HASH_SILENCE_MS, (this.hashEveryTicks / this.hub.tickRate) * 1000 * 3);\n for (const p of [...this.ready]) {\n const last = this.lastHashRx.get(p) ?? now;\n if (now - last > limit) this.hub.dropPeer(p, \"timeout\");\n }\n }\n\n // ---- message handling -------------------------------------------------------------------------------------------\n\n handle(from: number, msg: Message): void {\n switch (msg.type) {\n case \"cmd\":\n if (this.hosting) this.pending.push({ peerId: from, bytes: msg.bytes });\n break;\n case \"frame\":\n if (!this.hosting) this.onFrame(msg.tick, msg.commands);\n break;\n case \"hash\":\n if (this.hosting) this.onPeerHash(from, msg.tick, msg.hash);\n break;\n case \"desync\":\n if (!this.hosting) {\n this.awaitingSnapshot = true;\n this.queue = [];\n this.emit(\"desync\", { tick: msg.tick, hostHash: msg.hostHash, yourHash: msg.yourHash });\n }\n break;\n case \"snapshot_req\":\n if (this.hosting) void this.sendSnapshot(from);\n break;\n case \"snapshot\":\n if (!this.hosting) {\n this.markStarted();\n this.pendingSnapshot = { tick: msg.tick, total: msg.total, flags: msg.flags };\n }\n break;\n }\n }\n\n /** True when a `snapshot` header was seen and its chunk set has not completed yet. */\n get snapshotPending(): boolean {\n return this.pendingSnapshot !== null;\n }\n\n /** The reassembled chunk set that follows a `snapshot` header. */\n onSnapshotPayload(payload: Uint8Array): void {\n const ps = this.pendingSnapshot;\n this.pendingSnapshot = null;\n if (!ps) return;\n if (payload.length !== ps.total) {\n // spec: a receiver that gets a different sum drops the snapshot and re-requests\n this.awaitingSnapshot = false;\n this.requestSnapshot();\n return;\n }\n void this.applySnapshot(ps, payload);\n }\n\n private onFrame(tick: number, commands: FrameCommand[]): void {\n this.lastFrameRx = this.hub.clock.now();\n this.markStarted();\n if (this.awaitingSnapshot) return;\n const expected = Math.max(0, this.lastFrameTick) + 1;\n if (tick < expected) return; // duplicate (replay overlap)\n if (tick > expected) {\n this.requestSnapshot(); // gap: the host changed under us\n return;\n }\n const f: Frame = { tick, commands };\n this.lastFrameTick = tick;\n this.record(f);\n this.queue.push(f);\n if (this.queue.length > this.hub.tickRate * 3) this.requestSnapshot();\n }\n\n /** Joiner asks the host for a snapshot (far behind, gap, silent host). Throttled while one is outstanding. */\n requestSnapshot(): void {\n const now = this.hub.clock.now();\n if (this.awaitingSnapshot && now - this.lastSnapshotReq < 2000) return;\n this.lastSnapshotReq = now;\n this.awaitingSnapshot = true;\n this.queue = [];\n this.hub.sendToHost({ type: \"snapshot_req\", sinceTick: this.lastFrameTick });\n }\n\n /** 3 s without frames: ask the host for a snapshot without discarding anything (a live host answers). */\n pokeHost(): void {\n this.hub.sendToHost({ type: \"snapshot_req\", sinceTick: this.lastFrameTick });\n }\n\n private async applySnapshot(ps: { tick: number; total: number; flags: number }, payload: Uint8Array): Promise<void> {\n try {\n let bytes = payload;\n if (ps.flags & 1) bytes = await inflate(payload);\n if (!this.restore) throw new Error(\"lockstep.restore is not set; cannot apply a snapshot\");\n // Restore first: if the game's restore() throws, our bookkeeping must not have moved.\n this.restore(ps.tick, bytes);\n this.queue = [];\n this.history = [];\n this.lastFrameTick = ps.tick;\n this.consumedTick = ps.tick;\n this.awaitingSnapshot = false;\n this.restoreFailures = 0;\n this.markStarted();\n this.lastFrameRx = this.hub.clock.now();\n this.emit(\"restored\", ps.tick);\n } catch (e) {\n this.hub.reportError(e instanceof Error ? e : new Error(String(e)));\n // The game's state is unknown after a failed restore: keep our tick where it was, drop frames, ask again.\n if (++this.restoreFailures <= MAX_RESTORE_RETRIES) {\n this.awaitingSnapshot = false;\n this.lastSnapshotReq = -Infinity;\n this.requestSnapshot();\n } else {\n this.hub.reportError(new Error(`snapshot restore failed ${this.restoreFailures} times; giving up`));\n }\n }\n }\n\n // ---- host: hello, hashes, snapshots -----------------------------------------------------------------------------\n\n /** Host: a peer said hi (welcome_dc and chat_history already sent). Decide: nothing, replay, or snapshot. */\n onPeerHi(peerId: number, lastTick: number): void {\n this.lastHashRx.set(peerId, this.hub.clock.now());\n const hostTick = this.lastFrameTick;\n const first = this.history.length ? this.history[0].tick : hostTick + 1;\n if (lastTick < 0) {\n // fresh joiner: initial state if no game is running, otherwise a snapshot\n if (this._started && hostTick > 0) void this.sendSnapshot(peerId);\n else this.ready.add(peerId);\n return;\n }\n if (lastTick <= hostTick && lastTick + 1 >= first) {\n // The peer's timeline is a prefix of ours: replay what it lacks, then go live.\n for (const f of this.history) {\n if (f.tick > lastTick) this.hub.sendTo(peerId, { type: \"frame\", tick: f.tick, commands: f.commands }, 0);\n }\n this.ready.add(peerId);\n } else {\n void this.sendSnapshot(peerId);\n }\n }\n\n private onPeerHash(peerId: number, tick: number, hash: bigint): void {\n this.lastHashRx.set(peerId, this.hub.clock.now());\n if (this.isStale(peerId, tick)) return;\n const own = this.ownHashes.get(tick);\n if (own === undefined) {\n let m = this.peerHashes.get(tick);\n if (!m) this.peerHashes.set(tick, (m = new Map()));\n m.set(peerId, hash);\n this.prune(this.peerHashes);\n return;\n }\n this.compare(peerId, tick, own, hash);\n }\n\n private compare(peerId: number, tick: number, hostHash: bigint, yourHash: bigint): void {\n if (hostHash === yourHash) return;\n if (this.isStale(peerId, tick)) return;\n if (!this.ready.has(peerId) || this.snapshotting.has(peerId)) return; // already being resynced\n const n = (this.desyncs.get(peerId) ?? 0) + 1;\n this.desyncs.set(peerId, n);\n if (n >= MAX_DESYNCS) {\n this.hub.dropPeer(peerId, \"desync\");\n return;\n }\n this.hub.sendTo(peerId, { type: \"desync\", tick, hostHash, yourHash }, 0);\n void this.sendSnapshot(peerId, tick);\n }\n\n /** Hashes computed before a peer restored its snapshot (ticks after the strike, up to what we had emitted) are not evidence. */\n private isStale(peerId: number, tick: number): boolean {\n const w = this.staleUpTo.get(peerId);\n return !!w && tick > w.from && tick <= w.to;\n }\n\n private async sendSnapshot(peerId: number, strikeTick = Infinity): Promise<void> {\n if (this.snapshotting.has(peerId)) return;\n if (!this.snapshot) {\n this.hub.reportError(new Error(\"lockstep.snapshot is not set; cannot serve a snapshot\"));\n return;\n }\n this.snapshotting.add(peerId);\n this.ready.delete(peerId); // live frames now flow only through the replay below\n try {\n const S = this.consumedTick;\n const raw = this.snapshot();\n let payload = raw;\n let flags = 0;\n if (raw.length >= SNAPSHOT_COMPRESS_MIN && deflateAvailable()) {\n payload = await deflate(raw);\n flags = 1;\n }\n if (!this.snapshotting.has(peerId)) return; // peer left while compressing\n // header + one contiguous chunk set + frames S+1... with nothing else in between\n this.hub.sendTo(peerId, { type: \"snapshot\", tick: S, total: payload.length, flags }, 0);\n for (const c of splitChunks(this.hub.nextChunkId(), payload, this.chunkSize)) this.hub.sendTo(peerId, c, 0);\n for (const f of this.history) {\n if (f.tick > S) this.hub.sendTo(peerId, { type: \"frame\", tick: f.tick, commands: f.commands }, 0);\n }\n if (strikeTick !== Infinity) this.staleUpTo.set(peerId, { from: strikeTick, to: this.lastFrameTick });\n else this.staleUpTo.delete(peerId);\n for (const m of this.peerHashes.values()) m.delete(peerId);\n this.ready.add(peerId);\n this.lastHashRx.set(peerId, this.hub.clock.now());\n } catch (e) {\n this.hub.reportError(e instanceof Error ? e : new Error(String(e)));\n } finally {\n this.snapshotting.delete(peerId);\n }\n }\n\n private prune<T>(m: Map<number, T>): void {\n if (m.size <= 64) return;\n const keys = [...m.keys()].sort((a, b) => a - b);\n for (const k of keys.slice(0, keys.length - 64)) m.delete(k);\n }\n\n // ---- joiner watchdog --------------------------------------------------------------------------------------------\n\n /** Joiner: ms since the last frame from the host (only meaningful once in game). */\n get msSinceFrame(): number {\n return this.hub.clock.now() - this.lastFrameRx;\n }\n\n /** True when a started joiner has heard no frames for 3 s: the Lobby then runs the host-loss rule (s.9). */\n hostSilent(): boolean {\n return !this.hosting && this._started && this.msSinceFrame > HOST_SILENCE_MS;\n }\n\n /** Reset the silence timer, e.g. after the link to a new host opened. */\n touch(): void {\n this.lastFrameRx = this.hub.clock.now();\n }\n\n hiMessage(peerId: number): Message {\n return { type: \"hi\", proto: PROTO, peerId, lastTick: this.lastFrameTick };\n }\n\n get resyncCount(): Map<number, number> {\n return this.desyncs;\n }\n}\n"]}
1
+ {"version":3,"file":"lockstep.js","sourceRoot":"","sources":["../src/lockstep.ts"],"names":[],"mappings":"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","sourcesContent":["import { CHUNK_PAYLOAD, splitChunks } from \"./chunking.js\";\nimport { PROTO, type FrameCommand, type Message } from \"./codec.js\";\nimport { deflate, deflateAvailable, inflate } from \"./deflate.js\";\nimport { Emitter } from \"./emitter.js\";\nimport type { Hub } from \"./hub.js\";\n\nexport interface Frame {\n tick: number;\n commands: FrameCommand[];\n}\n\nexport interface DesyncInfo {\n tick: number;\n hostHash: bigint;\n yourHash: bigint;\n}\n\nexport type LockstepEvents = {\n desync: [DesyncInfo];\n /** A snapshot was applied (late join, desync, falling too far behind). */\n restored: [tick: number];\n /** First frame (or snapshot) of the game reached this peer: the session is in game. */\n started: [];\n error: [Error];\n};\n\nexport const MAX_DESYNCS = 3;\nexport const MAX_RESTORE_RETRIES = 3;\nexport const HOST_SILENCE_MS = 3000;\nexport const PEER_HASH_SILENCE_MS = 10_000;\n/** Commands one remote peer may have waiting for the host's next frame; the excess is dropped. */\nexport const MAX_PENDING_PER_PEER = 256;\nconst SNAPSHOT_COMPRESS_MIN = 1024;\n\nexport interface LockstepOptions {\n hashEveryTicks: number;\n /** Frames kept for replays (late join, migration). Default 30 s of ticks. */\n historyFrames?: number;\n}\n\n/**\n * Host-ordered lockstep (spec/binary.md, DESIGN s.8). One class for both roles: the host emits `frame(T)` on a fixed\n * cadence and never waits; joiners queue frames and hand them out through `nextFrame()`.\n *\n * Ticks: the initial state is tick 0 and has no frame; `frame(T)` produces tick T. `reportHash(tick, h)` is called\n * after applying `frame.tick` (the library only sends when `tick % hashEveryTicks == 0`, tick > 0). `snapshot()` must\n * return the state after the last frame returned by `nextFrame()`; `restore(tick, bytes)` receives that tick, and the\n * next frame applied is `tick + 1`.\n */\nexport class Lockstep extends Emitter<LockstepEvents> {\n snapshot: (() => Uint8Array) | null = null;\n restore: ((tick: number, bytes: Uint8Array) => void) | null = null;\n\n readonly hashEveryTicks: number;\n /** Chunk body size used when serving snapshots. */\n chunkSize = CHUNK_PAYLOAD;\n private historyMax: number;\n\n private queue: Frame[] = [];\n private history: Frame[] = [];\n /** Highest frame tick received/emitted; -1 = no state (a fresh joiner). */\n private lastFrameTick = -1;\n /** Tick of the state the game holds (last frame returned by nextFrame, or a restored snapshot). */\n private consumedTick = 0;\n private cmdSeq = 1;\n private awaitingSnapshot = false;\n private lastFrameRx = 0;\n private lastSnapshotReq = -Infinity;\n private restoreFailures = 0;\n\n // host side\n private hosting = false;\n private _started = false;\n private pending: FrameCommand[] = [];\n private _droppedCommands = 0;\n private ready = new Set<number>();\n private snapshotting = new Set<number>();\n private cancelTicker: (() => void) | null = null;\n private baseTime = 0;\n private emitted = 0;\n private ownHashes = new Map<number, bigint>();\n private peerHashes = new Map<number, Map<number, bigint>>();\n private desyncs = new Map<number, number>();\n private lastHashRx = new Map<number, number>();\n /** Hashes a peer sent for ticks up to this were computed before it restored a snapshot: stale, not evidence. */\n private staleUpTo = new Map<number, { from: number; to: number }>();\n\n // joiner side\n private pendingSnapshot: { tick: number; total: number; flags: number } | null = null;\n\n constructor(private hub: Hub, opts: LockstepOptions) {\n super();\n this.hashEveryTicks = Math.max(1, opts.hashEveryTicks);\n this.historyMax = opts.historyFrames ?? Math.max(60, hub.tickRate * 30);\n this.lastFrameRx = hub.clock.now();\n }\n\n // ---- public game-facing surface -------------------------------------------------------------------------------\n\n /** Remote commands the host dropped because that peer already had MAX_PENDING_PER_PEER waiting. */\n get droppedCommands(): number {\n return this._droppedCommands;\n }\n get started(): boolean {\n return this._started;\n }\n get framesBuffered(): number {\n return this.queue.length;\n }\n /** True when the game should run ticks without rendering (> 2 frames behind). */\n get catchingUp(): boolean {\n return this.queue.length > 2;\n }\n /** Tick of the game's current state. */\n get currentTick(): number {\n return this.consumedTick;\n }\n /** Highest frame tick known to this peer (0 when none yet; the host's `welcome_dc.tick`). */\n get lastTick(): number {\n return Math.max(0, this.lastFrameTick);\n }\n /** What `hi.lastTick` carries: -1 until the first frame/snapshot. */\n get hiTick(): number {\n return this.lastFrameTick;\n }\n\n /** Queue a command. Host: into its own next frame. Joiner: to the host (reliable). */\n submit(bytes: Uint8Array): void {\n if (this.hosting) {\n this.pending.push({ peerId: this.hub.localPeerId, bytes: bytes.slice() });\n } else {\n this.hub.sendToHost({ type: \"cmd\", seq: this.cmdSeq++ >>> 0, bytes });\n }\n }\n\n nextFrame(): Frame | null {\n const f = this.queue.shift() ?? null;\n if (f) this.consumedTick = f.tick;\n return f;\n }\n\n reportHash(tick: number, hash: bigint): void {\n if (this.awaitingSnapshot || tick <= 0 || tick % this.hashEveryTicks !== 0) return;\n if (this.hosting) {\n this.ownHashes.set(tick, hash);\n this.prune(this.ownHashes);\n const theirs = this.peerHashes.get(tick);\n if (theirs) {\n this.peerHashes.delete(tick);\n for (const [peer, h] of [...theirs].sort((a, b) => a[0] - b[0])) this.compare(peer, tick, hash, h);\n }\n } else {\n this.hub.sendToHost({ type: \"hash\", tick, hash });\n }\n }\n\n // ---- role control (called by Lobby) ---------------------------------------------------------------------------\n\n /** Host role without emitting yet (lobby phase): peers may already say hi. */\n hostIdle(): void {\n this.hosting = true;\n if (this.lastFrameTick < 0) this.lastFrameTick = 0;\n }\n\n /** Host begins emitting frames (game start, or after winning a migration), continuing after the last known tick. */\n hostStart(): void {\n this.hostIdle();\n this._started = true;\n this.emitted = 0;\n this.baseTime = this.hub.clock.now();\n this.cancelTicker?.();\n const interval = 1000 / this.hub.tickRate;\n this.cancelTicker = this.hub.clock.setInterval(() => this.pump(interval), Math.max(1, Math.floor(interval / 2)));\n }\n\n /** Winning a migration: continue from our last received frame. Pending commands are lost by design. */\n becomeHost(): void {\n this.pending = [];\n this.ready.clear();\n this.snapshotting.clear();\n this.ownHashes.clear();\n this.peerHashes.clear();\n this.desyncs.clear();\n this.awaitingSnapshot = false;\n this.hostIdle();\n if (this._started) this.hostStart();\n }\n\n /** Losing a claim, or a new host appeared: stop emitting. Keep history; the new host decides replay vs snapshot. */\n becomeJoiner(): void {\n this.hosting = false;\n this.cancelTicker?.();\n this.cancelTicker = null;\n this.pending = [];\n this.ready.clear();\n this.lastFrameRx = this.hub.clock.now();\n }\n\n /** A joiner just learned the session is in game (first frame, snapshot, or phase from the server). */\n markStarted(): void {\n if (this._started) return;\n this._started = true;\n this.emit(\"started\");\n }\n\n stop(): void {\n this.cancelTicker?.();\n this.cancelTicker = null;\n }\n\n onPeerLeft(peerId: number): void {\n this.ready.delete(peerId);\n this.snapshotting.delete(peerId);\n this.desyncs.delete(peerId);\n this.lastHashRx.delete(peerId);\n this.staleUpTo.delete(peerId);\n for (const m of this.peerHashes.values()) m.delete(peerId);\n this.pending = this.pending.filter((c) => c.peerId !== peerId);\n }\n\n // ---- host: frame emission ---------------------------------------------------------------------------------------\n\n private pump(interval: number): void {\n if (!this.hosting) return;\n const now = this.hub.clock.now();\n let due = Math.floor((now - this.baseTime + 1) / interval) - this.emitted;\n if (due > 10) {\n // Starved (e.g. a throttled tab): do not flood. Rebase rather than emit a burst of empty frames.\n this.baseTime = now - this.emitted * interval;\n due = 1;\n }\n for (let i = 0; i < due; i++) this.emitFrame();\n }\n\n private emitFrame(): void {\n // sorted by peerId, stable: one peer's commands keep their arrival order\n const commands = this.pending.splice(0).sort((a, b) => a.peerId - b.peerId);\n const frame: Frame = { tick: this.lastFrameTick + 1, commands };\n this.emitted++;\n this.lastFrameTick = frame.tick;\n this.record(frame);\n this.queue.push(frame);\n const msg: Message = { type: \"frame\", tick: frame.tick, commands };\n for (const p of this.ready) this.hub.sendTo(p, msg, 0);\n this.checkPeerSilence();\n }\n\n private record(f: Frame): void {\n this.history.push(f);\n if (this.history.length > this.historyMax) this.history.splice(0, this.history.length - this.historyMax);\n }\n\n /** The host drops a peer it has had no `hash` from for 10 s (scaled up when hashes are rare). */\n private checkPeerSilence(): void {\n if (this.ownHashes.size === 0) return;\n const now = this.hub.clock.now();\n const limit = Math.max(PEER_HASH_SILENCE_MS, (this.hashEveryTicks / this.hub.tickRate) * 1000 * 3);\n for (const p of [...this.ready]) {\n const last = this.lastHashRx.get(p) ?? now;\n if (now - last > limit) this.hub.dropPeer(p, \"timeout\");\n }\n }\n\n // ---- message handling -------------------------------------------------------------------------------------------\n\n handle(from: number, msg: Message): void {\n switch (msg.type) {\n case \"cmd\":\n if (this.hosting) {\n let waiting = 0;\n for (const c of this.pending) if (c.peerId === from) waiting++;\n if (waiting >= MAX_PENDING_PER_PEER) this._droppedCommands++;\n else this.pending.push({ peerId: from, bytes: msg.bytes });\n }\n break;\n case \"frame\":\n if (!this.hosting) this.onFrame(msg.tick, msg.commands);\n break;\n case \"hash\":\n if (this.hosting) this.onPeerHash(from, msg.tick, msg.hash);\n break;\n case \"desync\":\n if (!this.hosting) {\n this.awaitingSnapshot = true;\n this.queue = [];\n this.emit(\"desync\", { tick: msg.tick, hostHash: msg.hostHash, yourHash: msg.yourHash });\n }\n break;\n case \"snapshot_req\":\n if (this.hosting) void this.sendSnapshot(from);\n break;\n case \"snapshot\":\n if (!this.hosting) {\n this.markStarted();\n this.pendingSnapshot = { tick: msg.tick, total: msg.total, flags: msg.flags };\n }\n break;\n }\n }\n\n /** True when a `snapshot` header was seen and its chunk set has not completed yet. */\n get snapshotPending(): boolean {\n return this.pendingSnapshot !== null;\n }\n\n /** The reassembled chunk set that follows a `snapshot` header. */\n onSnapshotPayload(payload: Uint8Array): void {\n const ps = this.pendingSnapshot;\n this.pendingSnapshot = null;\n if (!ps) return;\n if (payload.length !== ps.total) {\n // spec: a receiver that gets a different sum drops the snapshot and re-requests\n this.awaitingSnapshot = false;\n this.requestSnapshot();\n return;\n }\n void this.applySnapshot(ps, payload);\n }\n\n private onFrame(tick: number, commands: FrameCommand[]): void {\n this.lastFrameRx = this.hub.clock.now();\n this.markStarted();\n if (this.awaitingSnapshot) return;\n const expected = Math.max(0, this.lastFrameTick) + 1;\n if (tick < expected) return; // duplicate (replay overlap)\n if (tick > expected) {\n this.requestSnapshot(); // gap: the host changed under us\n return;\n }\n const f: Frame = { tick, commands };\n this.lastFrameTick = tick;\n this.record(f);\n this.queue.push(f);\n if (this.queue.length > this.hub.tickRate * 3) this.requestSnapshot();\n }\n\n /** Joiner asks the host for a snapshot (far behind, gap, silent host). Throttled while one is outstanding. */\n requestSnapshot(): void {\n const now = this.hub.clock.now();\n if (this.awaitingSnapshot && now - this.lastSnapshotReq < 2000) return;\n this.lastSnapshotReq = now;\n this.awaitingSnapshot = true;\n this.queue = [];\n this.hub.sendToHost({ type: \"snapshot_req\", sinceTick: this.lastFrameTick });\n }\n\n /** 3 s without frames: ask the host for a snapshot without discarding anything (a live host answers). */\n pokeHost(): void {\n this.hub.sendToHost({ type: \"snapshot_req\", sinceTick: this.lastFrameTick });\n }\n\n private async applySnapshot(ps: { tick: number; total: number; flags: number }, payload: Uint8Array): Promise<void> {\n try {\n let bytes = payload;\n if (ps.flags & 1) bytes = await inflate(payload);\n if (!this.restore) throw new Error(\"lockstep.restore is not set; cannot apply a snapshot\");\n // Restore first: if the game's restore() throws, our bookkeeping must not have moved.\n this.restore(ps.tick, bytes);\n this.queue = [];\n this.history = [];\n this.lastFrameTick = ps.tick;\n this.consumedTick = ps.tick;\n this.awaitingSnapshot = false;\n this.restoreFailures = 0;\n this.markStarted();\n this.lastFrameRx = this.hub.clock.now();\n this.emit(\"restored\", ps.tick);\n } catch (e) {\n this.hub.reportError(e instanceof Error ? e : new Error(String(e)));\n // The game's state is unknown after a failed restore: keep our tick where it was, drop frames, ask again.\n if (++this.restoreFailures <= MAX_RESTORE_RETRIES) {\n this.awaitingSnapshot = false;\n this.lastSnapshotReq = -Infinity;\n this.requestSnapshot();\n } else {\n this.hub.reportError(new Error(`snapshot restore failed ${this.restoreFailures} times; giving up`));\n }\n }\n }\n\n // ---- host: hello, hashes, snapshots -----------------------------------------------------------------------------\n\n /** Host: a peer said hi (welcome_dc and chat_history already sent). Decide: nothing, replay, or snapshot. */\n onPeerHi(peerId: number, lastTick: number): void {\n this.lastHashRx.set(peerId, this.hub.clock.now());\n const hostTick = this.lastFrameTick;\n const first = this.history.length ? this.history[0].tick : hostTick + 1;\n if (lastTick < 0) {\n // fresh joiner: initial state if no game is running, otherwise a snapshot\n if (this._started && hostTick > 0) void this.sendSnapshot(peerId);\n else this.ready.add(peerId);\n return;\n }\n if (lastTick <= hostTick && lastTick + 1 >= first) {\n // The peer's timeline is a prefix of ours: replay what it lacks, then go live.\n for (const f of this.history) {\n if (f.tick > lastTick) this.hub.sendTo(peerId, { type: \"frame\", tick: f.tick, commands: f.commands }, 0);\n }\n this.ready.add(peerId);\n } else {\n void this.sendSnapshot(peerId);\n }\n }\n\n private onPeerHash(peerId: number, tick: number, hash: bigint): void {\n this.lastHashRx.set(peerId, this.hub.clock.now());\n if (this.isStale(peerId, tick)) return;\n const own = this.ownHashes.get(tick);\n if (own === undefined) {\n let m = this.peerHashes.get(tick);\n if (!m) this.peerHashes.set(tick, (m = new Map()));\n m.set(peerId, hash);\n this.prune(this.peerHashes);\n return;\n }\n this.compare(peerId, tick, own, hash);\n }\n\n private compare(peerId: number, tick: number, hostHash: bigint, yourHash: bigint): void {\n if (hostHash === yourHash) return;\n if (this.isStale(peerId, tick)) return;\n if (!this.ready.has(peerId) || this.snapshotting.has(peerId)) return; // already being resynced\n const n = (this.desyncs.get(peerId) ?? 0) + 1;\n this.desyncs.set(peerId, n);\n if (n >= MAX_DESYNCS) {\n this.hub.dropPeer(peerId, \"desync\");\n return;\n }\n this.hub.sendTo(peerId, { type: \"desync\", tick, hostHash, yourHash }, 0);\n void this.sendSnapshot(peerId, tick);\n }\n\n /** Hashes computed before a peer restored its snapshot (ticks after the strike, up to what we had emitted) are not evidence. */\n private isStale(peerId: number, tick: number): boolean {\n const w = this.staleUpTo.get(peerId);\n return !!w && tick > w.from && tick <= w.to;\n }\n\n private async sendSnapshot(peerId: number, strikeTick = Infinity): Promise<void> {\n if (this.snapshotting.has(peerId)) return;\n if (!this.snapshot) {\n this.hub.reportError(new Error(\"lockstep.snapshot is not set; cannot serve a snapshot\"));\n return;\n }\n this.snapshotting.add(peerId);\n this.ready.delete(peerId); // live frames now flow only through the replay below\n try {\n const S = this.consumedTick;\n const raw = this.snapshot();\n let payload = raw;\n let flags = 0;\n if (raw.length >= SNAPSHOT_COMPRESS_MIN && deflateAvailable()) {\n payload = await deflate(raw);\n flags = 1;\n }\n if (!this.snapshotting.has(peerId)) return; // peer left while compressing\n // header + one contiguous chunk set + frames S+1... with nothing else in between\n this.hub.sendTo(peerId, { type: \"snapshot\", tick: S, total: payload.length, flags }, 0);\n for (const c of splitChunks(this.hub.nextChunkId(), payload, this.chunkSize)) this.hub.sendTo(peerId, c, 0);\n for (const f of this.history) {\n if (f.tick > S) this.hub.sendTo(peerId, { type: \"frame\", tick: f.tick, commands: f.commands }, 0);\n }\n if (strikeTick !== Infinity) this.staleUpTo.set(peerId, { from: strikeTick, to: this.lastFrameTick });\n else this.staleUpTo.delete(peerId);\n for (const m of this.peerHashes.values()) m.delete(peerId);\n this.ready.add(peerId);\n this.lastHashRx.set(peerId, this.hub.clock.now());\n } catch (e) {\n this.hub.reportError(e instanceof Error ? e : new Error(String(e)));\n } finally {\n this.snapshotting.delete(peerId);\n }\n }\n\n private prune<T>(m: Map<number, T>): void {\n if (m.size <= 64) return;\n const keys = [...m.keys()].sort((a, b) => a - b);\n for (const k of keys.slice(0, keys.length - 64)) m.delete(k);\n }\n\n // ---- joiner watchdog --------------------------------------------------------------------------------------------\n\n /** Joiner: ms since the last frame from the host (only meaningful once in game). */\n get msSinceFrame(): number {\n return this.hub.clock.now() - this.lastFrameRx;\n }\n\n /** True when a started joiner has heard no frames for 3 s: the Lobby then runs the host-loss rule (s.9). */\n hostSilent(): boolean {\n return !this.hosting && this._started && this.msSinceFrame > HOST_SILENCE_MS;\n }\n\n /** Reset the silence timer, e.g. after the link to a new host opened. */\n touch(): void {\n this.lastFrameRx = this.hub.clock.now();\n }\n\n hiMessage(peerId: number): Message {\n return { type: \"hi\", proto: PROTO, peerId, lastTick: this.lastFrameTick };\n }\n\n get resyncCount(): Map<number, number> {\n return this.desyncs;\n }\n}\n"]}