react-websocket 3.1.0 → 3.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/README.md +6 -2
- package/dist/core.cjs +193 -4
- package/dist/core.cjs.map +1 -1
- package/dist/core.d.cts +169 -3
- package/dist/core.d.ts +169 -3
- package/dist/core.js +191 -4
- package/dist/core.js.map +1 -1
- package/dist/index.cjs +9 -0
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +186 -5
- package/dist/index.d.ts +186 -5
- package/dist/index.js +8 -0
- package/dist/index.js.map +1 -1
- package/package.json +10 -1
package/dist/core.d.cts
CHANGED
|
@@ -100,6 +100,21 @@ interface HeartbeatOptions {
|
|
|
100
100
|
*/
|
|
101
101
|
timeout?: number;
|
|
102
102
|
}
|
|
103
|
+
/** Per-message options for `send()`. */
|
|
104
|
+
interface SendOptions {
|
|
105
|
+
/**
|
|
106
|
+
* Drop the message instead of queueing it when the socket isn't open. For data that
|
|
107
|
+
* is stale by the time a reconnect finishes, such as player input or cursor positions.
|
|
108
|
+
* @default false
|
|
109
|
+
*/
|
|
110
|
+
volatile?: boolean;
|
|
111
|
+
/**
|
|
112
|
+
* Keep only the newest queued message with this key. Sending again with the same key
|
|
113
|
+
* while disconnected replaces the earlier one, which moves to the back of the queue.
|
|
114
|
+
* For state where only the latest value matters, such as "my position is now (x, y)".
|
|
115
|
+
*/
|
|
116
|
+
key?: string;
|
|
117
|
+
}
|
|
103
118
|
/** Bounds for the send queue. */
|
|
104
119
|
interface QueueOptions {
|
|
105
120
|
/**
|
|
@@ -285,10 +300,17 @@ interface WebSocketClient<In = unknown, Out = unknown> {
|
|
|
285
300
|
on<K extends keyof EventMap<In>>(type: K, listener: EventMap<In>[K]): () => void;
|
|
286
301
|
/**
|
|
287
302
|
* Send a message (encoded with `serialize`/`json` if configured). If the socket isn't
|
|
288
|
-
* open, the message is queued and sent on open (see the `queue` option)
|
|
303
|
+
* open, the message is queued and sent on open (see the `queue` option), unless
|
|
304
|
+
* `options.volatile` is set.
|
|
289
305
|
* @returns `true` if it was sent immediately, `false` if it was queued or dropped.
|
|
306
|
+
*
|
|
307
|
+
* @example
|
|
308
|
+
* ```ts
|
|
309
|
+
* game.send({ type: 'input', dx, dy }, { volatile: true }); // never replayed after a reconnect
|
|
310
|
+
* game.send({ type: 'position', x, y }, { key: 'position' }); // only the newest is queued
|
|
311
|
+
* ```
|
|
290
312
|
*/
|
|
291
|
-
send(message: Out): boolean;
|
|
313
|
+
send(message: Out, options?: SendOptions): boolean;
|
|
292
314
|
/**
|
|
293
315
|
* Start connecting, and keep the connection open even when no hook is using the
|
|
294
316
|
* client, until `close()` is called. Does nothing if already connecting or open.
|
|
@@ -335,6 +357,150 @@ interface WebSocketClient<In = unknown, Out = unknown> {
|
|
|
335
357
|
*/
|
|
336
358
|
declare function createWebSocket<In = unknown, Out = unknown>(url: UrlSource, options?: WebSocketOptions<In, Out>): WebSocketClient<In, Out>;
|
|
337
359
|
|
|
360
|
+
/** Options for {@link createWebSocketLatency}: how pings and pongs look on the wire. */
|
|
361
|
+
interface WebSocketLatencyOptions<In, Out> {
|
|
362
|
+
/** The ping message, tagged with `id`. Sent with `volatile`, so pings are never queued. */
|
|
363
|
+
ping: (id: number) => Out;
|
|
364
|
+
/**
|
|
365
|
+
* Recognize a pong: return the `id` of the ping it answers, and the server's clock
|
|
366
|
+
* (`Date.now()` on the server, in ms) when it replied. Return `undefined` (or `null`)
|
|
367
|
+
* for any other message. Without `time`, only the round-trip time is measured.
|
|
368
|
+
*/
|
|
369
|
+
pong: (message: In) => {
|
|
370
|
+
id: number | string;
|
|
371
|
+
time?: number;
|
|
372
|
+
} | null | undefined;
|
|
373
|
+
/**
|
|
374
|
+
* How often to ping while open, in ms. The first ping goes out as soon as the socket opens.
|
|
375
|
+
* @default 2000
|
|
376
|
+
*/
|
|
377
|
+
interval?: number;
|
|
378
|
+
/**
|
|
379
|
+
* How many recent measurements to keep. `rtt` is their median, and `offset` comes from
|
|
380
|
+
* the one with the lowest round trip, the most accurate estimate.
|
|
381
|
+
* @default 5
|
|
382
|
+
*/
|
|
383
|
+
samples?: number;
|
|
384
|
+
}
|
|
385
|
+
/** Latest latency measurements. A new object is created on every change. */
|
|
386
|
+
interface Latency {
|
|
387
|
+
/** Round-trip time in ms: the median of recent pings. `undefined` until the first pong. */
|
|
388
|
+
readonly rtt: number | undefined;
|
|
389
|
+
/**
|
|
390
|
+
* Estimated server clock minus local clock, in ms. Add it to `Date.now()` for server time.
|
|
391
|
+
* `undefined` until a pong carries `time`.
|
|
392
|
+
*/
|
|
393
|
+
readonly offset: number | undefined;
|
|
394
|
+
}
|
|
395
|
+
/** Round-trip time and server clock offset for one client. Create with {@link createWebSocketLatency}. */
|
|
396
|
+
interface WebSocketLatency {
|
|
397
|
+
/** The client being measured. Hooks reading the latency keep it connected. */
|
|
398
|
+
readonly client: WebSocketClient<any, any>;
|
|
399
|
+
/** The current {@link Latency}. Returns the same object until it changes. */
|
|
400
|
+
getSnapshot(): Latency;
|
|
401
|
+
/** Called whenever a measurement changes. Compatible with `useSyncExternalStore`. */
|
|
402
|
+
subscribe(listener: () => void): () => void;
|
|
403
|
+
/** The estimated server time: `Date.now() + offset`, or `Date.now()` before any estimate. */
|
|
404
|
+
serverNow(): number;
|
|
405
|
+
/** Ping now instead of waiting for the next interval. Does nothing unless the socket is open. */
|
|
406
|
+
ping(): void;
|
|
407
|
+
/** Stop pinging and listening. Only needed for instances created dynamically. */
|
|
408
|
+
dispose(): void;
|
|
409
|
+
}
|
|
410
|
+
/**
|
|
411
|
+
* Measure round-trip time and estimate the server's clock, for ping displays, countdowns
|
|
412
|
+
* and timestamps that must agree across players.
|
|
413
|
+
*
|
|
414
|
+
* Pings are sent on an interval while the socket is open, and paused otherwise. Measurements
|
|
415
|
+
* from a previous connection are discarded on reconnect, but the last values stay readable
|
|
416
|
+
* until new ones arrive. It doesn't open the connection by itself.
|
|
417
|
+
*
|
|
418
|
+
* Pongs are ordinary messages: they also reach `onMessage`, `lastMessage` and state stores.
|
|
419
|
+
*
|
|
420
|
+
* @example
|
|
421
|
+
* ```ts
|
|
422
|
+
* export const latency = createWebSocketLatency(game, {
|
|
423
|
+
* ping: id => ({ type: 'ping', id }),
|
|
424
|
+
* pong: message => (message.type === 'pong' ? { id: message.id, time: message.time } : undefined)
|
|
425
|
+
* });
|
|
426
|
+
*
|
|
427
|
+
* // In a component:
|
|
428
|
+
* const { rtt } = useWebSocketLatency(latency);
|
|
429
|
+
*
|
|
430
|
+
* // Anywhere: seconds left in a round that ends at the server's `endsAt`.
|
|
431
|
+
* const left = (endsAt - latency.serverNow()) / 1000;
|
|
432
|
+
* ```
|
|
433
|
+
*/
|
|
434
|
+
declare function createWebSocketLatency<In, const Out>(client: WebSocketClient<In, Out>, options: WebSocketLatencyOptions<NoInfer<In>, NoInfer<Out>>): WebSocketLatency;
|
|
435
|
+
|
|
436
|
+
/** Options for {@link createWebSocketRequests}: how requests and replies look on the wire. */
|
|
437
|
+
interface WebSocketRequestsOptions<Req, In, Out> {
|
|
438
|
+
/**
|
|
439
|
+
* Build the outgoing message for `request(message)`, tagged with `id`. Annotate `message`
|
|
440
|
+
* to type what `request()` accepts.
|
|
441
|
+
*/
|
|
442
|
+
send: (message: Req, id: number) => Out;
|
|
443
|
+
/**
|
|
444
|
+
* The id an incoming message replies to, or `undefined` (or `null`) if it isn't a reply.
|
|
445
|
+
* A string id matches the number it was sent as, so either echo works.
|
|
446
|
+
*/
|
|
447
|
+
reply: (message: In) => number | string | null | undefined;
|
|
448
|
+
/**
|
|
449
|
+
* Called on a reply. Return something other than `undefined` to reject the request with
|
|
450
|
+
* it instead of resolving, e.g. for a server-side error reply.
|
|
451
|
+
*/
|
|
452
|
+
error?: (message: In) => unknown;
|
|
453
|
+
/**
|
|
454
|
+
* Default time to wait for a reply, in ms. `Infinity` waits forever.
|
|
455
|
+
* @default 10000
|
|
456
|
+
*/
|
|
457
|
+
timeout?: number;
|
|
458
|
+
}
|
|
459
|
+
/** Per-call options for {@link WebSocketRequests.request}. */
|
|
460
|
+
interface RequestOptions extends SendOptions {
|
|
461
|
+
/** Time to wait for a reply, in ms. Overrides the manager's `timeout`. */
|
|
462
|
+
timeout?: number;
|
|
463
|
+
/** Abort the request. It rejects with `signal.reason`. A late reply is ignored. */
|
|
464
|
+
signal?: AbortSignal;
|
|
465
|
+
}
|
|
466
|
+
/** Request/response over one client. Create with {@link createWebSocketRequests}. */
|
|
467
|
+
interface WebSocketRequests<Req, In> {
|
|
468
|
+
/** The client requests are sent on. */
|
|
469
|
+
readonly client: WebSocketClient<any, any>;
|
|
470
|
+
/**
|
|
471
|
+
* Send a request and resolve with its reply. Holds the connection until it settles.
|
|
472
|
+
*
|
|
473
|
+
* Rejects with a `TimeoutError` `DOMException` after `timeout`, with `signal.reason` when
|
|
474
|
+
* aborted, and with an `Error` when the connection it was sent on closes (the reply can
|
|
475
|
+
* no longer arrive) or the client stops. A request made while disconnected is queued like
|
|
476
|
+
* any other message and waits for the connection.
|
|
477
|
+
*/
|
|
478
|
+
request(message: Req, options?: RequestOptions): Promise<In>;
|
|
479
|
+
/** Number of requests waiting for a reply. */
|
|
480
|
+
pending(): number;
|
|
481
|
+
/** Reject every pending request and stop listening. Only needed for managers created dynamically. */
|
|
482
|
+
dispose(): void;
|
|
483
|
+
}
|
|
484
|
+
/**
|
|
485
|
+
* Send requests and await their replies, matched by an id the library generates.
|
|
486
|
+
*
|
|
487
|
+
* Replies are ordinary messages: they also reach `onMessage`, `lastMessage` and state stores.
|
|
488
|
+
*
|
|
489
|
+
* @example
|
|
490
|
+
* ```ts
|
|
491
|
+
* type Request = { type: 'join'; room: string } | { type: 'leave' };
|
|
492
|
+
*
|
|
493
|
+
* export const rpc = createWebSocketRequests(game, {
|
|
494
|
+
* send: (message: Request, id) => ({ ...message, id }),
|
|
495
|
+
* reply: message => (message.type === 'reply' ? message.id : undefined),
|
|
496
|
+
* error: message => (message.type === 'reply' && message.error ? new Error(message.error) : undefined)
|
|
497
|
+
* });
|
|
498
|
+
*
|
|
499
|
+
* const reply = await rpc.request({ type: 'join', room: 'lobby' }, { timeout: 5000 });
|
|
500
|
+
* ```
|
|
501
|
+
*/
|
|
502
|
+
declare function createWebSocketRequests<Req, In, const Out>(client: WebSocketClient<In, Out>, options: WebSocketRequestsOptions<Req, NoInfer<In>, NoInfer<Out>>): WebSocketRequests<Req, In>;
|
|
503
|
+
|
|
338
504
|
/** Options for {@link createWebSocketState}. */
|
|
339
505
|
interface WebSocketStateOptions<S, In> {
|
|
340
506
|
/** The state before any message arrives, and what `reset()` returns to. */
|
|
@@ -432,4 +598,4 @@ interface WebSocketTopics {
|
|
|
432
598
|
*/
|
|
433
599
|
declare function createWebSocketTopics<const Out>(client: WebSocketClient<any, Out>, options: WebSocketTopicsOptions<NoInfer<Out>>): WebSocketTopics;
|
|
434
600
|
|
|
435
|
-
export { type CloseInfo, type EventMap, type HeartbeatOptions, MessageError, type QueueOptions, type ReconnectOptions, type SendData, type Snapshot, type StandardSchemaV1, type Status, type UrlSource, type WebSocketClient, type WebSocketOptions, type WebSocketState, type WebSocketStateOptions, type WebSocketTopics, type WebSocketTopicsOptions, createWebSocket, createWebSocketState, createWebSocketTopics };
|
|
601
|
+
export { type CloseInfo, type EventMap, type HeartbeatOptions, type Latency, MessageError, type QueueOptions, type ReconnectOptions, type RequestOptions, type SendData, type SendOptions, type Snapshot, type StandardSchemaV1, type Status, type UrlSource, type WebSocketClient, type WebSocketLatency, type WebSocketLatencyOptions, type WebSocketOptions, type WebSocketRequests, type WebSocketRequestsOptions, type WebSocketState, type WebSocketStateOptions, type WebSocketTopics, type WebSocketTopicsOptions, createWebSocket, createWebSocketLatency, createWebSocketRequests, createWebSocketState, createWebSocketTopics };
|
package/dist/core.d.ts
CHANGED
|
@@ -100,6 +100,21 @@ interface HeartbeatOptions {
|
|
|
100
100
|
*/
|
|
101
101
|
timeout?: number;
|
|
102
102
|
}
|
|
103
|
+
/** Per-message options for `send()`. */
|
|
104
|
+
interface SendOptions {
|
|
105
|
+
/**
|
|
106
|
+
* Drop the message instead of queueing it when the socket isn't open. For data that
|
|
107
|
+
* is stale by the time a reconnect finishes, such as player input or cursor positions.
|
|
108
|
+
* @default false
|
|
109
|
+
*/
|
|
110
|
+
volatile?: boolean;
|
|
111
|
+
/**
|
|
112
|
+
* Keep only the newest queued message with this key. Sending again with the same key
|
|
113
|
+
* while disconnected replaces the earlier one, which moves to the back of the queue.
|
|
114
|
+
* For state where only the latest value matters, such as "my position is now (x, y)".
|
|
115
|
+
*/
|
|
116
|
+
key?: string;
|
|
117
|
+
}
|
|
103
118
|
/** Bounds for the send queue. */
|
|
104
119
|
interface QueueOptions {
|
|
105
120
|
/**
|
|
@@ -285,10 +300,17 @@ interface WebSocketClient<In = unknown, Out = unknown> {
|
|
|
285
300
|
on<K extends keyof EventMap<In>>(type: K, listener: EventMap<In>[K]): () => void;
|
|
286
301
|
/**
|
|
287
302
|
* Send a message (encoded with `serialize`/`json` if configured). If the socket isn't
|
|
288
|
-
* open, the message is queued and sent on open (see the `queue` option)
|
|
303
|
+
* open, the message is queued and sent on open (see the `queue` option), unless
|
|
304
|
+
* `options.volatile` is set.
|
|
289
305
|
* @returns `true` if it was sent immediately, `false` if it was queued or dropped.
|
|
306
|
+
*
|
|
307
|
+
* @example
|
|
308
|
+
* ```ts
|
|
309
|
+
* game.send({ type: 'input', dx, dy }, { volatile: true }); // never replayed after a reconnect
|
|
310
|
+
* game.send({ type: 'position', x, y }, { key: 'position' }); // only the newest is queued
|
|
311
|
+
* ```
|
|
290
312
|
*/
|
|
291
|
-
send(message: Out): boolean;
|
|
313
|
+
send(message: Out, options?: SendOptions): boolean;
|
|
292
314
|
/**
|
|
293
315
|
* Start connecting, and keep the connection open even when no hook is using the
|
|
294
316
|
* client, until `close()` is called. Does nothing if already connecting or open.
|
|
@@ -335,6 +357,150 @@ interface WebSocketClient<In = unknown, Out = unknown> {
|
|
|
335
357
|
*/
|
|
336
358
|
declare function createWebSocket<In = unknown, Out = unknown>(url: UrlSource, options?: WebSocketOptions<In, Out>): WebSocketClient<In, Out>;
|
|
337
359
|
|
|
360
|
+
/** Options for {@link createWebSocketLatency}: how pings and pongs look on the wire. */
|
|
361
|
+
interface WebSocketLatencyOptions<In, Out> {
|
|
362
|
+
/** The ping message, tagged with `id`. Sent with `volatile`, so pings are never queued. */
|
|
363
|
+
ping: (id: number) => Out;
|
|
364
|
+
/**
|
|
365
|
+
* Recognize a pong: return the `id` of the ping it answers, and the server's clock
|
|
366
|
+
* (`Date.now()` on the server, in ms) when it replied. Return `undefined` (or `null`)
|
|
367
|
+
* for any other message. Without `time`, only the round-trip time is measured.
|
|
368
|
+
*/
|
|
369
|
+
pong: (message: In) => {
|
|
370
|
+
id: number | string;
|
|
371
|
+
time?: number;
|
|
372
|
+
} | null | undefined;
|
|
373
|
+
/**
|
|
374
|
+
* How often to ping while open, in ms. The first ping goes out as soon as the socket opens.
|
|
375
|
+
* @default 2000
|
|
376
|
+
*/
|
|
377
|
+
interval?: number;
|
|
378
|
+
/**
|
|
379
|
+
* How many recent measurements to keep. `rtt` is their median, and `offset` comes from
|
|
380
|
+
* the one with the lowest round trip, the most accurate estimate.
|
|
381
|
+
* @default 5
|
|
382
|
+
*/
|
|
383
|
+
samples?: number;
|
|
384
|
+
}
|
|
385
|
+
/** Latest latency measurements. A new object is created on every change. */
|
|
386
|
+
interface Latency {
|
|
387
|
+
/** Round-trip time in ms: the median of recent pings. `undefined` until the first pong. */
|
|
388
|
+
readonly rtt: number | undefined;
|
|
389
|
+
/**
|
|
390
|
+
* Estimated server clock minus local clock, in ms. Add it to `Date.now()` for server time.
|
|
391
|
+
* `undefined` until a pong carries `time`.
|
|
392
|
+
*/
|
|
393
|
+
readonly offset: number | undefined;
|
|
394
|
+
}
|
|
395
|
+
/** Round-trip time and server clock offset for one client. Create with {@link createWebSocketLatency}. */
|
|
396
|
+
interface WebSocketLatency {
|
|
397
|
+
/** The client being measured. Hooks reading the latency keep it connected. */
|
|
398
|
+
readonly client: WebSocketClient<any, any>;
|
|
399
|
+
/** The current {@link Latency}. Returns the same object until it changes. */
|
|
400
|
+
getSnapshot(): Latency;
|
|
401
|
+
/** Called whenever a measurement changes. Compatible with `useSyncExternalStore`. */
|
|
402
|
+
subscribe(listener: () => void): () => void;
|
|
403
|
+
/** The estimated server time: `Date.now() + offset`, or `Date.now()` before any estimate. */
|
|
404
|
+
serverNow(): number;
|
|
405
|
+
/** Ping now instead of waiting for the next interval. Does nothing unless the socket is open. */
|
|
406
|
+
ping(): void;
|
|
407
|
+
/** Stop pinging and listening. Only needed for instances created dynamically. */
|
|
408
|
+
dispose(): void;
|
|
409
|
+
}
|
|
410
|
+
/**
|
|
411
|
+
* Measure round-trip time and estimate the server's clock, for ping displays, countdowns
|
|
412
|
+
* and timestamps that must agree across players.
|
|
413
|
+
*
|
|
414
|
+
* Pings are sent on an interval while the socket is open, and paused otherwise. Measurements
|
|
415
|
+
* from a previous connection are discarded on reconnect, but the last values stay readable
|
|
416
|
+
* until new ones arrive. It doesn't open the connection by itself.
|
|
417
|
+
*
|
|
418
|
+
* Pongs are ordinary messages: they also reach `onMessage`, `lastMessage` and state stores.
|
|
419
|
+
*
|
|
420
|
+
* @example
|
|
421
|
+
* ```ts
|
|
422
|
+
* export const latency = createWebSocketLatency(game, {
|
|
423
|
+
* ping: id => ({ type: 'ping', id }),
|
|
424
|
+
* pong: message => (message.type === 'pong' ? { id: message.id, time: message.time } : undefined)
|
|
425
|
+
* });
|
|
426
|
+
*
|
|
427
|
+
* // In a component:
|
|
428
|
+
* const { rtt } = useWebSocketLatency(latency);
|
|
429
|
+
*
|
|
430
|
+
* // Anywhere: seconds left in a round that ends at the server's `endsAt`.
|
|
431
|
+
* const left = (endsAt - latency.serverNow()) / 1000;
|
|
432
|
+
* ```
|
|
433
|
+
*/
|
|
434
|
+
declare function createWebSocketLatency<In, const Out>(client: WebSocketClient<In, Out>, options: WebSocketLatencyOptions<NoInfer<In>, NoInfer<Out>>): WebSocketLatency;
|
|
435
|
+
|
|
436
|
+
/** Options for {@link createWebSocketRequests}: how requests and replies look on the wire. */
|
|
437
|
+
interface WebSocketRequestsOptions<Req, In, Out> {
|
|
438
|
+
/**
|
|
439
|
+
* Build the outgoing message for `request(message)`, tagged with `id`. Annotate `message`
|
|
440
|
+
* to type what `request()` accepts.
|
|
441
|
+
*/
|
|
442
|
+
send: (message: Req, id: number) => Out;
|
|
443
|
+
/**
|
|
444
|
+
* The id an incoming message replies to, or `undefined` (or `null`) if it isn't a reply.
|
|
445
|
+
* A string id matches the number it was sent as, so either echo works.
|
|
446
|
+
*/
|
|
447
|
+
reply: (message: In) => number | string | null | undefined;
|
|
448
|
+
/**
|
|
449
|
+
* Called on a reply. Return something other than `undefined` to reject the request with
|
|
450
|
+
* it instead of resolving, e.g. for a server-side error reply.
|
|
451
|
+
*/
|
|
452
|
+
error?: (message: In) => unknown;
|
|
453
|
+
/**
|
|
454
|
+
* Default time to wait for a reply, in ms. `Infinity` waits forever.
|
|
455
|
+
* @default 10000
|
|
456
|
+
*/
|
|
457
|
+
timeout?: number;
|
|
458
|
+
}
|
|
459
|
+
/** Per-call options for {@link WebSocketRequests.request}. */
|
|
460
|
+
interface RequestOptions extends SendOptions {
|
|
461
|
+
/** Time to wait for a reply, in ms. Overrides the manager's `timeout`. */
|
|
462
|
+
timeout?: number;
|
|
463
|
+
/** Abort the request. It rejects with `signal.reason`. A late reply is ignored. */
|
|
464
|
+
signal?: AbortSignal;
|
|
465
|
+
}
|
|
466
|
+
/** Request/response over one client. Create with {@link createWebSocketRequests}. */
|
|
467
|
+
interface WebSocketRequests<Req, In> {
|
|
468
|
+
/** The client requests are sent on. */
|
|
469
|
+
readonly client: WebSocketClient<any, any>;
|
|
470
|
+
/**
|
|
471
|
+
* Send a request and resolve with its reply. Holds the connection until it settles.
|
|
472
|
+
*
|
|
473
|
+
* Rejects with a `TimeoutError` `DOMException` after `timeout`, with `signal.reason` when
|
|
474
|
+
* aborted, and with an `Error` when the connection it was sent on closes (the reply can
|
|
475
|
+
* no longer arrive) or the client stops. A request made while disconnected is queued like
|
|
476
|
+
* any other message and waits for the connection.
|
|
477
|
+
*/
|
|
478
|
+
request(message: Req, options?: RequestOptions): Promise<In>;
|
|
479
|
+
/** Number of requests waiting for a reply. */
|
|
480
|
+
pending(): number;
|
|
481
|
+
/** Reject every pending request and stop listening. Only needed for managers created dynamically. */
|
|
482
|
+
dispose(): void;
|
|
483
|
+
}
|
|
484
|
+
/**
|
|
485
|
+
* Send requests and await their replies, matched by an id the library generates.
|
|
486
|
+
*
|
|
487
|
+
* Replies are ordinary messages: they also reach `onMessage`, `lastMessage` and state stores.
|
|
488
|
+
*
|
|
489
|
+
* @example
|
|
490
|
+
* ```ts
|
|
491
|
+
* type Request = { type: 'join'; room: string } | { type: 'leave' };
|
|
492
|
+
*
|
|
493
|
+
* export const rpc = createWebSocketRequests(game, {
|
|
494
|
+
* send: (message: Request, id) => ({ ...message, id }),
|
|
495
|
+
* reply: message => (message.type === 'reply' ? message.id : undefined),
|
|
496
|
+
* error: message => (message.type === 'reply' && message.error ? new Error(message.error) : undefined)
|
|
497
|
+
* });
|
|
498
|
+
*
|
|
499
|
+
* const reply = await rpc.request({ type: 'join', room: 'lobby' }, { timeout: 5000 });
|
|
500
|
+
* ```
|
|
501
|
+
*/
|
|
502
|
+
declare function createWebSocketRequests<Req, In, const Out>(client: WebSocketClient<In, Out>, options: WebSocketRequestsOptions<Req, NoInfer<In>, NoInfer<Out>>): WebSocketRequests<Req, In>;
|
|
503
|
+
|
|
338
504
|
/** Options for {@link createWebSocketState}. */
|
|
339
505
|
interface WebSocketStateOptions<S, In> {
|
|
340
506
|
/** The state before any message arrives, and what `reset()` returns to. */
|
|
@@ -432,4 +598,4 @@ interface WebSocketTopics {
|
|
|
432
598
|
*/
|
|
433
599
|
declare function createWebSocketTopics<const Out>(client: WebSocketClient<any, Out>, options: WebSocketTopicsOptions<NoInfer<Out>>): WebSocketTopics;
|
|
434
600
|
|
|
435
|
-
export { type CloseInfo, type EventMap, type HeartbeatOptions, MessageError, type QueueOptions, type ReconnectOptions, type SendData, type Snapshot, type StandardSchemaV1, type Status, type UrlSource, type WebSocketClient, type WebSocketOptions, type WebSocketState, type WebSocketStateOptions, type WebSocketTopics, type WebSocketTopicsOptions, createWebSocket, createWebSocketState, createWebSocketTopics };
|
|
601
|
+
export { type CloseInfo, type EventMap, type HeartbeatOptions, type Latency, MessageError, type QueueOptions, type ReconnectOptions, type RequestOptions, type SendData, type SendOptions, type Snapshot, type StandardSchemaV1, type Status, type UrlSource, type WebSocketClient, type WebSocketLatency, type WebSocketLatencyOptions, type WebSocketOptions, type WebSocketRequests, type WebSocketRequestsOptions, type WebSocketState, type WebSocketStateOptions, type WebSocketTopics, type WebSocketTopicsOptions, createWebSocket, createWebSocketLatency, createWebSocketRequests, createWebSocketState, createWebSocketTopics };
|
package/dist/core.js
CHANGED
|
@@ -220,7 +220,7 @@ function createClient(url, options = {}) {
|
|
|
220
220
|
if (ws !== socket) return;
|
|
221
221
|
const queued = pending;
|
|
222
222
|
pending = [];
|
|
223
|
-
for (const data of queued) socket.send(data);
|
|
223
|
+
for (const { data } of queued) socket.send(data);
|
|
224
224
|
};
|
|
225
225
|
socket.onmessage = (event) => receive(event, socket);
|
|
226
226
|
socket.onerror = (event) => emit("error", event);
|
|
@@ -316,13 +316,13 @@ function createClient(url, options = {}) {
|
|
|
316
316
|
set({ lastMessage: message });
|
|
317
317
|
emit("message", message, event);
|
|
318
318
|
}
|
|
319
|
-
function send(message) {
|
|
319
|
+
function send(message, sendOptions) {
|
|
320
320
|
const data = options.serialize ? options.serialize(message) : options.json ? JSON.stringify(message) : message;
|
|
321
321
|
if (ws && ws.readyState === OPEN) {
|
|
322
322
|
ws.send(data);
|
|
323
323
|
return true;
|
|
324
324
|
}
|
|
325
|
-
if (queueLimit === 0) {
|
|
325
|
+
if (queueLimit === 0 || sendOptions?.volatile) {
|
|
326
326
|
log("dropped message: socket is not open");
|
|
327
327
|
return false;
|
|
328
328
|
}
|
|
@@ -330,7 +330,9 @@ function createClient(url, options = {}) {
|
|
|
330
330
|
log("dropped message: no WebSocket implementation available");
|
|
331
331
|
return false;
|
|
332
332
|
}
|
|
333
|
-
|
|
333
|
+
const key = sendOptions?.key;
|
|
334
|
+
if (key !== void 0) pending = pending.filter((entry) => entry.key !== key);
|
|
335
|
+
pending.push({ data, key });
|
|
334
336
|
if (pending.length > queueLimit) {
|
|
335
337
|
pending.shift();
|
|
336
338
|
log(`dropped oldest queued message: queue limit (${queueLimit}) reached`);
|
|
@@ -463,6 +465,189 @@ function createWebSocket(url, options) {
|
|
|
463
465
|
return createClient(url, options);
|
|
464
466
|
}
|
|
465
467
|
|
|
468
|
+
// src/latency.ts
|
|
469
|
+
var EMPTY = Object.freeze({ rtt: void 0, offset: void 0 });
|
|
470
|
+
function median(values) {
|
|
471
|
+
const sorted = [...values].sort((a, b) => a - b);
|
|
472
|
+
const middle = sorted.length >> 1;
|
|
473
|
+
return sorted.length % 2 ? sorted[middle] : (sorted[middle - 1] + sorted[middle]) / 2;
|
|
474
|
+
}
|
|
475
|
+
function createWebSocketLatency(client, options) {
|
|
476
|
+
const interval = options.interval ?? 2e3;
|
|
477
|
+
const keep = Math.max(1, options.samples ?? 5);
|
|
478
|
+
const listeners = /* @__PURE__ */ new Set();
|
|
479
|
+
let snapshot = EMPTY;
|
|
480
|
+
const inFlight = /* @__PURE__ */ new Map();
|
|
481
|
+
let samples = [];
|
|
482
|
+
let nextId = 0;
|
|
483
|
+
let timer;
|
|
484
|
+
function ping() {
|
|
485
|
+
if (client.getSnapshot().status !== "open") return;
|
|
486
|
+
const id = ++nextId;
|
|
487
|
+
inFlight.set(String(id), performance.now());
|
|
488
|
+
if (inFlight.size > keep * 2) inFlight.delete(inFlight.keys().next().value);
|
|
489
|
+
client.send(options.ping(id), { volatile: true });
|
|
490
|
+
}
|
|
491
|
+
function start() {
|
|
492
|
+
stop();
|
|
493
|
+
inFlight.clear();
|
|
494
|
+
samples = [];
|
|
495
|
+
ping();
|
|
496
|
+
timer = setInterval(ping, interval);
|
|
497
|
+
}
|
|
498
|
+
function stop() {
|
|
499
|
+
clearInterval(timer);
|
|
500
|
+
timer = void 0;
|
|
501
|
+
}
|
|
502
|
+
function measure(id, serverTime) {
|
|
503
|
+
const sentAt = inFlight.get(id);
|
|
504
|
+
if (sentAt === void 0) return;
|
|
505
|
+
inFlight.delete(id);
|
|
506
|
+
const rtt = performance.now() - sentAt;
|
|
507
|
+
const offset = serverTime === void 0 ? void 0 : serverTime + rtt / 2 - Date.now();
|
|
508
|
+
samples.push({ rtt, offset });
|
|
509
|
+
if (samples.length > keep) samples.shift();
|
|
510
|
+
let best;
|
|
511
|
+
for (const sample of samples) {
|
|
512
|
+
if (sample.offset !== void 0 && (!best || sample.rtt < best.rtt)) best = sample;
|
|
513
|
+
}
|
|
514
|
+
snapshot = {
|
|
515
|
+
rtt: median(samples.map((sample) => sample.rtt)),
|
|
516
|
+
offset: best ? best.offset : snapshot.offset
|
|
517
|
+
};
|
|
518
|
+
for (const listener of [...listeners]) listener();
|
|
519
|
+
}
|
|
520
|
+
const offs = [
|
|
521
|
+
client.on("open", start),
|
|
522
|
+
client.on("message", (message) => {
|
|
523
|
+
const pong = options.pong(message);
|
|
524
|
+
if (pong != null) measure(String(pong.id), pong.time);
|
|
525
|
+
}),
|
|
526
|
+
client.subscribe(() => {
|
|
527
|
+
if (client.getSnapshot().status !== "open") stop();
|
|
528
|
+
})
|
|
529
|
+
];
|
|
530
|
+
if (client.getSnapshot().status === "open") start();
|
|
531
|
+
return {
|
|
532
|
+
client,
|
|
533
|
+
getSnapshot: () => snapshot,
|
|
534
|
+
subscribe(listener) {
|
|
535
|
+
listeners.add(listener);
|
|
536
|
+
return () => {
|
|
537
|
+
listeners.delete(listener);
|
|
538
|
+
};
|
|
539
|
+
},
|
|
540
|
+
serverNow: () => Date.now() + (snapshot.offset ?? 0),
|
|
541
|
+
ping,
|
|
542
|
+
dispose() {
|
|
543
|
+
stop();
|
|
544
|
+
offs.forEach((off) => off());
|
|
545
|
+
}
|
|
546
|
+
};
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
// src/requests.ts
|
|
550
|
+
function createWebSocketRequests(client, options) {
|
|
551
|
+
const waiting = /* @__PURE__ */ new Map();
|
|
552
|
+
let nextId = 0;
|
|
553
|
+
function rejectWhere(test, reason) {
|
|
554
|
+
for (const entry of [...waiting.values()]) if (test(entry)) entry.reject(reason());
|
|
555
|
+
}
|
|
556
|
+
const offs = [
|
|
557
|
+
client.on("message", (message) => {
|
|
558
|
+
const id = options.reply(message);
|
|
559
|
+
const entry = id == null ? void 0 : waiting.get(String(id));
|
|
560
|
+
if (!entry) return;
|
|
561
|
+
const error = options.error?.(message);
|
|
562
|
+
if (error === void 0) entry.resolve(message);
|
|
563
|
+
else entry.reject(error);
|
|
564
|
+
}),
|
|
565
|
+
// `open` listeners run before the client flushes its queue: queued requests go out on this socket.
|
|
566
|
+
client.on("open", () => {
|
|
567
|
+
for (const entry of waiting.values()) entry.socket ??= client.socket;
|
|
568
|
+
}),
|
|
569
|
+
client.subscribe(() => {
|
|
570
|
+
const { status } = client.getSnapshot();
|
|
571
|
+
if (status === "closed" || status === "idle") {
|
|
572
|
+
rejectWhere(
|
|
573
|
+
() => true,
|
|
574
|
+
() => new Error("WebSocket was closed before the request was answered")
|
|
575
|
+
);
|
|
576
|
+
} else {
|
|
577
|
+
rejectWhere(
|
|
578
|
+
(entry) => entry.socket !== null && entry.socket !== client.socket,
|
|
579
|
+
() => new Error("WebSocket connection was lost before the request was answered")
|
|
580
|
+
);
|
|
581
|
+
}
|
|
582
|
+
})
|
|
583
|
+
];
|
|
584
|
+
return {
|
|
585
|
+
client,
|
|
586
|
+
request(message, requestOptions = {}) {
|
|
587
|
+
const { signal } = requestOptions;
|
|
588
|
+
if (signal?.aborted) return Promise.reject(signal.reason);
|
|
589
|
+
const id = ++nextId;
|
|
590
|
+
const key = String(id);
|
|
591
|
+
const timeout = requestOptions.timeout ?? options.timeout ?? 1e4;
|
|
592
|
+
return new Promise((resolve, reject) => {
|
|
593
|
+
let timer;
|
|
594
|
+
const release = client.acquire();
|
|
595
|
+
const onAbort = () => entry.reject(signal.reason);
|
|
596
|
+
function settle() {
|
|
597
|
+
waiting.delete(key);
|
|
598
|
+
clearTimeout(timer);
|
|
599
|
+
signal?.removeEventListener("abort", onAbort);
|
|
600
|
+
release();
|
|
601
|
+
}
|
|
602
|
+
const entry = {
|
|
603
|
+
socket: null,
|
|
604
|
+
resolve(reply) {
|
|
605
|
+
settle();
|
|
606
|
+
resolve(reply);
|
|
607
|
+
},
|
|
608
|
+
reject(reason) {
|
|
609
|
+
settle();
|
|
610
|
+
reject(reason);
|
|
611
|
+
}
|
|
612
|
+
};
|
|
613
|
+
waiting.set(key, entry);
|
|
614
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
615
|
+
if (timeout > 0 && timeout < Infinity) {
|
|
616
|
+
timer = setTimeout(
|
|
617
|
+
() => entry.reject(
|
|
618
|
+
new DOMException(`Request ${id} timed out after ${timeout}ms`, "TimeoutError")
|
|
619
|
+
),
|
|
620
|
+
timeout
|
|
621
|
+
);
|
|
622
|
+
}
|
|
623
|
+
const { status } = client.getSnapshot();
|
|
624
|
+
if (status === "closed" || status === "idle") {
|
|
625
|
+
entry.reject(new Error(`WebSocket is ${status}: request not sent`));
|
|
626
|
+
return;
|
|
627
|
+
}
|
|
628
|
+
let sent;
|
|
629
|
+
try {
|
|
630
|
+
sent = client.send(options.send(message, id), requestOptions);
|
|
631
|
+
} catch (error) {
|
|
632
|
+
entry.reject(error);
|
|
633
|
+
return;
|
|
634
|
+
}
|
|
635
|
+
if (sent) entry.socket = client.socket;
|
|
636
|
+
else if (requestOptions.volatile)
|
|
637
|
+
entry.reject(new Error("WebSocket is not open: request dropped"));
|
|
638
|
+
});
|
|
639
|
+
},
|
|
640
|
+
pending: () => waiting.size,
|
|
641
|
+
dispose() {
|
|
642
|
+
offs.forEach((off) => off());
|
|
643
|
+
rejectWhere(
|
|
644
|
+
() => true,
|
|
645
|
+
() => new Error("Requests were disposed")
|
|
646
|
+
);
|
|
647
|
+
}
|
|
648
|
+
};
|
|
649
|
+
}
|
|
650
|
+
|
|
466
651
|
// src/state.ts
|
|
467
652
|
function createWebSocketState(client, options) {
|
|
468
653
|
const { initialState, reduce } = options;
|
|
@@ -558,6 +743,8 @@ function createWebSocketTopics(client, options) {
|
|
|
558
743
|
export {
|
|
559
744
|
MessageError,
|
|
560
745
|
createWebSocket,
|
|
746
|
+
createWebSocketLatency,
|
|
747
|
+
createWebSocketRequests,
|
|
561
748
|
createWebSocketState,
|
|
562
749
|
createWebSocketTopics
|
|
563
750
|
};
|