react-websocket 3.3.0 → 3.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +12 -4
- package/dist/chaos.cjs +284 -0
- package/dist/chaos.cjs.map +1 -0
- package/dist/chaos.d.cts +131 -0
- package/dist/chaos.d.ts +131 -0
- package/dist/chaos.js +283 -0
- package/dist/chaos.js.map +1 -0
- package/dist/core.cjs +161 -17
- package/dist/core.cjs.map +1 -1
- package/dist/core.d.cts +125 -5
- package/dist/core.d.ts +125 -5
- package/dist/core.js +161 -18
- package/dist/core.js.map +1 -1
- package/dist/index.d.cts +125 -5
- package/dist/index.d.ts +34 -4
- package/dist/timeline.cjs +162 -0
- package/dist/timeline.cjs.map +1 -0
- package/dist/timeline.d.cts +379 -0
- package/dist/timeline.d.ts +140 -0
- package/dist/timeline.js +161 -0
- package/dist/timeline.js.map +1 -0
- package/package.json +56 -15
package/dist/index.d.cts
CHANGED
|
@@ -112,14 +112,42 @@ interface SendOptions {
|
|
|
112
112
|
volatile?: boolean;
|
|
113
113
|
/**
|
|
114
114
|
* Keep only the newest queued message with this key. Sending again with the same key
|
|
115
|
-
* while disconnected replaces the earlier one, which moves to the back of the queue
|
|
115
|
+
* while disconnected replaces the earlier one, which moves to the back of the queue
|
|
116
|
+
* (or of the `priority` messages, for a priority one).
|
|
116
117
|
* For state where only the latest value matters, such as "my position is now (x, y)".
|
|
117
118
|
*/
|
|
118
119
|
key?: string;
|
|
120
|
+
/**
|
|
121
|
+
* Queue this message ahead of every message queued without `priority`, after earlier
|
|
122
|
+
* priority messages. For control messages that must not wait behind a backlog, such as
|
|
123
|
+
* cancel, interrupt or a token refresh. Messages sent from `open` listeners still go first.
|
|
124
|
+
*
|
|
125
|
+
* It only matters while the socket isn't open: once a message is handed to the browser,
|
|
126
|
+
* nothing can overtake it. To keep that buffer short, send bulky streams with `maxBuffered`.
|
|
127
|
+
* @default false
|
|
128
|
+
*/
|
|
129
|
+
priority?: boolean;
|
|
130
|
+
/**
|
|
131
|
+
* Drop the message instead of sending it while more than this many bytes are already
|
|
132
|
+
* waiting in the browser's send buffer (`socket.bufferedAmount`). For high-rate streams on
|
|
133
|
+
* a slow link, such as audio frames or per-tick input, where a late message is worse than
|
|
134
|
+
* none. `send()` returns `false`, and `onDrop` isn't called.
|
|
135
|
+
*
|
|
136
|
+
* Only checked while the socket is open: a message queued while disconnected is sent on
|
|
137
|
+
* open regardless. `bufferedAmount` includes what you sent earlier in the same task, so set
|
|
138
|
+
* it well above one burst of messages.
|
|
139
|
+
*
|
|
140
|
+
* @example
|
|
141
|
+
* ```ts
|
|
142
|
+
* voice.send(frame, { volatile: true, maxBuffered: 64 * 1024 });
|
|
143
|
+
* ```
|
|
144
|
+
*/
|
|
145
|
+
maxBuffered?: number;
|
|
119
146
|
/**
|
|
120
147
|
* Called if the message is dropped from the queue without being sent: replaced by a
|
|
121
148
|
* newer message with the same `key`, or evicted by `queue.limit`. Not called when the
|
|
122
|
-
* queue is cleared because the client closed or gave up
|
|
149
|
+
* queue is cleared because the client closed or gave up, or when `send()` drops the
|
|
150
|
+
* message right away (`volatile` while not open, or `maxBuffered`) and returns `false`.
|
|
123
151
|
*/
|
|
124
152
|
onDrop?: () => void;
|
|
125
153
|
}
|
|
@@ -127,7 +155,7 @@ interface SendOptions {
|
|
|
127
155
|
interface QueueOptions {
|
|
128
156
|
/**
|
|
129
157
|
* Maximum number of messages held while not connected. When full, the oldest
|
|
130
|
-
* message is dropped.
|
|
158
|
+
* message is dropped, skipping `priority` messages unless nothing else is queued.
|
|
131
159
|
* @default Infinity
|
|
132
160
|
*/
|
|
133
161
|
limit?: number;
|
|
@@ -310,12 +338,14 @@ interface WebSocketClient<In = unknown, Out = unknown> {
|
|
|
310
338
|
* Send a message (encoded with `serialize`/`json` if configured). If the socket isn't
|
|
311
339
|
* open, the message is queued and sent on open (see the `queue` option), unless
|
|
312
340
|
* `options.volatile` is set.
|
|
313
|
-
* @returns `true` if it was sent immediately, `false` if it was queued or dropped
|
|
341
|
+
* @returns `true` if it was sent immediately, `false` if it was queued or dropped
|
|
342
|
+
* (`volatile` while not open, or `maxBuffered` while open).
|
|
314
343
|
*
|
|
315
344
|
* @example
|
|
316
345
|
* ```ts
|
|
317
346
|
* game.send({ type: 'input', dx, dy }, { volatile: true }); // never replayed after a reconnect
|
|
318
347
|
* game.send({ type: 'position', x, y }, { key: 'position' }); // only the newest is queued
|
|
348
|
+
* game.send({ type: 'leave' }, { priority: true }); // ahead of everything already queued
|
|
319
349
|
* ```
|
|
320
350
|
*/
|
|
321
351
|
send(message: Out, options?: SendOptions): boolean;
|
|
@@ -650,6 +680,85 @@ interface WebSocketResponder<P = unknown, R = unknown> {
|
|
|
650
680
|
*/
|
|
651
681
|
export declare function createWebSocketResponder<Q extends RequestFields, R, In, const Out>(client: WebSocketClient<In, Out>, options: WebSocketResponderOptions<Q, R, NoInfer<In>, NoInfer<Out>>): WebSocketResponder<ParamsOf<Q>, R>;
|
|
652
682
|
//#endregion
|
|
683
|
+
//#region src/sendrate.d.ts
|
|
684
|
+
/** Options for {@link createWebSocketSendRate}. */
|
|
685
|
+
interface WebSocketSendRateOptions {
|
|
686
|
+
/**
|
|
687
|
+
* Ticks per second while the connection keeps up.
|
|
688
|
+
* @default 30
|
|
689
|
+
*/
|
|
690
|
+
maxRate?: number;
|
|
691
|
+
/**
|
|
692
|
+
* The rate never drops below this, in ticks per second.
|
|
693
|
+
* @default 5
|
|
694
|
+
*/
|
|
695
|
+
minRate?: number;
|
|
696
|
+
/**
|
|
697
|
+
* Bytes waiting in the browser's send buffer (`socket.bufferedAmount`) above which a tick
|
|
698
|
+
* sends nothing and the rate halves.
|
|
699
|
+
* @default 1024
|
|
700
|
+
*/
|
|
701
|
+
maxBuffered?: number;
|
|
702
|
+
/**
|
|
703
|
+
* Measurements from `createWebSocketLatency` on the same client. With them, a round-trip time
|
|
704
|
+
* that rises also lowers the rate. Without them, only the send buffer is watched.
|
|
705
|
+
*/
|
|
706
|
+
latency?: Pick<WebSocketLatency, 'getSnapshot'>;
|
|
707
|
+
/**
|
|
708
|
+
* How far `rtt` may rise above the lowest one measured on this connection before the rate
|
|
709
|
+
* halves, in ms. A steady high ping keeps the full rate; only a rising one counts.
|
|
710
|
+
* @default 100
|
|
711
|
+
*/
|
|
712
|
+
maxRttIncrease?: number;
|
|
713
|
+
}
|
|
714
|
+
/** The current send rate. A new object is created on every change. */
|
|
715
|
+
interface SendRate {
|
|
716
|
+
/** Ticks per second, rounded. */
|
|
717
|
+
readonly rate: number;
|
|
718
|
+
/**
|
|
719
|
+
* Whether the last tick found the link congested: the send buffer above `maxBuffered`, or
|
|
720
|
+
* `rtt` risen more than `maxRttIncrease`.
|
|
721
|
+
*/
|
|
722
|
+
readonly congested: boolean;
|
|
723
|
+
}
|
|
724
|
+
/** Keyed sends coalesced at a rate that adapts to the link. Create with {@link createWebSocketSendRate}. */
|
|
725
|
+
interface WebSocketSendRate<Out> {
|
|
726
|
+
/** The client the messages are sent on. */
|
|
727
|
+
readonly client: WebSocketClient<any, Out>;
|
|
728
|
+
/**
|
|
729
|
+
* Send `message` at the next tick. A message with the same `key` still waiting for it is
|
|
730
|
+
* replaced, so only the newest one goes out. Sent with `volatile`.
|
|
731
|
+
*/
|
|
732
|
+
send(message: Out, key: string): void;
|
|
733
|
+
/** Send everything waiting now, e.g. right before a one-shot action that must carry the latest aim. */
|
|
734
|
+
flush(): void;
|
|
735
|
+
/** The current {@link SendRate}. Returns the same object until it changes. */
|
|
736
|
+
getSnapshot(): SendRate;
|
|
737
|
+
/** Called whenever the rate or `congested` changes. Compatible with `useSyncExternalStore`. */
|
|
738
|
+
subscribe(listener: () => void): () => void;
|
|
739
|
+
/** Stop sending and listening. Only needed for instances created dynamically. */
|
|
740
|
+
dispose(): void;
|
|
741
|
+
}
|
|
742
|
+
/**
|
|
743
|
+
* Send high-rate state, such as aim, cursor or position, at a rate that adapts to the
|
|
744
|
+
* connection. Messages are coalesced per key between ticks. The rate drops by half when the
|
|
745
|
+
* send buffer fills up or the round trip grows, and climbs back one tick per second at a time
|
|
746
|
+
* when the link keeps up, so players on a poor connection degrade smoothly instead of building
|
|
747
|
+
* up lag.
|
|
748
|
+
*
|
|
749
|
+
* Ticks run only while the socket is open and something is waiting. Messages waiting during a
|
|
750
|
+
* reconnect go out after it; `close()` drops them. It doesn't open the connection by itself.
|
|
751
|
+
*
|
|
752
|
+
* @example
|
|
753
|
+
* ```ts
|
|
754
|
+
* export const stream = createWebSocketSendRate(game, { latency, maxRate: 30 });
|
|
755
|
+
*
|
|
756
|
+
* // On every pointer move: at most 30 a second, fewer on a congested link.
|
|
757
|
+
* stream.send({ type: 'aim', angle }, 'aim');
|
|
758
|
+
* ```
|
|
759
|
+
*/
|
|
760
|
+
export declare function createWebSocketSendRate<Out>(client: WebSocketClient<any, Out>, options?: WebSocketSendRateOptions): WebSocketSendRate<Out>;
|
|
761
|
+
//#endregion
|
|
653
762
|
//#region src/state.d.ts
|
|
654
763
|
/** Options for {@link createWebSocketState}. */
|
|
655
764
|
interface WebSocketStateOptions<S, In> {
|
|
@@ -660,6 +769,17 @@ interface WebSocketStateOptions<S, In> {
|
|
|
660
769
|
* change, or the same `state` to ignore the message (subscribers aren't notified).
|
|
661
770
|
*/
|
|
662
771
|
reduce: (state: S, message: In) => S;
|
|
772
|
+
/**
|
|
773
|
+
* `'frame'`: notify subscribers at most once per animation frame instead of once per
|
|
774
|
+
* message, so a fast stream (tokens, tickers, game state) renders once per frame.
|
|
775
|
+
* `getState()` is always current; only notifications wait. `setState` and `reset` still
|
|
776
|
+
* notify right away.
|
|
777
|
+
*
|
|
778
|
+
* Browsers pause animation frames in background tabs: the state keeps up, and subscribers
|
|
779
|
+
* hear about it when the tab is visible again. Without `requestAnimationFrame` (Node, some
|
|
780
|
+
* workers), it waits about 16 ms instead.
|
|
781
|
+
*/
|
|
782
|
+
batch?: 'frame';
|
|
663
783
|
}
|
|
664
784
|
/**
|
|
665
785
|
* State derived from a client's messages. Read it with `useWebSocketState(store, selector)`,
|
|
@@ -893,5 +1013,5 @@ export declare function useWebSocketLatency(latency: WebSocketLatency): Latency;
|
|
|
893
1013
|
*/
|
|
894
1014
|
export declare function useAgentApproval<P, R>(responder: WebSocketResponder<P, R> | null | undefined, method: string | null | undefined): readonly IncomingRequest<P, R>[];
|
|
895
1015
|
//#endregion
|
|
896
|
-
export type { CloseInfo, EventMap, HeartbeatOptions, IncomingRequest, Latency, QueueOptions, ReconnectOptions, RequestHandler, RequestOptions, SendData, SendOptions, Snapshot, StandardSchemaV1, Status, UrlSource, UseWebSocketOptions, UseWebSocketResult, WebSocketClient, WebSocketHandlers, WebSocketLatency, WebSocketLatencyOptions, WebSocketOptions, WebSocketRequests, WebSocketRequestsOptions, WebSocketResponder, WebSocketResponderOptions, WebSocketState, WebSocketStateOptions, WebSocketTopics, WebSocketTopicsOptions };
|
|
1016
|
+
export type { CloseInfo, EventMap, HeartbeatOptions, IncomingRequest, Latency, QueueOptions, ReconnectOptions, RequestHandler, RequestOptions, SendData, SendOptions, SendRate, Snapshot, StandardSchemaV1, Status, UrlSource, UseWebSocketOptions, UseWebSocketResult, WebSocketClient, WebSocketHandlers, WebSocketLatency, WebSocketLatencyOptions, WebSocketOptions, WebSocketRequests, WebSocketRequestsOptions, WebSocketResponder, WebSocketResponderOptions, WebSocketSendRate, WebSocketSendRateOptions, WebSocketState, WebSocketStateOptions, WebSocketTopics, WebSocketTopicsOptions };
|
|
897
1017
|
//# sourceMappingURL=index.d.cts.map
|
package/dist/index.d.ts
CHANGED
|
@@ -113,14 +113,42 @@ interface SendOptions {
|
|
|
113
113
|
volatile?: boolean;
|
|
114
114
|
/**
|
|
115
115
|
* Keep only the newest queued message with this key. Sending again with the same key
|
|
116
|
-
* while disconnected replaces the earlier one, which moves to the back of the queue
|
|
116
|
+
* while disconnected replaces the earlier one, which moves to the back of the queue
|
|
117
|
+
* (or of the `priority` messages, for a priority one).
|
|
117
118
|
* For state where only the latest value matters, such as "my position is now (x, y)".
|
|
118
119
|
*/
|
|
119
120
|
key?: string;
|
|
121
|
+
/**
|
|
122
|
+
* Queue this message ahead of every message queued without `priority`, after earlier
|
|
123
|
+
* priority messages. For control messages that must not wait behind a backlog, such as
|
|
124
|
+
* cancel, interrupt or a token refresh. Messages sent from `open` listeners still go first.
|
|
125
|
+
*
|
|
126
|
+
* It only matters while the socket isn't open: once a message is handed to the browser,
|
|
127
|
+
* nothing can overtake it. To keep that buffer short, send bulky streams with `maxBuffered`.
|
|
128
|
+
* @default false
|
|
129
|
+
*/
|
|
130
|
+
priority?: boolean;
|
|
131
|
+
/**
|
|
132
|
+
* Drop the message instead of sending it while more than this many bytes are already
|
|
133
|
+
* waiting in the browser's send buffer (`socket.bufferedAmount`). For high-rate streams on
|
|
134
|
+
* a slow link, such as audio frames or per-tick input, where a late message is worse than
|
|
135
|
+
* none. `send()` returns `false`, and `onDrop` isn't called.
|
|
136
|
+
*
|
|
137
|
+
* Only checked while the socket is open: a message queued while disconnected is sent on
|
|
138
|
+
* open regardless. `bufferedAmount` includes what you sent earlier in the same task, so set
|
|
139
|
+
* it well above one burst of messages.
|
|
140
|
+
*
|
|
141
|
+
* @example
|
|
142
|
+
* ```ts
|
|
143
|
+
* voice.send(frame, { volatile: true, maxBuffered: 64 * 1024 });
|
|
144
|
+
* ```
|
|
145
|
+
*/
|
|
146
|
+
maxBuffered?: number;
|
|
120
147
|
/**
|
|
121
148
|
* Called if the message is dropped from the queue without being sent: replaced by a
|
|
122
149
|
* newer message with the same `key`, or evicted by `queue.limit`. Not called when the
|
|
123
|
-
* queue is cleared because the client closed or gave up
|
|
150
|
+
* queue is cleared because the client closed or gave up, or when `send()` drops the
|
|
151
|
+
* message right away (`volatile` while not open, or `maxBuffered`) and returns `false`.
|
|
124
152
|
*/
|
|
125
153
|
onDrop?: () => void;
|
|
126
154
|
}
|
|
@@ -128,7 +156,7 @@ interface SendOptions {
|
|
|
128
156
|
interface QueueOptions {
|
|
129
157
|
/**
|
|
130
158
|
* Maximum number of messages held while not connected. When full, the oldest
|
|
131
|
-
* message is dropped.
|
|
159
|
+
* message is dropped, skipping `priority` messages unless nothing else is queued.
|
|
132
160
|
* @default Infinity
|
|
133
161
|
*/
|
|
134
162
|
limit?: number;
|
|
@@ -311,12 +339,14 @@ interface WebSocketClient<In = unknown, Out = unknown> {
|
|
|
311
339
|
* Send a message (encoded with `serialize`/`json` if configured). If the socket isn't
|
|
312
340
|
* open, the message is queued and sent on open (see the `queue` option), unless
|
|
313
341
|
* `options.volatile` is set.
|
|
314
|
-
* @returns `true` if it was sent immediately, `false` if it was queued or dropped
|
|
342
|
+
* @returns `true` if it was sent immediately, `false` if it was queued or dropped
|
|
343
|
+
* (`volatile` while not open, or `maxBuffered` while open).
|
|
315
344
|
*
|
|
316
345
|
* @example
|
|
317
346
|
* ```ts
|
|
318
347
|
* game.send({ type: 'input', dx, dy }, { volatile: true }); // never replayed after a reconnect
|
|
319
348
|
* game.send({ type: 'position', x, y }, { key: 'position' }); // only the newest is queued
|
|
349
|
+
* game.send({ type: 'leave' }, { priority: true }); // ahead of everything already queued
|
|
320
350
|
* ```
|
|
321
351
|
*/
|
|
322
352
|
send(message: Out, options?: SendOptions): boolean;
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
2
|
+
//#region src/timeline.ts
|
|
3
|
+
const MAX_PENDING = 1e3;
|
|
4
|
+
/**
|
|
5
|
+
* Keep server snapshots on a timeline for multiplayer games: render what the server moves
|
|
6
|
+
* slightly in the past, interpolated between snapshots, and what your inputs move right away,
|
|
7
|
+
* predicted and corrected when the server confirms them.
|
|
8
|
+
*
|
|
9
|
+
* Read it once per frame (in `requestAnimationFrame` or React Three Fiber's `useFrame`): `at()`
|
|
10
|
+
* for everyone else, `predicted()` for you. It listens from creation, starts over on reconnect,
|
|
11
|
+
* and doesn't open the connection by itself.
|
|
12
|
+
*
|
|
13
|
+
* @example
|
|
14
|
+
* ```ts
|
|
15
|
+
* import { createWebSocketTimeline } from 'react-websocket/timeline';
|
|
16
|
+
*
|
|
17
|
+
* export const timeline = createWebSocketTimeline(game, {
|
|
18
|
+
* snapshot: m => (m.type === 'snapshot' ? { time: m.time, ack: m.ack, state: m.world } : undefined),
|
|
19
|
+
* interpolate: (from, to, t) => lerpWorld(from, to, t),
|
|
20
|
+
* latency,
|
|
21
|
+
* input: {
|
|
22
|
+
* send: (input: Move, seq) => ({ type: 'input', seq, ...input }),
|
|
23
|
+
* predict: (world, input) => move(world, me, input)
|
|
24
|
+
* }
|
|
25
|
+
* });
|
|
26
|
+
*
|
|
27
|
+
* // Every frame:
|
|
28
|
+
* draw(timeline.at(), timeline.predicted());
|
|
29
|
+
* // On a key press:
|
|
30
|
+
* timeline.input({ dx: 1, dy: 0 });
|
|
31
|
+
* ```
|
|
32
|
+
*/
|
|
33
|
+
function createWebSocketTimeline(client, options) {
|
|
34
|
+
const { interpolate, latency, input: inputs } = options;
|
|
35
|
+
const delay = options.delay ?? 100;
|
|
36
|
+
const extrapolate = options.extrapolate ?? 0;
|
|
37
|
+
const keep = Math.max(2, options.buffer ?? 30);
|
|
38
|
+
const listeners = /* @__PURE__ */ new Set();
|
|
39
|
+
let buffer = [];
|
|
40
|
+
let arrival = -Infinity;
|
|
41
|
+
let rendered = -Infinity;
|
|
42
|
+
let stale = false;
|
|
43
|
+
let seq = 0;
|
|
44
|
+
let pending = [];
|
|
45
|
+
let prediction;
|
|
46
|
+
let folded = -1;
|
|
47
|
+
function notify() {
|
|
48
|
+
for (const listener of [...listeners]) listener();
|
|
49
|
+
}
|
|
50
|
+
function clear() {
|
|
51
|
+
buffer = [];
|
|
52
|
+
arrival = rendered = -Infinity;
|
|
53
|
+
stale = false;
|
|
54
|
+
}
|
|
55
|
+
function receive(snapshot) {
|
|
56
|
+
if (stale) clear();
|
|
57
|
+
const newest = buffer.at(-1);
|
|
58
|
+
if (newest && snapshot.time <= newest.snapshot.time) return;
|
|
59
|
+
buffer.push({
|
|
60
|
+
snapshot,
|
|
61
|
+
arrived: performance.now()
|
|
62
|
+
});
|
|
63
|
+
if (buffer.length > keep) buffer.shift();
|
|
64
|
+
arrival = -Infinity;
|
|
65
|
+
for (const entry of buffer) arrival = Math.max(arrival, entry.snapshot.time - entry.arrived);
|
|
66
|
+
const { ack } = snapshot;
|
|
67
|
+
if (ack != null) pending = pending.filter((entry) => entry.seq > ack);
|
|
68
|
+
folded = -1;
|
|
69
|
+
notify();
|
|
70
|
+
}
|
|
71
|
+
function renderTime() {
|
|
72
|
+
if (!buffer.length) return;
|
|
73
|
+
const measured = latency?.getSnapshot();
|
|
74
|
+
const edge = measured?.offset !== void 0 && measured.rtt !== void 0 ? latency.serverNow() - measured.rtt / 2 : performance.now() + arrival;
|
|
75
|
+
rendered = Math.max(rendered, edge - (typeof delay === "function" ? delay() : delay));
|
|
76
|
+
return rendered;
|
|
77
|
+
}
|
|
78
|
+
function at(time) {
|
|
79
|
+
const n = buffer.length;
|
|
80
|
+
if (!n) return;
|
|
81
|
+
const t = time ?? renderTime();
|
|
82
|
+
const last = buffer[n - 1].snapshot;
|
|
83
|
+
if (t >= last.time) {
|
|
84
|
+
const previous = buffer[n - 2]?.snapshot;
|
|
85
|
+
if (!previous || !interpolate || !extrapolate || t === last.time) return last.state;
|
|
86
|
+
const capped = Math.min(t, last.time + extrapolate);
|
|
87
|
+
return interpolate(previous.state, last.state, (capped - previous.time) / (last.time - previous.time));
|
|
88
|
+
}
|
|
89
|
+
let i = n - 2;
|
|
90
|
+
while (i >= 0 && buffer[i].snapshot.time > t) i--;
|
|
91
|
+
if (i < 0) return buffer[0].snapshot.state;
|
|
92
|
+
const from = buffer[i].snapshot;
|
|
93
|
+
const to = buffer[i + 1].snapshot;
|
|
94
|
+
return !interpolate || t === from.time ? from.state : interpolate(from.state, to.state, (t - from.time) / (to.time - from.time));
|
|
95
|
+
}
|
|
96
|
+
function predicted() {
|
|
97
|
+
const newest = buffer.at(-1);
|
|
98
|
+
if (!newest) return;
|
|
99
|
+
if (folded < 0) {
|
|
100
|
+
prediction = newest.snapshot.state;
|
|
101
|
+
folded = 0;
|
|
102
|
+
}
|
|
103
|
+
while (folded < pending.length) {
|
|
104
|
+
prediction = inputs.predict(prediction, pending[folded].input);
|
|
105
|
+
folded++;
|
|
106
|
+
}
|
|
107
|
+
return prediction;
|
|
108
|
+
}
|
|
109
|
+
const offs = [client.on("open", () => {
|
|
110
|
+
pending = [];
|
|
111
|
+
folded = -1;
|
|
112
|
+
stale = true;
|
|
113
|
+
}), client.on("message", (message) => {
|
|
114
|
+
const snapshot = options.snapshot(message);
|
|
115
|
+
if (snapshot != null) receive(snapshot);
|
|
116
|
+
})];
|
|
117
|
+
return {
|
|
118
|
+
client,
|
|
119
|
+
at,
|
|
120
|
+
predicted,
|
|
121
|
+
input(value) {
|
|
122
|
+
const next = seq + 1;
|
|
123
|
+
const message = inputs.send(value, next);
|
|
124
|
+
if (!client.send(message, {
|
|
125
|
+
volatile: true,
|
|
126
|
+
maxBuffered: inputs.maxBuffered
|
|
127
|
+
})) return;
|
|
128
|
+
seq = next;
|
|
129
|
+
pending.push({
|
|
130
|
+
seq,
|
|
131
|
+
input: value
|
|
132
|
+
});
|
|
133
|
+
if (pending.length > MAX_PENDING) {
|
|
134
|
+
pending.shift();
|
|
135
|
+
folded = -1;
|
|
136
|
+
}
|
|
137
|
+
return seq;
|
|
138
|
+
},
|
|
139
|
+
latest: () => buffer.at(-1)?.snapshot,
|
|
140
|
+
renderTime,
|
|
141
|
+
pending: () => pending.length,
|
|
142
|
+
subscribe(listener) {
|
|
143
|
+
listeners.add(listener);
|
|
144
|
+
return () => {
|
|
145
|
+
listeners.delete(listener);
|
|
146
|
+
};
|
|
147
|
+
},
|
|
148
|
+
reset() {
|
|
149
|
+
clear();
|
|
150
|
+
pending = [];
|
|
151
|
+
folded = -1;
|
|
152
|
+
notify();
|
|
153
|
+
},
|
|
154
|
+
dispose() {
|
|
155
|
+
offs.forEach((off) => off());
|
|
156
|
+
}
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
//#endregion
|
|
160
|
+
exports.createWebSocketTimeline = createWebSocketTimeline;
|
|
161
|
+
|
|
162
|
+
//# sourceMappingURL=timeline.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"timeline.cjs","names":[],"sources":["../src/timeline.ts"],"sourcesContent":["// Types only: the built entry has no imports, and its declarations point at `core`'s.\nimport type { WebSocketClient, WebSocketLatency } from './core';\n\n/** A server snapshot, as returned by the `snapshot` option. */\nexport interface TimelineSnapshot<S> {\n /** Server time the state is for, in ms. Must increase from one snapshot to the next. */\n readonly time: number;\n /** The authoritative state at `time`. */\n readonly state: S;\n /**\n * `seq` of the last input from this client that the server applied before taking the\n * snapshot. Inputs up to it are confirmed and no longer predicted. Omit it to keep the\n * previous one.\n */\n readonly ack?: number;\n}\n\n/** Client-side prediction for your own inputs. See {@link WebSocketTimelineOptions.input}. */\nexport interface TimelineInputOptions<S, I, Out> {\n /**\n * Build the message for an input, tagged with its `seq`. Annotate `input`: that's what\n * `timeline.input()` accepts. Sent with `volatile`, so inputs are never replayed late.\n */\n send: (input: I, seq: number) => Out;\n /**\n * Apply one input to a state, exactly as the server does. Must be pure: return a new state\n * instead of changing `state`, which is a snapshot from the server.\n */\n predict: (state: S, input: I) => S;\n /**\n * Drop an input instead of sending it while more than this many bytes are already waiting in\n * the browser's send buffer (see `SendOptions.maxBuffered`). For input sent every tick on a\n * slow link. A dropped input isn't predicted.\n */\n maxBuffered?: number;\n}\n\n/** Options for {@link createWebSocketTimeline}. */\nexport interface WebSocketTimelineOptions<S, I, In, Out> {\n /** Recognize a snapshot. Return `undefined` (or `null`) for any other message. */\n snapshot: (message: In) => TimelineSnapshot<S> | null | undefined;\n /**\n * Blend two states. `t` runs from 0 (`from`) to 1 (`to`), and above 1 only when\n * extrapolating. Without it, `at()` steps from one snapshot to the next.\n */\n interpolate?: (from: S, to: S, t: number) => S;\n /**\n * How far behind the newest snapshot to render, in ms: enough to cover the snapshot\n * interval plus jitter, so there are usually two snapshots to blend between. A function is\n * read on every call, for a delay that adapts.\n * @default 100\n */\n delay?: number | (() => number);\n /**\n * How far past the newest snapshot `at()` may extrapolate when the next one is late, in ms.\n * Needs `interpolate`. `0` holds the newest state instead.\n * @default 0\n */\n extrapolate?: number;\n /**\n * How many snapshots to keep (at least 2).\n * @default 30\n */\n buffer?: number;\n /**\n * Take the server clock from `createWebSocketLatency` on the same client. Its pongs must use\n * the same clock as snapshot `time`. Without it, or before the first pong with a time, the\n * server clock is estimated from when snapshots arrive.\n */\n latency?: Pick<WebSocketLatency, 'getSnapshot' | 'serverNow'>;\n /** Send inputs tagged with a sequence number, and predict their effect until the server confirms them. */\n input?: TimelineInputOptions<S, I, Out>;\n}\n\n/** Server snapshots on a timeline, with your own inputs predicted on top. Create with {@link createWebSocketTimeline}. */\nexport interface WebSocketTimeline<S, I> {\n /** The client whose snapshots this timeline keeps. */\n readonly client: WebSocketClient<any, any>;\n /**\n * The state at `time` (by default `renderTime()`), interpolated between the snapshots around\n * it. For what the server moves, such as other players. `undefined` before any snapshot.\n */\n at(time?: number): S | undefined;\n /**\n * The newest snapshot with your unconfirmed inputs replayed on top. For what your inputs\n * move, such as your own player. Cached, so it's cheap to call every frame.\n */\n predicted(): S | undefined;\n /**\n * Send an input and predict it. Returns its `seq`, or `undefined` if it couldn't be sent\n * (the socket isn't open, or `maxBuffered` dropped it), in which case it isn't predicted.\n */\n input(input: I): number | undefined;\n /** The newest snapshot, as `snapshot` returned it. */\n latest(): TimelineSnapshot<S> | undefined;\n /**\n * The server time `at()` renders by default: the newest snapshot's time, give or take, minus\n * `delay`. It never goes backward until the timeline starts over. `undefined` before any snapshot.\n */\n renderTime(): number | undefined;\n /** How many inputs were sent but not confirmed yet. */\n pending(): number;\n /**\n * Called after each new snapshot and on `reset()`. Compatible with `useSyncExternalStore` for\n * `latest` and `pending`, but not for `at`, `predicted` or `renderTime`, which change with time.\n */\n subscribe(listener: () => void): () => void;\n /** Forget all snapshots and pending inputs now. `seq` keeps counting. */\n reset(): void;\n /** Stop listening. Only needed for timelines created dynamically. */\n dispose(): void;\n}\n\n// Unconfirmed inputs kept at most. A server that stops confirming would otherwise make every\n// snapshot replay a growing list.\nconst MAX_PENDING = 1000;\n\n/**\n * Keep server snapshots on a timeline for multiplayer games: render what the server moves\n * slightly in the past, interpolated between snapshots, and what your inputs move right away,\n * predicted and corrected when the server confirms them.\n *\n * Read it once per frame (in `requestAnimationFrame` or React Three Fiber's `useFrame`): `at()`\n * for everyone else, `predicted()` for you. It listens from creation, starts over on reconnect,\n * and doesn't open the connection by itself.\n *\n * @example\n * ```ts\n * import { createWebSocketTimeline } from 'react-websocket/timeline';\n *\n * export const timeline = createWebSocketTimeline(game, {\n * snapshot: m => (m.type === 'snapshot' ? { time: m.time, ack: m.ack, state: m.world } : undefined),\n * interpolate: (from, to, t) => lerpWorld(from, to, t),\n * latency,\n * input: {\n * send: (input: Move, seq) => ({ type: 'input', seq, ...input }),\n * predict: (world, input) => move(world, me, input)\n * }\n * });\n *\n * // Every frame:\n * draw(timeline.at(), timeline.predicted());\n * // On a key press:\n * timeline.input({ dx: 1, dy: 0 });\n * ```\n */\n// `const` + `NoInfer`: take `In` and `Out` from the client only. `S` comes from `snapshot`, and\n// `I` from how `input.send` annotates its parameter (`never` without `input`).\nexport function createWebSocketTimeline<In, const Out, S, I = never>(\n client: WebSocketClient<In, Out>,\n options: WebSocketTimelineOptions<S, I, NoInfer<In>, NoInfer<Out>>\n): WebSocketTimeline<S, I> {\n const { interpolate, latency, input: inputs } = options;\n const delay = options.delay ?? 100;\n const extrapolate = options.extrapolate ?? 0;\n const keep = Math.max(2, options.buffer ?? 30);\n const listeners = new Set<() => void>();\n\n // Oldest first, `time` strictly increasing. `arrived` is `performance.now()` on receipt.\n let buffer: { snapshot: TimelineSnapshot<S>; arrived: number }[] = [];\n // Server time minus `performance.now()`, from the least delayed snapshot in the buffer.\n let arrival = -Infinity;\n // The last render time handed out, so that it never goes backward.\n let rendered = -Infinity;\n // A new connection opened: its first snapshot replaces the buffer.\n let stale = false;\n let seq = 0;\n let pending: { seq: number; input: I }[] = [];\n let prediction: S | undefined;\n // How many pending inputs `prediction` includes. `-1`: start again from the newest snapshot.\n let folded = -1;\n\n function notify() {\n for (const listener of [...listeners]) listener();\n }\n\n function clear() {\n buffer = [];\n arrival = rendered = -Infinity;\n stale = false;\n }\n\n function receive(snapshot: TimelineSnapshot<S>) {\n if (stale) clear();\n const newest = buffer.at(-1);\n // A WebSocket keeps order, so this is a duplicate or a server clock that stepped back.\n if (newest && snapshot.time <= newest.snapshot.time) return;\n buffer.push({ snapshot, arrived: performance.now() });\n if (buffer.length > keep) buffer.shift();\n arrival = -Infinity;\n for (const entry of buffer) arrival = Math.max(arrival, entry.snapshot.time - entry.arrived);\n const { ack } = snapshot;\n if (ack != null) pending = pending.filter(entry => entry.seq > ack);\n folded = -1;\n notify();\n }\n\n function renderTime() {\n if (!buffer.length) return;\n const measured = latency?.getSnapshot();\n // The server time of a snapshot arriving now: half a round trip behind the server's clock.\n const edge =\n measured?.offset !== undefined && measured.rtt !== undefined\n ? latency!.serverNow() - measured.rtt / 2\n : performance.now() + arrival;\n rendered = Math.max(rendered, edge - (typeof delay === 'function' ? delay() : delay));\n return rendered;\n }\n\n function at(time?: number): S | undefined {\n const n = buffer.length;\n if (!n) return;\n const t = time ?? renderTime()!;\n const last = buffer[n - 1]!.snapshot;\n if (t >= last.time) {\n const previous = buffer[n - 2]?.snapshot;\n if (!previous || !interpolate || !extrapolate || t === last.time) return last.state;\n const capped = Math.min(t, last.time + extrapolate);\n return interpolate(\n previous.state,\n last.state,\n (capped - previous.time) / (last.time - previous.time)\n );\n }\n // Reads are usually near the newest snapshot, so search from the end.\n let i = n - 2;\n while (i >= 0 && buffer[i]!.snapshot.time > t) i--;\n if (i < 0) return buffer[0]!.snapshot.state;\n const from = buffer[i]!.snapshot;\n const to = buffer[i + 1]!.snapshot;\n return !interpolate || t === from.time\n ? from.state\n : interpolate(from.state, to.state, (t - from.time) / (to.time - from.time));\n }\n\n function predicted(): S | undefined {\n const newest = buffer.at(-1);\n if (!newest) return;\n if (folded < 0) {\n prediction = newest.snapshot.state;\n folded = 0;\n }\n // Usually zero or one new input per frame; all of them once after each snapshot.\n while (folded < pending.length) {\n prediction = inputs!.predict(prediction!, pending[folded]!.input);\n folded++; // after `predict`, so one that throws is retried rather than skipped\n }\n return prediction;\n }\n\n const offs = [\n // Inputs on the old connection will never be confirmed. Keep showing the old snapshots\n // until the new connection sends its first one.\n client.on('open', () => {\n pending = [];\n folded = -1;\n stale = true;\n }),\n // A `snapshot` that throws leaves the timeline unchanged; the client re-throws it asynchronously.\n client.on('message', message => {\n const snapshot = options.snapshot(message);\n if (snapshot != null) receive(snapshot);\n })\n ];\n\n return {\n client,\n at,\n predicted,\n input(value) {\n const next = seq + 1;\n // Recorded only once it's sent, so a dropped input never shows as a move the server didn't make.\n const message = inputs!.send(value, next);\n if (!client.send(message, { volatile: true, maxBuffered: inputs!.maxBuffered })) return;\n seq = next;\n pending.push({ seq, input: value });\n if (pending.length > MAX_PENDING) {\n pending.shift();\n folded = -1;\n }\n return seq;\n },\n latest: () => buffer.at(-1)?.snapshot,\n renderTime,\n pending: () => pending.length,\n subscribe(listener) {\n listeners.add(listener);\n return () => {\n listeners.delete(listener);\n };\n },\n reset() {\n clear();\n pending = [];\n folded = -1;\n notify();\n },\n dispose() {\n offs.forEach(off => off());\n }\n };\n}\n"],"mappings":"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"}
|