@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.
@@ -66,7 +66,7 @@ export function isTrustedMessageSource(event) {
66
66
  * Core Usion object with init, _post, _request
67
67
  */
68
68
  export const core = {
69
- version: '2.1.0',
69
+ version: '__USION_SDK_VERSION__', // injected from package.json at build
70
70
  config: {},
71
71
  _initialized: false,
72
72
  _initCallback: null,
@@ -0,0 +1,75 @@
1
+ /**
2
+ * Usion SDK Errors — stable, machine-readable error codes.
3
+ *
4
+ * Developers should branch on `err.code`, never on message text. Messages
5
+ * are human-readable and may change; codes are part of the public API and
6
+ * follow the deprecation policy (never removed within a major version).
7
+ */
8
+
9
+ /** @type {Record<string, string>} */
10
+ export const ERROR_CODES = {
11
+ NOT_CONNECTED: 'NOT_CONNECTED', // No live connection for this call
12
+ NO_ROOM: 'NO_ROOM', // No room id provided/known
13
+ ROOM_NOT_FOUND: 'ROOM_NOT_FOUND', // Room does not exist server-side
14
+ NOT_PARTICIPANT: 'NOT_PARTICIPANT', // Caller is not a player in the room
15
+ NOT_AUTHORITY: 'NOT_AUTHORITY', // Authority-only call (e.g. setState)
16
+ NOT_AUTHENTICATED: 'NOT_AUTHENTICATED', // Missing/invalid auth server-side
17
+ JOIN_TIMEOUT: 'JOIN_TIMEOUT', // Join did not complete in time
18
+ CONNECT_TIMEOUT: 'CONNECT_TIMEOUT', // Connect did not complete in time
19
+ STATE_TOO_LARGE: 'STATE_TOO_LARGE', // setState payload over the quota
20
+ INVALID_STATE: 'INVALID_STATE', // setState payload not a JSON object
21
+ INVALID_NEXT_TURN: 'INVALID_NEXT_TURN', // nextTurn is not a player in the room
22
+ RATE_LIMITED: 'RATE_LIMITED', // Too many calls; back off
23
+ REQUEST_TIMEOUT: 'REQUEST_TIMEOUT', // Host/parent did not reply in time
24
+ QUEUE_FULL: 'QUEUE_FULL', // Offline action queue at capacity
25
+ UNSUPPORTED: 'UNSUPPORTED', // Not available in this transport
26
+ UNKNOWN: 'UNKNOWN', // Unmapped error (see message)
27
+ };
28
+
29
+ export class UsionError extends Error {
30
+ /**
31
+ * @param {string} code - One of ERROR_CODES
32
+ * @param {string} [message] - Human-readable detail (may change between versions)
33
+ */
34
+ constructor(code, message) {
35
+ super(message || code);
36
+ this.name = 'UsionError';
37
+ this.code = ERROR_CODES[code] ? code : ERROR_CODES.UNKNOWN;
38
+ }
39
+ }
40
+
41
+ // Backend error strings → stable codes. Order matters: first match wins.
42
+ /** @type {Array<[RegExp, string]>} */
43
+ const BACKEND_PATTERNS = [
44
+ [/not authenticated/i, ERROR_CODES.NOT_AUTHENTICATED],
45
+ [/room_id required|no room id/i, ERROR_CODES.NO_ROOM],
46
+ [/room not found/i, ERROR_CODES.ROOM_NOT_FOUND],
47
+ [/not a participant/i, ERROR_CODES.NOT_PARTICIPANT],
48
+ [/room authority/i, ERROR_CODES.NOT_AUTHORITY],
49
+ [/exceeds .*limit/i, ERROR_CODES.STATE_TOO_LARGE],
50
+ [/state must be/i, ERROR_CODES.INVALID_STATE],
51
+ [/next_turn must be/i, ERROR_CODES.INVALID_NEXT_TURN],
52
+ [/rate limit|too many/i, ERROR_CODES.RATE_LIMITED],
53
+ [/join timeout/i, ERROR_CODES.JOIN_TIMEOUT],
54
+ [/connection timeout|connect timeout/i, ERROR_CODES.CONNECT_TIMEOUT],
55
+ [/request timeout/i, ERROR_CODES.REQUEST_TIMEOUT],
56
+ [/not connected/i, ERROR_CODES.NOT_CONNECTED],
57
+ ];
58
+
59
+ /**
60
+ * Normalize anything (backend `{error}` string, Error, raw string) into a
61
+ * UsionError with the best-matching stable code.
62
+ * @param {*} err
63
+ * @param {string} [fallbackCode] - Code to use when nothing matches
64
+ * @returns {UsionError}
65
+ */
66
+ export function toUsionError(err, fallbackCode) {
67
+ if (err instanceof UsionError) return err;
68
+ const message = err && err.message ? err.message : String(err || 'Unknown error');
69
+ for (let i = 0; i < BACKEND_PATTERNS.length; i++) {
70
+ if (BACKEND_PATTERNS[i][0].test(message)) {
71
+ return new UsionError(BACKEND_PATTERNS[i][1], message);
72
+ }
73
+ }
74
+ return new UsionError(fallbackCode || ERROR_CODES.UNKNOWN, message);
75
+ }
@@ -8,6 +8,36 @@ import { applyGameProxy } from './game-proxy.js';
8
8
  import { applyGameMethods } from './game-methods.js';
9
9
  import { applyGameNetcode } from './game-netcode.js';
10
10
 
11
+ // Map any reasonable spelling of a game event onto the internal handler
12
+ // name: 'game:player_joined' / 'player_joined' / 'playerJoined' → 'playerJoined'.
13
+ const _EVENT_ALIASES = {
14
+ joined: 'joined',
15
+ player_joined: 'playerJoined',
16
+ player_left: 'playerLeft',
17
+ player_connection: 'playerConnection',
18
+ state: 'stateUpdate',
19
+ state_update: 'stateUpdate',
20
+ sync: 'sync',
21
+ action: 'action',
22
+ realtime: 'realtime',
23
+ finished: 'finished',
24
+ restarted: 'restarted',
25
+ error: 'error',
26
+ rematch_request: 'rematchRequest',
27
+ disconnect: 'disconnect',
28
+ reconnect: 'reconnect',
29
+ connection_error: 'connectionError',
30
+ };
31
+
32
+ function _normalizeEventName(event) {
33
+ let name = String(event || '');
34
+ if (name.indexOf('game:') === 0) name = name.slice(5);
35
+ if (_EVENT_ALIASES[name]) return _EVENT_ALIASES[name];
36
+ // camelCase passthrough ('playerJoined', 'stateUpdate', …)
37
+ const snake = name.replace(/([A-Z])/g, function(m) { return '_' + m.toLowerCase(); });
38
+ return _EVENT_ALIASES[snake] || name;
39
+ }
40
+
11
41
  /**
12
42
  * Create the game module with all sub-modules applied
13
43
  * @param {object} Usion - Reference to the main Usion object
@@ -33,6 +63,11 @@ export function createGameModule(Usion) {
33
63
  _heartbeatInterval: null,
34
64
  _pingMeter: null,
35
65
  _pongWaiters: [],
66
+ // Reliability state: highest action sequence already delivered to the
67
+ // game (duplicate echoes/replays are dropped), and the in-flight rejoin
68
+ // promise that gates action() sends right after a reconnect.
69
+ _lastActionApplied: 0,
70
+ _rejoinPromise: null,
36
71
 
37
72
  /**
38
73
  * Connect to the game socket server
@@ -115,34 +150,85 @@ export function createGameModule(Usion) {
115
150
  return self._connectPromise;
116
151
  },
117
152
 
118
- // Event handler registrations
119
- onJoined: function(callback) { this._eventHandlers.joined = callback; },
120
- onPlayerJoined: function(callback) { this._eventHandlers.playerJoined = callback; },
121
- onPlayerLeft: function(callback) { this._eventHandlers.playerLeft = callback; },
122
- onStateUpdate: function(callback) { this._eventHandlers.stateUpdate = callback; },
123
- onSync: function(callback) { this._eventHandlers.sync = callback; },
124
- onAction: function(callback) { this._eventHandlers.action = callback; },
125
- onRealtime: function(callback) { this._eventHandlers.realtime = callback; },
126
- onGameFinished: function(callback) { this._eventHandlers.finished = callback; },
127
- onGameRestarted: function(callback) { this._eventHandlers.restarted = callback; },
128
- onError: function(callback) { this._eventHandlers.error = callback; },
129
- onRematchRequest: function(callback) { this._eventHandlers.rematchRequest = callback; },
130
- onDisconnect: function(callback) { this._eventHandlers.disconnect = callback; },
131
- onReconnect: function(callback) { this._eventHandlers.reconnect = callback; },
132
- onConnectionError: function(callback) { this._eventHandlers.connectionError = callback; },
153
+ // Event handler registrations.
154
+ //
155
+ // Each onX(cb) keeps the long-standing "single handler, last one wins"
156
+ // behavior for back-compat, but now ALSO returns an unsubscribe
157
+ // function. For multiple listeners use game.on(event, cb), which
158
+ // supports any number of listeners, works before connect() in every
159
+ // transport, and returns an unsubscribe function.
160
+ onJoined: function(callback) { return this._setHandler('joined', callback); },
161
+ onPlayerJoined: function(callback) { return this._setHandler('playerJoined', callback); },
162
+ onPlayerLeft: function(callback) { return this._setHandler('playerLeft', callback); },
163
+ onStateUpdate: function(callback) { return this._setHandler('stateUpdate', callback); },
164
+ onSync: function(callback) { return this._setHandler('sync', callback); },
165
+ onAction: function(callback) { return this._setHandler('action', callback); },
166
+ onRealtime: function(callback) { return this._setHandler('realtime', callback); },
167
+ onGameFinished: function(callback) { return this._setHandler('finished', callback); },
168
+ onGameRestarted: function(callback) { return this._setHandler('restarted', callback); },
169
+ onError: function(callback) { return this._setHandler('error', callback); },
170
+ onRematchRequest: function(callback) { return this._setHandler('rematchRequest', callback); },
171
+ onDisconnect: function(callback) { return this._setHandler('disconnect', callback); },
172
+ onReconnect: function(callback) { return this._setHandler('reconnect', callback); },
173
+ onConnectionError: function(callback) { return this._setHandler('connectionError', callback); },
174
+ onPlayerConnection: function(callback) { return this._setHandler('playerConnection', callback); },
175
+
176
+ /** @private Set the single legacy handler; returns an unsubscribe fn. */
177
+ _setHandler: function(name, callback) {
178
+ const self = this;
179
+ self._eventHandlers[name] = callback;
180
+ return function() {
181
+ if (self._eventHandlers[name] === callback) {
182
+ self._eventHandlers[name] = null;
183
+ }
184
+ };
185
+ },
133
186
 
134
187
  /**
135
- * Register a generic event handler
188
+ * @private Deliver an event to the legacy single handler and every
189
+ * game.on() listener. All game event delivery flows through here.
190
+ */
191
+ _dispatch: function(name) {
192
+ const args = Array.prototype.slice.call(arguments, 1);
193
+ const single = this._eventHandlers[name];
194
+ if (single) {
195
+ try { single.apply(null, args); } catch (e) { Usion.log('game handler error (' + name + '): ' + e.message); }
196
+ }
197
+ const list = this._listeners[name];
198
+ if (list && list.length) {
199
+ const copy = list.slice();
200
+ for (let i = 0; i < copy.length; i++) {
201
+ try { copy[i].apply(null, args); } catch (e) { Usion.log('game listener error (' + name + '): ' + e.message); }
202
+ }
203
+ }
204
+ },
205
+
206
+ /**
207
+ * Register an additional event listener. Unlike the onX methods this
208
+ * supports multiple listeners per event, can be called before
209
+ * connect(), and works in every transport (standalone, embedded
210
+ * proxy, direct). Accepts internal names ('action'), wire names
211
+ * ('game:action'), or snake_case ('player_joined').
136
212
  * @param {string} event - Event name
137
213
  * @param {function} callback - Handler function
214
+ * @returns {function} Unsubscribe function
138
215
  */
139
216
  on: function(event, callback) {
140
- if (this.socket) {
141
- this.socket.on(event, callback);
142
- }
217
+ const self = this;
218
+ const name = _normalizeEventName(event);
219
+ if (!self._listeners[name]) self._listeners[name] = [];
220
+ self._listeners[name].push(callback);
221
+ return function() {
222
+ const list = self._listeners[name];
223
+ if (!list) return;
224
+ const i = list.indexOf(callback);
225
+ if (i >= 0) list.splice(i, 1);
226
+ };
143
227
  }
144
228
  };
145
229
 
230
+ game._listeners = {};
231
+
146
232
  // Apply sub-modules
147
233
  applyGameDirect(game, Usion);
148
234
  applyGameSocket(game, Usion);
@@ -43,9 +43,7 @@ export function applyGameDirect(game, Usion) {
43
43
  self._connecting = false;
44
44
  self.connected = false;
45
45
  self.directMode = false;
46
- if (self._eventHandlers.connectionError) {
47
- self._eventHandlers.connectionError(err);
48
- }
46
+ self._dispatch('connectionError', err);
49
47
  throw err;
50
48
  });
51
49
  return self._connectPromise;
@@ -154,9 +152,7 @@ export function applyGameDirect(game, Usion) {
154
152
  clearInterval(self._heartbeatInterval);
155
153
  self._heartbeatInterval = null;
156
154
  }
157
- if (self._eventHandlers.disconnect) {
158
- self._eventHandlers.disconnect(evt && evt.reason ? evt.reason : 'direct socket closed');
159
- }
155
+ self._dispatch('disconnect', evt && evt.reason ? evt.reason : 'direct socket closed');
160
156
  // Seamless resume: if the drop wasn't an intentional disconnect()
161
157
  // (which clears directMode), transparently re-establish + re-join +
162
158
  // resync from the last sequence.
@@ -190,7 +186,7 @@ export function applyGameDirect(game, Usion) {
190
186
  self.connected = true;
191
187
  self._reconnecting = false;
192
188
  self._reconnectAttempt = 0;
193
- if (self._eventHandlers.reconnect) self._eventHandlers.reconnect(attempt);
189
+ self._dispatch('reconnect', attempt);
194
190
  if (self.roomId) self.requestSync(self._lastSequence || 0); // resync / resume
195
191
  })
196
192
  .catch(function() {
@@ -228,20 +224,20 @@ export function applyGameDirect(game, Usion) {
228
224
 
229
225
  if (data.type === 'joined') {
230
226
  this._joined = true;
231
- if (this._eventHandlers.joined) this._eventHandlers.joined(payload);
227
+ this._dispatch('joined', payload);
232
228
  return;
233
229
  }
234
230
  if (data.type === 'player_joined') {
235
- if (this._eventHandlers.playerJoined) this._eventHandlers.playerJoined(payload);
231
+ this._dispatch('playerJoined', payload);
236
232
  return;
237
233
  }
238
234
  if (data.type === 'player_left') {
239
- if (this._eventHandlers.playerLeft) this._eventHandlers.playerLeft(payload);
235
+ this._dispatch('playerLeft', payload);
240
236
  return;
241
237
  }
242
238
  if (data.type === 'state_snapshot' || data.type === 'state_delta') {
243
- if (this._eventHandlers.realtime) this._eventHandlers.realtime(payload);
244
- if (this._eventHandlers.stateUpdate) this._eventHandlers.stateUpdate(payload);
239
+ this._dispatch('realtime', payload);
240
+ this._dispatch('stateUpdate', payload);
245
241
  return;
246
242
  }
247
243
  if (data.type === 'pong') {
@@ -250,15 +246,15 @@ export function applyGameDirect(game, Usion) {
250
246
  const waiter = this._pongWaiters.shift();
251
247
  if (waiter) waiter();
252
248
  }
253
- if (this._eventHandlers.sync) this._eventHandlers.sync(payload);
249
+ this._dispatch('sync', payload);
254
250
  return;
255
251
  }
256
252
  if (data.type === 'match_end') {
257
- if (this._eventHandlers.finished) this._eventHandlers.finished(payload);
253
+ this._dispatch('finished', payload);
258
254
  return;
259
255
  }
260
- if (data.type === 'error' && this._eventHandlers.error) {
261
- this._eventHandlers.error(payload);
256
+ if (data.type === 'error') {
257
+ this._dispatch('error', payload);
262
258
  }
263
259
  };
264
260
  }
@@ -2,6 +2,8 @@
2
2
  * Usion SDK Game Methods — join, leave, action, realtime, sync, etc.
3
3
  */
4
4
 
5
+ import { UsionError, toUsionError, ERROR_CODES } from './errors.js';
6
+
5
7
  /**
6
8
  * Add game action methods to game module
7
9
  * @param {object} game - The game module object
@@ -43,7 +45,7 @@ export function applyGameMethods(game, Usion) {
43
45
  setTimeout(function() {
44
46
  if (!self._joined && self._proxyJoinReject) {
45
47
  self._proxyJoinReject = null;
46
- reject(new Error('Join timeout'));
48
+ reject(new UsionError(ERROR_CODES.JOIN_TIMEOUT, 'Join timeout'));
47
49
  }
48
50
  }, 15000);
49
51
  });
@@ -52,19 +54,19 @@ export function applyGameMethods(game, Usion) {
52
54
 
53
55
  self._joinPromise = new Promise(function(resolve, reject) {
54
56
  if (!self.socket || !self.connected) {
55
- reject(new Error('Not connected'));
57
+ reject(new UsionError(ERROR_CODES.NOT_CONNECTED, 'Not connected'));
56
58
  return;
57
59
  }
58
60
 
59
61
  if (!roomId) {
60
- reject(new Error('No room ID provided'));
62
+ reject(new UsionError(ERROR_CODES.NO_ROOM, 'No room ID provided'));
61
63
  return;
62
64
  }
63
65
 
64
66
  self.socket.emit('game:join', { room_id: roomId }, function(response) {
65
67
  if (response.error) {
66
68
  self._joined = false;
67
- reject(new Error(response.message || response.error));
69
+ reject(toUsionError(response.message || response.error));
68
70
  } else {
69
71
  self._joined = true;
70
72
  if (response.sequence !== undefined) {
@@ -112,13 +114,25 @@ export function applyGameMethods(game, Usion) {
112
114
  };
113
115
 
114
116
  /**
115
- * Send a game action
117
+ * Send a game action.
118
+ *
119
+ * RELIABILITY CONTRACT: the platform echoes every action back to the
120
+ * sender (with the authoritative sequence number). Apply game state ONLY
121
+ * in onAction — never optimistically on send — so every client applies
122
+ * the same actions in the same order. The SDK deduplicates by sequence,
123
+ * so an action is delivered exactly once even across reconnect replays.
124
+ *
116
125
  * @param {string} actionType - Type of action (e.g., 'move')
117
126
  * @param {object} actionData - Action data
127
+ * @param {object} [opts] - Options. opts.nextTurn: player ID whose turn
128
+ * is next — the server remembers it and hands it to any (re)joining
129
+ * client as current_turn, so turn state survives reconnects.
118
130
  * @returns {Promise} Resolves when action is processed
119
131
  */
120
- game.action = function(actionType, actionData) {
132
+ game.action = function(actionType, actionData, opts) {
121
133
  const self = this;
134
+ const nextTurn = opts && opts.nextTurn;
135
+ const queueOffline = !!(opts && opts.queueOffline);
122
136
 
123
137
  if (self.directMode) {
124
138
  self._sendDirect('action', {
@@ -128,29 +142,153 @@ export function applyGameMethods(game, Usion) {
128
142
  return Promise.resolve({ success: true });
129
143
  }
130
144
 
145
+ // Opt-in offline queue: instead of failing while disconnected, hold
146
+ // the move and send it (in order) once the connection recovers.
147
+ // Turn-based games get "your move is saved and sends when you're
148
+ // back" for free; realtime games should NOT use this (stale inputs).
149
+ if (queueOffline && !self.connected) {
150
+ return self._queueOfflineAction(actionType, actionData, opts);
151
+ }
152
+
131
153
  if (self._useProxy) {
132
- Usion._post({ type: 'GAME_ACTION', room_id: self.roomId, action_type: actionType, action_data: actionData });
154
+ const proxyMsg = { type: 'GAME_ACTION', room_id: self.roomId, action_type: actionType, action_data: actionData };
155
+ if (nextTurn) proxyMsg.next_turn = nextTurn;
156
+ Usion._post(proxyMsg);
133
157
  return Promise.resolve({ success: true });
134
158
  }
135
159
 
160
+ // Gate sends while a post-reconnect rejoin is in flight, so a stale
161
+ // move can't go out before the client has resynced.
162
+ const gate = self._rejoinPromise || Promise.resolve();
163
+ return gate.then(function() {
164
+ return new Promise(function(resolve, reject) {
165
+ if (!self.socket || !self.connected) {
166
+ if (queueOffline) {
167
+ self._queueOfflineAction(actionType, actionData, opts).then(resolve, reject);
168
+ return;
169
+ }
170
+ reject(new UsionError(ERROR_CODES.NOT_CONNECTED, 'Not connected'));
171
+ return;
172
+ }
173
+
174
+ const payload = {
175
+ room_id: self.roomId,
176
+ action_type: actionType,
177
+ action_data: actionData
178
+ };
179
+ if (nextTurn) payload.next_turn = nextTurn;
180
+ self.socket.emit('game:action', payload, function(response) {
181
+ if (response.error) {
182
+ reject(toUsionError(response.message || response.error));
183
+ } else {
184
+ if (response.sequence !== undefined) {
185
+ self._lastSequence = response.sequence;
186
+ }
187
+ resolve(response);
188
+ }
189
+ });
190
+ });
191
+ });
192
+ };
193
+
194
+ // ── Offline action queue (opt-in via action(..., { queueOffline: true })) ──
195
+
196
+ const OFFLINE_QUEUE_MAX = 20;
197
+
198
+ /** @private Hold an action until the connection recovers. */
199
+ game._queueOfflineAction = function(actionType, actionData, opts) {
200
+ const self = this;
201
+ if (!self._offlineQueue) self._offlineQueue = [];
202
+ if (self._offlineQueue.length >= OFFLINE_QUEUE_MAX) {
203
+ return Promise.reject(new UsionError(ERROR_CODES.QUEUE_FULL,
204
+ 'Offline action queue is full (' + OFFLINE_QUEUE_MAX + ')'));
205
+ }
206
+ Usion.log('Queued action while offline: ' + actionType);
207
+ return new Promise(function(resolve, reject) {
208
+ self._offlineQueue.push({
209
+ actionType: actionType,
210
+ actionData: actionData,
211
+ opts: Object.assign({}, opts, { queueOffline: false }),
212
+ resolve: resolve,
213
+ reject: reject,
214
+ });
215
+ self._ensureOfflineFlushHook();
216
+ });
217
+ };
218
+
219
+ /** @private Flush queued actions, in order, once reconnected. */
220
+ game._ensureOfflineFlushHook = function() {
221
+ const self = this;
222
+ if (self._offlineFlushHooked) return;
223
+ self._offlineFlushHooked = true;
224
+ self.on('reconnect', function() { self._flushOfflineQueue(); });
225
+ };
226
+
227
+ /** @private */
228
+ game._flushOfflineQueue = function() {
229
+ const self = this;
230
+ const queue = self._offlineQueue;
231
+ if (!queue || !queue.length) return;
232
+ self._offlineQueue = [];
233
+ Usion.log('Flushing ' + queue.length + ' queued action(s) after reconnect');
234
+ // Send strictly in order: each action waits for the previous ack so
235
+ // the server assigns sequences in the order the player acted.
236
+ let chain = Promise.resolve();
237
+ queue.forEach(function(item) {
238
+ chain = chain.then(function() {
239
+ return self.action(item.actionType, item.actionData, item.opts)
240
+ .then(item.resolve, item.reject);
241
+ });
242
+ });
243
+ };
244
+
245
+ /**
246
+ * Checkpoint the authoritative game state on the server. Any client that
247
+ * joins or rejoins the room receives the latest checkpoint as game_state
248
+ * in the join ack and in game:sync — recovery becomes "load checkpoint,
249
+ * replay the tail" instead of replaying every action from zero.
250
+ *
251
+ * Only the room authority (player_ids[0] / host) may call this. The
252
+ * serialized state is capped at 64 KB.
253
+ *
254
+ * @param {*} state - JSON-serializable authoritative game state
255
+ * @returns {Promise<{success: boolean, error?: string}>}
256
+ */
257
+ game.setState = function(state) {
258
+ const self = this;
259
+
260
+ if (self.directMode) {
261
+ // Direct-mode game servers own their state; no platform checkpoint.
262
+ return Promise.resolve({ success: false, error: 'not_supported_in_direct_mode', code: ERROR_CODES.UNSUPPORTED });
263
+ }
264
+
265
+ if (self._useProxy) {
266
+ return Usion._request('GAME_SET_STATE', { room_id: self.roomId, state: state || {} })
267
+ .then(function(res) {
268
+ if (res && res.error) {
269
+ return { success: false, error: res.error, code: toUsionError(res.error).code };
270
+ }
271
+ return res || { success: true };
272
+ })
273
+ .catch(function(err) {
274
+ const ue = toUsionError(err, ERROR_CODES.REQUEST_TIMEOUT);
275
+ return { success: false, error: ue.message, code: ue.code };
276
+ });
277
+ }
278
+
136
279
  return new Promise(function(resolve, reject) {
137
280
  if (!self.socket || !self.connected) {
138
- reject(new Error('Not connected'));
281
+ reject(new UsionError(ERROR_CODES.NOT_CONNECTED, 'Not connected'));
139
282
  return;
140
283
  }
141
-
142
- self.socket.emit('game:action', {
284
+ self.socket.emit('game:set_state', {
143
285
  room_id: self.roomId,
144
- action_type: actionType,
145
- action_data: actionData
286
+ state: state || {}
146
287
  }, function(response) {
147
- if (response.error) {
148
- reject(new Error(response.message || response.error));
288
+ if (response && response.error) {
289
+ resolve({ success: false, error: response.error, code: toUsionError(response.error).code });
149
290
  } else {
150
- if (response.sequence !== undefined) {
151
- self._lastSequence = response.sequence;
152
- }
153
- resolve(response);
291
+ resolve(response || { success: true });
154
292
  }
155
293
  });
156
294
  });
@@ -254,13 +392,13 @@ export function applyGameMethods(game, Usion) {
254
392
 
255
393
  return new Promise(function(resolve, reject) {
256
394
  if (!self.socket || !self.connected) {
257
- reject(new Error('Not connected'));
395
+ reject(new UsionError(ERROR_CODES.NOT_CONNECTED, 'Not connected'));
258
396
  return;
259
397
  }
260
398
 
261
399
  self.socket.emit('game:forfeit', { room_id: self.roomId }, function(response) {
262
400
  if (response.error) {
263
- reject(new Error(response.message || response.error));
401
+ reject(toUsionError(response.message || response.error));
264
402
  } else {
265
403
  resolve(response);
266
404
  }
@@ -422,7 +560,9 @@ export function applyGameMethods(game, Usion) {
422
560
  var inFrame = window.parent && window.parent !== window;
423
561
  var inRNWebView = !!window.ReactNativeWebView;
424
562
  if (inFrame || inRNWebView) {
425
- Usion._post({ type: 'GAME_DEBUG', payload: payload || {} });
563
+ var body = Object.assign({}, payload || {});
564
+ body._diag = Usion.diagnostics ? Usion.diagnostics() : undefined;
565
+ Usion._post({ type: 'GAME_DEBUG', payload: body });
426
566
  }
427
567
  } catch (e) { /* non-fatal */ }
428
568
  };