@usions/sdk 2.11.1 → 2.13.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 +560 -122
- package/src/modules/core.js +17 -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 +6 -0
- package/src/modules/misc.js +20 -0
- package/src/modules/notify.js +70 -0
- package/src/modules/wallet.js +7 -1
- package/types/index.d.ts +166 -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
|
@@ -29,8 +29,10 @@ import { createLobbyModule } from './lobby.js';
|
|
|
29
29
|
import { createLeaderboardModule } from './leaderboard.js';
|
|
30
30
|
import { createCloudModule } from './cloud.js';
|
|
31
31
|
import { createMatchmakingModule } from './matchmaking.js';
|
|
32
|
+
import { createNotifyModule } from './notify.js';
|
|
32
33
|
import { applyBackendChannel } from './backend-channel.js';
|
|
33
34
|
import { netcode } from './netcode/index.js';
|
|
35
|
+
import { UsionError, ERROR_CODES } from './errors.js';
|
|
34
36
|
|
|
35
37
|
// Build the Usion object from core
|
|
36
38
|
const Usion = Object.assign({}, core);
|
|
@@ -44,12 +46,16 @@ Usion.chat = createChatModule(Usion);
|
|
|
44
46
|
Usion.bot = createBotModule(Usion);
|
|
45
47
|
Usion.fileStorage = createFileStorageModule(Usion);
|
|
46
48
|
Usion.game = createGameModule(Usion);
|
|
49
|
+
// Stable error class + codes — developers branch on err.code, not message text.
|
|
50
|
+
Usion.UsionError = UsionError;
|
|
51
|
+
Usion.ERROR_CODES = ERROR_CODES;
|
|
47
52
|
// Unified backend channel (used by lobby etc.; works standalone + embedded).
|
|
48
53
|
applyBackendChannel(Usion);
|
|
49
54
|
Usion.lobby = createLobbyModule(Usion);
|
|
50
55
|
Usion.leaderboard = createLeaderboardModule(Usion);
|
|
51
56
|
Usion.cloud = createCloudModule(Usion);
|
|
52
57
|
Usion.matchmaking = createMatchmakingModule(Usion);
|
|
58
|
+
Usion.notify = createNotifyModule(Usion);
|
|
53
59
|
|
|
54
60
|
// Netcode toolkit (transport-agnostic, zero-dependency).
|
|
55
61
|
Usion.netcode = netcode;
|
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
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Usion SDK Notify — let a mini-app notify its own user.
|
|
3
|
+
*
|
|
4
|
+
* A notification reaches the user even when they aren't looking at the app:
|
|
5
|
+
* - online elsewhere in Usion -> in-app banner
|
|
6
|
+
* - offline / app backgrounded -> OS push notification
|
|
7
|
+
* Tapping it re-opens THIS mini-app, optionally at a specific internal screen
|
|
8
|
+
* via `path` (read back on launch with `Usion.getLaunchParams().path`).
|
|
9
|
+
*
|
|
10
|
+
* Rides the unified backend channel, so it works standalone AND embedded.
|
|
11
|
+
* Scope & safety: a notification can only target the CURRENT user (you cannot
|
|
12
|
+
* notify other people from here), and the platform rate-limits per user per
|
|
13
|
+
* service. Users can silence a service with `setMuted(true)`.
|
|
14
|
+
*
|
|
15
|
+
* await Usion.notify.send({
|
|
16
|
+
* title: 'Render complete',
|
|
17
|
+
* body: 'Your video is ready to view.',
|
|
18
|
+
* path: '/render/abc123', // optional — deep-links inside the app
|
|
19
|
+
* });
|
|
20
|
+
*
|
|
21
|
+
* await Usion.notify.setMuted(true); // user opts out for this app
|
|
22
|
+
* const muted = await Usion.notify.isMuted();
|
|
23
|
+
*
|
|
24
|
+
* For server-triggered notifications (e.g. a long-running job finishing while
|
|
25
|
+
* the app is closed), a mini-app's own backend calls the signed REST endpoint
|
|
26
|
+
* `POST /services/{id}/notify` instead — see the publishing reference.
|
|
27
|
+
*/
|
|
28
|
+
export function createNotifyModule(Usion) {
|
|
29
|
+
function serviceId(opts) {
|
|
30
|
+
return (opts && opts.serviceId) || (Usion.config && Usion.config.serviceId);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
return {
|
|
34
|
+
/**
|
|
35
|
+
* Send a notification to the current user.
|
|
36
|
+
* @param {{title: string, body: string, path?: string, serviceId?: string}} opts
|
|
37
|
+
* @returns {Promise<{success: boolean, delivered?: string}>}
|
|
38
|
+
*/
|
|
39
|
+
send: function (opts) {
|
|
40
|
+
opts = opts || {};
|
|
41
|
+
return Usion._backendEmit('notify:send', {
|
|
42
|
+
service_id: serviceId(opts),
|
|
43
|
+
title: opts.title,
|
|
44
|
+
body: opts.body,
|
|
45
|
+
path: opts.path,
|
|
46
|
+
});
|
|
47
|
+
},
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Mute (or unmute) notifications from this app for the current user.
|
|
51
|
+
* @param {boolean} muted
|
|
52
|
+
* @returns {Promise<{success: boolean, muted: boolean}>}
|
|
53
|
+
*/
|
|
54
|
+
setMuted: function (muted, opts) {
|
|
55
|
+
return Usion._backendEmit('notify:set_pref', {
|
|
56
|
+
service_id: serviceId(opts),
|
|
57
|
+
muted: !!muted,
|
|
58
|
+
});
|
|
59
|
+
},
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Whether the current user has muted notifications from this app.
|
|
63
|
+
* @returns {Promise<boolean>}
|
|
64
|
+
*/
|
|
65
|
+
isMuted: function (opts) {
|
|
66
|
+
return Usion._backendEmit('notify:get_pref', { service_id: serviceId(opts) })
|
|
67
|
+
.then(function (r) { return !!(r && r.muted); });
|
|
68
|
+
},
|
|
69
|
+
};
|
|
70
|
+
}
|
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
|
}
|