onelobby 0.0.0-stage → 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.
Files changed (123) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +77 -2
  3. package/dist/chat.d.ts +60 -0
  4. package/dist/chat.js +163 -0
  5. package/dist/chat.js.map +1 -0
  6. package/dist/chunking.d.ts +17 -0
  7. package/dist/chunking.js +59 -0
  8. package/dist/chunking.js.map +1 -0
  9. package/dist/clock.d.ts +8 -0
  10. package/dist/clock.js +39 -0
  11. package/dist/clock.js.map +1 -0
  12. package/dist/codec.d.ts +141 -0
  13. package/dist/codec.js +308 -0
  14. package/dist/codec.js.map +1 -0
  15. package/dist/crockford.d.ts +14 -0
  16. package/dist/crockford.js +55 -0
  17. package/dist/crockford.js.map +1 -0
  18. package/dist/deflate.d.ts +4 -0
  19. package/dist/deflate.js +32 -0
  20. package/dist/deflate.js.map +1 -0
  21. package/dist/deprecated.d.ts +18 -0
  22. package/dist/deprecated.js +11 -0
  23. package/dist/deprecated.js.map +1 -0
  24. package/dist/direct/hub-host.d.ts +82 -0
  25. package/dist/direct/hub-host.js +393 -0
  26. package/dist/direct/hub-host.js.map +1 -0
  27. package/dist/direct/hub.d.ts +178 -0
  28. package/dist/direct/hub.js +862 -0
  29. package/dist/direct/hub.js.map +1 -0
  30. package/dist/direct/index.d.ts +57 -0
  31. package/dist/direct/index.js +71 -0
  32. package/dist/direct/index.js.map +1 -0
  33. package/dist/direct/portmap.d.ts +84 -0
  34. package/dist/direct/portmap.js +465 -0
  35. package/dist/direct/portmap.js.map +1 -0
  36. package/dist/direct-address.d.ts +20 -0
  37. package/dist/direct-address.js +61 -0
  38. package/dist/direct-address.js.map +1 -0
  39. package/dist/emitter.d.ts +9 -0
  40. package/dist/emitter.js +36 -0
  41. package/dist/emitter.js.map +1 -0
  42. package/dist/friendly.d.ts +20 -0
  43. package/dist/friendly.js +120 -0
  44. package/dist/friendly.js.map +1 -0
  45. package/dist/global.d.ts +8 -0
  46. package/dist/global.js +9 -0
  47. package/dist/global.js.map +1 -0
  48. package/dist/gridpaint.d.ts +27 -0
  49. package/dist/gridpaint.js +106 -0
  50. package/dist/gridpaint.js.map +1 -0
  51. package/dist/hub.d.ts +25 -0
  52. package/dist/hub.js +2 -0
  53. package/dist/hub.js.map +1 -0
  54. package/dist/index.d.ts +35 -0
  55. package/dist/index.js +25 -0
  56. package/dist/index.js.map +1 -0
  57. package/dist/internals.d.ts +28 -0
  58. package/dist/internals.js +24 -0
  59. package/dist/internals.js.map +1 -0
  60. package/dist/joincode.d.ts +21 -0
  61. package/dist/joincode.js +84 -0
  62. package/dist/joincode.js.map +1 -0
  63. package/dist/lobby.d.ts +203 -0
  64. package/dist/lobby.js +840 -0
  65. package/dist/lobby.js.map +1 -0
  66. package/dist/lockstep.d.ts +132 -0
  67. package/dist/lockstep.js +471 -0
  68. package/dist/lockstep.js.map +1 -0
  69. package/dist/migration.d.ts +17 -0
  70. package/dist/migration.js +34 -0
  71. package/dist/migration.js.map +1 -0
  72. package/dist/onelobby.bundle.mjs +140 -0
  73. package/dist/onelobby.bundle.mjs.map +7 -0
  74. package/dist/onelobby.d.ts +151 -0
  75. package/dist/onelobby.global.js +3897 -0
  76. package/dist/onelobby.global.js.map +7 -0
  77. package/dist/onelobby.global.min.js +140 -0
  78. package/dist/onelobby.global.min.js.map +7 -0
  79. package/dist/onelobby.js +337 -0
  80. package/dist/onelobby.js.map +1 -0
  81. package/dist/signaling.d.ts +94 -0
  82. package/dist/signaling.js +134 -0
  83. package/dist/signaling.js.map +1 -0
  84. package/dist/socket.d.ts +63 -0
  85. package/dist/socket.js +142 -0
  86. package/dist/socket.js.map +1 -0
  87. package/dist/testing.d.ts +135 -0
  88. package/dist/testing.js +438 -0
  89. package/dist/testing.js.map +1 -0
  90. package/dist/transport.d.ts +80 -0
  91. package/dist/transport.js +7 -0
  92. package/dist/transport.js.map +1 -0
  93. package/dist/ui/controller.d.ts +137 -0
  94. package/dist/ui/controller.js +337 -0
  95. package/dist/ui/controller.js.map +1 -0
  96. package/dist/ui/element.d.ts +22 -0
  97. package/dist/ui/element.js +284 -0
  98. package/dist/ui/element.js.map +1 -0
  99. package/dist/ui/index.d.ts +5 -0
  100. package/dist/ui/index.js +9 -0
  101. package/dist/ui/index.js.map +1 -0
  102. package/dist/ui/server-url.d.ts +9 -0
  103. package/dist/ui/server-url.js +39 -0
  104. package/dist/ui/server-url.js.map +1 -0
  105. package/dist/ui/styles.d.ts +2 -0
  106. package/dist/ui/styles.js +70 -0
  107. package/dist/ui/styles.js.map +1 -0
  108. package/dist/voice-browser.d.ts +19 -0
  109. package/dist/voice-browser.js +151 -0
  110. package/dist/voice-browser.js.map +1 -0
  111. package/dist/voice-core.d.ts +98 -0
  112. package/dist/voice-core.js +233 -0
  113. package/dist/voice-core.js.map +1 -0
  114. package/dist/voice-types.d.ts +35 -0
  115. package/dist/voice-types.js +3 -0
  116. package/dist/voice-types.js.map +1 -0
  117. package/dist/voice.d.ts +68 -0
  118. package/dist/voice.js +223 -0
  119. package/dist/voice.js.map +1 -0
  120. package/dist/webrtc.d.ts +29 -0
  121. package/dist/webrtc.js +169 -0
  122. package/dist/webrtc.js.map +1 -0
  123. package/package.json +93 -4
@@ -0,0 +1,63 @@
1
+ import type { Lobby } from "./lobby.js";
2
+ /**
3
+ * A WebSocket-shaped view of one peer link, so code written against `WebSocket` runs on a lobby unchanged:
4
+ * `lobby.asWebSocket()` on a joiner (the link to the host), `lobby.onConnection(conn => ...)` on the host (one per
5
+ * joiner). Not a real WebSocket: no URL, no subprotocols, no `addEventListener`; `bufferedAmount` is always 0.
6
+ */
7
+ export interface LobbySocket {
8
+ /** The peer at the other end (the host for a joiner's socket, the joiner for a host's connection). */
9
+ readonly peerId: number;
10
+ /** 0 CONNECTING (joiner, before `lobby.ready`), 1 OPEN, 3 CLOSED (there is no CLOSING). */
11
+ readonly readyState: 0 | 1 | 3;
12
+ readonly bufferedAmount: 0;
13
+ /** A string is sent as UTF-8; bytes as they are. Ignored unless OPEN. */
14
+ send(data: string | Uint8Array): void;
15
+ /**
16
+ * Closes this socket locally and fires `onclose` with `code` (default 1000). On a joiner it also leaves the lobby.
17
+ * On the host it only stops this connection's events: to remove the player too, call `lobby.kick(conn.peerId)`.
18
+ */
19
+ close(code?: number, reason?: string): void;
20
+ onopen: ((ev: {
21
+ type: "open";
22
+ }) => void) | null;
23
+ /** `data` is a string (UTF-8 decoded), or the bytes with `{ binary: true }`. */
24
+ onmessage: ((ev: {
25
+ type: "message";
26
+ data: string | Uint8Array;
27
+ }) => void) | null;
28
+ /** `reason` is the lobby's reason ("host_left", "timeout", "kicked", ...) unless `close()` gave one. */
29
+ onclose: ((ev: {
30
+ type: "close";
31
+ code: number;
32
+ reason: string;
33
+ wasClean: boolean;
34
+ }) => void) | null;
35
+ /** Never called (errors surface as `onclose` with code 1006); present so WebSocket code that assigns it works. */
36
+ onerror: ((ev: {
37
+ type: "error";
38
+ }) => void) | null;
39
+ }
40
+ export interface LobbySocketOptions {
41
+ /** Deliver `onmessage` data as bytes (Uint8Array) instead of UTF-8 text. Default false. */
42
+ binary?: boolean;
43
+ /** Channel for `send` (default "reliable"). "fast" is unordered and may drop, like UDP. */
44
+ channel?: "reliable" | "fast";
45
+ }
46
+ /**
47
+ * Close codes: 1000 a normal leave or `close()`, 1001 the other end went away (`host_left`, the host closing, a
48
+ * player who left), 4000 kicked, 1006 anything abnormal (`timeout`, `ice_failed`, `server_lost`, ...).
49
+ */
50
+ export declare function closeCodeFor(reason: string): number;
51
+ /**
52
+ * Joiner: the link to the host as a socket. OPEN once `lobby.ready` resolves (at once if it already has), with
53
+ * `onopen` one microtask later;
54
+ * closed when the session ends, or with 1001 "host_changed" when a server-backed lobby migrates to a new host.
55
+ */
56
+ export declare function joinerSocket(lobby: Lobby, o?: LobbySocketOptions): LobbySocket;
57
+ /**
58
+ * Host: calls `handler(conn)` once per joiner, as soon as its link is ready (and right away for joiners already
59
+ * connected). A message from a joiner the handler has not seen yet creates its connection first, so nothing is lost.
60
+ * `conn.onclose` fires when that peer leaves (code from closeCodeFor) or the host's session ends. Returns a function
61
+ * that stops new connections (existing ones keep working).
62
+ */
63
+ export declare function hostConnections(lobby: Lobby, handler: (conn: LobbySocket) => void, o?: LobbySocketOptions): () => void;
package/dist/socket.js ADDED
@@ -0,0 +1,142 @@
1
+ /**
2
+ * Close codes: 1000 a normal leave or `close()`, 1001 the other end went away (`host_left`, the host closing, a
3
+ * player who left), 4000 kicked, 1006 anything abnormal (`timeout`, `ice_failed`, `server_lost`, ...).
4
+ */
5
+ export function closeCodeFor(reason) {
6
+ if (reason === "left")
7
+ return 1000;
8
+ if (reason === "host_left" || reason === "closed")
9
+ return 1001;
10
+ if (reason === "kicked")
11
+ return 4000;
12
+ return 1006;
13
+ }
14
+ const enc = new TextEncoder();
15
+ const dec = new TextDecoder();
16
+ class PeerSocket {
17
+ peerId;
18
+ out;
19
+ binary;
20
+ onLocalClose;
21
+ readyState = 0;
22
+ bufferedAmount = 0;
23
+ onopen = null;
24
+ onmessage = null;
25
+ onclose = null;
26
+ onerror = null;
27
+ /** Unsubscribes from the lobby; set by the factory. */
28
+ detach = () => { };
29
+ constructor(peerId, out, binary, onLocalClose) {
30
+ this.peerId = peerId;
31
+ this.out = out;
32
+ this.binary = binary;
33
+ this.onLocalClose = onLocalClose;
34
+ }
35
+ send(data) {
36
+ if (this.readyState !== 1)
37
+ return;
38
+ this.out(typeof data === "string" ? enc.encode(data) : data);
39
+ }
40
+ close(code = 1000, reason = "left") {
41
+ if (this.readyState === 3)
42
+ return;
43
+ this.closed(code, reason);
44
+ this.onLocalClose();
45
+ }
46
+ /** @internal OPEN now; `onopen` now, or one microtask later so a handler assigned right after creation still runs. */
47
+ open(later = false) {
48
+ if (this.readyState !== 0)
49
+ return;
50
+ this.readyState = 1;
51
+ if (!later)
52
+ this.onopen?.({ type: "open" });
53
+ else
54
+ queueMicrotask(() => this.readyState === 1 && this.onopen?.({ type: "open" }));
55
+ }
56
+ /** @internal */
57
+ deliver(bytes) {
58
+ if (this.readyState === 1)
59
+ this.onmessage?.({ type: "message", data: this.binary ? bytes : dec.decode(bytes) });
60
+ }
61
+ /** @internal */
62
+ closed(code, reason) {
63
+ if (this.readyState === 3)
64
+ return;
65
+ this.readyState = 3;
66
+ this.detach();
67
+ this.onclose?.({ type: "close", code, reason, wasClean: code === 1000 || code === 1001 });
68
+ }
69
+ }
70
+ /**
71
+ * Joiner: the link to the host as a socket. OPEN once `lobby.ready` resolves (at once if it already has), with
72
+ * `onopen` one microtask later;
73
+ * closed when the session ends, or with 1001 "host_changed" when a server-backed lobby migrates to a new host.
74
+ */
75
+ export function joinerSocket(lobby, o = {}) {
76
+ if (lobby.isHost)
77
+ throw new Error("asWebSocket() is for joiners; the host uses lobby.onConnection()");
78
+ const channel = o.channel ?? "reliable";
79
+ const s = new PeerSocket(lobby.hostPeerId, (b) => lobby.send(lobby.hostPeerId, b, { channel }), !!o.binary, () => void lobby.leave());
80
+ const offs = [
81
+ lobby.on("message", (from, bytes) => {
82
+ if (from === lobby.hostPeerId)
83
+ s.deliver(bytes);
84
+ }),
85
+ lobby.on("sessionEnded", (reason) => s.closed(closeCodeFor(reason), reason)),
86
+ // The socket's far end is the host it opened to; after a migration that server is gone (direct lobbies end instead).
87
+ lobby.on("hostChanged", () => s.closed(1001, "host_changed")),
88
+ ];
89
+ s.detach = () => offs.forEach((f) => f());
90
+ // Already connected (made after `await lobby.ready`): OPEN at once, so a send() right away goes out.
91
+ if (lobby.connectedPeers.includes(lobby.hostPeerId))
92
+ s.open(true);
93
+ else
94
+ lobby.ready.then(() => s.open(true), () => { });
95
+ return s;
96
+ }
97
+ /**
98
+ * Host: calls `handler(conn)` once per joiner, as soon as its link is ready (and right away for joiners already
99
+ * connected). A message from a joiner the handler has not seen yet creates its connection first, so nothing is lost.
100
+ * `conn.onclose` fires when that peer leaves (code from closeCodeFor) or the host's session ends. Returns a function
101
+ * that stops new connections (existing ones keep working).
102
+ */
103
+ export function hostConnections(lobby, handler, o = {}) {
104
+ if (!lobby.isHost)
105
+ throw new Error("onConnection() is for the host; a joiner uses lobby.asWebSocket()");
106
+ const channel = o.channel ?? "reliable";
107
+ const conns = new Map();
108
+ let stopped = false;
109
+ const ensure = (peerId) => {
110
+ if (peerId === lobby.localPeerId)
111
+ return undefined;
112
+ let c = conns.get(peerId);
113
+ if (c || stopped)
114
+ return c;
115
+ c = new PeerSocket(peerId, (b) => lobby.send(peerId, b, { channel }), !!o.binary, () => { });
116
+ const conn = c;
117
+ conn.detach = () => {
118
+ if (conns.get(peerId) === conn)
119
+ conns.delete(peerId);
120
+ };
121
+ conns.set(peerId, conn);
122
+ conn.open();
123
+ handler(conn);
124
+ return conn;
125
+ };
126
+ const offs = [
127
+ lobby.on("peerReady", (p) => void ensure(p.peerId)),
128
+ lobby.on("message", (from, bytes) => ensure(from)?.deliver(bytes)),
129
+ lobby.on("peerLeft", (p, reason) => conns.get(p.peerId)?.closed(reason === "left" ? 1001 : closeCodeFor(reason), reason)),
130
+ lobby.on("sessionEnded", (reason) => {
131
+ for (const c of [...conns.values()])
132
+ c.closed(reason === "left" ? 1001 : closeCodeFor(reason), reason);
133
+ offs.forEach((f) => f());
134
+ }),
135
+ ];
136
+ for (const id of lobby.connectedPeers)
137
+ ensure(id);
138
+ return () => {
139
+ stopped = true;
140
+ };
141
+ }
142
+ //# sourceMappingURL=socket.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"socket.js","sourceRoot":"","sources":["../src/socket.ts"],"names":[],"mappings":"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","sourcesContent":["import type { Lobby, Peer } from \"./lobby.js\";\n\n/**\n * A WebSocket-shaped view of one peer link, so code written against `WebSocket` runs on a lobby unchanged:\n * `lobby.asWebSocket()` on a joiner (the link to the host), `lobby.onConnection(conn => ...)` on the host (one per\n * joiner). Not a real WebSocket: no URL, no subprotocols, no `addEventListener`; `bufferedAmount` is always 0.\n */\nexport interface LobbySocket {\n /** The peer at the other end (the host for a joiner's socket, the joiner for a host's connection). */\n readonly peerId: number;\n /** 0 CONNECTING (joiner, before `lobby.ready`), 1 OPEN, 3 CLOSED (there is no CLOSING). */\n readonly readyState: 0 | 1 | 3;\n readonly bufferedAmount: 0;\n /** A string is sent as UTF-8; bytes as they are. Ignored unless OPEN. */\n send(data: string | Uint8Array): void;\n /**\n * Closes this socket locally and fires `onclose` with `code` (default 1000). On a joiner it also leaves the lobby.\n * On the host it only stops this connection's events: to remove the player too, call `lobby.kick(conn.peerId)`.\n */\n close(code?: number, reason?: string): void;\n onopen: ((ev: { type: \"open\" }) => void) | null;\n /** `data` is a string (UTF-8 decoded), or the bytes with `{ binary: true }`. */\n onmessage: ((ev: { type: \"message\"; data: string | Uint8Array }) => void) | null;\n /** `reason` is the lobby's reason (\"host_left\", \"timeout\", \"kicked\", ...) unless `close()` gave one. */\n onclose: ((ev: { type: \"close\"; code: number; reason: string; wasClean: boolean }) => void) | null;\n /** Never called (errors surface as `onclose` with code 1006); present so WebSocket code that assigns it works. */\n onerror: ((ev: { type: \"error\" }) => void) | null;\n}\n\nexport interface LobbySocketOptions {\n /** Deliver `onmessage` data as bytes (Uint8Array) instead of UTF-8 text. Default false. */\n binary?: boolean;\n /** Channel for `send` (default \"reliable\"). \"fast\" is unordered and may drop, like UDP. */\n channel?: \"reliable\" | \"fast\";\n}\n\n/**\n * Close codes: 1000 a normal leave or `close()`, 1001 the other end went away (`host_left`, the host closing, a\n * player who left), 4000 kicked, 1006 anything abnormal (`timeout`, `ice_failed`, `server_lost`, ...).\n */\nexport function closeCodeFor(reason: string): number {\n if (reason === \"left\") return 1000;\n if (reason === \"host_left\" || reason === \"closed\") return 1001;\n if (reason === \"kicked\") return 4000;\n return 1006;\n}\n\nconst enc = new TextEncoder();\nconst dec = new TextDecoder();\n\nclass PeerSocket implements LobbySocket {\n readyState: 0 | 1 | 3 = 0;\n readonly bufferedAmount = 0 as const;\n onopen: LobbySocket[\"onopen\"] = null;\n onmessage: LobbySocket[\"onmessage\"] = null;\n onclose: LobbySocket[\"onclose\"] = null;\n onerror: LobbySocket[\"onerror\"] = null;\n /** Unsubscribes from the lobby; set by the factory. */\n detach: () => void = () => {};\n\n constructor(\n readonly peerId: number,\n private readonly out: (bytes: Uint8Array) => void,\n private readonly binary: boolean,\n private readonly onLocalClose: () => void,\n ) {}\n\n send(data: string | Uint8Array): void {\n if (this.readyState !== 1) return;\n this.out(typeof data === \"string\" ? enc.encode(data) : data);\n }\n\n close(code = 1000, reason = \"left\"): void {\n if (this.readyState === 3) return;\n this.closed(code, reason);\n this.onLocalClose();\n }\n\n /** @internal OPEN now; `onopen` now, or one microtask later so a handler assigned right after creation still runs. */\n open(later = false): void {\n if (this.readyState !== 0) return;\n this.readyState = 1;\n if (!later) this.onopen?.({ type: \"open\" });\n else queueMicrotask(() => this.readyState === 1 && this.onopen?.({ type: \"open\" }));\n }\n\n /** @internal */\n deliver(bytes: Uint8Array): void {\n if (this.readyState === 1) this.onmessage?.({ type: \"message\", data: this.binary ? bytes : dec.decode(bytes) });\n }\n\n /** @internal */\n closed(code: number, reason: string): void {\n if (this.readyState === 3) return;\n this.readyState = 3;\n this.detach();\n this.onclose?.({ type: \"close\", code, reason, wasClean: code === 1000 || code === 1001 });\n }\n}\n\n/**\n * Joiner: the link to the host as a socket. OPEN once `lobby.ready` resolves (at once if it already has), with\n * `onopen` one microtask later;\n * closed when the session ends, or with 1001 \"host_changed\" when a server-backed lobby migrates to a new host.\n */\nexport function joinerSocket(lobby: Lobby, o: LobbySocketOptions = {}): LobbySocket {\n if (lobby.isHost) throw new Error(\"asWebSocket() is for joiners; the host uses lobby.onConnection()\");\n const channel = o.channel ?? \"reliable\";\n const s = new PeerSocket(lobby.hostPeerId, (b) => lobby.send(lobby.hostPeerId, b, { channel }), !!o.binary, () => void lobby.leave());\n const offs = [\n lobby.on(\"message\", (from, bytes) => {\n if (from === lobby.hostPeerId) s.deliver(bytes);\n }),\n lobby.on(\"sessionEnded\", (reason) => s.closed(closeCodeFor(reason), reason)),\n // The socket's far end is the host it opened to; after a migration that server is gone (direct lobbies end instead).\n lobby.on(\"hostChanged\", () => s.closed(1001, \"host_changed\")),\n ];\n s.detach = () => offs.forEach((f) => f());\n // Already connected (made after `await lobby.ready`): OPEN at once, so a send() right away goes out.\n if (lobby.connectedPeers.includes(lobby.hostPeerId)) s.open(true);\n else lobby.ready.then(() => s.open(true), () => {});\n return s;\n}\n\n/**\n * Host: calls `handler(conn)` once per joiner, as soon as its link is ready (and right away for joiners already\n * connected). A message from a joiner the handler has not seen yet creates its connection first, so nothing is lost.\n * `conn.onclose` fires when that peer leaves (code from closeCodeFor) or the host's session ends. Returns a function\n * that stops new connections (existing ones keep working).\n */\nexport function hostConnections(lobby: Lobby, handler: (conn: LobbySocket) => void, o: LobbySocketOptions = {}): () => void {\n if (!lobby.isHost) throw new Error(\"onConnection() is for the host; a joiner uses lobby.asWebSocket()\");\n const channel = o.channel ?? \"reliable\";\n const conns = new Map<number, PeerSocket>();\n let stopped = false;\n const ensure = (peerId: number): PeerSocket | undefined => {\n if (peerId === lobby.localPeerId) return undefined;\n let c = conns.get(peerId);\n if (c || stopped) return c;\n c = new PeerSocket(peerId, (b) => lobby.send(peerId, b, { channel }), !!o.binary, () => {});\n const conn = c;\n conn.detach = () => {\n if (conns.get(peerId) === conn) conns.delete(peerId);\n };\n conns.set(peerId, conn);\n conn.open();\n handler(conn);\n return conn;\n };\n const offs = [\n lobby.on(\"peerReady\", (p: Peer) => void ensure(p.peerId)),\n lobby.on(\"message\", (from, bytes) => ensure(from)?.deliver(bytes)),\n lobby.on(\"peerLeft\", (p: Peer, reason) => conns.get(p.peerId)?.closed(reason === \"left\" ? 1001 : closeCodeFor(reason), reason)),\n lobby.on(\"sessionEnded\", (reason) => {\n for (const c of [...conns.values()]) c.closed(reason === \"left\" ? 1001 : closeCodeFor(reason), reason);\n offs.forEach((f) => f());\n }),\n ];\n for (const id of lobby.connectedPeers) ensure(id);\n return () => {\n stopped = true;\n };\n}\n"]}
@@ -0,0 +1,135 @@
1
+ /**
2
+ * Test doubles: a manual clock, an in-memory lobby server speaking the lk1 protocol (spec/signaling.md, the parts the
3
+ * client uses) and an in-memory peer network. Exported as `onelobby/testing`.
4
+ */
5
+ import type { Clock } from "./clock.js";
6
+ import type { ChannelName, LinkFactory, LinkOptions, PeerLink, SignalPayload, SocketFactory, SocketLike } from "./transport.js";
7
+ /** Deterministic clock: time only moves through `advance()`. Timers fire in (time, creation) order. */
8
+ export declare class ManualClock implements Clock {
9
+ private t;
10
+ private seq;
11
+ private timers;
12
+ now(): number;
13
+ setInterval(fn: () => void, ms: number): () => void;
14
+ setTimeout(fn: () => void, ms: number): () => void;
15
+ /** Advance time, firing due timers one at a time (microtasks flush between them when awaited via `run`). */
16
+ advance(ms: number): void;
17
+ /** Advance in `step` ms slices, letting promises and queued microtasks settle between slices. */
18
+ run(ms: number, step?: number): Promise<void>;
19
+ }
20
+ /** Let all pending microtasks (and chained promise continuations) run. */
21
+ export declare function settle(rounds?: number): Promise<void>;
22
+ /**
23
+ * Pairs `PeerLink`s through the signaling path like WebRTC would: the offerer's SDP string names it, the answerer
24
+ * resolves that name, and both open one microtask later. Messages are delivered FIFO, one microtask later.
25
+ */
26
+ export declare class MemoryNetwork {
27
+ private offers;
28
+ /** Every link ever created (for assertions and fault injection). */
29
+ readonly links: MemoryLink[];
30
+ /** Set to drop all traffic in one direction pair for fault injection: key `${from}->${to}` is not tracked; use link.mute(). */
31
+ readonly factory: LinkFactory;
32
+ /** @internal */
33
+ register(offer: string, link: MemoryLink): void;
34
+ /** @internal */
35
+ lookup(offer: string): MemoryLink | undefined;
36
+ /** Kill every link whose far end is this identity-tagged factory owner (set `tag` via MemoryLink.owner). */
37
+ killOwner(owner: string): void;
38
+ /** Per-owner factory so tests can tell whose link is whose and cut one peer off. */
39
+ factoryFor(owner: string): LinkFactory;
40
+ }
41
+ export declare class MemoryLink implements PeerLink {
42
+ private net;
43
+ readonly remoteId: number;
44
+ isOpen: boolean;
45
+ owner: string;
46
+ onSignal: PeerLink["onSignal"];
47
+ onOpen: PeerLink["onOpen"];
48
+ onClose: PeerLink["onClose"];
49
+ onMessage: PeerLink["onMessage"];
50
+ onPath: PeerLink["onPath"];
51
+ /** Bytes sent per channel, for assertions. */
52
+ readonly sent: Record<ChannelName, number>;
53
+ /** While true, outgoing messages vanish (a frozen or partitioned peer). */
54
+ blackhole: boolean;
55
+ private peer;
56
+ private closed;
57
+ private id;
58
+ constructor(net: MemoryNetwork, opts: LinkOptions);
59
+ startOffer(): void;
60
+ handleSignal(s: SignalPayload): void;
61
+ /** @internal */
62
+ accept(answerer: MemoryLink): void;
63
+ private open;
64
+ send(channel: ChannelName, bytes: Uint8Array): void;
65
+ close(): void;
66
+ private closeFromRemote;
67
+ }
68
+ type Json = Record<string, unknown>;
69
+ interface MPeer {
70
+ peerId: number;
71
+ name: string;
72
+ joinedAt: number;
73
+ sock: MemorySocket | null;
74
+ token: string;
75
+ }
76
+ interface MLobby {
77
+ code: string;
78
+ gameId: string;
79
+ netVersion: string;
80
+ name: string;
81
+ hostId: number;
82
+ epoch: number;
83
+ mode: string;
84
+ tickRate: number;
85
+ lateJoin: boolean;
86
+ phase: string;
87
+ meta: Record<string, string>;
88
+ visibility: string;
89
+ maxPlayers: number;
90
+ peers: Map<number, MPeer>;
91
+ next: number;
92
+ kicked: Set<number>;
93
+ reserved: Map<string, MPeer>;
94
+ }
95
+ /** Minimal onelobby-server (lk1) for client tests. Not a conformance target: stream A owns the real server. */
96
+ export declare class MemoryServer {
97
+ readonly lobbies: Map<string, MLobby>;
98
+ /** Every socket ever opened (to drop one from a test). */
99
+ readonly sockets: MemorySocket[];
100
+ readonly socketFactory: SocketFactory;
101
+ private n;
102
+ iceServers: {
103
+ urls: string[];
104
+ }[];
105
+ /** Log of every client message, for assertions. */
106
+ readonly received: Json[];
107
+ connect(): MemorySocket;
108
+ /** @internal */
109
+ newCode(): string;
110
+ }
111
+ export declare class MemorySocket implements SocketLike {
112
+ private srv;
113
+ readyState: number;
114
+ onopen: SocketLike["onopen"];
115
+ onmessage: SocketLike["onmessage"];
116
+ onclose: SocketLike["onclose"];
117
+ onerror: SocketLike["onerror"];
118
+ private hello;
119
+ private lobby;
120
+ private peer;
121
+ private closed;
122
+ constructor(srv: MemoryServer);
123
+ send(data: string): void;
124
+ close(): void;
125
+ /** @internal server -> this client */
126
+ push(m: Json): void;
127
+ private err;
128
+ private lobbyMsg;
129
+ private broadcast;
130
+ private leaveLobby;
131
+ private handle;
132
+ }
133
+ /** Convenience: kill a socket as if the network dropped (server sees a timeout, others get peer_left). */
134
+ export declare function dropSocket(s: SocketLike): void;
135
+ export {};