@couch-kit/client 0.13.0 → 0.14.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/CHANGELOG.md +53 -0
- package/dist/index.js +15 -2
- package/lib/relay-protocol.d.ts +19 -1
- package/lib/relay-protocol.d.ts.map +1 -1
- package/lib/time-sync.d.ts +13 -0
- package/lib/time-sync.d.ts.map +1 -1
- package/package.json +2 -2
- package/src/relay-protocol.ts +20 -3
- package/src/time-sync.ts +34 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,58 @@
|
|
|
1
1
|
# @couch-kit/client
|
|
2
2
|
|
|
3
|
+
## 0.14.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- [#164](https://github.com/faluciano/react-native-couch-kit/pull/164) [`a157126`](https://github.com/faluciano/react-native-couch-kit/commit/a157126424e4d73dcc7185118d5be0db6719792e) Thanks [@faluciano](https://github.com/faluciano)! - Send a projected state update as one relay frame instead of one per player
|
|
8
|
+
|
|
9
|
+
A game with a `project` function sends every player their own view, which meant
|
|
10
|
+
one WebSocket frame per player for every state change. Relays bill and
|
|
11
|
+
rate-limit per inbound frame, so a four-player table paid four messages for one
|
|
12
|
+
update and spent four of the display's 30-per-second budget.
|
|
13
|
+
|
|
14
|
+
`GameRuntimeTransport` gains an optional `sendMany(entries)`. When a transport
|
|
15
|
+
implements it, the runtime hands over the whole projected batch at once;
|
|
16
|
+
transports that do not — the LAN WebSocket path — keep receiving one `send` per
|
|
17
|
+
connection and are unaffected.
|
|
18
|
+
|
|
19
|
+
`RelayDisplayHost` implements it with a new `DATA_MULTI` envelope carrying a
|
|
20
|
+
peer-id-to-payload map, which the relay unpacks into ordinary `DATA` frames.
|
|
21
|
+
Phones need no update — nothing on the client side can tell a batched update
|
|
22
|
+
from a unicast one. If the combined frame would exceed the relay's 256KB
|
|
23
|
+
ceiling, the display falls back to individual frames rather than send something
|
|
24
|
+
the relay would drop.
|
|
25
|
+
|
|
26
|
+
Relays must be updated before displays: both bundled implementations
|
|
27
|
+
(`services/relay`, `services/relay-worker`) understand `DATA_MULTI`, and an
|
|
28
|
+
older relay answers it with `MALFORMED`. The type is host-only — a phone sending
|
|
29
|
+
it is rejected, so it cannot be used to reach another phone directly.
|
|
30
|
+
|
|
31
|
+
- [#164](https://github.com/faluciano/react-native-couch-kit/pull/164) [`a157126`](https://github.com/faluciano/react-native-couch-kit/commit/a157126424e4d73dcc7185118d5be0db6719792e) Thanks [@faluciano](https://github.com/faluciano)! - Back the time-sync ping off to 30s once the clock estimate settles
|
|
32
|
+
|
|
33
|
+
`useGameClient`'s clock sync pinged every 5 seconds for the life of the
|
|
34
|
+
connection. It now starts there and doubles to a 30-second ceiling
|
|
35
|
+
(`MAX_SYNC_INTERVAL`), resetting to the fast interval whenever a new socket is
|
|
36
|
+
established, including after a reconnect.
|
|
37
|
+
|
|
38
|
+
The first few pings are what converge the offset; the clock difference they
|
|
39
|
+
measure does not drift on a human timescale, so the fast interval stops earning
|
|
40
|
+
its cost within about a minute. On a relay transport it is not free: each ping
|
|
41
|
+
is a message in and a `PONG` back out, and pings are the only traffic an idle
|
|
42
|
+
table generates at all. A four-player lobby went from 5,760 relay messages an
|
|
43
|
+
hour to under 1,000 while sitting untouched.
|
|
44
|
+
|
|
45
|
+
`rtt` and `getServerTime()` are unchanged in accuracy — both are updated by the
|
|
46
|
+
same `PONG` handling as before, just less often once settled. Games needing the
|
|
47
|
+
old cadence can dispatch their own pings; the constants
|
|
48
|
+
(`DEFAULT_SYNC_INTERVAL`, `MAX_SYNC_INTERVAL`, `SYNC_BACKOFF_FACTOR`) are
|
|
49
|
+
exported from `@couch-kit/core`.
|
|
50
|
+
|
|
51
|
+
### Patch Changes
|
|
52
|
+
|
|
53
|
+
- Updated dependencies [[`a157126`](https://github.com/faluciano/react-native-couch-kit/commit/a157126424e4d73dcc7185118d5be0db6719792e)]:
|
|
54
|
+
- @couch-kit/core@0.10.0
|
|
55
|
+
|
|
3
56
|
## 0.13.0
|
|
4
57
|
|
|
5
58
|
### Minor Changes
|
package/dist/index.js
CHANGED
|
@@ -15,6 +15,8 @@ import {
|
|
|
15
15
|
MessageTypes,
|
|
16
16
|
generateId,
|
|
17
17
|
DEFAULT_SYNC_INTERVAL,
|
|
18
|
+
MAX_SYNC_INTERVAL,
|
|
19
|
+
SYNC_BACKOFF_FACTOR,
|
|
18
20
|
MAX_PENDING_PINGS
|
|
19
21
|
} from "@couch-kit/core";
|
|
20
22
|
|
|
@@ -53,6 +55,9 @@ function calculateTimeSync(clientSendTime, clientReceiveTime, serverTime) {
|
|
|
53
55
|
const offset = expectedServerTime - clientReceiveTime;
|
|
54
56
|
return { offset, rtt };
|
|
55
57
|
}
|
|
58
|
+
function nextSyncInterval(current) {
|
|
59
|
+
return Math.min(current * SYNC_BACKOFF_FACTOR, MAX_SYNC_INTERVAL);
|
|
60
|
+
}
|
|
56
61
|
function useServerTime(socket) {
|
|
57
62
|
const [timeSync, setTimeSync] = useState({
|
|
58
63
|
offset: 0,
|
|
@@ -74,6 +79,8 @@ function useServerTime(socket) {
|
|
|
74
79
|
useEffect(() => {
|
|
75
80
|
if (!socket || socket.readyState !== TransportReadyState.OPEN)
|
|
76
81
|
return;
|
|
82
|
+
let delay = DEFAULT_SYNC_INTERVAL;
|
|
83
|
+
let timer = null;
|
|
77
84
|
const sync = () => {
|
|
78
85
|
if (pings.current.size >= MAX_PENDING_PINGS) {
|
|
79
86
|
const oldest = pings.current.keys().next().value;
|
|
@@ -87,10 +94,14 @@ function useServerTime(socket) {
|
|
|
87
94
|
type: MessageTypes.PING,
|
|
88
95
|
payload: { id, timestamp }
|
|
89
96
|
}));
|
|
97
|
+
delay = nextSyncInterval(delay);
|
|
98
|
+
timer = setTimeout(sync, delay);
|
|
90
99
|
};
|
|
91
100
|
sync();
|
|
92
|
-
|
|
93
|
-
|
|
101
|
+
return () => {
|
|
102
|
+
if (timer !== null)
|
|
103
|
+
clearTimeout(timer);
|
|
104
|
+
};
|
|
94
105
|
}, [socket]);
|
|
95
106
|
return { getServerTime, rtt: timeSync.rtt, handlePong };
|
|
96
107
|
}
|
|
@@ -330,6 +341,7 @@ var RelayMessageTypes = {
|
|
|
330
341
|
PEER_JOINED: "PEER_JOINED",
|
|
331
342
|
PEER_LEFT: "PEER_LEFT",
|
|
332
343
|
DATA: "DATA",
|
|
344
|
+
DATA_MULTI: "DATA_MULTI",
|
|
333
345
|
ERROR: "ERROR"
|
|
334
346
|
};
|
|
335
347
|
var RelayErrorCodes = {
|
|
@@ -638,6 +650,7 @@ export {
|
|
|
638
650
|
resolveSessionSecret,
|
|
639
651
|
relayRoomUrl,
|
|
640
652
|
normalizeRoomCode,
|
|
653
|
+
nextSyncInterval,
|
|
641
654
|
interpretHostMessage,
|
|
642
655
|
describeRelayError,
|
|
643
656
|
createWebSocketTransport,
|
package/lib/relay-protocol.d.ts
CHANGED
|
@@ -27,6 +27,8 @@ export declare const RelayMessageTypes: {
|
|
|
27
27
|
readonly PEER_LEFT: "PEER_LEFT";
|
|
28
28
|
/** Bidirectional: carries an opaque Couch Kit JSON message as `data`. */
|
|
29
29
|
readonly DATA: "DATA";
|
|
30
|
+
/** Display → relay: one frame carrying a different payload per phone. */
|
|
31
|
+
readonly DATA_MULTI: "DATA_MULTI";
|
|
30
32
|
/** Relay → client: a protocol/room error. */
|
|
31
33
|
readonly ERROR: "ERROR";
|
|
32
34
|
};
|
|
@@ -97,6 +99,22 @@ export interface DataMessage {
|
|
|
97
99
|
to?: string;
|
|
98
100
|
data: string;
|
|
99
101
|
}
|
|
102
|
+
/**
|
|
103
|
+
* Display → relay: one envelope carrying a different payload per phone.
|
|
104
|
+
*
|
|
105
|
+
* `payloads` maps a phone's `peerId` to the already-serialized Couch Kit
|
|
106
|
+
* message for that phone. The relay unpacks it into ordinary
|
|
107
|
+
* {@link DataMessage} frames, so phones never see this type — it exists purely
|
|
108
|
+
* so a projected game costs one inbound relay message per state change instead
|
|
109
|
+
* of one per player. Peer ids the room does not know are skipped.
|
|
110
|
+
*
|
|
111
|
+
* Host-only: the relay rejects it from a phone.
|
|
112
|
+
*/
|
|
113
|
+
export interface DataMultiMessage {
|
|
114
|
+
type: typeof RelayMessageTypes.DATA_MULTI;
|
|
115
|
+
roomId: string;
|
|
116
|
+
payloads: Record<string, string>;
|
|
117
|
+
}
|
|
100
118
|
/** Relay → client: a protocol/room error. */
|
|
101
119
|
export interface RelayErrorMessage {
|
|
102
120
|
type: typeof RelayMessageTypes.ERROR;
|
|
@@ -104,7 +122,7 @@ export interface RelayErrorMessage {
|
|
|
104
122
|
message: string;
|
|
105
123
|
}
|
|
106
124
|
/** Any message a client may send to the relay. */
|
|
107
|
-
export type RelayClientMessage = CreateRoomMessage | JoinRoomMessage | DataMessage;
|
|
125
|
+
export type RelayClientMessage = CreateRoomMessage | JoinRoomMessage | DataMessage | DataMultiMessage;
|
|
108
126
|
/** Any message the relay may send to a client. */
|
|
109
127
|
export type RelayServerMessage = RoomCreatedMessage | RoomJoinedMessage | PeerJoinedMessage | PeerLeftMessage | DataMessage | RelayErrorMessage;
|
|
110
128
|
/** Every relay wire message. */
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"relay-protocol.d.ts","sourceRoot":"","sources":["../src/relay-protocol.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAEH,+EAA+E;AAC/E,eAAO,MAAM,iBAAiB;IAC5B,8DAA8D;aAC9D,WAAW,EAAE,aAAa;IAC1B,0EAA0E;aAC1E,YAAY,EAAE,cAAc;IAC5B,4CAA4C;aAC5C,SAAS,EAAE,WAAW;IACtB,qEAAqE;aACrE,WAAW,EAAE,aAAa;IAC1B,gDAAgD;aAChD,WAAW,EAAE,aAAa;IAC1B,8CAA8C;aAC9C,SAAS,EAAE,WAAW;IACtB,yEAAyE;aACzE,IAAI,EAAE,MAAM;IACZ,6CAA6C;aAC7C,KAAK,EAAE,OAAO;CACN,CAAC;AAEX,wEAAwE;AACxE,eAAO,MAAM,eAAe;aAC1B,cAAc,EAAE,gBAAgB;aAChC,WAAW,EAAE,aAAa;aAC1B,SAAS,EAAE,WAAW;aACtB,WAAW,EAAE,aAAa;aAC1B,iBAAiB,EAAE,mBAAmB;aACtC,SAAS,EAAE,WAAW;IACtB,yEAAyE;aACzE,YAAY,EAAE,cAAc;IAC5B,qCAAqC;aACrC,WAAW,EAAE,aAAa;CAClB,CAAC;AAEX,MAAM,MAAM,cAAc,GACxB,CAAC,OAAO,eAAe,CAAC,CAAC,MAAM,OAAO,eAAe,CAAC,CAAC;AAEzD,+CAA+C;AAC/C,MAAM,WAAW,iBAAiB;IAChC,IAAI,EAAE,OAAO,iBAAiB,CAAC,WAAW,CAAC;IAC3C,MAAM,EAAE,MAAM,CAAC;CAChB;AAED,uEAAuE;AACvE,MAAM,WAAW,kBAAkB;IACjC,IAAI,EAAE,OAAO,iBAAiB,CAAC,YAAY,CAAC;IAC5C,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,MAAM,CAAC;CAChB;AAED,4CAA4C;AAC5C,MAAM,WAAW,eAAe;IAC9B,IAAI,EAAE,OAAO,iBAAiB,CAAC,SAAS,CAAC;IACzC,MAAM,EAAE,MAAM,CAAC;CAChB;AAED,wEAAwE;AACxE,MAAM,WAAW,iBAAiB;IAChC,IAAI,EAAE,OAAO,iBAAiB,CAAC,WAAW,CAAC;IAC3C,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,MAAM,CAAC;CAChB;AAED,2EAA2E;AAC3E,MAAM,WAAW,iBAAiB;IAChC,IAAI,EAAE,OAAO,iBAAiB,CAAC,WAAW,CAAC;IAC3C,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,MAAM,CAAC;CAChB;AAED,qCAAqC;AACrC,MAAM,WAAW,eAAe;IAC9B,IAAI,EAAE,OAAO,iBAAiB,CAAC,SAAS,CAAC;IACzC,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,MAAM,CAAC;CAChB;AAED;;;;;;;;;;;GAWG;AACH,MAAM,WAAW,WAAW;IAC1B,IAAI,EAAE,OAAO,iBAAiB,CAAC,IAAI,CAAC;IACpC,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,EAAE,CAAC,EAAE,MAAM,CAAC;IACZ,IAAI,EAAE,MAAM,CAAC;CACd;AAED,6CAA6C;AAC7C,MAAM,WAAW,iBAAiB;IAChC,IAAI,EAAE,OAAO,iBAAiB,CAAC,KAAK,CAAC;IACrC,IAAI,EAAE,cAAc,CAAC;IACrB,OAAO,EAAE,MAAM,CAAC;CACjB;AAED,kDAAkD;AAClD,MAAM,MAAM,kBAAkB,
|
|
1
|
+
{"version":3,"file":"relay-protocol.d.ts","sourceRoot":"","sources":["../src/relay-protocol.ts"],"names":[],"mappings":"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"}
|
package/lib/time-sync.d.ts
CHANGED
|
@@ -15,6 +15,19 @@ export declare function calculateTimeSync(clientSendTime: number, clientReceiveT
|
|
|
15
15
|
offset: number;
|
|
16
16
|
rtt: number;
|
|
17
17
|
};
|
|
18
|
+
/**
|
|
19
|
+
* The interval to wait before the next PING, given the one just used.
|
|
20
|
+
*
|
|
21
|
+
* Grows geometrically to {@link MAX_SYNC_INTERVAL}: the first pings after
|
|
22
|
+
* connecting are what converge the offset, and re-measuring it every few
|
|
23
|
+
* seconds forever buys nothing — the clock difference does not move, while on a
|
|
24
|
+
* relay transport each ping is a billed message in both directions and the only
|
|
25
|
+
* traffic an idle table generates at all.
|
|
26
|
+
*
|
|
27
|
+
* @param current - Interval (ms) used for the ping just sent.
|
|
28
|
+
* @returns The next interval, capped at {@link MAX_SYNC_INTERVAL}.
|
|
29
|
+
*/
|
|
30
|
+
export declare function nextSyncInterval(current: number): number;
|
|
18
31
|
/**
|
|
19
32
|
* React hook that synchronizes the client clock with the host server.
|
|
20
33
|
*
|
package/lib/time-sync.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"time-sync.d.ts","sourceRoot":"","sources":["../src/time-sync.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"time-sync.d.ts","sourceRoot":"","sources":["../src/time-sync.ts"],"names":[],"mappings":"AASA,OAAO,EAAuB,KAAK,eAAe,EAAE,MAAM,aAAa,CAAC;AAOxE;;;;;;;;;;;GAWG;AAEH,wBAAgB,iBAAiB,CAC/B,cAAc,EAAE,MAAM,EACtB,iBAAiB,EAAE,MAAM,EACzB,UAAU,EAAE,MAAM;;;EAQnB;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAExD;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,aAAa,CAAC,MAAM,EAAE,eAAe,GAAG,IAAI;;;0BAgB9C;QAAE,EAAE,EAAE,MAAM,CAAC;QAAC,aAAa,EAAE,MAAM,CAAC;QAAC,UAAU,EAAE,MAAM,CAAA;KAAE;EA8DtE"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@couch-kit/client",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.14.0",
|
|
4
4
|
"publishConfig": {
|
|
5
5
|
"access": "public",
|
|
6
6
|
"provenance": true
|
|
@@ -49,7 +49,7 @@
|
|
|
49
49
|
"clean": "rm -rf dist lib"
|
|
50
50
|
},
|
|
51
51
|
"dependencies": {
|
|
52
|
-
"@couch-kit/core": "0.
|
|
52
|
+
"@couch-kit/core": "0.10.0"
|
|
53
53
|
},
|
|
54
54
|
"devDependencies": {
|
|
55
55
|
"react": "^19.0.0",
|
package/src/relay-protocol.ts
CHANGED
|
@@ -28,6 +28,8 @@ export const RelayMessageTypes = {
|
|
|
28
28
|
PEER_LEFT: "PEER_LEFT",
|
|
29
29
|
/** Bidirectional: carries an opaque Couch Kit JSON message as `data`. */
|
|
30
30
|
DATA: "DATA",
|
|
31
|
+
/** Display → relay: one frame carrying a different payload per phone. */
|
|
32
|
+
DATA_MULTI: "DATA_MULTI",
|
|
31
33
|
/** Relay → client: a protocol/room error. */
|
|
32
34
|
ERROR: "ERROR",
|
|
33
35
|
} as const;
|
|
@@ -109,6 +111,23 @@ export interface DataMessage {
|
|
|
109
111
|
data: string;
|
|
110
112
|
}
|
|
111
113
|
|
|
114
|
+
/**
|
|
115
|
+
* Display → relay: one envelope carrying a different payload per phone.
|
|
116
|
+
*
|
|
117
|
+
* `payloads` maps a phone's `peerId` to the already-serialized Couch Kit
|
|
118
|
+
* message for that phone. The relay unpacks it into ordinary
|
|
119
|
+
* {@link DataMessage} frames, so phones never see this type — it exists purely
|
|
120
|
+
* so a projected game costs one inbound relay message per state change instead
|
|
121
|
+
* of one per player. Peer ids the room does not know are skipped.
|
|
122
|
+
*
|
|
123
|
+
* Host-only: the relay rejects it from a phone.
|
|
124
|
+
*/
|
|
125
|
+
export interface DataMultiMessage {
|
|
126
|
+
type: typeof RelayMessageTypes.DATA_MULTI;
|
|
127
|
+
roomId: string;
|
|
128
|
+
payloads: Record<string, string>;
|
|
129
|
+
}
|
|
130
|
+
|
|
112
131
|
/** Relay → client: a protocol/room error. */
|
|
113
132
|
export interface RelayErrorMessage {
|
|
114
133
|
type: typeof RelayMessageTypes.ERROR;
|
|
@@ -118,9 +137,7 @@ export interface RelayErrorMessage {
|
|
|
118
137
|
|
|
119
138
|
/** Any message a client may send to the relay. */
|
|
120
139
|
export type RelayClientMessage =
|
|
121
|
-
|
|
|
122
|
-
| JoinRoomMessage
|
|
123
|
-
| DataMessage;
|
|
140
|
+
CreateRoomMessage | JoinRoomMessage | DataMessage | DataMultiMessage;
|
|
124
141
|
|
|
125
142
|
/** Any message the relay may send to a client. */
|
|
126
143
|
export type RelayServerMessage =
|
package/src/time-sync.ts
CHANGED
|
@@ -3,6 +3,8 @@ import {
|
|
|
3
3
|
MessageTypes,
|
|
4
4
|
generateId,
|
|
5
5
|
DEFAULT_SYNC_INTERVAL,
|
|
6
|
+
MAX_SYNC_INTERVAL,
|
|
7
|
+
SYNC_BACKOFF_FACTOR,
|
|
6
8
|
MAX_PENDING_PINGS,
|
|
7
9
|
} from "@couch-kit/core";
|
|
8
10
|
import { TransportReadyState, type ClientTransport } from "./transport";
|
|
@@ -38,6 +40,22 @@ export function calculateTimeSync(
|
|
|
38
40
|
return { offset, rtt };
|
|
39
41
|
}
|
|
40
42
|
|
|
43
|
+
/**
|
|
44
|
+
* The interval to wait before the next PING, given the one just used.
|
|
45
|
+
*
|
|
46
|
+
* Grows geometrically to {@link MAX_SYNC_INTERVAL}: the first pings after
|
|
47
|
+
* connecting are what converge the offset, and re-measuring it every few
|
|
48
|
+
* seconds forever buys nothing — the clock difference does not move, while on a
|
|
49
|
+
* relay transport each ping is a billed message in both directions and the only
|
|
50
|
+
* traffic an idle table generates at all.
|
|
51
|
+
*
|
|
52
|
+
* @param current - Interval (ms) used for the ping just sent.
|
|
53
|
+
* @returns The next interval, capped at {@link MAX_SYNC_INTERVAL}.
|
|
54
|
+
*/
|
|
55
|
+
export function nextSyncInterval(current: number): number {
|
|
56
|
+
return Math.min(current * SYNC_BACKOFF_FACTOR, MAX_SYNC_INTERVAL);
|
|
57
|
+
}
|
|
58
|
+
|
|
41
59
|
/**
|
|
42
60
|
* React hook that synchronizes the client clock with the host server.
|
|
43
61
|
*
|
|
@@ -85,9 +103,19 @@ export function useServerTime(socket: ClientTransport | null) {
|
|
|
85
103
|
);
|
|
86
104
|
|
|
87
105
|
// Periodic Sync
|
|
106
|
+
//
|
|
107
|
+
// The interval backs off from DEFAULT_SYNC_INTERVAL to MAX_SYNC_INTERVAL
|
|
108
|
+
// rather than staying fast forever: the first few pings are what converge the
|
|
109
|
+
// offset, and after that we are re-measuring a clock difference that does not
|
|
110
|
+
// move. A self-rescheduling timeout is used instead of setInterval because
|
|
111
|
+
// the delay changes between ticks. Backoff state lives inside the effect, so
|
|
112
|
+
// a new socket — including a reconnect — starts fast again.
|
|
88
113
|
useEffect(() => {
|
|
89
114
|
if (!socket || socket.readyState !== TransportReadyState.OPEN) return;
|
|
90
115
|
|
|
116
|
+
let delay = DEFAULT_SYNC_INTERVAL;
|
|
117
|
+
let timer: ReturnType<typeof setTimeout> | null = null;
|
|
118
|
+
|
|
91
119
|
const sync = () => {
|
|
92
120
|
// Prevent unbounded growth if PONGs are lost
|
|
93
121
|
if (pings.current.size >= MAX_PENDING_PINGS) {
|
|
@@ -105,13 +133,17 @@ export function useServerTime(socket: ClientTransport | null) {
|
|
|
105
133
|
payload: { id, timestamp },
|
|
106
134
|
}),
|
|
107
135
|
);
|
|
136
|
+
|
|
137
|
+
delay = nextSyncInterval(delay);
|
|
138
|
+
timer = setTimeout(sync, delay);
|
|
108
139
|
};
|
|
109
140
|
|
|
110
141
|
// Initial sync
|
|
111
142
|
sync();
|
|
112
143
|
|
|
113
|
-
|
|
114
|
-
|
|
144
|
+
return () => {
|
|
145
|
+
if (timer !== null) clearTimeout(timer);
|
|
146
|
+
};
|
|
115
147
|
}, [socket]);
|
|
116
148
|
|
|
117
149
|
return { getServerTime, rtt: timeSync.rtt, handlePong };
|