@couch-kit/client 0.5.1 → 0.6.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,33 @@
1
1
  # @couch-kit/client
2
2
 
3
+ ## 0.6.0
4
+
5
+ ### Minor Changes
6
+
7
+ - Fix player state not restored on page refresh
8
+
9
+ Players now maintain their identity across page refreshes and network reconnections. The library uses a stable player ID derived from a persistent session secret (stored in localStorage) instead of the ephemeral WebSocket socket ID.
10
+
11
+ **New features:**
12
+ - `__PLAYER_RECONNECTED__` internal action: dispatched when a returning player reconnects, preserving all game data (hand, score, turn, etc.)
13
+ - `__PLAYER_REMOVED__` internal action: dispatched when a disconnected player times out (default: 5 minutes), permanently removing them from state
14
+ - Race-safe disconnect handling: prevents marking a player as disconnected if they've already reconnected on a new socket
15
+ - Secret validation: malformed session secrets are rejected at the JOIN boundary
16
+
17
+ **Breaking changes:**
18
+ - `state.players` keys are now stable derived player IDs instead of ephemeral socket IDs
19
+ - `playerId` returned by `useGameClient()` and sent in `WELCOME` messages is now a stable identifier that persists across reconnections
20
+ - `secret` field in `JOIN` payload is now required (was optional)
21
+
22
+ **Security:**
23
+ - Raw session secret is never stored on `IPlayer` or broadcast to clients
24
+ - Only the derived public player ID is shared in game state
25
+
26
+ ### Patch Changes
27
+
28
+ - Updated dependencies
29
+ - @couch-kit/core@0.5.0
30
+
3
31
  ## 0.5.1
4
32
 
5
33
  ### Patch Changes
package/README.md CHANGED
@@ -40,7 +40,7 @@ Returns:
40
40
 
41
41
  - `status`: `'connecting' | 'connected' | 'disconnected' | 'error'`
42
42
  - `state`: current controller state (optimistic + hydrated)
43
- - `playerId`: server-assigned player id (after `WELCOME`)
43
+ - `playerId`: stable public identifier derived from the session secret. Persists across page refreshes and reconnections (the same player always gets the same `playerId`).
44
44
  - `sendAction(action)`: optimistic dispatch + send to host
45
45
  - `getServerTime()`: NTP-ish server time based on periodic ping/pong
46
46
  - `rtt`: round-trip time (ms) to the server, updated periodically via PING/PONG
@@ -94,7 +94,7 @@ export default function Controller() {
94
94
  const {
95
95
  status, // 'connecting' | 'connected' | 'disconnected' | 'error'
96
96
  state, // The current game state (synced with Host)
97
- playerId, // Your unique session ID
97
+ playerId, // Stable identifier, persists across reconnections
98
98
  sendAction, // Function to send actions to Host
99
99
  } = useGameClient({
100
100
  reducer: gameReducer,
package/dist/index.js CHANGED
@@ -156,6 +156,13 @@ function useGameClient(config) {
156
156
  case MessageTypes2.PONG:
157
157
  handlePongRef.current(msg.payload);
158
158
  break;
159
+ case MessageTypes2.RECONNECTED:
160
+ setPlayerId(msg.payload.playerId);
161
+ dispatchLocal({
162
+ type: InternalActionTypes.HYDRATE,
163
+ payload: msg.payload.state
164
+ });
165
+ break;
159
166
  }
160
167
  } catch (e) {
161
168
  console.error("Failed to parse message", e);
@@ -1 +1 @@
1
- {"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"AACA,OAAO,EAUL,KAAK,UAAU,EACf,KAAK,OAAO,EAEb,MAAM,iBAAiB,CAAC;AAGzB,MAAM,WAAW,YAAY,CAAC,CAAC,SAAS,UAAU,EAAE,CAAC,SAAS,OAAO;IACnE,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,OAAO,EAAE,CAAC,KAAK,EAAE,CAAC,EAAE,MAAM,EAAE,CAAC,KAAK,CAAC,CAAC;IACpC,YAAY,EAAE,CAAC,CAAC;IAChB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,mEAAmE;IACnE,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,4EAA4E;IAC5E,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,wEAAwE;IACxE,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,SAAS,CAAC,EAAE,MAAM,IAAI,CAAC;IACvB,YAAY,CAAC,EAAE,MAAM,IAAI,CAAC;IAC1B,KAAK,CAAC,EAAE,OAAO,CAAC;CACjB;AAED;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,aAAa,CAAC,CAAC,SAAS,UAAU,EAAE,CAAC,SAAS,OAAO,EACnE,MAAM,EAAE,YAAY,CAAC,CAAC,EAAE,CAAC,CAAC;;;;yBA8Mc,CAAC;;IAqBvC,8EAA8E;;IAE9E,0EAA0E;;IAE1E,+EAA+E;;EAGlF"}
1
+ {"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"AACA,OAAO,EAUL,KAAK,UAAU,EACf,KAAK,OAAO,EAEb,MAAM,iBAAiB,CAAC;AAGzB,MAAM,WAAW,YAAY,CAAC,CAAC,SAAS,UAAU,EAAE,CAAC,SAAS,OAAO;IACnE,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,OAAO,EAAE,CAAC,KAAK,EAAE,CAAC,EAAE,MAAM,EAAE,CAAC,KAAK,CAAC,CAAC;IACpC,YAAY,EAAE,CAAC,CAAC;IAChB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,mEAAmE;IACnE,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,4EAA4E;IAC5E,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,wEAAwE;IACxE,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,SAAS,CAAC,EAAE,MAAM,IAAI,CAAC;IACvB,YAAY,CAAC,EAAE,MAAM,IAAI,CAAC;IAC1B,KAAK,CAAC,EAAE,OAAO,CAAC;CACjB;AAED;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,aAAa,CAAC,CAAC,SAAS,UAAU,EAAE,CAAC,SAAS,OAAO,EACnE,MAAM,EAAE,YAAY,CAAC,CAAC,EAAE,CAAC,CAAC;;;;yBAsNc,CAAC;;IAqBvC,8EAA8E;;IAE9E,0EAA0E;;IAE1E,+EAA+E;;EAGlF"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@couch-kit/client",
3
- "version": "0.5.1",
3
+ "version": "0.6.0",
4
4
  "description": "React hooks for phone controllers in Couch Kit party games — WebSocket client and action dispatch",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -43,7 +43,7 @@
43
43
  "clean": "rm -rf dist lib"
44
44
  },
45
45
  "dependencies": {
46
- "@couch-kit/core": "0.4.0"
46
+ "@couch-kit/core": "0.5.0"
47
47
  },
48
48
  "devDependencies": {
49
49
  "react": "^18.2.0",
package/src/client.ts CHANGED
@@ -179,6 +179,14 @@ export function useGameClient<S extends IGameState, A extends IAction>(
179
179
  case MessageTypes.PONG:
180
180
  handlePongRef.current(msg.payload);
181
181
  break;
182
+
183
+ case MessageTypes.RECONNECTED:
184
+ setPlayerId(msg.payload.playerId);
185
+ dispatchLocal({
186
+ type: InternalActionTypes.HYDRATE,
187
+ payload: msg.payload.state as S,
188
+ } as InternalAction<S>);
189
+ break;
182
190
  }
183
191
  } catch (e) {
184
192
  console.error("Failed to parse message", e);