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.
- package/LICENSE +21 -0
- package/README.md +77 -2
- package/dist/chat.d.ts +60 -0
- package/dist/chat.js +163 -0
- package/dist/chat.js.map +1 -0
- package/dist/chunking.d.ts +17 -0
- package/dist/chunking.js +59 -0
- package/dist/chunking.js.map +1 -0
- package/dist/clock.d.ts +8 -0
- package/dist/clock.js +39 -0
- package/dist/clock.js.map +1 -0
- package/dist/codec.d.ts +141 -0
- package/dist/codec.js +308 -0
- package/dist/codec.js.map +1 -0
- package/dist/crockford.d.ts +14 -0
- package/dist/crockford.js +55 -0
- package/dist/crockford.js.map +1 -0
- package/dist/deflate.d.ts +4 -0
- package/dist/deflate.js +32 -0
- package/dist/deflate.js.map +1 -0
- package/dist/deprecated.d.ts +18 -0
- package/dist/deprecated.js +11 -0
- package/dist/deprecated.js.map +1 -0
- package/dist/direct/hub-host.d.ts +82 -0
- package/dist/direct/hub-host.js +393 -0
- package/dist/direct/hub-host.js.map +1 -0
- package/dist/direct/hub.d.ts +178 -0
- package/dist/direct/hub.js +862 -0
- package/dist/direct/hub.js.map +1 -0
- package/dist/direct/index.d.ts +57 -0
- package/dist/direct/index.js +71 -0
- package/dist/direct/index.js.map +1 -0
- package/dist/direct/portmap.d.ts +84 -0
- package/dist/direct/portmap.js +465 -0
- package/dist/direct/portmap.js.map +1 -0
- package/dist/direct-address.d.ts +20 -0
- package/dist/direct-address.js +61 -0
- package/dist/direct-address.js.map +1 -0
- package/dist/emitter.d.ts +9 -0
- package/dist/emitter.js +36 -0
- package/dist/emitter.js.map +1 -0
- package/dist/friendly.d.ts +20 -0
- package/dist/friendly.js +120 -0
- package/dist/friendly.js.map +1 -0
- package/dist/global.d.ts +8 -0
- package/dist/global.js +9 -0
- package/dist/global.js.map +1 -0
- package/dist/gridpaint.d.ts +27 -0
- package/dist/gridpaint.js +106 -0
- package/dist/gridpaint.js.map +1 -0
- package/dist/hub.d.ts +25 -0
- package/dist/hub.js +2 -0
- package/dist/hub.js.map +1 -0
- package/dist/index.d.ts +35 -0
- package/dist/index.js +25 -0
- package/dist/index.js.map +1 -0
- package/dist/internals.d.ts +28 -0
- package/dist/internals.js +24 -0
- package/dist/internals.js.map +1 -0
- package/dist/joincode.d.ts +21 -0
- package/dist/joincode.js +84 -0
- package/dist/joincode.js.map +1 -0
- package/dist/lobby.d.ts +203 -0
- package/dist/lobby.js +840 -0
- package/dist/lobby.js.map +1 -0
- package/dist/lockstep.d.ts +132 -0
- package/dist/lockstep.js +471 -0
- package/dist/lockstep.js.map +1 -0
- package/dist/migration.d.ts +17 -0
- package/dist/migration.js +34 -0
- package/dist/migration.js.map +1 -0
- package/dist/onelobby.bundle.mjs +140 -0
- package/dist/onelobby.bundle.mjs.map +7 -0
- package/dist/onelobby.d.ts +151 -0
- package/dist/onelobby.global.js +3897 -0
- package/dist/onelobby.global.js.map +7 -0
- package/dist/onelobby.global.min.js +140 -0
- package/dist/onelobby.global.min.js.map +7 -0
- package/dist/onelobby.js +337 -0
- package/dist/onelobby.js.map +1 -0
- package/dist/signaling.d.ts +94 -0
- package/dist/signaling.js +134 -0
- package/dist/signaling.js.map +1 -0
- package/dist/socket.d.ts +63 -0
- package/dist/socket.js +142 -0
- package/dist/socket.js.map +1 -0
- package/dist/testing.d.ts +135 -0
- package/dist/testing.js +438 -0
- package/dist/testing.js.map +1 -0
- package/dist/transport.d.ts +80 -0
- package/dist/transport.js +7 -0
- package/dist/transport.js.map +1 -0
- package/dist/ui/controller.d.ts +137 -0
- package/dist/ui/controller.js +337 -0
- package/dist/ui/controller.js.map +1 -0
- package/dist/ui/element.d.ts +22 -0
- package/dist/ui/element.js +284 -0
- package/dist/ui/element.js.map +1 -0
- package/dist/ui/index.d.ts +5 -0
- package/dist/ui/index.js +9 -0
- package/dist/ui/index.js.map +1 -0
- package/dist/ui/server-url.d.ts +9 -0
- package/dist/ui/server-url.js +39 -0
- package/dist/ui/server-url.js.map +1 -0
- package/dist/ui/styles.d.ts +2 -0
- package/dist/ui/styles.js +70 -0
- package/dist/ui/styles.js.map +1 -0
- package/dist/voice-browser.d.ts +19 -0
- package/dist/voice-browser.js +151 -0
- package/dist/voice-browser.js.map +1 -0
- package/dist/voice-core.d.ts +98 -0
- package/dist/voice-core.js +233 -0
- package/dist/voice-core.js.map +1 -0
- package/dist/voice-types.d.ts +35 -0
- package/dist/voice-types.js +3 -0
- package/dist/voice-types.js.map +1 -0
- package/dist/voice.d.ts +68 -0
- package/dist/voice.js +223 -0
- package/dist/voice.js.map +1 -0
- package/dist/webrtc.d.ts +29 -0
- package/dist/webrtc.js +169 -0
- package/dist/webrtc.js.map +1 -0
- package/package.json +93 -4
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Parker Tonra
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,78 @@
|
|
|
1
|
-
#
|
|
1
|
+
# onelobby
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Multiplayer for browser games. Players host a lobby, share a six-letter code, and join each other over peer-to-peer
|
|
4
|
+
WebRTC. Includes lockstep sync, raw messages, text chat and voice chat. No runtime dependencies.
|
|
5
|
+
|
|
6
|
+
You need a OneLobby lobby server (the WebSocket URL, like `wss://lobby.example.com/ws`). See the
|
|
7
|
+
[docs](https://github.com/ParkerTonra/onelobby#readme) to run one.
|
|
8
|
+
|
|
9
|
+
## Use it from a `<script>` tag (no build tools)
|
|
10
|
+
|
|
11
|
+
```html
|
|
12
|
+
<script src="https://cdn.jsdelivr.net/npm/onelobby@0.2.0/dist/onelobby.global.min.js"></script>
|
|
13
|
+
<script>
|
|
14
|
+
const kit = new OneLobby.OneLobby({
|
|
15
|
+
gameId: "com.you.yourgame", // any unique name for your game
|
|
16
|
+
netVersion: "1",
|
|
17
|
+
lobbyServer: "wss://lobby.example.com/ws",
|
|
18
|
+
mode: "raw",
|
|
19
|
+
});
|
|
20
|
+
// Host: const lobby = await kit.host({ name: "My game", maxPlayers: 4 }); show OneLobby.formatCode(lobby.code)
|
|
21
|
+
// Join: const lobby = await kit.join("C5A FX8");
|
|
22
|
+
</script>
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
The file also works saved next to your page (`<script src="onelobby.global.min.js">`), including pages opened from disk.
|
|
26
|
+
|
|
27
|
+
## No code at all
|
|
28
|
+
|
|
29
|
+
After the `<script>` tag above, this gives a complete lobby box (the sentence inside only shows if the script did not
|
|
30
|
+
load):
|
|
31
|
+
|
|
32
|
+
```html
|
|
33
|
+
<one-lobby server="https://your-name.onrender.com" game="my-game" max-players="4">
|
|
34
|
+
If you can read this, the OneLobby script didn't load.
|
|
35
|
+
</one-lobby>
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
See the [no-code guide](https://github.com/ParkerTonra/onelobby/blob/main/docs/no-code.md).
|
|
39
|
+
|
|
40
|
+
## Install with npm
|
|
41
|
+
|
|
42
|
+
```
|
|
43
|
+
npm install onelobby
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
```ts
|
|
47
|
+
import { OneLobby, formatCode } from "onelobby";
|
|
48
|
+
|
|
49
|
+
const kit = new OneLobby({ gameId: "com.you.yourgame", netVersion: "1", lobbyServer: "wss://lobby.example.com/ws" });
|
|
50
|
+
const lobby = await kit.host({ name: "My game", maxPlayers: 4 });
|
|
51
|
+
console.log("Tell your friend this code:", formatCode(lobby.code));
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
Without a bundler: `import { OneLobby } from "https://cdn.jsdelivr.net/npm/onelobby@0.2.0/dist/onelobby.bundle.mjs"`.
|
|
55
|
+
|
|
56
|
+
## Made for players
|
|
57
|
+
|
|
58
|
+
- Lobbies are **unlisted** by default: friends join with the code or a link (`OneLobby.shareUrl(lobby.code)` makes
|
|
59
|
+
`https://your.game/?join=C5A-FX8`; `OneLobby.codeFromUrl()` reads it back). Pass `visibility: "public"` to list it.
|
|
60
|
+
- Players without a name show as **"Player 2"**, "Player 3", ...
|
|
61
|
+
- A joiner that can't connect gets an error after 15 s (`readyTimeoutMs`) instead of waiting forever: code
|
|
62
|
+
`ice_failed`, and `sessionEnded` with that reason. One relay retry happens first when the server offers TURN.
|
|
63
|
+
- A page **reload keeps the seat** (`rememberSeat`, on by default): `kit.join(sameCode)` or `kit.resume()` takes it back;
|
|
64
|
+
`kit.rememberedCode` says which lobby.
|
|
65
|
+
- `OneLobby.describeError(err)` turns any error into `{ message, hint }` you can show as-is.
|
|
66
|
+
- Test seams go under `advanced: { socketFactory, linkFactory, clock, storage, ... }`.
|
|
67
|
+
|
|
68
|
+
## Files
|
|
69
|
+
|
|
70
|
+
| Import | What |
|
|
71
|
+
|---|---|
|
|
72
|
+
| `onelobby` | the public API: `OneLobby`, `Lobby`, `Lockstep`, `Chat`, `Voice`, `OneLobbyError`, `normalizeCode`, `formatCode`, `describeError`, `shareUrl`, `codeFromUrl` |
|
|
73
|
+
| `onelobby/ui` | the `<one-lobby>` drop-in element (host, join, invite link, players, start); also in the `<script>` build |
|
|
74
|
+
| `onelobby/testing` | in-memory server, peer network and clock for your own tests |
|
|
75
|
+
| `onelobby/internals` | low-level pieces (codec, chunking, migration, jitter buffer, ...); no stability promise |
|
|
76
|
+
| `dist/onelobby.global.min.js` | the `<script>` build; sets `window.OneLobby` |
|
|
77
|
+
|
|
78
|
+
Guides, recipes and the API reference: [OneLobby docs](https://github.com/ParkerTonra/onelobby#readme). MIT license.
|
package/dist/chat.d.ts
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { type ChatRejectReason, type Message } from "./codec.js";
|
|
2
|
+
import { Emitter } from "./emitter.js";
|
|
3
|
+
import type { Hub } from "./hub.js";
|
|
4
|
+
export declare const CHAT_MAX_BYTES = 256;
|
|
5
|
+
export declare const CHAT_BURST = 3;
|
|
6
|
+
export declare const CHAT_WINDOW_MS = 5000;
|
|
7
|
+
export declare const CHAT_PER_WINDOW = 5;
|
|
8
|
+
export declare const CHAT_HISTORY_LINES = 100;
|
|
9
|
+
export declare function sanitizeChat(text: string): string;
|
|
10
|
+
export declare function utf8Length(text: string): number;
|
|
11
|
+
/**
|
|
12
|
+
* Token bucket per peer: `CHAT_PER_WINDOW` messages per `CHAT_WINDOW_MS`, burst `CHAT_BURST`
|
|
13
|
+
* (capacity 3, refilled one token per second).
|
|
14
|
+
*/
|
|
15
|
+
export declare class ChatLimiter {
|
|
16
|
+
private buckets;
|
|
17
|
+
private refillMs;
|
|
18
|
+
tryTake(peerId: number, now: number): boolean;
|
|
19
|
+
forget(peerId: number): void;
|
|
20
|
+
}
|
|
21
|
+
export interface ChatMessage {
|
|
22
|
+
peerId: number;
|
|
23
|
+
id: number;
|
|
24
|
+
ts: number;
|
|
25
|
+
text: string;
|
|
26
|
+
}
|
|
27
|
+
export type ChatEvents = {
|
|
28
|
+
message: [ChatMessage];
|
|
29
|
+
rejected: [id: number, reason: ChatRejectReason];
|
|
30
|
+
/** The host's `chat_history` arrived (joiners, right after the handshake). */
|
|
31
|
+
history: [readonly ChatMessage[]];
|
|
32
|
+
};
|
|
33
|
+
/** Text chat (DESIGN s.9b): sender -> host (validates, stamps) -> everyone, over the reliable channel. */
|
|
34
|
+
export declare class Chat extends Emitter<ChatEvents> {
|
|
35
|
+
private hub;
|
|
36
|
+
private wall;
|
|
37
|
+
private isLocallyMuted;
|
|
38
|
+
/** Game-provided profanity hook; applied to every line a player is shown. */
|
|
39
|
+
filter: (text: string) => string;
|
|
40
|
+
private lines;
|
|
41
|
+
private limiter;
|
|
42
|
+
private hostMuted;
|
|
43
|
+
private localId;
|
|
44
|
+
private stampId;
|
|
45
|
+
constructor(hub: Hub, wall: () => number, isLocallyMuted: (peerId: number) => boolean);
|
|
46
|
+
get history(): readonly ChatMessage[];
|
|
47
|
+
/** Returns the local message id (what `rejected` will refer to). */
|
|
48
|
+
send(text: string): number;
|
|
49
|
+
/** Host only: drop a peer's chat for everyone (`chat_reject` reason 3). */
|
|
50
|
+
hostMute(peerId: number, muted: boolean): void;
|
|
51
|
+
isHostMuted(peerId: number): boolean;
|
|
52
|
+
onPeerLeft(peerId: number): void;
|
|
53
|
+
/** Host: send recent lines to a freshly connected peer. */
|
|
54
|
+
sendHistory(peerId: number): void;
|
|
55
|
+
handle(from: number, route: number, msg: Message): void;
|
|
56
|
+
private hostProcess;
|
|
57
|
+
private deliver;
|
|
58
|
+
private push;
|
|
59
|
+
private safeFilter;
|
|
60
|
+
}
|
package/dist/chat.js
ADDED
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
import { CHAT_REJECT, chatRejectName } from "./codec.js";
|
|
2
|
+
import { Emitter } from "./emitter.js";
|
|
3
|
+
export const CHAT_MAX_BYTES = 256;
|
|
4
|
+
export const CHAT_BURST = 3;
|
|
5
|
+
export const CHAT_WINDOW_MS = 5000;
|
|
6
|
+
export const CHAT_PER_WINDOW = 5;
|
|
7
|
+
export const CHAT_HISTORY_LINES = 100;
|
|
8
|
+
/** Control characters (C0, DEL, C1) and bidi overrides/isolates/marks are removed at the host. */
|
|
9
|
+
// eslint-disable-next-line no-control-regex
|
|
10
|
+
const STRIP = /[\u0000-\u001f\u007f-\u009f\u202a-\u202e\u2066-\u2069]/g;
|
|
11
|
+
export function sanitizeChat(text) {
|
|
12
|
+
return text.replace(STRIP, "");
|
|
13
|
+
}
|
|
14
|
+
export function utf8Length(text) {
|
|
15
|
+
return new TextEncoder().encode(text).length;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Token bucket per peer: `CHAT_PER_WINDOW` messages per `CHAT_WINDOW_MS`, burst `CHAT_BURST`
|
|
19
|
+
* (capacity 3, refilled one token per second).
|
|
20
|
+
*/
|
|
21
|
+
export class ChatLimiter {
|
|
22
|
+
buckets = new Map();
|
|
23
|
+
refillMs = CHAT_WINDOW_MS / CHAT_PER_WINDOW;
|
|
24
|
+
tryTake(peerId, now) {
|
|
25
|
+
let b = this.buckets.get(peerId);
|
|
26
|
+
if (!b)
|
|
27
|
+
this.buckets.set(peerId, (b = { tokens: CHAT_BURST, at: now }));
|
|
28
|
+
b.tokens = Math.min(CHAT_BURST, b.tokens + (now - b.at) / this.refillMs);
|
|
29
|
+
b.at = now;
|
|
30
|
+
if (b.tokens < 1)
|
|
31
|
+
return false;
|
|
32
|
+
b.tokens -= 1;
|
|
33
|
+
return true;
|
|
34
|
+
}
|
|
35
|
+
forget(peerId) {
|
|
36
|
+
this.buckets.delete(peerId);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
/** Text chat (DESIGN s.9b): sender -> host (validates, stamps) -> everyone, over the reliable channel. */
|
|
40
|
+
export class Chat extends Emitter {
|
|
41
|
+
hub;
|
|
42
|
+
wall;
|
|
43
|
+
isLocallyMuted;
|
|
44
|
+
/** Game-provided profanity hook; applied to every line a player is shown. */
|
|
45
|
+
filter = (t) => t;
|
|
46
|
+
lines = [];
|
|
47
|
+
limiter = new ChatLimiter();
|
|
48
|
+
hostMuted = new Set();
|
|
49
|
+
localId = 0;
|
|
50
|
+
stampId = 0;
|
|
51
|
+
constructor(hub, wall, isLocallyMuted) {
|
|
52
|
+
super();
|
|
53
|
+
this.hub = hub;
|
|
54
|
+
this.wall = wall;
|
|
55
|
+
this.isLocallyMuted = isLocallyMuted;
|
|
56
|
+
}
|
|
57
|
+
get history() {
|
|
58
|
+
return this.lines;
|
|
59
|
+
}
|
|
60
|
+
/** Returns the local message id (what `rejected` will refer to). */
|
|
61
|
+
send(text) {
|
|
62
|
+
const id = ++this.localId >>> 0;
|
|
63
|
+
if (utf8Length(text) > CHAT_MAX_BYTES * 4) {
|
|
64
|
+
// Do not even ship absurd payloads; the host would reject on length anyway.
|
|
65
|
+
queueMicrotask(() => this.emit("rejected", id, "length"));
|
|
66
|
+
return id;
|
|
67
|
+
}
|
|
68
|
+
if (this.hub.isHost)
|
|
69
|
+
this.hostProcess(this.hub.localPeerId, id, text);
|
|
70
|
+
else
|
|
71
|
+
this.hub.sendToHost({ type: "chat", id, ts: 0, text });
|
|
72
|
+
return id;
|
|
73
|
+
}
|
|
74
|
+
/** Host only: drop a peer's chat for everyone (`chat_reject` reason 3). */
|
|
75
|
+
hostMute(peerId, muted) {
|
|
76
|
+
if (!this.hub.isHost)
|
|
77
|
+
throw new Error("only the host can mute chat");
|
|
78
|
+
if (muted)
|
|
79
|
+
this.hostMuted.add(peerId);
|
|
80
|
+
else
|
|
81
|
+
this.hostMuted.delete(peerId);
|
|
82
|
+
}
|
|
83
|
+
isHostMuted(peerId) {
|
|
84
|
+
return this.hostMuted.has(peerId);
|
|
85
|
+
}
|
|
86
|
+
onPeerLeft(peerId) {
|
|
87
|
+
this.limiter.forget(peerId);
|
|
88
|
+
this.hostMuted.delete(peerId);
|
|
89
|
+
}
|
|
90
|
+
/** Host: send recent lines to a freshly connected peer. */
|
|
91
|
+
sendHistory(peerId) {
|
|
92
|
+
const lines = this.lines.slice(-CHAT_HISTORY_LINES).map((l) => ({ ...l }));
|
|
93
|
+
this.hub.sendTo(peerId, { type: "chat_history", lines }, 0);
|
|
94
|
+
}
|
|
95
|
+
handle(from, route, msg) {
|
|
96
|
+
switch (msg.type) {
|
|
97
|
+
case "chat":
|
|
98
|
+
if (this.hub.isHost)
|
|
99
|
+
this.hostProcess(from, msg.id, msg.text);
|
|
100
|
+
else
|
|
101
|
+
this.deliver({ peerId: route, id: msg.id, ts: msg.ts, text: msg.text });
|
|
102
|
+
break;
|
|
103
|
+
case "chat_history":
|
|
104
|
+
if (!this.hub.isHost) {
|
|
105
|
+
this.lines = [];
|
|
106
|
+
for (const l of msg.lines.slice(-CHAT_HISTORY_LINES))
|
|
107
|
+
this.push({ ...l, text: this.safeFilter(l.text) });
|
|
108
|
+
this.emit("history", this.lines);
|
|
109
|
+
}
|
|
110
|
+
break;
|
|
111
|
+
case "chat_reject":
|
|
112
|
+
this.emit("rejected", msg.id, chatRejectName(msg.reason));
|
|
113
|
+
break;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
// ---- host path ----------------------------------------------------------------------------------------------
|
|
117
|
+
hostProcess(from, id, rawText) {
|
|
118
|
+
const reject = (code) => {
|
|
119
|
+
if (from === this.hub.localPeerId)
|
|
120
|
+
this.emit("rejected", id, chatRejectName(code));
|
|
121
|
+
else
|
|
122
|
+
this.hub.sendTo(from, { type: "chat_reject", id, reason: code }, 0);
|
|
123
|
+
};
|
|
124
|
+
if (this.hostMuted.has(from))
|
|
125
|
+
return reject(CHAT_REJECT.mutedByHost);
|
|
126
|
+
// spec order: muted, length (before stripping), rate (a reject spends no token), strip, drop if empty
|
|
127
|
+
if (utf8Length(rawText) > CHAT_MAX_BYTES)
|
|
128
|
+
return reject(CHAT_REJECT.length);
|
|
129
|
+
if (!this.limiter.tryTake(from, this.hub.clock.now()))
|
|
130
|
+
return reject(CHAT_REJECT.rate);
|
|
131
|
+
const text = sanitizeChat(rawText);
|
|
132
|
+
if (text.length === 0)
|
|
133
|
+
return;
|
|
134
|
+
const line = { peerId: from, id: ++this.stampId >>> 0, ts: this.wall(), text };
|
|
135
|
+
const out = { type: "chat", id: line.id, ts: line.ts, text: line.text };
|
|
136
|
+
for (const p of this.hub.connectedPeers())
|
|
137
|
+
this.hub.sendTo(p, out, from);
|
|
138
|
+
this.deliver(line);
|
|
139
|
+
}
|
|
140
|
+
deliver(line) {
|
|
141
|
+
const shown = { ...line, text: this.safeFilter(line.text) };
|
|
142
|
+
this.push(shown);
|
|
143
|
+
if (this.isLocallyMuted(line.peerId))
|
|
144
|
+
return; // local mute drops the peer's chat, history included
|
|
145
|
+
this.emit("message", shown);
|
|
146
|
+
}
|
|
147
|
+
push(line) {
|
|
148
|
+
if (this.isLocallyMuted(line.peerId))
|
|
149
|
+
return;
|
|
150
|
+
this.lines.push(line);
|
|
151
|
+
if (this.lines.length > CHAT_HISTORY_LINES)
|
|
152
|
+
this.lines.splice(0, this.lines.length - CHAT_HISTORY_LINES);
|
|
153
|
+
}
|
|
154
|
+
safeFilter(text) {
|
|
155
|
+
try {
|
|
156
|
+
return this.filter(text);
|
|
157
|
+
}
|
|
158
|
+
catch {
|
|
159
|
+
return text;
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
//# sourceMappingURL=chat.js.map
|
package/dist/chat.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"chat.js","sourceRoot":"","sources":["../src/chat.ts"],"names":[],"mappings":"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","sourcesContent":["import { CHAT_REJECT, chatRejectName, type ChatLine, type ChatRejectReason, type Message } from \"./codec.js\";\nimport { Emitter } from \"./emitter.js\";\nimport type { Hub } from \"./hub.js\";\n\nexport const CHAT_MAX_BYTES = 256;\nexport const CHAT_BURST = 3;\nexport const CHAT_WINDOW_MS = 5000;\nexport const CHAT_PER_WINDOW = 5;\nexport const CHAT_HISTORY_LINES = 100;\n\n/** Control characters (C0, DEL, C1) and bidi overrides/isolates/marks are removed at the host. */\n// eslint-disable-next-line no-control-regex\nconst STRIP = /[\\u0000-\\u001f\\u007f-\\u009f\\u202a-\\u202e\\u2066-\\u2069]/g;\n\nexport function sanitizeChat(text: string): string {\n return text.replace(STRIP, \"\");\n}\n\nexport function utf8Length(text: string): number {\n return new TextEncoder().encode(text).length;\n}\n\n/**\n * Token bucket per peer: `CHAT_PER_WINDOW` messages per `CHAT_WINDOW_MS`, burst `CHAT_BURST`\n * (capacity 3, refilled one token per second).\n */\nexport class ChatLimiter {\n private buckets = new Map<number, { tokens: number; at: number }>();\n private refillMs = CHAT_WINDOW_MS / CHAT_PER_WINDOW;\n\n tryTake(peerId: number, now: number): boolean {\n let b = this.buckets.get(peerId);\n if (!b) this.buckets.set(peerId, (b = { tokens: CHAT_BURST, at: now }));\n b.tokens = Math.min(CHAT_BURST, b.tokens + (now - b.at) / this.refillMs);\n b.at = now;\n if (b.tokens < 1) return false;\n b.tokens -= 1;\n return true;\n }\n\n forget(peerId: number): void {\n this.buckets.delete(peerId);\n }\n}\n\nexport interface ChatMessage {\n peerId: number;\n id: number;\n ts: number;\n text: string;\n}\n\nexport type ChatEvents = {\n message: [ChatMessage];\n rejected: [id: number, reason: ChatRejectReason];\n /** The host's `chat_history` arrived (joiners, right after the handshake). */\n history: [readonly ChatMessage[]];\n};\n\n/** Text chat (DESIGN s.9b): sender -> host (validates, stamps) -> everyone, over the reliable channel. */\nexport class Chat extends Emitter<ChatEvents> {\n /** Game-provided profanity hook; applied to every line a player is shown. */\n filter: (text: string) => string = (t) => t;\n\n private lines: ChatMessage[] = [];\n private limiter = new ChatLimiter();\n private hostMuted = new Set<number>();\n private localId = 0;\n private stampId = 0;\n\n constructor(\n private hub: Hub,\n private wall: () => number,\n private isLocallyMuted: (peerId: number) => boolean,\n ) {\n super();\n }\n\n get history(): readonly ChatMessage[] {\n return this.lines;\n }\n\n /** Returns the local message id (what `rejected` will refer to). */\n send(text: string): number {\n const id = ++this.localId >>> 0;\n if (utf8Length(text) > CHAT_MAX_BYTES * 4) {\n // Do not even ship absurd payloads; the host would reject on length anyway.\n queueMicrotask(() => this.emit(\"rejected\", id, \"length\"));\n return id;\n }\n if (this.hub.isHost) this.hostProcess(this.hub.localPeerId, id, text);\n else this.hub.sendToHost({ type: \"chat\", id, ts: 0, text });\n return id;\n }\n\n /** Host only: drop a peer's chat for everyone (`chat_reject` reason 3). */\n hostMute(peerId: number, muted: boolean): void {\n if (!this.hub.isHost) throw new Error(\"only the host can mute chat\");\n if (muted) this.hostMuted.add(peerId);\n else this.hostMuted.delete(peerId);\n }\n\n isHostMuted(peerId: number): boolean {\n return this.hostMuted.has(peerId);\n }\n\n onPeerLeft(peerId: number): void {\n this.limiter.forget(peerId);\n this.hostMuted.delete(peerId);\n }\n\n /** Host: send recent lines to a freshly connected peer. */\n sendHistory(peerId: number): void {\n const lines: ChatLine[] = this.lines.slice(-CHAT_HISTORY_LINES).map((l) => ({ ...l }));\n this.hub.sendTo(peerId, { type: \"chat_history\", lines }, 0);\n }\n\n handle(from: number, route: number, msg: Message): void {\n switch (msg.type) {\n case \"chat\":\n if (this.hub.isHost) this.hostProcess(from, msg.id, msg.text);\n else this.deliver({ peerId: route, id: msg.id, ts: msg.ts, text: msg.text });\n break;\n case \"chat_history\":\n if (!this.hub.isHost) {\n this.lines = [];\n for (const l of msg.lines.slice(-CHAT_HISTORY_LINES)) this.push({ ...l, text: this.safeFilter(l.text) });\n this.emit(\"history\", this.lines);\n }\n break;\n case \"chat_reject\":\n this.emit(\"rejected\", msg.id, chatRejectName(msg.reason));\n break;\n }\n }\n\n // ---- host path ----------------------------------------------------------------------------------------------\n\n private hostProcess(from: number, id: number, rawText: string): void {\n const reject = (code: number) => {\n if (from === this.hub.localPeerId) this.emit(\"rejected\", id, chatRejectName(code));\n else this.hub.sendTo(from, { type: \"chat_reject\", id, reason: code }, 0);\n };\n if (this.hostMuted.has(from)) return reject(CHAT_REJECT.mutedByHost);\n // spec order: muted, length (before stripping), rate (a reject spends no token), strip, drop if empty\n if (utf8Length(rawText) > CHAT_MAX_BYTES) return reject(CHAT_REJECT.length);\n if (!this.limiter.tryTake(from, this.hub.clock.now())) return reject(CHAT_REJECT.rate);\n const text = sanitizeChat(rawText);\n if (text.length === 0) return;\n const line: ChatMessage = { peerId: from, id: ++this.stampId >>> 0, ts: this.wall(), text };\n const out: Message = { type: \"chat\", id: line.id, ts: line.ts, text: line.text };\n for (const p of this.hub.connectedPeers()) this.hub.sendTo(p, out, from);\n this.deliver(line);\n }\n\n private deliver(line: ChatMessage): void {\n const shown: ChatMessage = { ...line, text: this.safeFilter(line.text) };\n this.push(shown);\n if (this.isLocallyMuted(line.peerId)) return; // local mute drops the peer's chat, history included\n this.emit(\"message\", shown);\n }\n\n private push(line: ChatMessage): void {\n if (this.isLocallyMuted(line.peerId)) return;\n this.lines.push(line);\n if (this.lines.length > CHAT_HISTORY_LINES) this.lines.splice(0, this.lines.length - CHAT_HISTORY_LINES);\n }\n\n private safeFilter(text: string): string {\n try {\n return this.filter(text);\n } catch {\n return text;\n }\n }\n}\n"]}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { type Message } from "./codec.js";
|
|
2
|
+
/** Chunk body capacity: 16 KiB frame - 3 header - 8 chunk header, rounded down for headroom. */
|
|
3
|
+
export declare const CHUNK_PAYLOAD = 16000;
|
|
4
|
+
export declare function splitChunks(id: number, bytes: Uint8Array, size?: number): Message[];
|
|
5
|
+
/** Reassembles chunked payloads by id. Duplicates are ignored; out-of-order chunks are fine. */
|
|
6
|
+
export declare class Reassembler {
|
|
7
|
+
private parts;
|
|
8
|
+
/** Returns the full payload when the last chunk arrives, otherwise null. */
|
|
9
|
+
push(chunk: {
|
|
10
|
+
id: number;
|
|
11
|
+
index: number;
|
|
12
|
+
count: number;
|
|
13
|
+
bytes: Uint8Array;
|
|
14
|
+
}): Uint8Array | null;
|
|
15
|
+
get pending(): number;
|
|
16
|
+
clear(): void;
|
|
17
|
+
}
|
package/dist/chunking.js
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { CodecError, MAX_REASSEMBLED_BYTES } from "./codec.js";
|
|
2
|
+
/** Chunk body capacity: 16 KiB frame - 3 header - 8 chunk header, rounded down for headroom. */
|
|
3
|
+
export const CHUNK_PAYLOAD = 16000;
|
|
4
|
+
export function splitChunks(id, bytes, size = CHUNK_PAYLOAD) {
|
|
5
|
+
const count = Math.max(1, Math.ceil(bytes.length / size));
|
|
6
|
+
if (count > 0xffff)
|
|
7
|
+
throw new RangeError("payload needs more than 65535 chunks");
|
|
8
|
+
const out = [];
|
|
9
|
+
for (let i = 0; i < count; i++) {
|
|
10
|
+
out.push({
|
|
11
|
+
type: "chunk",
|
|
12
|
+
id,
|
|
13
|
+
index: i,
|
|
14
|
+
count,
|
|
15
|
+
bytes: bytes.subarray(i * size, Math.min(bytes.length, (i + 1) * size)),
|
|
16
|
+
});
|
|
17
|
+
}
|
|
18
|
+
return out;
|
|
19
|
+
}
|
|
20
|
+
/** Reassembles chunked payloads by id. Duplicates are ignored; out-of-order chunks are fine. */
|
|
21
|
+
export class Reassembler {
|
|
22
|
+
parts = new Map();
|
|
23
|
+
/** Returns the full payload when the last chunk arrives, otherwise null. */
|
|
24
|
+
push(chunk) {
|
|
25
|
+
if (chunk.count === 0 || chunk.index >= chunk.count)
|
|
26
|
+
return null;
|
|
27
|
+
let e = this.parts.get(chunk.id);
|
|
28
|
+
if (!e || e.count !== chunk.count) {
|
|
29
|
+
e = { count: chunk.count, got: 0, parts: new Array(chunk.count), bytes: 0 };
|
|
30
|
+
this.parts.set(chunk.id, e);
|
|
31
|
+
}
|
|
32
|
+
if (e.parts[chunk.index])
|
|
33
|
+
return null;
|
|
34
|
+
e.parts[chunk.index] = chunk.bytes;
|
|
35
|
+
e.got++;
|
|
36
|
+
e.bytes += chunk.bytes.length;
|
|
37
|
+
if (e.bytes > MAX_REASSEMBLED_BYTES) {
|
|
38
|
+
this.parts.delete(chunk.id);
|
|
39
|
+
throw new CodecError("too_long", `chunk set over ${MAX_REASSEMBLED_BYTES} bytes`);
|
|
40
|
+
}
|
|
41
|
+
if (e.got < e.count)
|
|
42
|
+
return null;
|
|
43
|
+
this.parts.delete(chunk.id);
|
|
44
|
+
const out = new Uint8Array(e.bytes);
|
|
45
|
+
let pos = 0;
|
|
46
|
+
for (const p of e.parts) {
|
|
47
|
+
out.set(p, pos);
|
|
48
|
+
pos += p.length;
|
|
49
|
+
}
|
|
50
|
+
return out;
|
|
51
|
+
}
|
|
52
|
+
get pending() {
|
|
53
|
+
return this.parts.size;
|
|
54
|
+
}
|
|
55
|
+
clear() {
|
|
56
|
+
this.parts.clear();
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
//# sourceMappingURL=chunking.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"chunking.js","sourceRoot":"","sources":["../src/chunking.ts"],"names":[],"mappings":"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","sourcesContent":["import { CodecError, MAX_REASSEMBLED_BYTES, type Message } from \"./codec.js\";\n\n/** Chunk body capacity: 16 KiB frame - 3 header - 8 chunk header, rounded down for headroom. */\nexport const CHUNK_PAYLOAD = 16000;\n\nexport function splitChunks(id: number, bytes: Uint8Array, size = CHUNK_PAYLOAD): Message[] {\n const count = Math.max(1, Math.ceil(bytes.length / size));\n if (count > 0xffff) throw new RangeError(\"payload needs more than 65535 chunks\");\n const out: Message[] = [];\n for (let i = 0; i < count; i++) {\n out.push({\n type: \"chunk\",\n id,\n index: i,\n count,\n bytes: bytes.subarray(i * size, Math.min(bytes.length, (i + 1) * size)),\n });\n }\n return out;\n}\n\ninterface Partial {\n count: number;\n got: number;\n parts: (Uint8Array | undefined)[];\n bytes: number;\n}\n\n/** Reassembles chunked payloads by id. Duplicates are ignored; out-of-order chunks are fine. */\nexport class Reassembler {\n private parts = new Map<number, Partial>();\n\n /** Returns the full payload when the last chunk arrives, otherwise null. */\n push(chunk: { id: number; index: number; count: number; bytes: Uint8Array }): Uint8Array | null {\n if (chunk.count === 0 || chunk.index >= chunk.count) return null;\n let e = this.parts.get(chunk.id);\n if (!e || e.count !== chunk.count) {\n e = { count: chunk.count, got: 0, parts: new Array(chunk.count), bytes: 0 };\n this.parts.set(chunk.id, e);\n }\n if (e.parts[chunk.index]) return null;\n e.parts[chunk.index] = chunk.bytes;\n e.got++;\n e.bytes += chunk.bytes.length;\n if (e.bytes > MAX_REASSEMBLED_BYTES) {\n this.parts.delete(chunk.id);\n throw new CodecError(\"too_long\", `chunk set over ${MAX_REASSEMBLED_BYTES} bytes`);\n }\n if (e.got < e.count) return null;\n this.parts.delete(chunk.id);\n const out = new Uint8Array(e.bytes);\n let pos = 0;\n for (const p of e.parts) {\n out.set(p!, pos);\n pos += p!.length;\n }\n return out;\n }\n\n get pending(): number {\n return this.parts.size;\n }\n clear(): void {\n this.parts.clear();\n }\n}\n"]}
|
package/dist/clock.d.ts
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/** Time source. Lockstep timing never uses requestAnimationFrame (DESIGN s.4). */
|
|
2
|
+
export interface Clock {
|
|
3
|
+
now(): number;
|
|
4
|
+
/** Repeating timer; returns a cancel function. */
|
|
5
|
+
setInterval(fn: () => void, ms: number): () => void;
|
|
6
|
+
setTimeout(fn: () => void, ms: number): () => void;
|
|
7
|
+
}
|
|
8
|
+
export declare const systemClock: Clock;
|
package/dist/clock.js
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Interval driven from a dedicated Worker when available (hidden tabs clamp main-thread timers to 1 Hz),
|
|
3
|
+
* main-thread setInterval otherwise.
|
|
4
|
+
*/
|
|
5
|
+
function workerInterval(fn, ms) {
|
|
6
|
+
if (typeof Worker === "undefined" || typeof Blob === "undefined" || typeof URL === "undefined" || !URL.createObjectURL) {
|
|
7
|
+
return null;
|
|
8
|
+
}
|
|
9
|
+
try {
|
|
10
|
+
const src = `let h=0;onmessage=e=>{if(e.data&&e.data.start){h=setInterval(()=>postMessage(0),e.data.ms)}else{clearInterval(h);close()}}`;
|
|
11
|
+
const url = URL.createObjectURL(new Blob([src], { type: "text/javascript" }));
|
|
12
|
+
const w = new Worker(url);
|
|
13
|
+
URL.revokeObjectURL(url);
|
|
14
|
+
w.onmessage = () => fn();
|
|
15
|
+
w.postMessage({ start: true, ms });
|
|
16
|
+
return () => {
|
|
17
|
+
w.postMessage({ stop: true });
|
|
18
|
+
w.terminate();
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
catch {
|
|
22
|
+
return null;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
export const systemClock = {
|
|
26
|
+
now: () => (typeof performance !== "undefined" ? performance.now() : Date.now()),
|
|
27
|
+
setInterval(fn, ms) {
|
|
28
|
+
const w = workerInterval(fn, ms);
|
|
29
|
+
if (w)
|
|
30
|
+
return w;
|
|
31
|
+
const h = setInterval(fn, ms);
|
|
32
|
+
return () => clearInterval(h);
|
|
33
|
+
},
|
|
34
|
+
setTimeout(fn, ms) {
|
|
35
|
+
const h = setTimeout(fn, ms);
|
|
36
|
+
return () => clearTimeout(h);
|
|
37
|
+
},
|
|
38
|
+
};
|
|
39
|
+
//# sourceMappingURL=clock.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"clock.js","sourceRoot":"","sources":["../src/clock.ts"],"names":[],"mappings":"AAQA;;;GAGG;AACH,SAAS,cAAc,CAAC,EAAc,EAAE,EAAU;IAChD,IAAI,OAAO,MAAM,KAAK,WAAW,IAAI,OAAO,IAAI,KAAK,WAAW,IAAI,OAAO,GAAG,KAAK,WAAW,IAAI,CAAC,GAAG,CAAC,eAAe,EAAE,CAAC;QACvH,OAAO,IAAI,CAAC;IACd,CAAC;IACD,IAAI,CAAC;QACH,MAAM,GAAG,GAAG,4HAA4H,CAAC;QACzI,MAAM,GAAG,GAAG,GAAG,CAAC,eAAe,CAAC,IAAI,IAAI,CAAC,CAAC,GAAG,CAAC,EAAE,EAAE,IAAI,EAAE,iBAAiB,EAAE,CAAC,CAAC,CAAC;QAC9E,MAAM,CAAC,GAAG,IAAI,MAAM,CAAC,GAAG,CAAC,CAAC;QAC1B,GAAG,CAAC,eAAe,CAAC,GAAG,CAAC,CAAC;QACzB,CAAC,CAAC,SAAS,GAAG,GAAG,EAAE,CAAC,EAAE,EAAE,CAAC;QACzB,CAAC,CAAC,WAAW,CAAC,EAAE,KAAK,EAAE,IAAI,EAAE,EAAE,EAAE,CAAC,CAAC;QACnC,OAAO,GAAG,EAAE;YACV,CAAC,CAAC,WAAW,CAAC,EAAE,IAAI,EAAE,IAAI,EAAE,CAAC,CAAC;YAC9B,CAAC,CAAC,SAAS,EAAE,CAAC;QAChB,CAAC,CAAC;IACJ,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,IAAI,CAAC;IACd,CAAC;AACH,CAAC;AAED,MAAM,CAAC,MAAM,WAAW,GAAU;IAChC,GAAG,EAAE,GAAG,EAAE,CAAC,CAAC,OAAO,WAAW,KAAK,WAAW,CAAC,CAAC,CAAC,WAAW,CAAC,GAAG,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,GAAG,EAAE,CAAC;IAChF,WAAW,CAAC,EAAE,EAAE,EAAE;QAChB,MAAM,CAAC,GAAG,cAAc,CAAC,EAAE,EAAE,EAAE,CAAC,CAAC;QACjC,IAAI,CAAC;YAAE,OAAO,CAAC,CAAC;QAChB,MAAM,CAAC,GAAG,WAAW,CAAC,EAAE,EAAE,EAAE,CAAC,CAAC;QAC9B,OAAO,GAAG,EAAE,CAAC,aAAa,CAAC,CAAC,CAAC,CAAC;IAChC,CAAC;IACD,UAAU,CAAC,EAAE,EAAE,EAAE;QACf,MAAM,CAAC,GAAG,UAAU,CAAC,EAAE,EAAE,EAAE,CAAC,CAAC;QAC7B,OAAO,GAAG,EAAE,CAAC,YAAY,CAAC,CAAC,CAAC,CAAC;IAC/B,CAAC;CACF,CAAC","sourcesContent":["/** Time source. Lockstep timing never uses requestAnimationFrame (DESIGN s.4). */\nexport interface Clock {\n now(): number;\n /** Repeating timer; returns a cancel function. */\n setInterval(fn: () => void, ms: number): () => void;\n setTimeout(fn: () => void, ms: number): () => void;\n}\n\n/**\n * Interval driven from a dedicated Worker when available (hidden tabs clamp main-thread timers to 1 Hz),\n * main-thread setInterval otherwise.\n */\nfunction workerInterval(fn: () => void, ms: number): (() => void) | null {\n if (typeof Worker === \"undefined\" || typeof Blob === \"undefined\" || typeof URL === \"undefined\" || !URL.createObjectURL) {\n return null;\n }\n try {\n const src = `let h=0;onmessage=e=>{if(e.data&&e.data.start){h=setInterval(()=>postMessage(0),e.data.ms)}else{clearInterval(h);close()}}`;\n const url = URL.createObjectURL(new Blob([src], { type: \"text/javascript\" }));\n const w = new Worker(url);\n URL.revokeObjectURL(url);\n w.onmessage = () => fn();\n w.postMessage({ start: true, ms });\n return () => {\n w.postMessage({ stop: true });\n w.terminate();\n };\n } catch {\n return null;\n }\n}\n\nexport const systemClock: Clock = {\n now: () => (typeof performance !== \"undefined\" ? performance.now() : Date.now()),\n setInterval(fn, ms) {\n const w = workerInterval(fn, ms);\n if (w) return w;\n const h = setInterval(fn, ms);\n return () => clearInterval(h);\n },\n setTimeout(fn, ms) {\n const h = setTimeout(fn, ms);\n return () => clearTimeout(h);\n },\n};\n"]}
|
package/dist/codec.d.ts
ADDED
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Binary codec for the peer data channels (DESIGN s.8). Little-endian, one message per frame,
|
|
3
|
+
* 3-byte header `type:u8, route:u16`. Pure functions; no I/O.
|
|
4
|
+
*/
|
|
5
|
+
export declare const MAX_FRAME_BYTES: number;
|
|
6
|
+
/** Cap on one reassembled chunk set (spec/binary.md "Snapshot and chunks"). */
|
|
7
|
+
export declare const MAX_REASSEMBLED_BYTES: number;
|
|
8
|
+
export declare const HEADER_BYTES = 3;
|
|
9
|
+
export declare const PROTO = 1;
|
|
10
|
+
export declare const MsgType: {
|
|
11
|
+
readonly hi: 1;
|
|
12
|
+
readonly welcome_dc: 2;
|
|
13
|
+
readonly cmd: 16;
|
|
14
|
+
readonly frame: 17;
|
|
15
|
+
readonly hash: 18;
|
|
16
|
+
readonly desync: 19;
|
|
17
|
+
readonly snapshot_req: 20;
|
|
18
|
+
readonly snapshot: 21;
|
|
19
|
+
readonly chunk: 22;
|
|
20
|
+
readonly raw: 32;
|
|
21
|
+
readonly ping: 48;
|
|
22
|
+
readonly pong: 49;
|
|
23
|
+
readonly host_claim: 64;
|
|
24
|
+
readonly voice: 80;
|
|
25
|
+
readonly chat: 96;
|
|
26
|
+
readonly chat_history: 97;
|
|
27
|
+
readonly chat_reject: 98;
|
|
28
|
+
};
|
|
29
|
+
/** welcome_dc.mode: 1 = lockstep, 2 = raw (0 reserved) per spec/binary.md. */
|
|
30
|
+
export declare const MODE_LOCKSTEP = 1;
|
|
31
|
+
export declare const MODE_RAW = 2;
|
|
32
|
+
export type CodecErrorCode = "too_short" | "trailing" | "too_long" | "bad_utf8" | "bad_value";
|
|
33
|
+
export declare class CodecError extends Error {
|
|
34
|
+
code: CodecErrorCode;
|
|
35
|
+
constructor(code: CodecErrorCode, detail?: string);
|
|
36
|
+
}
|
|
37
|
+
export interface FrameCommand {
|
|
38
|
+
peerId: number;
|
|
39
|
+
bytes: Uint8Array;
|
|
40
|
+
}
|
|
41
|
+
export interface ChatLine {
|
|
42
|
+
peerId: number;
|
|
43
|
+
id: number;
|
|
44
|
+
ts: number;
|
|
45
|
+
text: string;
|
|
46
|
+
}
|
|
47
|
+
export type Message = {
|
|
48
|
+
type: "hi";
|
|
49
|
+
proto: number;
|
|
50
|
+
peerId: number;
|
|
51
|
+
lastTick: number;
|
|
52
|
+
} | {
|
|
53
|
+
type: "welcome_dc";
|
|
54
|
+
tick: number;
|
|
55
|
+
tickRate: number;
|
|
56
|
+
mode: number;
|
|
57
|
+
hostId: number;
|
|
58
|
+
} | {
|
|
59
|
+
type: "cmd";
|
|
60
|
+
seq: number;
|
|
61
|
+
bytes: Uint8Array;
|
|
62
|
+
} | {
|
|
63
|
+
type: "frame";
|
|
64
|
+
tick: number;
|
|
65
|
+
commands: FrameCommand[];
|
|
66
|
+
} | {
|
|
67
|
+
type: "hash";
|
|
68
|
+
tick: number;
|
|
69
|
+
hash: bigint;
|
|
70
|
+
} | {
|
|
71
|
+
type: "desync";
|
|
72
|
+
tick: number;
|
|
73
|
+
hostHash: bigint;
|
|
74
|
+
yourHash: bigint;
|
|
75
|
+
} | {
|
|
76
|
+
type: "snapshot_req";
|
|
77
|
+
sinceTick: number;
|
|
78
|
+
} | {
|
|
79
|
+
type: "snapshot";
|
|
80
|
+
tick: number;
|
|
81
|
+
total: number;
|
|
82
|
+
flags: number;
|
|
83
|
+
} | {
|
|
84
|
+
type: "chunk";
|
|
85
|
+
id: number;
|
|
86
|
+
index: number;
|
|
87
|
+
count: number;
|
|
88
|
+
bytes: Uint8Array;
|
|
89
|
+
} | {
|
|
90
|
+
type: "raw";
|
|
91
|
+
bytes: Uint8Array;
|
|
92
|
+
} | {
|
|
93
|
+
type: "ping";
|
|
94
|
+
n: number;
|
|
95
|
+
t: number;
|
|
96
|
+
} | {
|
|
97
|
+
type: "pong";
|
|
98
|
+
n: number;
|
|
99
|
+
t: number;
|
|
100
|
+
} | {
|
|
101
|
+
type: "host_claim";
|
|
102
|
+
epoch: number;
|
|
103
|
+
hostId: number;
|
|
104
|
+
tick: number;
|
|
105
|
+
} | {
|
|
106
|
+
type: "voice";
|
|
107
|
+
seq: number;
|
|
108
|
+
flags: number;
|
|
109
|
+
frames: Uint8Array[];
|
|
110
|
+
} | {
|
|
111
|
+
type: "chat";
|
|
112
|
+
id: number;
|
|
113
|
+
ts: number;
|
|
114
|
+
text: string;
|
|
115
|
+
} | {
|
|
116
|
+
type: "chat_history";
|
|
117
|
+
lines: ChatLine[];
|
|
118
|
+
} | {
|
|
119
|
+
type: "chat_reject";
|
|
120
|
+
id: number;
|
|
121
|
+
reason: number;
|
|
122
|
+
};
|
|
123
|
+
export interface Decoded {
|
|
124
|
+
route: number;
|
|
125
|
+
msg: Message;
|
|
126
|
+
}
|
|
127
|
+
export declare function encodeMessage(msg: Message, route?: number, allowOversize?: boolean): Uint8Array;
|
|
128
|
+
/**
|
|
129
|
+
* Decode one frame. Returns null for unknown types (ignored for forward compatibility); throws on a truncated body.
|
|
130
|
+
* A frame reassembled from chunks passes `reassembled` so the cap is MAX_REASSEMBLED_BYTES instead of one frame's.
|
|
131
|
+
*/
|
|
132
|
+
export declare function decodeMessage(buf: Uint8Array, reassembled?: boolean): Decoded | null;
|
|
133
|
+
/** Rewrite the route field on a copy (the host does this when relaying). */
|
|
134
|
+
export declare function withRoute(buf: Uint8Array, route: number): Uint8Array;
|
|
135
|
+
export declare const CHAT_REJECT: {
|
|
136
|
+
readonly rate: 1;
|
|
137
|
+
readonly length: 2;
|
|
138
|
+
readonly mutedByHost: 3;
|
|
139
|
+
};
|
|
140
|
+
export type ChatRejectReason = "rate" | "length" | "muted_by_host" | "unknown";
|
|
141
|
+
export declare function chatRejectName(code: number): ChatRejectReason;
|