@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.
- package/README.md +36 -0
- package/package.json +11 -4
- package/src/browser.js +472 -122
- package/src/modules/core.js +1 -1
- package/src/modules/errors.js +75 -0
- package/src/modules/game-core.js +105 -19
- package/src/modules/game-direct.js +12 -16
- package/src/modules/game-methods.js +161 -21
- package/src/modules/game-proxy.js +46 -17
- package/src/modules/game-socket.js +43 -47
- package/src/modules/index.js +4 -0
- package/src/modules/misc.js +20 -0
- package/src/modules/wallet.js +7 -1
- package/types/index.d.ts +137 -20
|
@@ -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
|
|
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)
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
124
|
+
self._dispatch('playerJoined', data);
|
|
106
125
|
break;
|
|
107
126
|
|
|
108
127
|
case 'GAME_PLAYER_LEFT':
|
|
109
|
-
|
|
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
|
-
|
|
133
|
+
self._dispatch('stateUpdate', data);
|
|
115
134
|
break;
|
|
116
135
|
|
|
117
136
|
case 'GAME_ACTION_DATA':
|
|
118
|
-
if (data.sequence !== undefined)
|
|
119
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
158
|
+
self._dispatch('error', data);
|
|
134
159
|
break;
|
|
135
160
|
|
|
136
161
|
case 'GAME_RESTARTED':
|
|
137
162
|
self._lastSequence = 0;
|
|
138
|
-
|
|
163
|
+
self._lastActionApplied = 0;
|
|
164
|
+
self._dispatch('restarted', data);
|
|
139
165
|
break;
|
|
140
166
|
|
|
141
167
|
case 'GAME_REMATCH_REQUEST':
|
|
142
|
-
|
|
168
|
+
self._dispatch('rematchRequest', data);
|
|
143
169
|
break;
|
|
144
170
|
|
|
145
171
|
case 'GAME_SYNC':
|
|
146
|
-
if (data.sequence !== undefined)
|
|
147
|
-
|
|
148
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
143
|
-
self._eventHandlers.sync(data);
|
|
144
|
-
}
|
|
144
|
+
self._dispatch('sync', data);
|
|
145
145
|
// Also trigger stateUpdate for backwards compat
|
|
146
|
-
|
|
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
|
-
|
|
156
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
191
|
-
|
|
192
|
-
}
|
|
187
|
+
self._lastActionApplied = 0;
|
|
188
|
+
self._dispatch('restarted', data);
|
|
193
189
|
});
|
|
194
190
|
|
|
195
191
|
} catch (err) {
|
package/src/modules/index.js
CHANGED
|
@@ -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);
|
package/src/modules/misc.js
CHANGED
|
@@ -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
|
package/src/modules/wallet.js
CHANGED
|
@@ -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
|
-
|
|
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):
|
|
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
|
|
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):
|
|
230
|
-
onPlayerJoined(callback: (data: PlayerJoinedData) => void):
|
|
231
|
-
onPlayerLeft(callback: (data: PlayerLeftData) => void):
|
|
232
|
-
onStateUpdate(callback: (data: StateUpdateData) => void):
|
|
233
|
-
onSync(callback: (data: SyncData) => void):
|
|
234
|
-
onAction(callback: (data: GameActionData) => void):
|
|
235
|
-
onRealtime(callback: (data: Record<string, any>) => void):
|
|
236
|
-
onGameFinished(callback: (data: GameFinishedData) => void):
|
|
237
|
-
onGameRestarted(callback: (data: Record<string, any>) => void):
|
|
238
|
-
onError(callback: (data: { message: string; code?: string }) => void):
|
|
239
|
-
onRematchRequest(callback: (data: RematchData) => void):
|
|
240
|
-
onDisconnect(callback: (reason: string) => void):
|
|
241
|
-
onReconnect(callback: (attemptNumber: number) => void):
|
|
242
|
-
onConnectionError(callback: (error: Error) => void):
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
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):
|
|
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 };
|