@pylonsync/realtime 0.18.1 → 0.19.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/dist/connection.d.ts +32 -4
- package/dist/game.d.ts +6 -1
- package/dist/replication.d.ts +70 -1
- package/dist/wire.d.ts +84 -1
- package/package.json +1 -1
- package/src/connection-webtransport.test.ts +543 -0
- package/src/connection.test.ts +48 -1
- package/src/connection.ts +602 -94
- package/src/game.ts +16 -2
- package/src/replication.fixtures.json +2416 -0
- package/src/replication.test.ts +46 -0
- package/src/replication.ts +167 -2
- package/src/wire.ts +185 -1
package/dist/connection.d.ts
CHANGED
|
@@ -1,12 +1,25 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* A shard connection with no framework: one WebSocket
|
|
3
|
-
* backoff, that decodes frames, applies
|
|
4
|
-
* `EntityTable`, and sends inputs. `useShard` in
|
|
5
|
-
* `connectShardGame` build on it.
|
|
2
|
+
* A shard connection with no framework: one WebSocket or WebTransport
|
|
3
|
+
* session, reconnected with backoff, that decodes frames, applies
|
|
4
|
+
* replication frames to an `EntityTable`, and sends inputs. `useShard` in
|
|
5
|
+
* `@pylonsync/react` and `connectShardGame` build on it.
|
|
6
6
|
*/
|
|
7
7
|
import { ShardClock } from "./clock";
|
|
8
8
|
import { EntityTable, type ReplicationSummary } from "./replication";
|
|
9
9
|
import { type ShardInputRejection, type ShardPayloadDecoder } from "./wire";
|
|
10
|
+
/**
|
|
11
|
+
* How the client reaches the shard.
|
|
12
|
+
*
|
|
13
|
+
* - `"websocket"`: a WebSocket (TCP). Works everywhere.
|
|
14
|
+
* - `"webtransport"`: a WebTransport session (QUIC over UDP). Entity
|
|
15
|
+
* updates come as datagrams, so a lost packet delays only itself. Needs
|
|
16
|
+
* the browser API and an app that serves WebTransport
|
|
17
|
+
* (`PYLON_WEBTRANSPORT_PORT`); fails otherwise.
|
|
18
|
+
* - `"auto"`: WebTransport when the browser and the app support it, else a
|
|
19
|
+
* WebSocket. After a WebTransport session fails to open (UDP blocked, an
|
|
20
|
+
* old browser), the client uses WebSockets for the rest of its life.
|
|
21
|
+
*/
|
|
22
|
+
export type ShardTransport = "websocket" | "webtransport" | "auto";
|
|
10
23
|
export interface ShardConnectOptions {
|
|
11
24
|
/** Subscriber ID (usually the logged-in user ID). Required for multiplayer. */
|
|
12
25
|
subscriberId: string;
|
|
@@ -39,6 +52,19 @@ export interface ShardConnectOptions {
|
|
|
39
52
|
* `shard` query parameter is replaced with the new shard.
|
|
40
53
|
*/
|
|
41
54
|
wsUrl?: string;
|
|
55
|
+
/** Default `"websocket"`. See `ShardTransport`. */
|
|
56
|
+
transport?: ShardTransport;
|
|
57
|
+
/**
|
|
58
|
+
* Where to fetch the WebTransport URL and certificate hashes. Default
|
|
59
|
+
* `/_pylon/shard/webtransport` on `baseUrl` (or the page's host), or on
|
|
60
|
+
* the host of `wsUrl` when only that is set.
|
|
61
|
+
*/
|
|
62
|
+
webTransportInfoUrl?: string;
|
|
63
|
+
/**
|
|
64
|
+
* How long a WebTransport session may take to open before `"auto"`
|
|
65
|
+
* falls back to a WebSocket, in ms (default 3000).
|
|
66
|
+
*/
|
|
67
|
+
webTransportTimeoutMs?: number;
|
|
42
68
|
/** Reconnect on unexpected close (default: true). */
|
|
43
69
|
autoReconnect?: boolean;
|
|
44
70
|
/** First reconnect delay in ms (default 500; doubles to at most 10 000). */
|
|
@@ -101,6 +127,8 @@ export interface ShardClient<TSnapshot = unknown, TInput = unknown> {
|
|
|
101
127
|
send: (input: TInput) => number;
|
|
102
128
|
close: () => void;
|
|
103
129
|
readonly connected: boolean;
|
|
130
|
+
/** The transport of the open (or last) connection, or null before one opened. */
|
|
131
|
+
readonly transport: "websocket" | "webtransport" | null;
|
|
104
132
|
}
|
|
105
133
|
/**
|
|
106
134
|
* True when a shard ticket (`v1.<payload>.<signature>`, the payload
|
package/dist/game.d.ts
CHANGED
|
@@ -25,12 +25,17 @@
|
|
|
25
25
|
* ```
|
|
26
26
|
*/
|
|
27
27
|
import type { ShardClock } from "./clock";
|
|
28
|
-
import { type ShardClient, type ShardConnectOptions } from "./connection";
|
|
28
|
+
import { type ShardClient, type ShardConnectOptions, type ShardTransport } from "./connection";
|
|
29
29
|
import { type InterpolatedEntity, type InterpolationOptions } from "./interpolation";
|
|
30
30
|
import { Predictor, type PredictorOptions } from "./prediction";
|
|
31
31
|
import type { EntityTable, ReplicationSummary } from "./replication";
|
|
32
32
|
import type { ShardInputRejection } from "./wire";
|
|
33
33
|
export interface ShardGameOptions extends ShardConnectOptions {
|
|
34
|
+
/**
|
|
35
|
+
* Default `"auto"`: WebTransport where the browser and the app support
|
|
36
|
+
* it, else a WebSocket. See `ShardTransport`.
|
|
37
|
+
*/
|
|
38
|
+
transport?: ShardTransport;
|
|
34
39
|
/**
|
|
35
40
|
* How far behind the shard's estimated current tick entities are drawn,
|
|
36
41
|
* in ms. It must cover the time between frames plus network jitter.
|
package/dist/replication.d.ts
CHANGED
|
@@ -23,8 +23,30 @@
|
|
|
23
23
|
* JavaScript numbers hold integers exactly up to 2^53. Entity ids and
|
|
24
24
|
* quantized positions must stay below that; a frame that exceeds it is
|
|
25
25
|
* refused rather than rounded.
|
|
26
|
+
*
|
|
27
|
+
* **Datagrams** (`pylon_replication::datagram`): on WebTransport, spawns,
|
|
28
|
+
* despawns, and full frames come as frames on the stream, and updates as
|
|
29
|
+
* datagrams that may be lost, duplicated, or reordered. A datagram update
|
|
30
|
+
* carries the entity's absolute position and the tick of the stream frame
|
|
31
|
+
* that spawned it (low 16 bits). {@link EntityTable.applyDatagram} applies
|
|
32
|
+
* an update only to the entity spawned at that tick and only when it is
|
|
33
|
+
* newer than the entity's last datagram, and skips it otherwise. Stream
|
|
34
|
+
* frames must go through `apply(frame, tick)` so the table knows the
|
|
35
|
+
* ticks; a full frame starts them over.
|
|
36
|
+
*
|
|
37
|
+
* ```text
|
|
38
|
+
* u8 version (2)
|
|
39
|
+
* varint frame number, tick, input ack, stream tick (of the last stream
|
|
40
|
+
* frame sent by this tick)
|
|
41
|
+
* f32 LE precision
|
|
42
|
+
* varint parts (datagrams sent for this tick)
|
|
43
|
+
* varint update count, per entity (ids ascending, delta-coded): id,
|
|
44
|
+
* u16 LE spawn tick, u8 mask, the masked axes (zigzag, absolute),
|
|
45
|
+
* components if bit 8
|
|
46
|
+
* ```
|
|
26
47
|
*/
|
|
27
48
|
export declare const REPLICATION_VERSION = 1;
|
|
49
|
+
export declare const DATAGRAM_VERSION = 2;
|
|
28
50
|
export declare class ReplicationError extends Error {
|
|
29
51
|
constructor(message: string);
|
|
30
52
|
}
|
|
@@ -41,6 +63,23 @@ export interface ReplicatedEntity {
|
|
|
41
63
|
/** Component id to bytes, in the game's own encoding. */
|
|
42
64
|
components: Map<number, Uint8Array>;
|
|
43
65
|
}
|
|
66
|
+
/** What one datagram did. */
|
|
67
|
+
export interface DatagramSummary {
|
|
68
|
+
frame: number;
|
|
69
|
+
tick: number;
|
|
70
|
+
ack: number;
|
|
71
|
+
/** The tick of the last stream frame the server had sent by `tick`. */
|
|
72
|
+
streamTick: number;
|
|
73
|
+
/** Datagrams the server sent for `tick`. */
|
|
74
|
+
parts: number;
|
|
75
|
+
/** Entities it updated. */
|
|
76
|
+
updated: number[];
|
|
77
|
+
/**
|
|
78
|
+
* Updates it skipped: an unknown entity, another spawn, an older
|
|
79
|
+
* datagram than the entity's last, or another precision.
|
|
80
|
+
*/
|
|
81
|
+
skipped: number;
|
|
82
|
+
}
|
|
44
83
|
/** What one frame did. */
|
|
45
84
|
export interface ReplicationSummary {
|
|
46
85
|
full: boolean;
|
|
@@ -48,6 +87,11 @@ export interface ReplicationSummary {
|
|
|
48
87
|
updated: number[];
|
|
49
88
|
despawned: number[];
|
|
50
89
|
}
|
|
90
|
+
/**
|
|
91
|
+
* A datagram's frame number, tick, and input ack, without applying it.
|
|
92
|
+
* Throws on bytes that are not a datagram.
|
|
93
|
+
*/
|
|
94
|
+
export declare function readDatagramHeader(datagram: Uint8Array): Omit<DatagramSummary, "updated" | "skipped">;
|
|
51
95
|
/**
|
|
52
96
|
* The entities one subscriber has been told about. Apply every
|
|
53
97
|
* replication frame in order; a frame that fails to apply means the
|
|
@@ -58,8 +102,33 @@ export declare class EntityTable {
|
|
|
58
102
|
readonly entities: Map<number, ReplicatedEntity>;
|
|
59
103
|
/** World units per quantization step, from the last frame. */
|
|
60
104
|
precision: number;
|
|
105
|
+
/**
|
|
106
|
+
* The tick of the stream frame that spawned each entity, when frames are
|
|
107
|
+
* applied with their tick. Datagram updates name it.
|
|
108
|
+
*/
|
|
109
|
+
readonly spawnTicks: Map<number, number>;
|
|
110
|
+
/** The last datagram applied to each entity. */
|
|
111
|
+
readonly datagramFrames: Map<number, number>;
|
|
112
|
+
/**
|
|
113
|
+
* The tick of the last frame applied with its tick. A client acks each
|
|
114
|
+
* datagram with it, so the server knows which spawns it had.
|
|
115
|
+
*/
|
|
116
|
+
streamTick: number;
|
|
61
117
|
get size(): number;
|
|
62
118
|
get(id: number): ReplicatedEntity | undefined;
|
|
119
|
+
/** Forget everything. */
|
|
63
120
|
clear(): void;
|
|
64
|
-
|
|
121
|
+
/**
|
|
122
|
+
* Apply a replication frame. On a connection that also gets datagrams,
|
|
123
|
+
* pass the tick from the frame's header: the table records the tick each
|
|
124
|
+
* entity spawned at and the tick of the last frame, which datagrams and
|
|
125
|
+
* their acks name. A full frame starts that record over.
|
|
126
|
+
*/
|
|
127
|
+
apply(frame: Uint8Array, tick?: number): ReplicationSummary;
|
|
128
|
+
/**
|
|
129
|
+
* Apply one datagram. It changes nothing it cannot apply exactly (see
|
|
130
|
+
* `DatagramSummary.skipped`). Throws only on bytes that are not a
|
|
131
|
+
* datagram.
|
|
132
|
+
*/
|
|
133
|
+
applyDatagram(datagram: Uint8Array): DatagramSummary;
|
|
65
134
|
}
|
package/dist/wire.d.ts
CHANGED
|
@@ -5,7 +5,8 @@
|
|
|
5
5
|
* message is a binary frame with an 18-byte header:
|
|
6
6
|
*
|
|
7
7
|
* 0 1 frame kind: 1 snapshot, 2 input rejected, 3 entity replication,
|
|
8
|
-
* 4 transfer (JSON `{ shard, ticket }`: the last frame; reconnect there)
|
|
8
|
+
* 4 transfer (JSON `{ shard, ticket }`: the last frame; reconnect there),
|
|
9
|
+
* 6 closing (WebTransport only, JSON `{ code, reason }`)
|
|
9
10
|
* 1 1 codec: 0 JSON, 1 MessagePack, 2 bincode, 3 custom, 4 replication
|
|
10
11
|
* 2 8 tick (u64 big-endian)
|
|
11
12
|
* 10 8 ack: highest client_seq the shard processed for this subscriber (0 = none)
|
|
@@ -13,6 +14,12 @@
|
|
|
13
14
|
*
|
|
14
15
|
* Inputs go up as `{ input, client_seq }`: JSON in a text frame, or the
|
|
15
16
|
* shard's codec in a binary frame.
|
|
17
|
+
*
|
|
18
|
+
* Over WebTransport (wire version 3 in Rust) the same frames travel on one
|
|
19
|
+
* bidirectional stream, each after a 4-byte big-endian length, and entity
|
|
20
|
+
* updates travel as QUIC datagrams (`EntityTable.applyDatagram`). The
|
|
21
|
+
* client's hello, its inputs, and its datagram acks are described at
|
|
22
|
+
* `encodeWebTransportHello`, `ShardClientMessage`, and `encodeDatagramAcks`.
|
|
16
23
|
*/
|
|
17
24
|
export declare const SHARD_PROTOCOL_VERSION = 2;
|
|
18
25
|
export declare const SHARD_HEADER_LEN = 18;
|
|
@@ -26,6 +33,13 @@ export declare const ShardFrameKind: {
|
|
|
26
33
|
* last frame on the connection.
|
|
27
34
|
*/
|
|
28
35
|
readonly Transfer: 4;
|
|
36
|
+
/** A datagram frame (wire version 3 WebSocket only). */
|
|
37
|
+
readonly Datagram: 5;
|
|
38
|
+
/**
|
|
39
|
+
* WebTransport only: the server is about to close the session, with
|
|
40
|
+
* JSON `{ code, reason }` (`WEBTRANSPORT_CLOSE`). The client closes it.
|
|
41
|
+
*/
|
|
42
|
+
readonly Closing: 6;
|
|
29
43
|
};
|
|
30
44
|
/** Where the subscriber went: connect to `shard` with `ticket`. */
|
|
31
45
|
export interface ShardTransferNotice {
|
|
@@ -74,3 +88,72 @@ export declare function decodeShardRejection(codec: number, payload: Uint8Array,
|
|
|
74
88
|
* text, which the server always accepts.
|
|
75
89
|
*/
|
|
76
90
|
export declare function encodeShardInput(codec: number | null, input: unknown, clientSeq: number): string | Uint8Array;
|
|
91
|
+
/** Acks one message may carry (the server refuses more). */
|
|
92
|
+
export declare const MAX_ACKS_PER_MESSAGE = 512;
|
|
93
|
+
/**
|
|
94
|
+
* Type bytes of the client's messages on a WebTransport stream (the first
|
|
95
|
+
* byte; the rest is the message).
|
|
96
|
+
*/
|
|
97
|
+
export declare const ShardClientMessage: {
|
|
98
|
+
/** An input envelope in the shard's codec (MessagePack shards). */
|
|
99
|
+
readonly Input: 0;
|
|
100
|
+
/** Datagram acks (`encodeDatagramAcks`). */
|
|
101
|
+
readonly Acks: 1;
|
|
102
|
+
/** An input envelope as JSON. */
|
|
103
|
+
readonly JsonInput: 2;
|
|
104
|
+
};
|
|
105
|
+
/** WebTransport session close codes the server uses. */
|
|
106
|
+
export declare const WEBTRANSPORT_CLOSE: {
|
|
107
|
+
readonly Normal: 0;
|
|
108
|
+
/** Refused: bad credentials, an unknown shard. */
|
|
109
|
+
readonly Policy: 1;
|
|
110
|
+
/** The client broke the protocol. */
|
|
111
|
+
readonly Protocol: 2;
|
|
112
|
+
/** Try again: the client was too slow, or the server was busy. */
|
|
113
|
+
readonly Again: 3;
|
|
114
|
+
};
|
|
115
|
+
/** What `GET /_pylon/shard/webtransport` returns. */
|
|
116
|
+
export interface WebTransportInfo {
|
|
117
|
+
/** The `https://` URL of the WebTransport endpoint. */
|
|
118
|
+
url: string;
|
|
119
|
+
/**
|
|
120
|
+
* SHA-256 hashes of the server's self-signed certificates, for
|
|
121
|
+
* `serverCertificateHashes`. Empty when the certificate has a public CA.
|
|
122
|
+
*/
|
|
123
|
+
certHashes: Uint8Array[];
|
|
124
|
+
}
|
|
125
|
+
/** Parse the body of `GET /_pylon/shard/webtransport`. */
|
|
126
|
+
export declare function decodeWebTransportInfo(body: unknown): WebTransportInfo;
|
|
127
|
+
/**
|
|
128
|
+
* Datagram acks: the type byte `ShardClientMessage.Acks`, a varint count,
|
|
129
|
+
* then per ack the datagram's frame number and `EntityTable.streamTick`
|
|
130
|
+
* when the client applied it, both varints. Sent as a datagram, or on the
|
|
131
|
+
* stream.
|
|
132
|
+
*/
|
|
133
|
+
export declare function encodeDatagramAcks(acks: ReadonlyArray<readonly [number, number]>): Uint8Array;
|
|
134
|
+
/** A message for a WebTransport stream: a 4-byte big-endian length, then `bytes`. */
|
|
135
|
+
export declare function lengthPrefixed(bytes: Uint8Array): Uint8Array;
|
|
136
|
+
/** The first message on a WebTransport stream: who connects to which shard. */
|
|
137
|
+
export declare function encodeWebTransportHello(hello: {
|
|
138
|
+
shard: string;
|
|
139
|
+
sid: string;
|
|
140
|
+
ticket?: string;
|
|
141
|
+
token?: string;
|
|
142
|
+
}): Uint8Array;
|
|
143
|
+
/**
|
|
144
|
+
* An input as a WebTransport stream message: `encodeShardInput`'s output
|
|
145
|
+
* after its type byte.
|
|
146
|
+
*/
|
|
147
|
+
export declare function encodeWebTransportInput(encoded: string | Uint8Array): Uint8Array;
|
|
148
|
+
/**
|
|
149
|
+
* Splits a WebTransport stream's bytes into its length-prefixed frames.
|
|
150
|
+
* Chunks can end anywhere: in a length, in a frame.
|
|
151
|
+
*/
|
|
152
|
+
export declare class StreamFrames {
|
|
153
|
+
private chunks;
|
|
154
|
+
private buffered;
|
|
155
|
+
/** Add a chunk and return the frames it completed. */
|
|
156
|
+
push(chunk: Uint8Array): ArrayBuffer[];
|
|
157
|
+
private peek;
|
|
158
|
+
private take;
|
|
159
|
+
}
|
package/package.json
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
"publishConfig": {
|
|
4
4
|
"access": "public"
|
|
5
5
|
},
|
|
6
|
-
"version": "0.
|
|
6
|
+
"version": "0.19.0",
|
|
7
7
|
"description": "Pylon realtime shard client pieces with no framework: the wire protocol, codecs, and the entity replication decoder and table.",
|
|
8
8
|
"type": "module",
|
|
9
9
|
"main": "./src/index.ts",
|