@couch-kit/client 0.12.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,87 @@
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
+
56
+ ## 0.13.0
57
+
58
+ ### Minor Changes
59
+
60
+ - [#158](https://github.com/faluciano/react-native-couch-kit/pull/158) [`509ea7c`](https://github.com/faluciano/react-native-couch-kit/commit/509ea7c02aa6f2e56ebf01e781d6de74f0ded021) Thanks [@faluciano](https://github.com/faluciano)! - Let the relay assign room codes
61
+
62
+ `RelayDisplayHost`'s `roomId` is now optional. Omit it and the relay mints an
63
+ unused six-character code, reported through the new `onRoomCode` callback and
64
+ the `roomCode` getter.
65
+
66
+ A display could never check its own code for collisions — only the relay knows
67
+ which codes are live — so a self-chosen code could land on a game already in
68
+ progress, and did so only after it was on screen. Minted codes are drawn from
69
+ the CSPRNG over a 32-character alphabet without `O`/`0` or `I`/`1`, giving about
70
+ 1.07 billion codes.
71
+
72
+ Existing callers that pass `roomId` keep their current behaviour, including
73
+ `ROOM_EXISTS` when the code is taken. New callers should expect the code to
74
+ arrive one round trip after connecting rather than being known up front:
75
+
76
+ ```ts
77
+ const [roomCode, setRoomCode] = useState<string | null>(null);
78
+ new RelayDisplayHost({ url, onRoomCode: setRoomCode, reducer, initialState });
79
+ ```
80
+
81
+ Relays need a matching update to mint: both bundled implementations
82
+ (`services/relay`, `services/relay-worker`) support it. A display that omits
83
+ `roomId` against an older relay gets `MALFORMED`.
84
+
3
85
  ## 0.12.0
4
86
 
5
87
  ### 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 = {
@@ -345,9 +357,10 @@ var RelayErrorCodes = {
345
357
  function relayRoomUrl(url, roomId) {
346
358
  const trimmed = url.replace(/\/+$/, "");
347
359
  const [base, query] = trimmed.split("?", 2);
348
- const path = `${base}/r/${encodeURIComponent(roomId)}`;
360
+ const path = roomId === undefined ? `${base}${RELAY_MINT_PATH}` : `${base}/r/${encodeURIComponent(roomId)}`;
349
361
  return query ? `${path}?${query}` : path;
350
362
  }
363
+ var RELAY_MINT_PATH = "/new";
351
364
  // src/relay-transport.ts
352
365
  var POLICY_CLOSE_CODE = 1008;
353
366
 
@@ -637,6 +650,7 @@ export {
637
650
  resolveSessionSecret,
638
651
  relayRoomUrl,
639
652
  normalizeRoomCode,
653
+ nextSyncInterval,
640
654
  interpretHostMessage,
641
655
  describeRelayError,
642
656
  createWebSocketTransport,
@@ -647,5 +661,6 @@ export {
647
661
  SESSION_SECRET_KEY,
648
662
  RelayMessageTypes,
649
663
  RelayErrorCodes,
650
- RelayClientTransport
664
+ RelayClientTransport,
665
+ RELAY_MINT_PATH
651
666
  };
@@ -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. */
@@ -119,8 +137,19 @@ export type RelayMessage = RelayClientMessage | RelayServerMessage;
119
137
  * that keep every room in one process (the Bun reference server) ignore the
120
138
  * path, so this is safe to send to either.
121
139
  *
140
+ * Passing no room code addresses {@link RELAY_MINT_PATH} instead, asking the
141
+ * relay to allocate one; the code comes back in `ROOM_CREATED`.
142
+ *
122
143
  * @param url - Base relay URL, e.g. `wss://relay.example.com`.
123
- * @param roomId - Room code to address.
144
+ * @param roomId - Room code to address, or omitted to have one minted.
145
+ */
146
+ export declare function relayRoomUrl(url: string, roomId?: string): string;
147
+ /**
148
+ * Path that asks the relay to allocate a room code.
149
+ *
150
+ * Reserved, so it can never be mistaken for a room code. Single-process relays
151
+ * ignore the path and mint from the `CREATE_ROOM` message alone; sharded relays
152
+ * need it, because they must choose the shard before any frame arrives.
124
153
  */
125
- export declare function relayRoomUrl(url: string, roomId: string): string;
154
+ export declare const RELAY_MINT_PATH = "/new";
126
155
  //# sourceMappingURL=relay-protocol.d.ts.map
@@ -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.12.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 =
@@ -144,12 +161,27 @@ export type RelayMessage = RelayClientMessage | RelayServerMessage;
144
161
  * that keep every room in one process (the Bun reference server) ignore the
145
162
  * path, so this is safe to send to either.
146
163
  *
164
+ * Passing no room code addresses {@link RELAY_MINT_PATH} instead, asking the
165
+ * relay to allocate one; the code comes back in `ROOM_CREATED`.
166
+ *
147
167
  * @param url - Base relay URL, e.g. `wss://relay.example.com`.
148
- * @param roomId - Room code to address.
168
+ * @param roomId - Room code to address, or omitted to have one minted.
149
169
  */
150
- export function relayRoomUrl(url: string, roomId: string): string {
170
+ export function relayRoomUrl(url: string, roomId?: string): string {
151
171
  const trimmed = url.replace(/\/+$/, "");
152
172
  const [base, query] = trimmed.split("?", 2);
153
- const path = `${base}/r/${encodeURIComponent(roomId)}`;
173
+ const path =
174
+ roomId === undefined
175
+ ? `${base}${RELAY_MINT_PATH}`
176
+ : `${base}/r/${encodeURIComponent(roomId)}`;
154
177
  return query ? `${path}?${query}` : path;
155
178
  }
179
+
180
+ /**
181
+ * Path that asks the relay to allocate a room code.
182
+ *
183
+ * Reserved, so it can never be mistaken for a room code. Single-process relays
184
+ * ignore the path and mint from the `CREATE_ROOM` message alone; sharded relays
185
+ * need it, because they must choose the shard before any frame arrives.
186
+ */
187
+ export const RELAY_MINT_PATH = "/new";
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 };