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/core.d.cts
CHANGED
|
@@ -111,14 +111,42 @@ interface SendOptions {
|
|
|
111
111
|
volatile?: boolean;
|
|
112
112
|
/**
|
|
113
113
|
* Keep only the newest queued message with this key. Sending again with the same key
|
|
114
|
-
* while disconnected replaces the earlier one, which moves to the back of the queue
|
|
114
|
+
* while disconnected replaces the earlier one, which moves to the back of the queue
|
|
115
|
+
* (or of the `priority` messages, for a priority one).
|
|
115
116
|
* For state where only the latest value matters, such as "my position is now (x, y)".
|
|
116
117
|
*/
|
|
117
118
|
key?: string;
|
|
119
|
+
/**
|
|
120
|
+
* Queue this message ahead of every message queued without `priority`, after earlier
|
|
121
|
+
* priority messages. For control messages that must not wait behind a backlog, such as
|
|
122
|
+
* cancel, interrupt or a token refresh. Messages sent from `open` listeners still go first.
|
|
123
|
+
*
|
|
124
|
+
* It only matters while the socket isn't open: once a message is handed to the browser,
|
|
125
|
+
* nothing can overtake it. To keep that buffer short, send bulky streams with `maxBuffered`.
|
|
126
|
+
* @default false
|
|
127
|
+
*/
|
|
128
|
+
priority?: boolean;
|
|
129
|
+
/**
|
|
130
|
+
* Drop the message instead of sending it while more than this many bytes are already
|
|
131
|
+
* waiting in the browser's send buffer (`socket.bufferedAmount`). For high-rate streams on
|
|
132
|
+
* a slow link, such as audio frames or per-tick input, where a late message is worse than
|
|
133
|
+
* none. `send()` returns `false`, and `onDrop` isn't called.
|
|
134
|
+
*
|
|
135
|
+
* Only checked while the socket is open: a message queued while disconnected is sent on
|
|
136
|
+
* open regardless. `bufferedAmount` includes what you sent earlier in the same task, so set
|
|
137
|
+
* it well above one burst of messages.
|
|
138
|
+
*
|
|
139
|
+
* @example
|
|
140
|
+
* ```ts
|
|
141
|
+
* voice.send(frame, { volatile: true, maxBuffered: 64 * 1024 });
|
|
142
|
+
* ```
|
|
143
|
+
*/
|
|
144
|
+
maxBuffered?: number;
|
|
118
145
|
/**
|
|
119
146
|
* Called if the message is dropped from the queue without being sent: replaced by a
|
|
120
147
|
* newer message with the same `key`, or evicted by `queue.limit`. Not called when the
|
|
121
|
-
* queue is cleared because the client closed or gave up
|
|
148
|
+
* queue is cleared because the client closed or gave up, or when `send()` drops the
|
|
149
|
+
* message right away (`volatile` while not open, or `maxBuffered`) and returns `false`.
|
|
122
150
|
*/
|
|
123
151
|
onDrop?: () => void;
|
|
124
152
|
}
|
|
@@ -126,7 +154,7 @@ interface SendOptions {
|
|
|
126
154
|
interface QueueOptions {
|
|
127
155
|
/**
|
|
128
156
|
* Maximum number of messages held while not connected. When full, the oldest
|
|
129
|
-
* message is dropped.
|
|
157
|
+
* message is dropped, skipping `priority` messages unless nothing else is queued.
|
|
130
158
|
* @default Infinity
|
|
131
159
|
*/
|
|
132
160
|
limit?: number;
|
|
@@ -309,12 +337,14 @@ interface WebSocketClient<In = unknown, Out = unknown> {
|
|
|
309
337
|
* Send a message (encoded with `serialize`/`json` if configured). If the socket isn't
|
|
310
338
|
* open, the message is queued and sent on open (see the `queue` option), unless
|
|
311
339
|
* `options.volatile` is set.
|
|
312
|
-
* @returns `true` if it was sent immediately, `false` if it was queued or dropped
|
|
340
|
+
* @returns `true` if it was sent immediately, `false` if it was queued or dropped
|
|
341
|
+
* (`volatile` while not open, or `maxBuffered` while open).
|
|
313
342
|
*
|
|
314
343
|
* @example
|
|
315
344
|
* ```ts
|
|
316
345
|
* game.send({ type: 'input', dx, dy }, { volatile: true }); // never replayed after a reconnect
|
|
317
346
|
* game.send({ type: 'position', x, y }, { key: 'position' }); // only the newest is queued
|
|
347
|
+
* game.send({ type: 'leave' }, { priority: true }); // ahead of everything already queued
|
|
318
348
|
* ```
|
|
319
349
|
*/
|
|
320
350
|
send(message: Out, options?: SendOptions): boolean;
|
|
@@ -649,6 +679,85 @@ interface WebSocketResponder<P = unknown, R = unknown> {
|
|
|
649
679
|
*/
|
|
650
680
|
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>;
|
|
651
681
|
//#endregion
|
|
682
|
+
//#region src/sendrate.d.ts
|
|
683
|
+
/** Options for {@link createWebSocketSendRate}. */
|
|
684
|
+
interface WebSocketSendRateOptions {
|
|
685
|
+
/**
|
|
686
|
+
* Ticks per second while the connection keeps up.
|
|
687
|
+
* @default 30
|
|
688
|
+
*/
|
|
689
|
+
maxRate?: number;
|
|
690
|
+
/**
|
|
691
|
+
* The rate never drops below this, in ticks per second.
|
|
692
|
+
* @default 5
|
|
693
|
+
*/
|
|
694
|
+
minRate?: number;
|
|
695
|
+
/**
|
|
696
|
+
* Bytes waiting in the browser's send buffer (`socket.bufferedAmount`) above which a tick
|
|
697
|
+
* sends nothing and the rate halves.
|
|
698
|
+
* @default 1024
|
|
699
|
+
*/
|
|
700
|
+
maxBuffered?: number;
|
|
701
|
+
/**
|
|
702
|
+
* Measurements from `createWebSocketLatency` on the same client. With them, a round-trip time
|
|
703
|
+
* that rises also lowers the rate. Without them, only the send buffer is watched.
|
|
704
|
+
*/
|
|
705
|
+
latency?: Pick<WebSocketLatency, 'getSnapshot'>;
|
|
706
|
+
/**
|
|
707
|
+
* How far `rtt` may rise above the lowest one measured on this connection before the rate
|
|
708
|
+
* halves, in ms. A steady high ping keeps the full rate; only a rising one counts.
|
|
709
|
+
* @default 100
|
|
710
|
+
*/
|
|
711
|
+
maxRttIncrease?: number;
|
|
712
|
+
}
|
|
713
|
+
/** The current send rate. A new object is created on every change. */
|
|
714
|
+
interface SendRate {
|
|
715
|
+
/** Ticks per second, rounded. */
|
|
716
|
+
readonly rate: number;
|
|
717
|
+
/**
|
|
718
|
+
* Whether the last tick found the link congested: the send buffer above `maxBuffered`, or
|
|
719
|
+
* `rtt` risen more than `maxRttIncrease`.
|
|
720
|
+
*/
|
|
721
|
+
readonly congested: boolean;
|
|
722
|
+
}
|
|
723
|
+
/** Keyed sends coalesced at a rate that adapts to the link. Create with {@link createWebSocketSendRate}. */
|
|
724
|
+
interface WebSocketSendRate<Out> {
|
|
725
|
+
/** The client the messages are sent on. */
|
|
726
|
+
readonly client: WebSocketClient<any, Out>;
|
|
727
|
+
/**
|
|
728
|
+
* Send `message` at the next tick. A message with the same `key` still waiting for it is
|
|
729
|
+
* replaced, so only the newest one goes out. Sent with `volatile`.
|
|
730
|
+
*/
|
|
731
|
+
send(message: Out, key: string): void;
|
|
732
|
+
/** Send everything waiting now, e.g. right before a one-shot action that must carry the latest aim. */
|
|
733
|
+
flush(): void;
|
|
734
|
+
/** The current {@link SendRate}. Returns the same object until it changes. */
|
|
735
|
+
getSnapshot(): SendRate;
|
|
736
|
+
/** Called whenever the rate or `congested` changes. Compatible with `useSyncExternalStore`. */
|
|
737
|
+
subscribe(listener: () => void): () => void;
|
|
738
|
+
/** Stop sending and listening. Only needed for instances created dynamically. */
|
|
739
|
+
dispose(): void;
|
|
740
|
+
}
|
|
741
|
+
/**
|
|
742
|
+
* Send high-rate state, such as aim, cursor or position, at a rate that adapts to the
|
|
743
|
+
* connection. Messages are coalesced per key between ticks. The rate drops by half when the
|
|
744
|
+
* send buffer fills up or the round trip grows, and climbs back one tick per second at a time
|
|
745
|
+
* when the link keeps up, so players on a poor connection degrade smoothly instead of building
|
|
746
|
+
* up lag.
|
|
747
|
+
*
|
|
748
|
+
* Ticks run only while the socket is open and something is waiting. Messages waiting during a
|
|
749
|
+
* reconnect go out after it; `close()` drops them. It doesn't open the connection by itself.
|
|
750
|
+
*
|
|
751
|
+
* @example
|
|
752
|
+
* ```ts
|
|
753
|
+
* export const stream = createWebSocketSendRate(game, { latency, maxRate: 30 });
|
|
754
|
+
*
|
|
755
|
+
* // On every pointer move: at most 30 a second, fewer on a congested link.
|
|
756
|
+
* stream.send({ type: 'aim', angle }, 'aim');
|
|
757
|
+
* ```
|
|
758
|
+
*/
|
|
759
|
+
export declare function createWebSocketSendRate<Out>(client: WebSocketClient<any, Out>, options?: WebSocketSendRateOptions): WebSocketSendRate<Out>;
|
|
760
|
+
//#endregion
|
|
652
761
|
//#region src/state.d.ts
|
|
653
762
|
/** Options for {@link createWebSocketState}. */
|
|
654
763
|
interface WebSocketStateOptions<S, In> {
|
|
@@ -659,6 +768,17 @@ interface WebSocketStateOptions<S, In> {
|
|
|
659
768
|
* change, or the same `state` to ignore the message (subscribers aren't notified).
|
|
660
769
|
*/
|
|
661
770
|
reduce: (state: S, message: In) => S;
|
|
771
|
+
/**
|
|
772
|
+
* `'frame'`: notify subscribers at most once per animation frame instead of once per
|
|
773
|
+
* message, so a fast stream (tokens, tickers, game state) renders once per frame.
|
|
774
|
+
* `getState()` is always current; only notifications wait. `setState` and `reset` still
|
|
775
|
+
* notify right away.
|
|
776
|
+
*
|
|
777
|
+
* Browsers pause animation frames in background tabs: the state keeps up, and subscribers
|
|
778
|
+
* hear about it when the tab is visible again. Without `requestAnimationFrame` (Node, some
|
|
779
|
+
* workers), it waits about 16 ms instead.
|
|
780
|
+
*/
|
|
781
|
+
batch?: 'frame';
|
|
662
782
|
}
|
|
663
783
|
/**
|
|
664
784
|
* State derived from a client's messages. Read it with `useWebSocketState(store, selector)`,
|
|
@@ -748,5 +868,5 @@ interface WebSocketTopics {
|
|
|
748
868
|
*/
|
|
749
869
|
export declare function createWebSocketTopics<const Out>(client: WebSocketClient<any, Out>, options: WebSocketTopicsOptions<NoInfer<Out>>): WebSocketTopics;
|
|
750
870
|
//#endregion
|
|
751
|
-
export type { CloseInfo, EventMap, HeartbeatOptions, IncomingRequest, Latency, QueueOptions, ReconnectOptions, RequestHandler, RequestOptions, SendData, SendOptions, Snapshot, StandardSchemaV1, Status, UrlSource, WebSocketClient, WebSocketLatency, WebSocketLatencyOptions, WebSocketOptions, WebSocketRequests, WebSocketRequestsOptions, WebSocketResponder, WebSocketResponderOptions, WebSocketState, WebSocketStateOptions, WebSocketTopics, WebSocketTopicsOptions };
|
|
871
|
+
export type { CloseInfo, EventMap, HeartbeatOptions, IncomingRequest, Latency, QueueOptions, ReconnectOptions, RequestHandler, RequestOptions, SendData, SendOptions, SendRate, Snapshot, StandardSchemaV1, Status, UrlSource, WebSocketClient, WebSocketLatency, WebSocketLatencyOptions, WebSocketOptions, WebSocketRequests, WebSocketRequestsOptions, WebSocketResponder, WebSocketResponderOptions, WebSocketSendRate, WebSocketSendRateOptions, WebSocketState, WebSocketStateOptions, WebSocketTopics, WebSocketTopicsOptions };
|
|
752
872
|
//# sourceMappingURL=core.d.cts.map
|
package/dist/core.d.ts
CHANGED
|
@@ -111,14 +111,42 @@ interface SendOptions {
|
|
|
111
111
|
volatile?: boolean;
|
|
112
112
|
/**
|
|
113
113
|
* Keep only the newest queued message with this key. Sending again with the same key
|
|
114
|
-
* while disconnected replaces the earlier one, which moves to the back of the queue
|
|
114
|
+
* while disconnected replaces the earlier one, which moves to the back of the queue
|
|
115
|
+
* (or of the `priority` messages, for a priority one).
|
|
115
116
|
* For state where only the latest value matters, such as "my position is now (x, y)".
|
|
116
117
|
*/
|
|
117
118
|
key?: string;
|
|
119
|
+
/**
|
|
120
|
+
* Queue this message ahead of every message queued without `priority`, after earlier
|
|
121
|
+
* priority messages. For control messages that must not wait behind a backlog, such as
|
|
122
|
+
* cancel, interrupt or a token refresh. Messages sent from `open` listeners still go first.
|
|
123
|
+
*
|
|
124
|
+
* It only matters while the socket isn't open: once a message is handed to the browser,
|
|
125
|
+
* nothing can overtake it. To keep that buffer short, send bulky streams with `maxBuffered`.
|
|
126
|
+
* @default false
|
|
127
|
+
*/
|
|
128
|
+
priority?: boolean;
|
|
129
|
+
/**
|
|
130
|
+
* Drop the message instead of sending it while more than this many bytes are already
|
|
131
|
+
* waiting in the browser's send buffer (`socket.bufferedAmount`). For high-rate streams on
|
|
132
|
+
* a slow link, such as audio frames or per-tick input, where a late message is worse than
|
|
133
|
+
* none. `send()` returns `false`, and `onDrop` isn't called.
|
|
134
|
+
*
|
|
135
|
+
* Only checked while the socket is open: a message queued while disconnected is sent on
|
|
136
|
+
* open regardless. `bufferedAmount` includes what you sent earlier in the same task, so set
|
|
137
|
+
* it well above one burst of messages.
|
|
138
|
+
*
|
|
139
|
+
* @example
|
|
140
|
+
* ```ts
|
|
141
|
+
* voice.send(frame, { volatile: true, maxBuffered: 64 * 1024 });
|
|
142
|
+
* ```
|
|
143
|
+
*/
|
|
144
|
+
maxBuffered?: number;
|
|
118
145
|
/**
|
|
119
146
|
* Called if the message is dropped from the queue without being sent: replaced by a
|
|
120
147
|
* newer message with the same `key`, or evicted by `queue.limit`. Not called when the
|
|
121
|
-
* queue is cleared because the client closed or gave up
|
|
148
|
+
* queue is cleared because the client closed or gave up, or when `send()` drops the
|
|
149
|
+
* message right away (`volatile` while not open, or `maxBuffered`) and returns `false`.
|
|
122
150
|
*/
|
|
123
151
|
onDrop?: () => void;
|
|
124
152
|
}
|
|
@@ -126,7 +154,7 @@ interface SendOptions {
|
|
|
126
154
|
interface QueueOptions {
|
|
127
155
|
/**
|
|
128
156
|
* Maximum number of messages held while not connected. When full, the oldest
|
|
129
|
-
* message is dropped.
|
|
157
|
+
* message is dropped, skipping `priority` messages unless nothing else is queued.
|
|
130
158
|
* @default Infinity
|
|
131
159
|
*/
|
|
132
160
|
limit?: number;
|
|
@@ -309,12 +337,14 @@ interface WebSocketClient<In = unknown, Out = unknown> {
|
|
|
309
337
|
* Send a message (encoded with `serialize`/`json` if configured). If the socket isn't
|
|
310
338
|
* open, the message is queued and sent on open (see the `queue` option), unless
|
|
311
339
|
* `options.volatile` is set.
|
|
312
|
-
* @returns `true` if it was sent immediately, `false` if it was queued or dropped
|
|
340
|
+
* @returns `true` if it was sent immediately, `false` if it was queued or dropped
|
|
341
|
+
* (`volatile` while not open, or `maxBuffered` while open).
|
|
313
342
|
*
|
|
314
343
|
* @example
|
|
315
344
|
* ```ts
|
|
316
345
|
* game.send({ type: 'input', dx, dy }, { volatile: true }); // never replayed after a reconnect
|
|
317
346
|
* game.send({ type: 'position', x, y }, { key: 'position' }); // only the newest is queued
|
|
347
|
+
* game.send({ type: 'leave' }, { priority: true }); // ahead of everything already queued
|
|
318
348
|
* ```
|
|
319
349
|
*/
|
|
320
350
|
send(message: Out, options?: SendOptions): boolean;
|
|
@@ -649,6 +679,85 @@ interface WebSocketResponder<P = unknown, R = unknown> {
|
|
|
649
679
|
*/
|
|
650
680
|
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>;
|
|
651
681
|
//#endregion
|
|
682
|
+
//#region src/sendrate.d.ts
|
|
683
|
+
/** Options for {@link createWebSocketSendRate}. */
|
|
684
|
+
interface WebSocketSendRateOptions {
|
|
685
|
+
/**
|
|
686
|
+
* Ticks per second while the connection keeps up.
|
|
687
|
+
* @default 30
|
|
688
|
+
*/
|
|
689
|
+
maxRate?: number;
|
|
690
|
+
/**
|
|
691
|
+
* The rate never drops below this, in ticks per second.
|
|
692
|
+
* @default 5
|
|
693
|
+
*/
|
|
694
|
+
minRate?: number;
|
|
695
|
+
/**
|
|
696
|
+
* Bytes waiting in the browser's send buffer (`socket.bufferedAmount`) above which a tick
|
|
697
|
+
* sends nothing and the rate halves.
|
|
698
|
+
* @default 1024
|
|
699
|
+
*/
|
|
700
|
+
maxBuffered?: number;
|
|
701
|
+
/**
|
|
702
|
+
* Measurements from `createWebSocketLatency` on the same client. With them, a round-trip time
|
|
703
|
+
* that rises also lowers the rate. Without them, only the send buffer is watched.
|
|
704
|
+
*/
|
|
705
|
+
latency?: Pick<WebSocketLatency, 'getSnapshot'>;
|
|
706
|
+
/**
|
|
707
|
+
* How far `rtt` may rise above the lowest one measured on this connection before the rate
|
|
708
|
+
* halves, in ms. A steady high ping keeps the full rate; only a rising one counts.
|
|
709
|
+
* @default 100
|
|
710
|
+
*/
|
|
711
|
+
maxRttIncrease?: number;
|
|
712
|
+
}
|
|
713
|
+
/** The current send rate. A new object is created on every change. */
|
|
714
|
+
interface SendRate {
|
|
715
|
+
/** Ticks per second, rounded. */
|
|
716
|
+
readonly rate: number;
|
|
717
|
+
/**
|
|
718
|
+
* Whether the last tick found the link congested: the send buffer above `maxBuffered`, or
|
|
719
|
+
* `rtt` risen more than `maxRttIncrease`.
|
|
720
|
+
*/
|
|
721
|
+
readonly congested: boolean;
|
|
722
|
+
}
|
|
723
|
+
/** Keyed sends coalesced at a rate that adapts to the link. Create with {@link createWebSocketSendRate}. */
|
|
724
|
+
interface WebSocketSendRate<Out> {
|
|
725
|
+
/** The client the messages are sent on. */
|
|
726
|
+
readonly client: WebSocketClient<any, Out>;
|
|
727
|
+
/**
|
|
728
|
+
* Send `message` at the next tick. A message with the same `key` still waiting for it is
|
|
729
|
+
* replaced, so only the newest one goes out. Sent with `volatile`.
|
|
730
|
+
*/
|
|
731
|
+
send(message: Out, key: string): void;
|
|
732
|
+
/** Send everything waiting now, e.g. right before a one-shot action that must carry the latest aim. */
|
|
733
|
+
flush(): void;
|
|
734
|
+
/** The current {@link SendRate}. Returns the same object until it changes. */
|
|
735
|
+
getSnapshot(): SendRate;
|
|
736
|
+
/** Called whenever the rate or `congested` changes. Compatible with `useSyncExternalStore`. */
|
|
737
|
+
subscribe(listener: () => void): () => void;
|
|
738
|
+
/** Stop sending and listening. Only needed for instances created dynamically. */
|
|
739
|
+
dispose(): void;
|
|
740
|
+
}
|
|
741
|
+
/**
|
|
742
|
+
* Send high-rate state, such as aim, cursor or position, at a rate that adapts to the
|
|
743
|
+
* connection. Messages are coalesced per key between ticks. The rate drops by half when the
|
|
744
|
+
* send buffer fills up or the round trip grows, and climbs back one tick per second at a time
|
|
745
|
+
* when the link keeps up, so players on a poor connection degrade smoothly instead of building
|
|
746
|
+
* up lag.
|
|
747
|
+
*
|
|
748
|
+
* Ticks run only while the socket is open and something is waiting. Messages waiting during a
|
|
749
|
+
* reconnect go out after it; `close()` drops them. It doesn't open the connection by itself.
|
|
750
|
+
*
|
|
751
|
+
* @example
|
|
752
|
+
* ```ts
|
|
753
|
+
* export const stream = createWebSocketSendRate(game, { latency, maxRate: 30 });
|
|
754
|
+
*
|
|
755
|
+
* // On every pointer move: at most 30 a second, fewer on a congested link.
|
|
756
|
+
* stream.send({ type: 'aim', angle }, 'aim');
|
|
757
|
+
* ```
|
|
758
|
+
*/
|
|
759
|
+
export declare function createWebSocketSendRate<Out>(client: WebSocketClient<any, Out>, options?: WebSocketSendRateOptions): WebSocketSendRate<Out>;
|
|
760
|
+
//#endregion
|
|
652
761
|
//#region src/state.d.ts
|
|
653
762
|
/** Options for {@link createWebSocketState}. */
|
|
654
763
|
interface WebSocketStateOptions<S, In> {
|
|
@@ -659,6 +768,17 @@ interface WebSocketStateOptions<S, In> {
|
|
|
659
768
|
* change, or the same `state` to ignore the message (subscribers aren't notified).
|
|
660
769
|
*/
|
|
661
770
|
reduce: (state: S, message: In) => S;
|
|
771
|
+
/**
|
|
772
|
+
* `'frame'`: notify subscribers at most once per animation frame instead of once per
|
|
773
|
+
* message, so a fast stream (tokens, tickers, game state) renders once per frame.
|
|
774
|
+
* `getState()` is always current; only notifications wait. `setState` and `reset` still
|
|
775
|
+
* notify right away.
|
|
776
|
+
*
|
|
777
|
+
* Browsers pause animation frames in background tabs: the state keeps up, and subscribers
|
|
778
|
+
* hear about it when the tab is visible again. Without `requestAnimationFrame` (Node, some
|
|
779
|
+
* workers), it waits about 16 ms instead.
|
|
780
|
+
*/
|
|
781
|
+
batch?: 'frame';
|
|
662
782
|
}
|
|
663
783
|
/**
|
|
664
784
|
* State derived from a client's messages. Read it with `useWebSocketState(store, selector)`,
|
|
@@ -748,5 +868,5 @@ interface WebSocketTopics {
|
|
|
748
868
|
*/
|
|
749
869
|
export declare function createWebSocketTopics<const Out>(client: WebSocketClient<any, Out>, options: WebSocketTopicsOptions<NoInfer<Out>>): WebSocketTopics;
|
|
750
870
|
//#endregion
|
|
751
|
-
export type { CloseInfo, EventMap, HeartbeatOptions, IncomingRequest, Latency, QueueOptions, ReconnectOptions, RequestHandler, RequestOptions, SendData, SendOptions, Snapshot, StandardSchemaV1, Status, UrlSource, WebSocketClient, WebSocketLatency, WebSocketLatencyOptions, WebSocketOptions, WebSocketRequests, WebSocketRequestsOptions, WebSocketResponder, WebSocketResponderOptions, WebSocketState, WebSocketStateOptions, WebSocketTopics, WebSocketTopicsOptions };
|
|
871
|
+
export type { CloseInfo, EventMap, HeartbeatOptions, IncomingRequest, Latency, QueueOptions, ReconnectOptions, RequestHandler, RequestOptions, SendData, SendOptions, SendRate, Snapshot, StandardSchemaV1, Status, UrlSource, WebSocketClient, WebSocketLatency, WebSocketLatencyOptions, WebSocketOptions, WebSocketRequests, WebSocketRequestsOptions, WebSocketResponder, WebSocketResponderOptions, WebSocketSendRate, WebSocketSendRateOptions, WebSocketState, WebSocketStateOptions, WebSocketTopics, WebSocketTopicsOptions };
|
|
752
872
|
//# sourceMappingURL=core.d.ts.map
|
package/dist/core.js
CHANGED
|
@@ -348,13 +348,18 @@ function createClient(url, options = {}) {
|
|
|
348
348
|
set({ lastMessage: message });
|
|
349
349
|
emit("message", message, event);
|
|
350
350
|
}
|
|
351
|
-
function send(message, sendOptions) {
|
|
351
|
+
function send(message, sendOptions = {}) {
|
|
352
352
|
const data = options.serialize ? options.serialize(message) : options.json ? JSON.stringify(message) : message;
|
|
353
|
+
const { key, priority, maxBuffered } = sendOptions;
|
|
353
354
|
if (ws && ws.readyState === OPEN) {
|
|
355
|
+
if (maxBuffered !== void 0 && ws.bufferedAmount > maxBuffered) {
|
|
356
|
+
log("dropped message: send buffer above maxBuffered");
|
|
357
|
+
return false;
|
|
358
|
+
}
|
|
354
359
|
ws.send(data);
|
|
355
360
|
return true;
|
|
356
361
|
}
|
|
357
|
-
if (queueLimit === 0 || sendOptions
|
|
362
|
+
if (queueLimit === 0 || sendOptions.volatile) {
|
|
358
363
|
log("dropped message: socket is not open");
|
|
359
364
|
return false;
|
|
360
365
|
}
|
|
@@ -362,19 +367,23 @@ function createClient(url, options = {}) {
|
|
|
362
367
|
log("dropped message: no WebSocket implementation available");
|
|
363
368
|
return false;
|
|
364
369
|
}
|
|
365
|
-
const key = sendOptions?.key;
|
|
366
370
|
if (key !== void 0) pending = pending.filter((entry) => {
|
|
367
371
|
if (entry.key !== key) return true;
|
|
368
372
|
dropped(entry);
|
|
369
373
|
return false;
|
|
370
374
|
});
|
|
371
|
-
pending.
|
|
375
|
+
let head = pending.findIndex((entry) => !entry.priority);
|
|
376
|
+
if (head < 0) head = pending.length;
|
|
377
|
+
const entry = {
|
|
372
378
|
data,
|
|
373
379
|
key,
|
|
374
|
-
|
|
375
|
-
|
|
380
|
+
priority,
|
|
381
|
+
onDrop: sendOptions.onDrop
|
|
382
|
+
};
|
|
383
|
+
if (priority) pending.splice(head++, 0, entry);
|
|
384
|
+
else pending.push(entry);
|
|
376
385
|
if (pending.length > queueLimit) {
|
|
377
|
-
dropped(pending.
|
|
386
|
+
dropped(pending.splice(head < pending.length ? head : 0, 1)[0]);
|
|
378
387
|
log(`dropped oldest queued message: queue limit (${queueLimit}) reached`);
|
|
379
388
|
}
|
|
380
389
|
return false;
|
|
@@ -743,6 +752,7 @@ function createWebSocketRequests(client, options) {
|
|
|
743
752
|
return;
|
|
744
753
|
}
|
|
745
754
|
if (sent) entry.socket = client.socket;
|
|
755
|
+
else if (client.socket?.readyState === 1) entry.reject(/* @__PURE__ */ new Error("WebSocket send buffer is full: request dropped"));
|
|
746
756
|
else if (requestOptions.volatile) entry.reject(/* @__PURE__ */ new Error("WebSocket is not open: request dropped"));
|
|
747
757
|
});
|
|
748
758
|
},
|
|
@@ -755,7 +765,7 @@ function createWebSocketRequests(client, options) {
|
|
|
755
765
|
}
|
|
756
766
|
//#endregion
|
|
757
767
|
//#region src/responder.ts
|
|
758
|
-
const VOLATILE = { volatile: true };
|
|
768
|
+
const VOLATILE$1 = { volatile: true };
|
|
759
769
|
const abortError = (message) => new DOMException(message, "AbortError");
|
|
760
770
|
/**
|
|
761
771
|
* Answer requests the server sends: run a handler for tools that only exist in the browser
|
|
@@ -796,9 +806,9 @@ function createWebSocketResponder(client, options) {
|
|
|
796
806
|
/** Send a reply, or an `error` reply if building or encoding it throws. */
|
|
797
807
|
function reply(id, build) {
|
|
798
808
|
try {
|
|
799
|
-
return client.send(build(), VOLATILE);
|
|
809
|
+
return client.send(build(), VOLATILE$1);
|
|
800
810
|
} catch (error) {
|
|
801
|
-
return client.send(options.error(id, error), VOLATILE);
|
|
811
|
+
return client.send(options.error(id, error), VOLATILE$1);
|
|
802
812
|
}
|
|
803
813
|
}
|
|
804
814
|
function receive(incoming) {
|
|
@@ -897,6 +907,123 @@ function createWebSocketResponder(client, options) {
|
|
|
897
907
|
};
|
|
898
908
|
}
|
|
899
909
|
//#endregion
|
|
910
|
+
//#region src/sendrate.ts
|
|
911
|
+
const VOLATILE = { volatile: true };
|
|
912
|
+
/**
|
|
913
|
+
* Send high-rate state, such as aim, cursor or position, at a rate that adapts to the
|
|
914
|
+
* connection. Messages are coalesced per key between ticks. The rate drops by half when the
|
|
915
|
+
* send buffer fills up or the round trip grows, and climbs back one tick per second at a time
|
|
916
|
+
* when the link keeps up, so players on a poor connection degrade smoothly instead of building
|
|
917
|
+
* up lag.
|
|
918
|
+
*
|
|
919
|
+
* Ticks run only while the socket is open and something is waiting. Messages waiting during a
|
|
920
|
+
* reconnect go out after it; `close()` drops them. It doesn't open the connection by itself.
|
|
921
|
+
*
|
|
922
|
+
* @example
|
|
923
|
+
* ```ts
|
|
924
|
+
* export const stream = createWebSocketSendRate(game, { latency, maxRate: 30 });
|
|
925
|
+
*
|
|
926
|
+
* // On every pointer move: at most 30 a second, fewer on a congested link.
|
|
927
|
+
* stream.send({ type: 'aim', angle }, 'aim');
|
|
928
|
+
* ```
|
|
929
|
+
*/
|
|
930
|
+
function createWebSocketSendRate(client, options = {}) {
|
|
931
|
+
const { maxBuffered = 1024, latency, maxRttIncrease = 100 } = options;
|
|
932
|
+
const maxRate = options.maxRate ?? 30;
|
|
933
|
+
const minRate = Math.min(options.minRate ?? 5, maxRate);
|
|
934
|
+
const listeners = /* @__PURE__ */ new Set();
|
|
935
|
+
const pending = /* @__PURE__ */ new Map();
|
|
936
|
+
let rate = maxRate;
|
|
937
|
+
let snapshot = {
|
|
938
|
+
rate: maxRate,
|
|
939
|
+
congested: false
|
|
940
|
+
};
|
|
941
|
+
let last = -Infinity;
|
|
942
|
+
let best = Infinity;
|
|
943
|
+
let slow = false;
|
|
944
|
+
let measured;
|
|
945
|
+
let timer;
|
|
946
|
+
let disposed = false;
|
|
947
|
+
function flush() {
|
|
948
|
+
for (const [key, message] of pending) {
|
|
949
|
+
if (!client.send(message, VOLATILE)) return;
|
|
950
|
+
pending.delete(key);
|
|
951
|
+
}
|
|
952
|
+
}
|
|
953
|
+
function publish(congested) {
|
|
954
|
+
const rounded = Math.round(rate);
|
|
955
|
+
if (rounded === snapshot.rate && congested === snapshot.congested) return;
|
|
956
|
+
snapshot = {
|
|
957
|
+
rate: rounded,
|
|
958
|
+
congested
|
|
959
|
+
};
|
|
960
|
+
for (const listener of [...listeners]) listener();
|
|
961
|
+
}
|
|
962
|
+
function schedule() {
|
|
963
|
+
if (disposed || timer !== void 0 || !pending.size || client.getSnapshot().status !== "open") return;
|
|
964
|
+
timer = setTimeout(tick, Math.max(0, last + 1e3 / rate - performance.now()));
|
|
965
|
+
}
|
|
966
|
+
function stop() {
|
|
967
|
+
clearTimeout(timer);
|
|
968
|
+
timer = void 0;
|
|
969
|
+
}
|
|
970
|
+
function tick() {
|
|
971
|
+
timer = void 0;
|
|
972
|
+
last = performance.now();
|
|
973
|
+
const measurement = latency?.getSnapshot();
|
|
974
|
+
if (measurement !== measured) {
|
|
975
|
+
measured = measurement;
|
|
976
|
+
const rtt = measurement?.rtt;
|
|
977
|
+
if (rtt !== void 0) {
|
|
978
|
+
best = Math.min(best, rtt);
|
|
979
|
+
slow = rtt - best > maxRttIncrease;
|
|
980
|
+
if (slow) rate /= 2;
|
|
981
|
+
}
|
|
982
|
+
}
|
|
983
|
+
const full = (client.socket?.bufferedAmount ?? 0) > maxBuffered;
|
|
984
|
+
if (full) rate /= 2;
|
|
985
|
+
else {
|
|
986
|
+
flush();
|
|
987
|
+
if (!slow) rate++;
|
|
988
|
+
}
|
|
989
|
+
rate = Math.min(maxRate, Math.max(minRate, rate));
|
|
990
|
+
publish(full || slow);
|
|
991
|
+
schedule();
|
|
992
|
+
}
|
|
993
|
+
const offs = [client.on("open", () => {
|
|
994
|
+
best = Infinity;
|
|
995
|
+
slow = false;
|
|
996
|
+
measured = latency?.getSnapshot();
|
|
997
|
+
schedule();
|
|
998
|
+
}), client.subscribe(() => {
|
|
999
|
+
const { status } = client.getSnapshot();
|
|
1000
|
+
if (status !== "open") stop();
|
|
1001
|
+
if (status === "closed" || status === "idle") pending.clear();
|
|
1002
|
+
})];
|
|
1003
|
+
return {
|
|
1004
|
+
client,
|
|
1005
|
+
send(message, key) {
|
|
1006
|
+
pending.delete(key);
|
|
1007
|
+
pending.set(key, message);
|
|
1008
|
+
schedule();
|
|
1009
|
+
},
|
|
1010
|
+
flush,
|
|
1011
|
+
getSnapshot: () => snapshot,
|
|
1012
|
+
subscribe(listener) {
|
|
1013
|
+
listeners.add(listener);
|
|
1014
|
+
return () => {
|
|
1015
|
+
listeners.delete(listener);
|
|
1016
|
+
};
|
|
1017
|
+
},
|
|
1018
|
+
dispose() {
|
|
1019
|
+
disposed = true;
|
|
1020
|
+
stop();
|
|
1021
|
+
pending.clear();
|
|
1022
|
+
offs.forEach((off) => off());
|
|
1023
|
+
}
|
|
1024
|
+
};
|
|
1025
|
+
}
|
|
1026
|
+
//#endregion
|
|
900
1027
|
//#region src/state.ts
|
|
901
1028
|
/**
|
|
902
1029
|
* Keep state derived from a client's messages, like a reducer that runs on every message.
|
|
@@ -917,14 +1044,30 @@ function createWebSocketResponder(client, options) {
|
|
|
917
1044
|
* ```
|
|
918
1045
|
*/
|
|
919
1046
|
function createWebSocketState(client, options) {
|
|
920
|
-
const { initialState, reduce } = options;
|
|
1047
|
+
const { initialState, reduce, batch } = options;
|
|
921
1048
|
let state = initialState;
|
|
1049
|
+
let notified = state;
|
|
1050
|
+
let frame = false;
|
|
922
1051
|
const listeners = /* @__PURE__ */ new Set();
|
|
923
|
-
function
|
|
924
|
-
|
|
1052
|
+
function notify() {
|
|
1053
|
+
if (Object.is(state, notified)) return;
|
|
1054
|
+
notified = state;
|
|
1055
|
+
for (const listener of [...listeners]) listener();
|
|
1056
|
+
}
|
|
1057
|
+
function flush() {
|
|
1058
|
+
frame = false;
|
|
1059
|
+
notify();
|
|
1060
|
+
}
|
|
1061
|
+
function update(change, later) {
|
|
1062
|
+
const next = typeof change === "function" ? change(state) : change;
|
|
925
1063
|
if (Object.is(next, state)) return;
|
|
926
1064
|
state = next;
|
|
927
|
-
|
|
1065
|
+
if (!later) notify();
|
|
1066
|
+
else if (!frame) {
|
|
1067
|
+
frame = true;
|
|
1068
|
+
if (typeof requestAnimationFrame === "function") requestAnimationFrame(flush);
|
|
1069
|
+
else setTimeout(flush, 16);
|
|
1070
|
+
}
|
|
928
1071
|
}
|
|
929
1072
|
return {
|
|
930
1073
|
client,
|
|
@@ -936,9 +1079,9 @@ function createWebSocketState(client, options) {
|
|
|
936
1079
|
listeners.delete(listener);
|
|
937
1080
|
};
|
|
938
1081
|
},
|
|
939
|
-
setState,
|
|
940
|
-
reset: () =>
|
|
941
|
-
dispose: client.on("message", (message) =>
|
|
1082
|
+
setState: (change) => update(change),
|
|
1083
|
+
reset: () => update(initialState),
|
|
1084
|
+
dispose: client.on("message", (message) => update((current) => reduce(current, message), batch === "frame"))
|
|
942
1085
|
};
|
|
943
1086
|
}
|
|
944
1087
|
//#endregion
|
|
@@ -1025,6 +1168,6 @@ function createWebSocketTopics(client, options) {
|
|
|
1025
1168
|
};
|
|
1026
1169
|
}
|
|
1027
1170
|
//#endregion
|
|
1028
|
-
export { MessageError, createWebSocket, createWebSocketLatency, createWebSocketRequests, createWebSocketResponder, createWebSocketState, createWebSocketTopics };
|
|
1171
|
+
export { MessageError, createWebSocket, createWebSocketLatency, createWebSocketRequests, createWebSocketResponder, createWebSocketSendRate, createWebSocketState, createWebSocketTopics };
|
|
1029
1172
|
|
|
1030
1173
|
//# sourceMappingURL=core.js.map
|