@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 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
- const interval = setInterval(sync, DEFAULT_SYNC_INTERVAL);
93
- return () => clearInterval(interval);
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,
@@ -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":"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"}
1
+ {"version":3,"file":"relay-protocol.d.ts","sourceRoot":"","sources":["../src/relay-protocol.ts"],"names":[],"mappings":"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"}
@@ -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
  *
@@ -1 +1 @@
1
- {"version":3,"file":"time-sync.d.ts","sourceRoot":"","sources":["../src/time-sync.ts"],"names":[],"mappings":"AAOA,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;;;;;;;;;;;;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;EAgDtE"}
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.13.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.9.3"
52
+ "@couch-kit/core": "0.10.0"
53
53
  },
54
54
  "devDependencies": {
55
55
  "react": "^19.0.0",
@@ -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
- | CreateRoomMessage
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
- const interval = setInterval(sync, DEFAULT_SYNC_INTERVAL);
114
- return () => clearInterval(interval);
144
+ return () => {
145
+ if (timer !== null) clearTimeout(timer);
146
+ };
115
147
  }, [socket]);
116
148
 
117
149
  return { getServerTime, rtt: timeSync.rtt, handlePong };