@usions/sdk 2.11.1 → 2.12.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.
@@ -3,6 +3,7 @@
3
3
  */
4
4
 
5
5
  import { isTrustedMessageSource } from './core.js';
6
+ import { UsionError, toUsionError, ERROR_CODES } from './errors.js';
6
7
 
7
8
  /**
8
9
  * Add proxy connection methods to game module
@@ -42,7 +43,7 @@ export function applyGameProxy(game, Usion) {
42
43
  setTimeout(function() {
43
44
  if (!self.connected) {
44
45
  self._connecting = false;
45
- reject(new Error('Proxy connection timeout'));
46
+ reject(new UsionError(ERROR_CODES.CONNECT_TIMEOUT, 'Proxy connection timeout'));
46
47
  }
47
48
  }, 10000);
48
49
  });
@@ -83,69 +84,97 @@ export function applyGameProxy(game, Usion) {
83
84
  self._connecting = false;
84
85
  break;
85
86
 
87
+ case 'GAME_DISCONNECTED':
88
+ // The host app's socket dropped; it will rejoin and resync for us.
89
+ self.connected = false;
90
+ self._dispatch('disconnect', data.reason || 'transport');
91
+ break;
92
+
93
+ case 'GAME_RECONNECTED':
94
+ self.connected = true;
95
+ self._dispatch('reconnect', data.attempts || 1);
96
+ break;
97
+
98
+ case 'GAME_PLAYER_CONNECTION':
99
+ self._dispatch('playerConnection', data);
100
+ break;
101
+
86
102
  case 'GAME_JOINED':
87
103
  self._joined = true;
88
- if (data.sequence !== undefined) self._lastSequence = data.sequence;
104
+ if (data.sequence !== undefined) {
105
+ self._lastSequence = data.sequence;
106
+ self._lastActionApplied = Math.max(self._lastActionApplied, data.sequence);
107
+ }
89
108
  if (self._proxyJoinResolve) {
90
109
  self._proxyJoinResolve(data);
91
110
  self._proxyJoinResolve = null;
92
111
  }
93
- if (self._eventHandlers.joined) self._eventHandlers.joined(data);
112
+ self._dispatch('joined', data);
94
113
  break;
95
114
 
96
115
  case 'GAME_JOIN_ERROR':
97
116
  self._joined = false;
98
117
  if (self._proxyJoinReject) {
99
- self._proxyJoinReject(new Error(data.message || 'Join failed'));
118
+ self._proxyJoinReject(toUsionError(data.message || 'Join failed'));
100
119
  self._proxyJoinReject = null;
101
120
  }
102
121
  break;
103
122
 
104
123
  case 'GAME_PLAYER_JOINED':
105
- if (self._eventHandlers.playerJoined) self._eventHandlers.playerJoined(data);
124
+ self._dispatch('playerJoined', data);
106
125
  break;
107
126
 
108
127
  case 'GAME_PLAYER_LEFT':
109
- if (self._eventHandlers.playerLeft) self._eventHandlers.playerLeft(data);
128
+ self._dispatch('playerLeft', data);
110
129
  break;
111
130
 
112
131
  case 'GAME_STATE':
113
132
  if (data.sequence !== undefined) self._lastSequence = Math.max(self._lastSequence, data.sequence);
114
- if (self._eventHandlers.stateUpdate) self._eventHandlers.stateUpdate(data);
133
+ self._dispatch('stateUpdate', data);
115
134
  break;
116
135
 
117
136
  case 'GAME_ACTION_DATA':
118
- if (data.sequence !== undefined) self._lastSequence = Math.max(self._lastSequence, data.sequence);
119
- if (self._eventHandlers.action) self._eventHandlers.action(data);
137
+ if (data.sequence !== undefined) {
138
+ // Drop duplicates: actions are delivered exactly once per
139
+ // sequence (live, sender echo, or replay after reconnect).
140
+ if (data.sequence <= self._lastActionApplied) break;
141
+ self._lastActionApplied = data.sequence;
142
+ self._lastSequence = Math.max(self._lastSequence, data.sequence);
143
+ }
144
+ self._dispatch('action', data);
120
145
  break;
121
146
 
122
147
  case 'GAME_REALTIME_DATA':
123
- if (self._eventHandlers.realtime) self._eventHandlers.realtime(data);
148
+ self._dispatch('realtime', data);
124
149
  break;
125
150
 
126
151
  case 'GAME_FINISHED':
127
152
  if (data.sequence !== undefined) self._lastSequence = data.sequence;
128
- if (self._eventHandlers.finished) self._eventHandlers.finished(data);
153
+ self._dispatch('finished', data);
129
154
  break;
130
155
 
131
156
  case 'GAME_ERROR':
132
157
  Usion.log('Game error via proxy: ' + (data.message || data.code));
133
- if (self._eventHandlers.error) self._eventHandlers.error(data);
158
+ self._dispatch('error', data);
134
159
  break;
135
160
 
136
161
  case 'GAME_RESTARTED':
137
162
  self._lastSequence = 0;
138
- if (self._eventHandlers.restarted) self._eventHandlers.restarted(data);
163
+ self._lastActionApplied = 0;
164
+ self._dispatch('restarted', data);
139
165
  break;
140
166
 
141
167
  case 'GAME_REMATCH_REQUEST':
142
- if (self._eventHandlers.rematchRequest) self._eventHandlers.rematchRequest(data);
168
+ self._dispatch('rematchRequest', data);
143
169
  break;
144
170
 
145
171
  case 'GAME_SYNC':
146
- if (data.sequence !== undefined) self._lastSequence = data.sequence;
147
- if (self._eventHandlers.sync) self._eventHandlers.sync(data);
148
- if (self._eventHandlers.stateUpdate) self._eventHandlers.stateUpdate(data);
172
+ if (data.sequence !== undefined) {
173
+ self._lastSequence = data.sequence;
174
+ self._lastActionApplied = Math.max(self._lastActionApplied, data.sequence);
175
+ }
176
+ self._dispatch('sync', data);
177
+ self._dispatch('stateUpdate', data);
149
178
  break;
150
179
  }
151
180
  });
@@ -57,17 +57,27 @@ export function applyGameSocket(game, Usion) {
57
57
  }
58
58
  }, 25000);
59
59
 
60
- // Re-join room after reconnect
60
+ // Re-join room after reconnect. The promise is kept on
61
+ // _rejoinPromise so action() can gate sends until the room
62
+ // membership and sync are restored — otherwise a stale move could
63
+ // go out before the client has caught up.
61
64
  if (self.roomId) {
62
65
  self._joined = false;
63
66
  self._joinPromise = null;
64
- self.join(self.roomId)
67
+ self._rejoinPromise = self.join(self.roomId)
65
68
  .then(function() {
66
69
  Usion.log('Reconnected - joined room ' + self.roomId);
67
70
  self.requestSync(self._lastSequence || 0);
68
71
  })
69
72
  .catch(function(err) {
70
73
  Usion.log('Rejoin failed: ' + (err && err.message ? err.message : String(err)));
74
+ })
75
+ .then(function() {
76
+ self._rejoinPromise = null;
77
+ // Send any moves queued while offline, now that membership
78
+ // and sync are restored. (Socket.IO v4 emits 'reconnect' on
79
+ // the Manager, not the Socket, so this is the reliable hook.)
80
+ if (self._flushOfflineQueue) self._flushOfflineQueue();
71
81
  });
72
82
  }
73
83
 
@@ -77,9 +87,7 @@ export function applyGameSocket(game, Usion) {
77
87
  self.socket.on('connect_error', function(err) {
78
88
  self._connecting = false;
79
89
  Usion.log('Game socket error: ' + err.message);
80
- if (self._eventHandlers.connectionError) {
81
- self._eventHandlers.connectionError(err);
82
- }
90
+ self._dispatch('connectionError', err);
83
91
  reject(err);
84
92
  });
85
93
 
@@ -92,104 +100,92 @@ export function applyGameSocket(game, Usion) {
92
100
  self._heartbeatInterval = null;
93
101
  }
94
102
  Usion.log('Game socket disconnected: ' + reason);
95
- if (self._eventHandlers.disconnect) {
96
- self._eventHandlers.disconnect(reason);
97
- }
103
+ self._dispatch('disconnect', reason);
98
104
  });
99
105
 
100
106
  self.socket.on('reconnect', function(attemptNumber) {
101
107
  Usion.log('Game socket reconnected after ' + attemptNumber + ' attempts');
102
- if (self._eventHandlers.reconnect) {
103
- self._eventHandlers.reconnect(attemptNumber);
104
- }
108
+ self._dispatch('reconnect', attemptNumber);
105
109
  });
106
110
 
107
111
  // Game event handlers
108
112
  self.socket.on('game:joined', function(data) {
109
113
  if (data.sequence !== undefined) {
110
114
  self._lastSequence = data.sequence;
115
+ // Everything up to this sequence is reflected in the joined
116
+ // state — actions at or below it must not be re-delivered.
117
+ self._lastActionApplied = Math.max(self._lastActionApplied, data.sequence);
111
118
  }
112
- if (self._eventHandlers.joined) {
113
- self._eventHandlers.joined(data);
114
- }
119
+ self._dispatch('joined', data);
115
120
  });
116
121
 
117
122
  self.socket.on('game:player_joined', function(data) {
118
- if (self._eventHandlers.playerJoined) {
119
- self._eventHandlers.playerJoined(data);
120
- }
123
+ self._dispatch('playerJoined', data);
121
124
  });
122
125
 
123
126
  self.socket.on('game:player_left', function(data) {
124
- if (self._eventHandlers.playerLeft) {
125
- self._eventHandlers.playerLeft(data);
126
- }
127
+ self._dispatch('playerLeft', data);
127
128
  });
128
129
 
129
130
  self.socket.on('game:state', function(data) {
130
131
  if (data.sequence !== undefined) {
131
132
  self._lastSequence = Math.max(self._lastSequence, data.sequence);
132
133
  }
133
- if (self._eventHandlers.stateUpdate) {
134
- self._eventHandlers.stateUpdate(data);
135
- }
134
+ self._dispatch('stateUpdate', data);
136
135
  });
137
136
 
138
137
  self.socket.on('game:sync', function(data) {
139
138
  if (data.sequence !== undefined) {
140
139
  self._lastSequence = data.sequence;
140
+ // The sync payload carries all actions up to this sequence; a
141
+ // live echo of one of them must not be applied a second time.
142
+ self._lastActionApplied = Math.max(self._lastActionApplied, data.sequence);
141
143
  }
142
- if (self._eventHandlers.sync) {
143
- self._eventHandlers.sync(data);
144
- }
144
+ self._dispatch('sync', data);
145
145
  // Also trigger stateUpdate for backwards compat
146
- if (self._eventHandlers.stateUpdate) {
147
- self._eventHandlers.stateUpdate(data);
148
- }
146
+ self._dispatch('stateUpdate', data);
149
147
  });
150
148
 
151
149
  self.socket.on('game:action', function(data) {
152
150
  if (data.sequence !== undefined) {
151
+ // Drop duplicates: actions are delivered exactly once per
152
+ // sequence, whether they arrive live, as the sender's own echo,
153
+ // or replayed after a reconnect.
154
+ if (data.sequence <= self._lastActionApplied) return;
155
+ self._lastActionApplied = data.sequence;
153
156
  self._lastSequence = Math.max(self._lastSequence, data.sequence);
154
157
  }
155
- if (self._eventHandlers.action) {
156
- self._eventHandlers.action(data);
157
- }
158
+ self._dispatch('action', data);
159
+ });
160
+
161
+ self.socket.on('game:player_connection', function(data) {
162
+ self._dispatch('playerConnection', data);
158
163
  });
159
164
 
160
165
  self.socket.on('game:realtime', function(data) {
161
- if (self._eventHandlers.realtime) {
162
- self._eventHandlers.realtime(data);
163
- }
166
+ self._dispatch('realtime', data);
164
167
  });
165
168
 
166
169
  self.socket.on('game:finished', function(data) {
167
170
  if (data.sequence !== undefined) {
168
171
  self._lastSequence = data.sequence;
169
172
  }
170
- if (self._eventHandlers.finished) {
171
- self._eventHandlers.finished(data);
172
- }
173
+ self._dispatch('finished', data);
173
174
  });
174
175
 
175
176
  self.socket.on('game:error', function(data) {
176
177
  Usion.log('Game error: ' + (data.message || data.code));
177
- if (self._eventHandlers.error) {
178
- self._eventHandlers.error(data);
179
- }
178
+ self._dispatch('error', data);
180
179
  });
181
180
 
182
181
  self.socket.on('game:rematch_request', function(data) {
183
- if (self._eventHandlers.rematchRequest) {
184
- self._eventHandlers.rematchRequest(data);
185
- }
182
+ self._dispatch('rematchRequest', data);
186
183
  });
187
184
 
188
185
  self.socket.on('game:restarted', function(data) {
189
186
  self._lastSequence = 0; // Reset sequence on rematch
190
- if (self._eventHandlers.restarted) {
191
- self._eventHandlers.restarted(data);
192
- }
187
+ self._lastActionApplied = 0;
188
+ self._dispatch('restarted', data);
193
189
  });
194
190
 
195
191
  } catch (err) {
@@ -31,6 +31,7 @@ import { createCloudModule } from './cloud.js';
31
31
  import { createMatchmakingModule } from './matchmaking.js';
32
32
  import { applyBackendChannel } from './backend-channel.js';
33
33
  import { netcode } from './netcode/index.js';
34
+ import { UsionError, ERROR_CODES } from './errors.js';
34
35
 
35
36
  // Build the Usion object from core
36
37
  const Usion = Object.assign({}, core);
@@ -44,6 +45,9 @@ Usion.chat = createChatModule(Usion);
44
45
  Usion.bot = createBotModule(Usion);
45
46
  Usion.fileStorage = createFileStorageModule(Usion);
46
47
  Usion.game = createGameModule(Usion);
48
+ // Stable error class + codes — developers branch on err.code, not message text.
49
+ Usion.UsionError = UsionError;
50
+ Usion.ERROR_CODES = ERROR_CODES;
47
51
  // Unified backend channel (used by lobby etc.; works standalone + embedded).
48
52
  applyBackendChannel(Usion);
49
53
  Usion.lobby = createLobbyModule(Usion);
@@ -13,6 +13,26 @@ export const miscMethods = {
13
13
  return this.wallet.requestPayment(amount, reason, data);
14
14
  },
15
15
 
16
+ /**
17
+ * Live SDK diagnostics snapshot: version, transport, connection and
18
+ * sequence state. Surfaced automatically in the platform debug overlay
19
+ * (game.debug attaches it as _diag) and useful in bug reports.
20
+ */
21
+ diagnostics: function() {
22
+ const game = this.game || {};
23
+ return {
24
+ version: this.version,
25
+ transport: game.directMode ? 'direct' : (game._useProxy ? 'proxy' : (game.socket ? 'socket' : 'none')),
26
+ connected: !!game.connected,
27
+ joined: !!game._joined,
28
+ roomId: game.roomId || null,
29
+ playerId: game.playerId || null,
30
+ lastSequence: game._lastSequence || 0,
31
+ lastActionApplied: game._lastActionApplied || 0,
32
+ rejoining: !!game._rejoinPromise,
33
+ };
34
+ },
35
+
16
36
  /**
17
37
  * Submit result and signal completion
18
38
  * @param {object} data - Result data to send to parent
@@ -112,7 +112,13 @@ export function createWalletModule(Usion) {
112
112
  * @param {function} callback - Called with new balance
113
113
  */
114
114
  onBalanceChange: function(callback) {
115
- this._balanceChangeHandler = callback;
115
+ const self = this;
116
+ self._balanceChangeHandler = callback;
117
+ return function() {
118
+ if (self._balanceChangeHandler === callback) {
119
+ self._balanceChangeHandler = null;
120
+ }
121
+ };
116
122
  }
117
123
  };
118
124
  }
package/types/index.d.ts CHANGED
@@ -76,7 +76,7 @@ export interface WalletModule {
76
76
  getBalance(): Promise<number>;
77
77
  hasCredits(amount: number): Promise<boolean>;
78
78
  requestPayment(amount: number, reason: string, data?: Record<string, any>): Promise<PaymentResponse>;
79
- onBalanceChange(callback: (balance: number) => void): void;
79
+ onBalanceChange(callback: (balance: number) => void): UnsubscribeFn;
80
80
  }
81
81
 
82
82
  // ─── Session ─────────────────────────────────────────────────────
@@ -178,6 +178,38 @@ export interface GameActionData {
178
178
  action_type: string;
179
179
  action_data: Record<string, any>;
180
180
  sequence?: number;
181
+ /** Present when the sender passed { nextTurn } — the server-remembered turn. */
182
+ current_turn?: string;
183
+ }
184
+
185
+ /** Returned by event registrations; call to remove the handler. */
186
+ export type UnsubscribeFn = () => void;
187
+
188
+ export interface GameActionOptions {
189
+ /**
190
+ * Player ID whose turn comes after this action. The server stores it (no
191
+ * game logic — a relay that remembers) and returns it as current_turn in
192
+ * join acks and game:sync, so turn state survives reconnects.
193
+ */
194
+ nextTurn?: string;
195
+ /**
196
+ * Hold this action while disconnected and send it (in order) once the
197
+ * connection recovers, instead of rejecting with NOT_CONNECTED — "your
198
+ * move is saved and sends when you're back". For turn-based games only;
199
+ * never queue realtime-style inputs. Queue cap 20 (then QUEUE_FULL).
200
+ */
201
+ queueOffline?: boolean;
202
+ }
203
+
204
+ export interface PlayerConnectionData {
205
+ room_id: string;
206
+ player_id: string;
207
+ /**
208
+ * connected — player (re)joined the room.
209
+ * reconnecting — player's connection dropped; grace window running (~15s).
210
+ * gone — player did not return before the grace/heartbeat expiry.
211
+ */
212
+ state: 'connected' | 'reconnecting' | 'gone';
181
213
  }
182
214
 
183
215
  export interface GameFinishedData {
@@ -191,6 +223,7 @@ export interface SyncData {
191
223
  room_id: string;
192
224
  actions: GameActionData[];
193
225
  game_state: Record<string, any>;
226
+ current_turn?: string | null;
194
227
  sequence: number;
195
228
  }
196
229
 
@@ -207,14 +240,35 @@ export interface GameModule {
207
240
  isConnected(): boolean;
208
241
 
209
242
  // Room & actions
243
+ //
244
+ // RELIABILITY CONTRACT (what the platform guarantees / what your game must do):
245
+ // - Every action is echoed back to the sender with the authoritative
246
+ // sequence number. Apply game state ONLY in onAction — never
247
+ // optimistically on send — so all clients apply identical actions in
248
+ // identical order. The SDK deduplicates by sequence, so each action is
249
+ // delivered exactly once even across reconnect replays.
250
+ // - On a connection blip the host/SDK rejoins the room and replays missed
251
+ // actions via onSync automatically. Use onDisconnect/onReconnect to
252
+ // pause input and show a "reconnecting" indicator.
253
+ // - For turn-based games, pass { nextTurn } on each move and trust
254
+ // current_turn from join/sync instead of deriving the turn locally.
255
+ // - The room authority (player_ids[0]) should checkpoint via setState()
256
+ // at meaningful transitions so rejoining clients can restore instantly.
210
257
  join(roomId?: string): Promise<GameJoinResult>;
211
258
  leave(): void;
212
- action(actionType: string, actionData?: Record<string, any>): Promise<ActionResult>;
259
+ action(actionType: string, actionData?: Record<string, any>, opts?: GameActionOptions): Promise<ActionResult>;
213
260
  realtime(actionType: string, actionData?: Record<string, any>): void;
214
261
  requestSync(lastSequence?: number): void;
215
262
  requestRematch(): void;
216
263
  forfeit(): Promise<{ success: boolean; error?: string }>;
217
264
 
265
+ /**
266
+ * Checkpoint authoritative game state on the server (authority only —
267
+ * player_ids[0]/host). (Re)joining clients receive the latest checkpoint
268
+ * as game_state in the join ack and in game:sync. Max 64 KB serialized.
269
+ */
270
+ setState(state: Record<string, any>): Promise<{ success: boolean; error?: string; code?: UsionErrorCode }>;
271
+
218
272
  // Persisted state — survives iframe unmount/remount.
219
273
  // Keyed by (player_id, room_id) in localStorage. Schema is up to the game.
220
274
  saveState<T = any>(state: T): boolean;
@@ -226,23 +280,31 @@ export interface GameModule {
226
280
  debug(payload: Record<string, any>): void;
227
281
 
228
282
  // Event handlers
229
- onJoined(callback: (data: GameJoinResult) => void): void;
230
- onPlayerJoined(callback: (data: PlayerJoinedData) => void): void;
231
- onPlayerLeft(callback: (data: PlayerLeftData) => void): void;
232
- onStateUpdate(callback: (data: StateUpdateData) => void): void;
233
- onSync(callback: (data: SyncData) => void): void;
234
- onAction(callback: (data: GameActionData) => void): void;
235
- onRealtime(callback: (data: Record<string, any>) => void): void;
236
- onGameFinished(callback: (data: GameFinishedData) => void): void;
237
- onGameRestarted(callback: (data: Record<string, any>) => void): void;
238
- onError(callback: (data: { message: string; code?: string }) => void): void;
239
- onRematchRequest(callback: (data: RematchData) => void): void;
240
- onDisconnect(callback: (reason: string) => void): void;
241
- onReconnect(callback: (attemptNumber: number) => void): void;
242
- onConnectionError(callback: (error: Error) => void): void;
243
-
244
- // Generic event listener
245
- on(event: string, callback: (data: any) => void): void;
283
+ onJoined(callback: (data: GameJoinResult) => void): UnsubscribeFn;
284
+ onPlayerJoined(callback: (data: PlayerJoinedData) => void): UnsubscribeFn;
285
+ onPlayerLeft(callback: (data: PlayerLeftData) => void): UnsubscribeFn;
286
+ onStateUpdate(callback: (data: StateUpdateData) => void): UnsubscribeFn;
287
+ onSync(callback: (data: SyncData) => void): UnsubscribeFn;
288
+ onAction(callback: (data: GameActionData) => void): UnsubscribeFn;
289
+ onRealtime(callback: (data: Record<string, any>) => void): UnsubscribeFn;
290
+ onGameFinished(callback: (data: GameFinishedData) => void): UnsubscribeFn;
291
+ onGameRestarted(callback: (data: Record<string, any>) => void): UnsubscribeFn;
292
+ onError(callback: (data: { message: string; code?: string }) => void): UnsubscribeFn;
293
+ onRematchRequest(callback: (data: RematchData) => void): UnsubscribeFn;
294
+ onDisconnect(callback: (reason: string) => void): UnsubscribeFn;
295
+ onReconnect(callback: (attemptNumber: number) => void): UnsubscribeFn;
296
+ onConnectionError(callback: (error: Error) => void): UnsubscribeFn;
297
+ /** Peer connection lifecycle: connected / reconnecting (grace) / gone. */
298
+ onPlayerConnection(callback: (data: PlayerConnectionData) => void): UnsubscribeFn;
299
+
300
+ /**
301
+ * Register an ADDITIONAL event listener. Unlike the onX methods this
302
+ * supports multiple listeners per event, can be called before connect(),
303
+ * and works in every transport. Accepts internal names ('action'), wire
304
+ * names ('game:action'), or snake_case ('player_joined').
305
+ * @returns Unsubscribe function.
306
+ */
307
+ on(event: string, callback: (data: any) => void): UnsubscribeFn;
246
308
 
247
309
  // ─── Netcode helpers (low-latency realtime) ──────────────────
248
310
  // JSON state delta compression.
@@ -667,7 +729,7 @@ export interface BotModule {
667
729
  sendMessage(text: string): void;
668
730
  updateContext(ctx: Record<string, any>): void;
669
731
  close(result?: any): void;
670
- onMessage(callback: (message: BotMessage) => void): void;
732
+ onMessage(callback: (message: BotMessage) => void): UnsubscribeFn;
671
733
  }
672
734
 
673
735
  // ─── Selection Grid ──────────────────────────────────────────────
@@ -733,6 +795,29 @@ export interface UsionSDK {
733
795
  onChange: (selected: string, item: HTMLElement) => void
734
796
  ): SelectionGrid;
735
797
 
798
+ /** SDK version (injected from package.json at build time). */
799
+ version: string;
800
+
801
+ /**
802
+ * Live diagnostics snapshot: version, transport, connection + sequence
803
+ * state. game.debug() attaches this automatically as payload._diag.
804
+ */
805
+ diagnostics(): {
806
+ version: string;
807
+ transport: 'socket' | 'proxy' | 'direct' | 'none';
808
+ connected: boolean;
809
+ joined: boolean;
810
+ roomId: string | null;
811
+ playerId: string | null;
812
+ lastSequence: number;
813
+ lastActionApplied: number;
814
+ rejoining: boolean;
815
+ };
816
+
817
+ // Errors — branch on err.code (stable), never on message text.
818
+ UsionError: typeof UsionError;
819
+ ERROR_CODES: typeof ERROR_CODES;
820
+
736
821
  // Modules
737
822
  user: UserModule;
738
823
  storage: StorageModule;
@@ -822,6 +907,38 @@ export interface LobbyModule {
822
907
  queue(serviceId: string, opts?: { conversationId?: string }): Promise<any>;
823
908
  }
824
909
 
910
+ /**
911
+ * Stable machine-readable error codes. Part of the public API: codes are
912
+ * never removed within a major version. Messages may change — branch on
913
+ * `err.code`, never on message text.
914
+ */
915
+ export declare const ERROR_CODES: {
916
+ readonly NOT_CONNECTED: 'NOT_CONNECTED';
917
+ readonly NO_ROOM: 'NO_ROOM';
918
+ readonly ROOM_NOT_FOUND: 'ROOM_NOT_FOUND';
919
+ readonly NOT_PARTICIPANT: 'NOT_PARTICIPANT';
920
+ readonly NOT_AUTHORITY: 'NOT_AUTHORITY';
921
+ readonly NOT_AUTHENTICATED: 'NOT_AUTHENTICATED';
922
+ readonly JOIN_TIMEOUT: 'JOIN_TIMEOUT';
923
+ readonly CONNECT_TIMEOUT: 'CONNECT_TIMEOUT';
924
+ readonly STATE_TOO_LARGE: 'STATE_TOO_LARGE';
925
+ readonly INVALID_STATE: 'INVALID_STATE';
926
+ readonly INVALID_NEXT_TURN: 'INVALID_NEXT_TURN';
927
+ readonly RATE_LIMITED: 'RATE_LIMITED';
928
+ readonly REQUEST_TIMEOUT: 'REQUEST_TIMEOUT';
929
+ readonly QUEUE_FULL: 'QUEUE_FULL';
930
+ readonly UNSUPPORTED: 'UNSUPPORTED';
931
+ readonly UNKNOWN: 'UNKNOWN';
932
+ };
933
+
934
+ export type UsionErrorCode = keyof typeof ERROR_CODES;
935
+
936
+ export declare class UsionError extends Error {
937
+ readonly name: 'UsionError';
938
+ readonly code: UsionErrorCode;
939
+ constructor(code: UsionErrorCode | string, message?: string);
940
+ }
941
+
825
942
  declare const Usion: UsionSDK;
826
943
 
827
944
  export { Usion };