@usions/sdk 2.21.0 → 2.22.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/package.json +1 -1
- package/src/browser.js +401 -156
- package/src/modules/backend-channel.js +41 -8
- package/src/modules/core.js +59 -8
- package/src/modules/errors.js +47 -4
- package/src/modules/game-core.js +22 -17
- package/src/modules/game-methods.js +90 -29
- package/src/modules/matchmaking.js +53 -11
- package/src/modules/misc.js +7 -2
- package/src/modules/wallet.js +12 -5
- package/types/index.d.ts +39 -6
package/src/browser.js
CHANGED
|
@@ -1,10 +1,130 @@
|
|
|
1
1
|
var Usion = (function () {
|
|
2
2
|
'use strict';
|
|
3
3
|
|
|
4
|
+
/**
|
|
5
|
+
* Usion SDK Errors — stable, machine-readable error codes.
|
|
6
|
+
*
|
|
7
|
+
* Developers should branch on `err.code`, never on message text. Messages
|
|
8
|
+
* are human-readable and may change; codes are part of the public API and
|
|
9
|
+
* follow the deprecation policy (never removed within a major version).
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
/** @type {Record<string, string>} */
|
|
13
|
+
const ERROR_CODES = {
|
|
14
|
+
NOT_CONNECTED: 'NOT_CONNECTED', // No live connection for this call
|
|
15
|
+
NO_ROOM: 'NO_ROOM', // No room id provided/known
|
|
16
|
+
ROOM_NOT_FOUND: 'ROOM_NOT_FOUND', // Room does not exist server-side
|
|
17
|
+
NOT_PARTICIPANT: 'NOT_PARTICIPANT', // Caller is not a player in the room
|
|
18
|
+
NOT_IN_ROOM: 'NOT_IN_ROOM', // Socket not in the room — rejoin required
|
|
19
|
+
NOT_AUTHORITY: 'NOT_AUTHORITY', // Authority-only call (e.g. lobby start)
|
|
20
|
+
NOT_AUTHENTICATED: 'NOT_AUTHENTICATED', // Missing/invalid auth server-side
|
|
21
|
+
JOIN_TIMEOUT: 'JOIN_TIMEOUT', // Join did not complete in time
|
|
22
|
+
CONNECT_TIMEOUT: 'CONNECT_TIMEOUT', // Connect did not complete in time
|
|
23
|
+
INIT_TIMEOUT: 'INIT_TIMEOUT', // init() got no INIT from the host in time
|
|
24
|
+
MATCH_TIMEOUT: 'MATCH_TIMEOUT', // matchmaking.find() opts.timeout elapsed
|
|
25
|
+
STATE_TOO_LARGE: 'STATE_TOO_LARGE', // setState payload over the quota
|
|
26
|
+
INVALID_STATE: 'INVALID_STATE', // setState payload not a JSON object
|
|
27
|
+
STALE_STATE: 'STALE_STATE', // setState older than the stored checkpoint
|
|
28
|
+
INVALID_NEXT_TURN: 'INVALID_NEXT_TURN', // nextTurn is not a player in the room
|
|
29
|
+
INVALID_INPUT: 'INVALID_INPUT', // Missing/malformed argument server-side
|
|
30
|
+
NOT_FOUND: 'NOT_FOUND', // Referenced entity (service, lobby, …) missing
|
|
31
|
+
QUOTA_EXCEEDED: 'QUOTA_EXCEEDED', // Bucket key/size quota reached (cloud KV)
|
|
32
|
+
VALUE_TOO_LARGE: 'VALUE_TOO_LARGE', // Single value over the per-value quota
|
|
33
|
+
LOBBY_FULL: 'LOBBY_FULL', // Lobby at max_players
|
|
34
|
+
LOBBY_CLOSED: 'LOBBY_CLOSED', // Lobby exists but is not open
|
|
35
|
+
CONFLICT: 'CONFLICT', // Concurrent-write conflict; retry
|
|
36
|
+
RATE_LIMITED: 'RATE_LIMITED', // Too many calls; back off (see retryAfter)
|
|
37
|
+
REQUEST_TIMEOUT: 'REQUEST_TIMEOUT', // Host/parent did not reply in time
|
|
38
|
+
QUEUE_FULL: 'QUEUE_FULL', // Offline action queue at capacity
|
|
39
|
+
CANCELLED: 'CANCELLED', // Cancelled by a local cancel() call
|
|
40
|
+
SUPERSEDED: 'SUPERSEDED', // Replaced by a newer identical request
|
|
41
|
+
UNSUPPORTED: 'UNSUPPORTED', // Not available in this transport
|
|
42
|
+
UNKNOWN: 'UNKNOWN', // Unmapped error (see message)
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
class UsionError extends Error {
|
|
46
|
+
/**
|
|
47
|
+
* @param {string} code - One of ERROR_CODES
|
|
48
|
+
* @param {string} [message] - Human-readable detail (may change between versions)
|
|
49
|
+
*/
|
|
50
|
+
constructor(code, message) {
|
|
51
|
+
super(message || code);
|
|
52
|
+
this.name = 'UsionError';
|
|
53
|
+
this.code = ERROR_CODES[code] ? code : ERROR_CODES.UNKNOWN;
|
|
54
|
+
/** @type {number|undefined} Seconds until a RATE_LIMITED call may retry. */
|
|
55
|
+
this.retryAfter = undefined;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Backend error strings → stable codes. Order matters: first match wins.
|
|
60
|
+
// Fallback path only: the backend now sends a structured `code` on every
|
|
61
|
+
// error ack, which toUsionError prefers. These patterns cover old backends
|
|
62
|
+
// and hosts that strip the code.
|
|
63
|
+
/** @type {Array<[RegExp, string]>} */
|
|
64
|
+
const BACKEND_PATTERNS = [
|
|
65
|
+
[/not authenticated/i, ERROR_CODES.NOT_AUTHENTICATED],
|
|
66
|
+
[/room_id required|no room id/i, ERROR_CODES.NO_ROOM],
|
|
67
|
+
[/room not found/i, ERROR_CODES.ROOM_NOT_FOUND],
|
|
68
|
+
[/not in room/i, ERROR_CODES.NOT_IN_ROOM],
|
|
69
|
+
[/not a participant/i, ERROR_CODES.NOT_PARTICIPANT],
|
|
70
|
+
[/room authority/i, ERROR_CODES.NOT_AUTHORITY],
|
|
71
|
+
[/exceeds .*limit/i, ERROR_CODES.STATE_TOO_LARGE],
|
|
72
|
+
[/stale checkpoint|stale state/i, ERROR_CODES.STALE_STATE],
|
|
73
|
+
[/state must be/i, ERROR_CODES.INVALID_STATE],
|
|
74
|
+
[/next_turn must be/i, ERROR_CODES.INVALID_NEXT_TURN],
|
|
75
|
+
[/rate limit|too many/i, ERROR_CODES.RATE_LIMITED],
|
|
76
|
+
[/join timeout/i, ERROR_CODES.JOIN_TIMEOUT],
|
|
77
|
+
[/connection timeout|connect timeout/i, ERROR_CODES.CONNECT_TIMEOUT],
|
|
78
|
+
[/request timeout/i, ERROR_CODES.REQUEST_TIMEOUT],
|
|
79
|
+
[/not connected/i, ERROR_CODES.NOT_CONNECTED],
|
|
80
|
+
[/quota exceeded/i, ERROR_CODES.QUOTA_EXCEEDED],
|
|
81
|
+
[/value too large/i, ERROR_CODES.VALUE_TOO_LARGE],
|
|
82
|
+
[/lobby is full/i, ERROR_CODES.LOBBY_FULL],
|
|
83
|
+
[/lobby is not open/i, ERROR_CODES.LOBBY_CLOSED],
|
|
84
|
+
[/not found|unknown service/i, ERROR_CODES.NOT_FOUND],
|
|
85
|
+
[/conflict/i, ERROR_CODES.CONFLICT],
|
|
86
|
+
[/cancelled/i, ERROR_CODES.CANCELLED],
|
|
87
|
+
[/superseded/i, ERROR_CODES.SUPERSEDED],
|
|
88
|
+
];
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Normalize anything — a structured backend ack ({ error, code?, retry_after? }),
|
|
92
|
+
* an Error, or a raw string — into a UsionError with the best stable code.
|
|
93
|
+
* A machine-readable `code` on the input always wins over message matching.
|
|
94
|
+
* @param {*} err
|
|
95
|
+
* @param {string} [fallbackCode] - Code to use when nothing matches
|
|
96
|
+
* @returns {UsionError}
|
|
97
|
+
*/
|
|
98
|
+
function toUsionError(err, fallbackCode) {
|
|
99
|
+
if (err instanceof UsionError) return err;
|
|
100
|
+
|
|
101
|
+
// Structured input (backend ack object or an Error carrying a code).
|
|
102
|
+
if (err && typeof err === 'object') {
|
|
103
|
+
const code = typeof err.code === 'string' && ERROR_CODES[err.code] ? err.code : null;
|
|
104
|
+
const message = err.message || err.error;
|
|
105
|
+
if (code) {
|
|
106
|
+
const ue = new UsionError(code, typeof message === 'string' ? message : code);
|
|
107
|
+
const retryAfter = err.retry_after != null ? err.retry_after : err.retryAfter;
|
|
108
|
+
if (typeof retryAfter === 'number' && isFinite(retryAfter)) ue.retryAfter = retryAfter;
|
|
109
|
+
return ue;
|
|
110
|
+
}
|
|
111
|
+
if (!(err instanceof Error) && typeof message === 'string') err = message;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const message = err && err.message ? err.message : String(err || 'Unknown error');
|
|
115
|
+
for (let i = 0; i < BACKEND_PATTERNS.length; i++) {
|
|
116
|
+
if (BACKEND_PATTERNS[i][0].test(message)) {
|
|
117
|
+
return new UsionError(BACKEND_PATTERNS[i][1], message);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
return new UsionError(fallbackCode || ERROR_CODES.UNKNOWN, message);
|
|
121
|
+
}
|
|
122
|
+
|
|
4
123
|
/**
|
|
5
124
|
* Usion SDK Core — init, _post, _request, message handling
|
|
6
125
|
*/
|
|
7
126
|
|
|
127
|
+
|
|
8
128
|
// Request ID counter for tracking async responses
|
|
9
129
|
let _requestId = 0;
|
|
10
130
|
const _pendingRequests = {};
|
|
@@ -69,7 +189,7 @@ var Usion = (function () {
|
|
|
69
189
|
* Core Usion object with init, _post, _request
|
|
70
190
|
*/
|
|
71
191
|
const core = {
|
|
72
|
-
version: '2.
|
|
192
|
+
version: '2.22.0', // injected from package.json at build
|
|
73
193
|
config: {},
|
|
74
194
|
_initialized: false,
|
|
75
195
|
_initCallback: null,
|
|
@@ -78,24 +198,60 @@ var Usion = (function () {
|
|
|
78
198
|
_backButtonCallback: null,
|
|
79
199
|
|
|
80
200
|
/**
|
|
81
|
-
* Initialize the SDK with config from parent app
|
|
82
|
-
*
|
|
201
|
+
* Initialize the SDK with config from parent app.
|
|
202
|
+
*
|
|
203
|
+
* Callback form (unchanged): `Usion.init(cb)` — cb(config) fires when the
|
|
204
|
+
* host's INIT arrives. Promise form: every call ALSO returns a
|
|
205
|
+
* Promise<config>, so `const config = await Usion.init()` works.
|
|
206
|
+
*
|
|
207
|
+
* `opts.timeout` (ms, optional) rejects the promise with
|
|
208
|
+
* UsionError(INIT_TIMEOUT) if no INIT arrives in time — the "embedded but
|
|
209
|
+
* host silent" case that otherwise hangs forever. The callback still fires
|
|
210
|
+
* if INIT arrives late. Both `init(cb, opts)` and `init(opts)` are accepted.
|
|
211
|
+
*
|
|
212
|
+
* @param {function|{timeout?: number}} [callback] - Called with config when ready
|
|
213
|
+
* @param {{timeout?: number}} [opts]
|
|
214
|
+
* @returns {Promise<object>} Resolves with config
|
|
83
215
|
*/
|
|
84
|
-
init: function(callback) {
|
|
216
|
+
init: function(callback, opts) {
|
|
85
217
|
const self = this;
|
|
218
|
+
if (callback && typeof callback === 'object') {
|
|
219
|
+
opts = callback;
|
|
220
|
+
callback = null;
|
|
221
|
+
}
|
|
222
|
+
opts = opts || {};
|
|
86
223
|
|
|
87
224
|
// Prevent double initialization - just update callback
|
|
88
225
|
if (self._initialized) {
|
|
89
226
|
if (callback) callback(self.config);
|
|
90
|
-
return;
|
|
227
|
+
return Promise.resolve(self.config);
|
|
91
228
|
}
|
|
92
229
|
|
|
93
230
|
// Store callback for when config arrives
|
|
94
|
-
self._initCallback = callback;
|
|
231
|
+
if (callback) self._initCallback = callback;
|
|
232
|
+
|
|
233
|
+
// One shared promise for every init() call before INIT arrives.
|
|
234
|
+
if (!self._initPromise) {
|
|
235
|
+
self._initPromise = new Promise(function(resolve, reject) {
|
|
236
|
+
self._initResolve = resolve;
|
|
237
|
+
self._initReject = reject;
|
|
238
|
+
});
|
|
239
|
+
// Callback-style users never touch the promise — don't let an
|
|
240
|
+
// INIT_TIMEOUT rejection surface as an unhandled-rejection error.
|
|
241
|
+
self._initPromise.catch(function() {});
|
|
242
|
+
}
|
|
243
|
+
if (opts.timeout > 0 && !self._initTimer) {
|
|
244
|
+
self._initTimer = setTimeout(function() {
|
|
245
|
+
self._initTimer = null;
|
|
246
|
+
if (self._initialized || !self._initReject) return;
|
|
247
|
+
self._initReject(new UsionError(ERROR_CODES.INIT_TIMEOUT,
|
|
248
|
+
'No INIT from host within ' + opts.timeout + 'ms — host silent or running standalone'));
|
|
249
|
+
}, opts.timeout);
|
|
250
|
+
}
|
|
95
251
|
|
|
96
252
|
// Only register message handler once
|
|
97
253
|
if (self._messageHandlerRegistered) {
|
|
98
|
-
return;
|
|
254
|
+
return self._initPromise;
|
|
99
255
|
}
|
|
100
256
|
self._messageHandlerRegistered = true;
|
|
101
257
|
|
|
@@ -151,6 +307,15 @@ var Usion = (function () {
|
|
|
151
307
|
if (self._initCallback) {
|
|
152
308
|
self._initCallback(data.config);
|
|
153
309
|
}
|
|
310
|
+
|
|
311
|
+
// Settle the promise form
|
|
312
|
+
if (self._initTimer) {
|
|
313
|
+
clearTimeout(self._initTimer);
|
|
314
|
+
self._initTimer = null;
|
|
315
|
+
}
|
|
316
|
+
if (self._initResolve) {
|
|
317
|
+
self._initResolve(data.config);
|
|
318
|
+
}
|
|
154
319
|
}
|
|
155
320
|
|
|
156
321
|
// Handle response messages for async requests
|
|
@@ -159,7 +324,9 @@ var Usion = (function () {
|
|
|
159
324
|
delete _pendingRequests[data._requestId];
|
|
160
325
|
|
|
161
326
|
if (data.error) {
|
|
162
|
-
|
|
327
|
+
// Preserve the backend's stable code/retry_after when the host
|
|
328
|
+
// relayed them (see errors.js).
|
|
329
|
+
reject(toUsionError(data));
|
|
163
330
|
} else {
|
|
164
331
|
resolve(data);
|
|
165
332
|
}
|
|
@@ -218,6 +385,8 @@ var Usion = (function () {
|
|
|
218
385
|
|
|
219
386
|
// Signal ready to parent
|
|
220
387
|
this._post({ type: 'READY' });
|
|
388
|
+
|
|
389
|
+
return self._initPromise;
|
|
221
390
|
},
|
|
222
391
|
|
|
223
392
|
/**
|
|
@@ -339,7 +508,7 @@ var Usion = (function () {
|
|
|
339
508
|
// Setup timeout
|
|
340
509
|
const timer = setTimeout(function() {
|
|
341
510
|
delete _pendingRequests[requestId];
|
|
342
|
-
reject(new
|
|
511
|
+
reject(new UsionError(ERROR_CODES.REQUEST_TIMEOUT, 'Request timeout'));
|
|
343
512
|
}, timeout);
|
|
344
513
|
|
|
345
514
|
// Store pending request
|
|
@@ -513,13 +682,17 @@ var Usion = (function () {
|
|
|
513
682
|
|
|
514
683
|
/**
|
|
515
684
|
* Get current wallet balance
|
|
685
|
+
* @param {object} [opts]
|
|
686
|
+
* @param {boolean} [opts.fresh] - Bypass the cache and re-query the host.
|
|
687
|
+
* Use after out-of-band balance changes (e.g. a server-side settle).
|
|
516
688
|
* @returns {Promise<number>} Balance in credits
|
|
517
689
|
*/
|
|
518
|
-
getBalance: function() {
|
|
690
|
+
getBalance: function(opts) {
|
|
519
691
|
const self = this;
|
|
692
|
+
const fresh = !!(opts && opts.fresh);
|
|
520
693
|
|
|
521
694
|
// If we have cached balance, return it
|
|
522
|
-
if (self._balance !== null) {
|
|
695
|
+
if (!fresh && self._balance !== null) {
|
|
523
696
|
return Promise.resolve(self._balance);
|
|
524
697
|
}
|
|
525
698
|
|
|
@@ -586,11 +759,14 @@ var Usion = (function () {
|
|
|
586
759
|
settled = true;
|
|
587
760
|
cleanup();
|
|
588
761
|
// Update cached balance from the authoritative new balance, else
|
|
589
|
-
// best-effort subtract
|
|
762
|
+
// best-effort subtract now and re-query the host so the cache
|
|
763
|
+
// converges on the real value (an estimate would otherwise stick
|
|
764
|
+
// until reload).
|
|
590
765
|
if (response && response.newBalance !== undefined) {
|
|
591
766
|
self._balance = response.newBalance;
|
|
592
|
-
} else
|
|
593
|
-
self._balance -= amount;
|
|
767
|
+
} else {
|
|
768
|
+
if (self._balance !== null) self._balance -= amount;
|
|
769
|
+
self.getBalance({ fresh: true }).catch(function() { /* keep estimate */ });
|
|
594
770
|
}
|
|
595
771
|
resolve(response);
|
|
596
772
|
}
|
|
@@ -1191,9 +1367,10 @@ var Usion = (function () {
|
|
|
1191
1367
|
* Listen for messages from parent app
|
|
1192
1368
|
* @param {string} type - Message type to listen for
|
|
1193
1369
|
* @param {function} callback - Handler function
|
|
1370
|
+
* @returns {function} Unsubscribe function
|
|
1194
1371
|
*/
|
|
1195
1372
|
on: function(type, callback) {
|
|
1196
|
-
|
|
1373
|
+
function listener(event) {
|
|
1197
1374
|
if (!isTrustedMessageSource(event)) return;
|
|
1198
1375
|
|
|
1199
1376
|
let data;
|
|
@@ -1206,7 +1383,11 @@ var Usion = (function () {
|
|
|
1206
1383
|
if (data.type === type) {
|
|
1207
1384
|
callback(data);
|
|
1208
1385
|
}
|
|
1209
|
-
}
|
|
1386
|
+
}
|
|
1387
|
+
window.addEventListener('message', listener);
|
|
1388
|
+
return function() {
|
|
1389
|
+
window.removeEventListener('message', listener);
|
|
1390
|
+
};
|
|
1210
1391
|
}
|
|
1211
1392
|
};
|
|
1212
1393
|
|
|
@@ -1731,82 +1912,6 @@ var Usion = (function () {
|
|
|
1731
1912
|
};
|
|
1732
1913
|
}
|
|
1733
1914
|
|
|
1734
|
-
/**
|
|
1735
|
-
* Usion SDK Errors — stable, machine-readable error codes.
|
|
1736
|
-
*
|
|
1737
|
-
* Developers should branch on `err.code`, never on message text. Messages
|
|
1738
|
-
* are human-readable and may change; codes are part of the public API and
|
|
1739
|
-
* follow the deprecation policy (never removed within a major version).
|
|
1740
|
-
*/
|
|
1741
|
-
|
|
1742
|
-
/** @type {Record<string, string>} */
|
|
1743
|
-
const ERROR_CODES = {
|
|
1744
|
-
NOT_CONNECTED: 'NOT_CONNECTED', // No live connection for this call
|
|
1745
|
-
NO_ROOM: 'NO_ROOM', // No room id provided/known
|
|
1746
|
-
ROOM_NOT_FOUND: 'ROOM_NOT_FOUND', // Room does not exist server-side
|
|
1747
|
-
NOT_PARTICIPANT: 'NOT_PARTICIPANT', // Caller is not a player in the room
|
|
1748
|
-
NOT_AUTHORITY: 'NOT_AUTHORITY', // Authority-only call (e.g. setState)
|
|
1749
|
-
NOT_AUTHENTICATED: 'NOT_AUTHENTICATED', // Missing/invalid auth server-side
|
|
1750
|
-
JOIN_TIMEOUT: 'JOIN_TIMEOUT', // Join did not complete in time
|
|
1751
|
-
CONNECT_TIMEOUT: 'CONNECT_TIMEOUT', // Connect did not complete in time
|
|
1752
|
-
STATE_TOO_LARGE: 'STATE_TOO_LARGE', // setState payload over the quota
|
|
1753
|
-
INVALID_STATE: 'INVALID_STATE', // setState payload not a JSON object
|
|
1754
|
-
INVALID_NEXT_TURN: 'INVALID_NEXT_TURN', // nextTurn is not a player in the room
|
|
1755
|
-
RATE_LIMITED: 'RATE_LIMITED', // Too many calls; back off
|
|
1756
|
-
REQUEST_TIMEOUT: 'REQUEST_TIMEOUT', // Host/parent did not reply in time
|
|
1757
|
-
QUEUE_FULL: 'QUEUE_FULL', // Offline action queue at capacity
|
|
1758
|
-
UNSUPPORTED: 'UNSUPPORTED', // Not available in this transport
|
|
1759
|
-
UNKNOWN: 'UNKNOWN', // Unmapped error (see message)
|
|
1760
|
-
};
|
|
1761
|
-
|
|
1762
|
-
class UsionError extends Error {
|
|
1763
|
-
/**
|
|
1764
|
-
* @param {string} code - One of ERROR_CODES
|
|
1765
|
-
* @param {string} [message] - Human-readable detail (may change between versions)
|
|
1766
|
-
*/
|
|
1767
|
-
constructor(code, message) {
|
|
1768
|
-
super(message || code);
|
|
1769
|
-
this.name = 'UsionError';
|
|
1770
|
-
this.code = ERROR_CODES[code] ? code : ERROR_CODES.UNKNOWN;
|
|
1771
|
-
}
|
|
1772
|
-
}
|
|
1773
|
-
|
|
1774
|
-
// Backend error strings → stable codes. Order matters: first match wins.
|
|
1775
|
-
/** @type {Array<[RegExp, string]>} */
|
|
1776
|
-
const BACKEND_PATTERNS = [
|
|
1777
|
-
[/not authenticated/i, ERROR_CODES.NOT_AUTHENTICATED],
|
|
1778
|
-
[/room_id required|no room id/i, ERROR_CODES.NO_ROOM],
|
|
1779
|
-
[/room not found/i, ERROR_CODES.ROOM_NOT_FOUND],
|
|
1780
|
-
[/not a participant/i, ERROR_CODES.NOT_PARTICIPANT],
|
|
1781
|
-
[/room authority/i, ERROR_CODES.NOT_AUTHORITY],
|
|
1782
|
-
[/exceeds .*limit/i, ERROR_CODES.STATE_TOO_LARGE],
|
|
1783
|
-
[/state must be/i, ERROR_CODES.INVALID_STATE],
|
|
1784
|
-
[/next_turn must be/i, ERROR_CODES.INVALID_NEXT_TURN],
|
|
1785
|
-
[/rate limit|too many/i, ERROR_CODES.RATE_LIMITED],
|
|
1786
|
-
[/join timeout/i, ERROR_CODES.JOIN_TIMEOUT],
|
|
1787
|
-
[/connection timeout|connect timeout/i, ERROR_CODES.CONNECT_TIMEOUT],
|
|
1788
|
-
[/request timeout/i, ERROR_CODES.REQUEST_TIMEOUT],
|
|
1789
|
-
[/not connected/i, ERROR_CODES.NOT_CONNECTED],
|
|
1790
|
-
];
|
|
1791
|
-
|
|
1792
|
-
/**
|
|
1793
|
-
* Normalize anything (backend `{error}` string, Error, raw string) into a
|
|
1794
|
-
* UsionError with the best-matching stable code.
|
|
1795
|
-
* @param {*} err
|
|
1796
|
-
* @param {string} [fallbackCode] - Code to use when nothing matches
|
|
1797
|
-
* @returns {UsionError}
|
|
1798
|
-
*/
|
|
1799
|
-
function toUsionError(err, fallbackCode) {
|
|
1800
|
-
if (err instanceof UsionError) return err;
|
|
1801
|
-
const message = err && err.message ? err.message : String(err || 'Unknown error');
|
|
1802
|
-
for (let i = 0; i < BACKEND_PATTERNS.length; i++) {
|
|
1803
|
-
if (BACKEND_PATTERNS[i][0].test(message)) {
|
|
1804
|
-
return new UsionError(BACKEND_PATTERNS[i][1], message);
|
|
1805
|
-
}
|
|
1806
|
-
}
|
|
1807
|
-
return new UsionError(fallbackCode || ERROR_CODES.UNKNOWN, message);
|
|
1808
|
-
}
|
|
1809
|
-
|
|
1810
1915
|
/**
|
|
1811
1916
|
* Usion SDK Game Proxy — postMessage relay through parent app
|
|
1812
1917
|
*/
|
|
@@ -2055,7 +2160,7 @@ var Usion = (function () {
|
|
|
2055
2160
|
self.socket.emit('game:join', { room_id: roomId }, function(response) {
|
|
2056
2161
|
if (response.error) {
|
|
2057
2162
|
self._joined = false;
|
|
2058
|
-
reject(toUsionError(response
|
|
2163
|
+
reject(toUsionError(response));
|
|
2059
2164
|
} else {
|
|
2060
2165
|
self._joined = true;
|
|
2061
2166
|
if (response.sequence !== undefined) {
|
|
@@ -2158,36 +2263,81 @@ var Usion = (function () {
|
|
|
2158
2263
|
// move can't go out before the client has resynced.
|
|
2159
2264
|
const gate = self._rejoinPromise || Promise.resolve();
|
|
2160
2265
|
return gate.then(function() {
|
|
2161
|
-
|
|
2162
|
-
if (
|
|
2163
|
-
|
|
2164
|
-
self._queueOfflineAction(actionType, actionData, opts).then(resolve, reject);
|
|
2165
|
-
return;
|
|
2166
|
-
}
|
|
2167
|
-
reject(new UsionError(ERROR_CODES.NOT_CONNECTED, 'Not connected'));
|
|
2168
|
-
return;
|
|
2266
|
+
if (!self.socket || !self.connected) {
|
|
2267
|
+
if (queueOffline) {
|
|
2268
|
+
return self._queueOfflineAction(actionType, actionData, opts);
|
|
2169
2269
|
}
|
|
2270
|
+
throw new UsionError(ERROR_CODES.NOT_CONNECTED, 'Not connected');
|
|
2271
|
+
}
|
|
2272
|
+
const payload = {
|
|
2273
|
+
room_id: self.roomId,
|
|
2274
|
+
action_type: actionType,
|
|
2275
|
+
action_data: actionData
|
|
2276
|
+
};
|
|
2277
|
+
if (nextTurn) payload.next_turn = nextTurn;
|
|
2278
|
+
return self._emitAction(payload, true);
|
|
2279
|
+
});
|
|
2280
|
+
};
|
|
2170
2281
|
|
|
2171
|
-
|
|
2172
|
-
|
|
2173
|
-
|
|
2174
|
-
|
|
2175
|
-
|
|
2176
|
-
|
|
2177
|
-
|
|
2178
|
-
|
|
2179
|
-
|
|
2180
|
-
|
|
2181
|
-
|
|
2182
|
-
|
|
2183
|
-
|
|
2184
|
-
|
|
2282
|
+
/**
|
|
2283
|
+
* @private Emit game:action and handle the ack. On NOT_IN_ROOM (the server
|
|
2284
|
+
* detected a detached socket — connected but no room membership), auto-
|
|
2285
|
+
* rejoin + resync once and retry the same action, so a move made in the
|
|
2286
|
+
* detached window is delivered instead of silently lost.
|
|
2287
|
+
*/
|
|
2288
|
+
game._emitAction = function(payload, retryOnNotInRoom) {
|
|
2289
|
+
const self = this;
|
|
2290
|
+
return new Promise(function(resolve, reject) {
|
|
2291
|
+
if (!self.socket || !self.connected) {
|
|
2292
|
+
reject(new UsionError(ERROR_CODES.NOT_CONNECTED, 'Not connected'));
|
|
2293
|
+
return;
|
|
2294
|
+
}
|
|
2295
|
+
self.socket.emit('game:action', payload, function(response) {
|
|
2296
|
+
if (response && response.error) {
|
|
2297
|
+
const ue = toUsionError(response);
|
|
2298
|
+
if (retryOnNotInRoom && ue.code === ERROR_CODES.NOT_IN_ROOM) {
|
|
2299
|
+
self._autoRejoin()
|
|
2300
|
+
.then(function() { return self._emitAction(payload, false); })
|
|
2301
|
+
.then(resolve, reject);
|
|
2302
|
+
return;
|
|
2185
2303
|
}
|
|
2186
|
-
|
|
2304
|
+
reject(ue);
|
|
2305
|
+
} else {
|
|
2306
|
+
if (response && response.sequence !== undefined) {
|
|
2307
|
+
self._lastSequence = response.sequence;
|
|
2308
|
+
}
|
|
2309
|
+
resolve(response);
|
|
2310
|
+
}
|
|
2187
2311
|
});
|
|
2188
2312
|
});
|
|
2189
2313
|
};
|
|
2190
2314
|
|
|
2315
|
+
/**
|
|
2316
|
+
* @private Recover a detached socket: the server said we're not in the
|
|
2317
|
+
* Socket.IO room (NOT_IN_ROOM), so rejoin + resync exactly like the
|
|
2318
|
+
* post-reconnect path, gating action() sends on _rejoinPromise meanwhile.
|
|
2319
|
+
*/
|
|
2320
|
+
game._autoRejoin = function() {
|
|
2321
|
+
const self = this;
|
|
2322
|
+
if (self._rejoinPromise) return self._rejoinPromise;
|
|
2323
|
+
if (!self.roomId || !self.socket || !self.connected) return Promise.resolve();
|
|
2324
|
+
Usion.log('Server reports socket not in room - auto-rejoining ' + self.roomId);
|
|
2325
|
+
self._joined = false;
|
|
2326
|
+
self._joinPromise = null;
|
|
2327
|
+
self._rejoinPromise = self.join(self.roomId)
|
|
2328
|
+
.then(function() {
|
|
2329
|
+
self.requestSync(self._lastSequence || 0);
|
|
2330
|
+
})
|
|
2331
|
+
.catch(function(err) {
|
|
2332
|
+
Usion.log('Auto-rejoin failed: ' + (err && err.message ? err.message : String(err)));
|
|
2333
|
+
})
|
|
2334
|
+
.then(function() {
|
|
2335
|
+
self._rejoinPromise = null;
|
|
2336
|
+
if (self._flushOfflineQueue) self._flushOfflineQueue();
|
|
2337
|
+
});
|
|
2338
|
+
return self._rejoinPromise;
|
|
2339
|
+
};
|
|
2340
|
+
|
|
2191
2341
|
// ── Offline action queue (opt-in via action(..., { queueOffline: true })) ──
|
|
2192
2342
|
|
|
2193
2343
|
const OFFLINE_QUEUE_MAX = 20;
|
|
@@ -2262,11 +2412,17 @@ var Usion = (function () {
|
|
|
2262
2412
|
return Promise.resolve({ success: false, error: 'not_supported_in_direct_mode', code: ERROR_CODES.UNSUPPORTED });
|
|
2263
2413
|
}
|
|
2264
2414
|
|
|
2415
|
+
// The checkpoint carries the action sequence this state reflects. The
|
|
2416
|
+
// server persists it and rejects OLDER checkpoints (STALE_STATE), so two
|
|
2417
|
+
// concurrent writers can't clobber newer state with older.
|
|
2418
|
+
const checkpointSeq = self._lastSequence || 0;
|
|
2419
|
+
|
|
2265
2420
|
if (self._useProxy) {
|
|
2266
|
-
return Usion._request('GAME_SET_STATE', { room_id: self.roomId, state: state || {} })
|
|
2421
|
+
return Usion._request('GAME_SET_STATE', { room_id: self.roomId, state: state || {}, sequence: checkpointSeq })
|
|
2267
2422
|
.then(function(res) {
|
|
2268
2423
|
if (res && res.error) {
|
|
2269
|
-
|
|
2424
|
+
const rue = toUsionError(res);
|
|
2425
|
+
return { success: false, error: rue.message, code: rue.code };
|
|
2270
2426
|
}
|
|
2271
2427
|
return res || { success: true };
|
|
2272
2428
|
})
|
|
@@ -2283,10 +2439,12 @@ var Usion = (function () {
|
|
|
2283
2439
|
}
|
|
2284
2440
|
self.socket.emit('game:set_state', {
|
|
2285
2441
|
room_id: self.roomId,
|
|
2286
|
-
state: state || {}
|
|
2442
|
+
state: state || {},
|
|
2443
|
+
sequence: checkpointSeq
|
|
2287
2444
|
}, function(response) {
|
|
2288
2445
|
if (response && response.error) {
|
|
2289
|
-
|
|
2446
|
+
const ue = toUsionError(response);
|
|
2447
|
+
resolve({ success: false, error: ue.message, code: ue.code });
|
|
2290
2448
|
} else {
|
|
2291
2449
|
resolve(response || { success: true });
|
|
2292
2450
|
}
|
|
@@ -2319,10 +2477,18 @@ var Usion = (function () {
|
|
|
2319
2477
|
return;
|
|
2320
2478
|
}
|
|
2321
2479
|
|
|
2480
|
+
// Still fire-and-forget for the game, but the server now acks failures
|
|
2481
|
+
// (previously dropped silently): surface them via onError, and on
|
|
2482
|
+
// NOT_IN_ROOM (detached socket) auto-rejoin so the stream recovers.
|
|
2322
2483
|
self.socket.emit('game:realtime', {
|
|
2323
2484
|
room_id: self.roomId,
|
|
2324
2485
|
action_type: actionType,
|
|
2325
2486
|
action_data: actionData
|
|
2487
|
+
}, function(response) {
|
|
2488
|
+
if (!response || !response.error) return;
|
|
2489
|
+
const ue = toUsionError(response);
|
|
2490
|
+
self._dispatch('error', { code: ue.code, message: ue.message, source: 'realtime' });
|
|
2491
|
+
if (ue.code === ERROR_CODES.NOT_IN_ROOM) self._autoRejoin();
|
|
2326
2492
|
});
|
|
2327
2493
|
};
|
|
2328
2494
|
|
|
@@ -2398,7 +2564,7 @@ var Usion = (function () {
|
|
|
2398
2564
|
|
|
2399
2565
|
self.socket.emit('game:forfeit', { room_id: self.roomId }, function(response) {
|
|
2400
2566
|
if (response.error) {
|
|
2401
|
-
reject(toUsionError(response
|
|
2567
|
+
reject(toUsionError(response));
|
|
2402
2568
|
} else {
|
|
2403
2569
|
resolve(response);
|
|
2404
2570
|
}
|
|
@@ -5041,26 +5207,31 @@ var Usion = (function () {
|
|
|
5041
5207
|
self._connectPromise = new Promise(function(resolve, reject) {
|
|
5042
5208
|
// Check if socket.io-client is available
|
|
5043
5209
|
if (typeof io === 'undefined') {
|
|
5044
|
-
// Load
|
|
5045
|
-
|
|
5046
|
-
|
|
5047
|
-
|
|
5048
|
-
|
|
5049
|
-
|
|
5050
|
-
|
|
5051
|
-
|
|
5052
|
-
|
|
5053
|
-
|
|
5054
|
-
|
|
5055
|
-
self._initSocket(socketUrl, token, resolve, reject);
|
|
5056
|
-
};
|
|
5057
|
-
cdnScript.onerror = function() {
|
|
5210
|
+
// Load the Socket.IO client: same-origin copy first, then the
|
|
5211
|
+
// platform-hosted copy (same origin as the SDK itself — reachable
|
|
5212
|
+
// wherever the app is), and only then the public CDN, which
|
|
5213
|
+
// restricted networks often block.
|
|
5214
|
+
var sources = [
|
|
5215
|
+
'/socket.io.min.js',
|
|
5216
|
+
'https://usions.com/socket.io.min.js',
|
|
5217
|
+
'https://cdn.socket.io/4.7.2/socket.io.min.js'
|
|
5218
|
+
];
|
|
5219
|
+
(function tryLoad(i) {
|
|
5220
|
+
if (i >= sources.length) {
|
|
5058
5221
|
self._connecting = false;
|
|
5059
5222
|
reject(new Error('Failed to load Socket.IO client'));
|
|
5223
|
+
return;
|
|
5224
|
+
}
|
|
5225
|
+
var script = document.createElement('script');
|
|
5226
|
+
script.src = sources[i];
|
|
5227
|
+
script.onload = function() {
|
|
5228
|
+
self._initSocket(socketUrl, token, resolve, reject);
|
|
5229
|
+
};
|
|
5230
|
+
script.onerror = function() {
|
|
5231
|
+
tryLoad(i + 1);
|
|
5060
5232
|
};
|
|
5061
|
-
document.head.appendChild(
|
|
5062
|
-
};
|
|
5063
|
-
document.head.appendChild(script);
|
|
5233
|
+
document.head.appendChild(script);
|
|
5234
|
+
})(0);
|
|
5064
5235
|
} else {
|
|
5065
5236
|
self._initSocket(socketUrl, token, resolve, reject);
|
|
5066
5237
|
}
|
|
@@ -5411,12 +5582,14 @@ var Usion = (function () {
|
|
|
5411
5582
|
*
|
|
5412
5583
|
* const m = await Usion.matchmaking.find(); // resolves when matched
|
|
5413
5584
|
* await Usion.game.connect(); await Usion.game.join(m.roomId);
|
|
5414
|
-
* // ...or cancel while waiting:
|
|
5585
|
+
* // ...or bound the wait / cancel while waiting:
|
|
5586
|
+
* const m2 = await Usion.matchmaking.find(null, { timeout: 30000 });
|
|
5415
5587
|
* Usion.matchmaking.cancel();
|
|
5416
5588
|
* Usion.matchmaking.onMatch(({ roomId, players }) => { ... });
|
|
5417
5589
|
*/
|
|
5590
|
+
|
|
5418
5591
|
function createMatchmakingModule(Usion) {
|
|
5419
|
-
let pending = null; // { resolve, reject }
|
|
5592
|
+
let pending = null; // active find() entry: { resolve, reject }
|
|
5420
5593
|
let onMatchCb = null;
|
|
5421
5594
|
let bound = false;
|
|
5422
5595
|
|
|
@@ -5440,24 +5613,64 @@ var Usion = (function () {
|
|
|
5440
5613
|
|
|
5441
5614
|
/**
|
|
5442
5615
|
* Join the queue for `serviceId` (defaults to the current game) and resolve
|
|
5443
|
-
* when matched with { roomId, players, serviceId }.
|
|
5444
|
-
* match (use cancel() to stop waiting)
|
|
5616
|
+
* when matched with { roomId, players, serviceId }. Without `opts.timeout`
|
|
5617
|
+
* it stays pending until a match (use cancel() to stop waiting); with it,
|
|
5618
|
+
* rejects MATCH_TIMEOUT (and leaves the queue) once the wait elapses.
|
|
5619
|
+
* @param {string} [serviceId]
|
|
5620
|
+
* @param {{size?: number, timeout?: number}} [opts]
|
|
5445
5621
|
*/
|
|
5446
5622
|
find: function (serviceId, opts) {
|
|
5447
5623
|
opts = opts || {};
|
|
5448
5624
|
const sid = serviceId || (Usion.config && Usion.config.serviceId);
|
|
5449
5625
|
bind();
|
|
5450
|
-
|
|
5451
|
-
|
|
5452
|
-
|
|
5453
|
-
|
|
5454
|
-
|
|
5626
|
+
|
|
5627
|
+
if (pending) {
|
|
5628
|
+
const old = pending; pending = null;
|
|
5629
|
+
old.reject(new UsionError(ERROR_CODES.SUPERSEDED, 'Superseded by a newer find()'));
|
|
5630
|
+
}
|
|
5631
|
+
|
|
5632
|
+
let timer = null;
|
|
5633
|
+
const entry = {};
|
|
5634
|
+
const promise = new Promise(function (resolve, reject) {
|
|
5635
|
+
entry.resolve = function (r) {
|
|
5636
|
+
if (timer) clearTimeout(timer);
|
|
5637
|
+
resolve(r);
|
|
5638
|
+
};
|
|
5639
|
+
entry.reject = function (e) {
|
|
5640
|
+
if (timer) clearTimeout(timer);
|
|
5641
|
+
if (pending === entry) pending = null;
|
|
5642
|
+
reject(e);
|
|
5643
|
+
};
|
|
5455
5644
|
});
|
|
5645
|
+
|
|
5646
|
+
// Register BEFORE emitting mm:join: when someone is already waiting in
|
|
5647
|
+
// the queue, mm:matched can arrive ahead of the join ack — registering
|
|
5648
|
+
// afterwards dropped that instant match and left find() hanging forever.
|
|
5649
|
+
pending = entry;
|
|
5650
|
+
|
|
5651
|
+
if (opts.timeout > 0) {
|
|
5652
|
+
timer = setTimeout(function () {
|
|
5653
|
+
timer = null;
|
|
5654
|
+
Usion._backendEmit('mm:cancel', {}).catch(function () { /* best-effort dequeue */ });
|
|
5655
|
+
entry.reject(new UsionError(ERROR_CODES.MATCH_TIMEOUT,
|
|
5656
|
+
'No match within ' + opts.timeout + 'ms'));
|
|
5657
|
+
}, opts.timeout);
|
|
5658
|
+
}
|
|
5659
|
+
|
|
5660
|
+
Usion._backendEmit('mm:join', { service_id: sid, size: opts.size || 2 })
|
|
5661
|
+
.catch(function (err) {
|
|
5662
|
+
if (pending === entry) entry.reject(toUsionError(err));
|
|
5663
|
+
});
|
|
5664
|
+
|
|
5665
|
+
return promise;
|
|
5456
5666
|
},
|
|
5457
5667
|
|
|
5458
5668
|
/** Leave the queue / stop waiting. */
|
|
5459
5669
|
cancel: function () {
|
|
5460
|
-
if (pending) {
|
|
5670
|
+
if (pending) {
|
|
5671
|
+
const old = pending; pending = null;
|
|
5672
|
+
old.reject(new UsionError(ERROR_CODES.CANCELLED, 'cancelled'));
|
|
5673
|
+
}
|
|
5461
5674
|
return Usion._backendEmit('mm:cancel', {});
|
|
5462
5675
|
},
|
|
5463
5676
|
};
|
|
@@ -5707,6 +5920,7 @@ var Usion = (function () {
|
|
|
5707
5920
|
* safe, namespaced allow-list (e.g. lobby:* / mm:*) so a mini-app can't abuse
|
|
5708
5921
|
* the user's authenticated connection. The backend re-validates every call.
|
|
5709
5922
|
*/
|
|
5923
|
+
|
|
5710
5924
|
function applyBackendChannel(Usion) {
|
|
5711
5925
|
Usion._backendHandlers = {};
|
|
5712
5926
|
Usion._boundSockets = typeof WeakSet !== 'undefined' ? new WeakSet() : null;
|
|
@@ -5741,30 +5955,61 @@ var Usion = (function () {
|
|
|
5741
5955
|
/**
|
|
5742
5956
|
* Emit a backend request and await its ack. Routes to the SDK socket when
|
|
5743
5957
|
* standalone, or through the parent host when embedded.
|
|
5958
|
+
* Standalone apps that never call Usion.game.connect() (e.g. a non-game
|
|
5959
|
+
* app using cloud/leaderboard/notify) get an automatic one-time connect —
|
|
5960
|
+
* the backend channel is not coupled to starting a game.
|
|
5961
|
+
* Rejections are always UsionError (stable `code`, plus `retryAfter` on
|
|
5962
|
+
* RATE_LIMITED), regardless of transport.
|
|
5744
5963
|
* @returns {Promise<any>}
|
|
5745
5964
|
*/
|
|
5746
5965
|
Usion._backendEmit = function (event, data, timeout) {
|
|
5747
5966
|
const self = this;
|
|
5748
5967
|
timeout = timeout || 8000;
|
|
5749
|
-
|
|
5750
|
-
|
|
5968
|
+
|
|
5969
|
+
function emitOn(sock) {
|
|
5751
5970
|
return new Promise(function (resolve, reject) {
|
|
5752
5971
|
let done = false;
|
|
5753
|
-
const timer = setTimeout(function () {
|
|
5972
|
+
const timer = setTimeout(function () {
|
|
5973
|
+
if (done) return; done = true;
|
|
5974
|
+
reject(new UsionError(ERROR_CODES.REQUEST_TIMEOUT, 'Backend request timeout'));
|
|
5975
|
+
}, timeout);
|
|
5754
5976
|
try {
|
|
5755
|
-
|
|
5977
|
+
sock.emit(event, data || {}, function (resp) {
|
|
5756
5978
|
if (done) return; done = true; clearTimeout(timer);
|
|
5757
|
-
if (resp && resp.error) reject(
|
|
5979
|
+
if (resp && resp.error) reject(toUsionError(resp));
|
|
5758
5980
|
else resolve(resp);
|
|
5759
5981
|
});
|
|
5760
|
-
} catch (e) { clearTimeout(timer); reject(e); }
|
|
5982
|
+
} catch (e) { clearTimeout(timer); reject(toUsionError(e)); }
|
|
5761
5983
|
});
|
|
5762
5984
|
}
|
|
5985
|
+
|
|
5986
|
+
const s = self.game && self.game.socket;
|
|
5987
|
+
if (s && s.connected) return emitOn(s);
|
|
5763
5988
|
if (self._isEmbedded) {
|
|
5764
5989
|
// Host relays this onto its authenticated socket and replies with the ack.
|
|
5765
|
-
return self._request('BACKEND_EMIT', { event: event, data: data || {} }, timeout)
|
|
5990
|
+
return self._request('BACKEND_EMIT', { event: event, data: data || {} }, timeout)
|
|
5991
|
+
.catch(function (e) { throw toUsionError(e); });
|
|
5992
|
+
}
|
|
5993
|
+
|
|
5994
|
+
// Standalone without a live socket. If no socket exists yet, auto-connect
|
|
5995
|
+
// once (connect() dedupes concurrent callers via _connectPromise). A
|
|
5996
|
+
// socket that exists but is mid-reconnect is left to Socket.IO's own
|
|
5997
|
+
// reconnection — the caller gets a coded error and can retry.
|
|
5998
|
+
const connectionMode = (self.config && self.config.connectionMode) || 'platform';
|
|
5999
|
+
if (!s && connectionMode !== 'direct'
|
|
6000
|
+
&& self.game && typeof self.game.connect === 'function') {
|
|
6001
|
+
return self.game.connect().then(function () {
|
|
6002
|
+
const sock = self.game.socket;
|
|
6003
|
+
if (sock && sock.connected) return emitOn(sock);
|
|
6004
|
+
throw new UsionError(ERROR_CODES.NOT_CONNECTED, 'No backend connection');
|
|
6005
|
+
}).catch(function (e) {
|
|
6006
|
+
if (e instanceof UsionError) throw e;
|
|
6007
|
+
throw new UsionError(ERROR_CODES.NOT_CONNECTED,
|
|
6008
|
+
'Backend connect failed: ' + (e && e.message ? e.message : String(e)));
|
|
6009
|
+
});
|
|
5766
6010
|
}
|
|
5767
|
-
return Promise.reject(new
|
|
6011
|
+
return Promise.reject(new UsionError(ERROR_CODES.NOT_CONNECTED,
|
|
6012
|
+
'No backend connection — socket is offline or reconnecting'));
|
|
5768
6013
|
};
|
|
5769
6014
|
}
|
|
5770
6015
|
|