@usions/sdk 2.0.1 → 2.1.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.
@@ -0,0 +1,219 @@
1
+ /**
2
+ * Usion SDK Game Direct — WebSocket direct connection to game server
3
+ */
4
+
5
+ /**
6
+ * Add direct WebSocket connection methods to game module
7
+ * @param {object} game - The game module object
8
+ * @param {object} Usion - Reference to the main Usion object
9
+ */
10
+ export function applyGameDirect(game, Usion) {
11
+ /**
12
+ * Connect directly to creator-controlled WebSocket server.
13
+ * Uses backend-issued short-lived room token.
14
+ * @returns {Promise}
15
+ */
16
+ game.connectDirect = function(config) {
17
+ var self = this;
18
+ config = config || {};
19
+
20
+ if (self.directMode && self.directSocket && self.connected) {
21
+ return Promise.resolve();
22
+ }
23
+ if (self._connecting && self._connectPromise) {
24
+ return self._connectPromise;
25
+ }
26
+
27
+ self._connecting = true;
28
+ self.directMode = true;
29
+ self._connectPromise = self._fetchDirectAccess(config)
30
+ .then(function(access) {
31
+ self.directConfig = access;
32
+ return self._initDirectSocket(access);
33
+ })
34
+ .then(function() {
35
+ self.connected = true;
36
+ self._connecting = false;
37
+ Usion.log('Direct game socket connected');
38
+ })
39
+ .catch(function(err) {
40
+ self._connecting = false;
41
+ self.connected = false;
42
+ self.directMode = false;
43
+ if (self._eventHandlers.connectionError) {
44
+ self._eventHandlers.connectionError(err);
45
+ }
46
+ throw err;
47
+ });
48
+ return self._connectPromise;
49
+ };
50
+
51
+ game._fetchDirectAccess = function(config) {
52
+ var roomId = config.roomId || this.roomId || Usion.config.roomId;
53
+ var serviceId = config.serviceId || Usion.config.serviceId;
54
+ var apiUrl = config.apiUrl || Usion.config.apiUrl || '';
55
+ var token = config.token || Usion.user.getToken();
56
+
57
+ if (!roomId) return Promise.reject(new Error('No room ID provided'));
58
+ if (!serviceId) return Promise.reject(new Error('No service ID provided'));
59
+
60
+ this.roomId = roomId;
61
+ this.playerId = Usion.user.getId();
62
+
63
+ // When embedded (iframe/WebView), proxy through parent app to avoid
64
+ // CORS / Private Network Access issues
65
+ if (Usion._isEmbedded) {
66
+ return Usion._request('GAME_ACCESS_REQUEST', {
67
+ room_id: roomId,
68
+ service_id: serviceId,
69
+ protocol_version: (config.protocolVersion || Usion.config.protocolVersion || Usion.config.protocol_version || '2')
70
+ }, 10000);
71
+ }
72
+
73
+ if (!apiUrl) return Promise.reject(new Error('No API URL provided'));
74
+ if (!token) return Promise.reject(new Error('No auth token available'));
75
+
76
+ var cleanApiUrl = String(apiUrl).replace(/\/$/, '');
77
+ var endpoint = cleanApiUrl + '/games/rooms/' + encodeURIComponent(roomId) + '/access';
78
+ return fetch(endpoint, {
79
+ method: 'POST',
80
+ headers: {
81
+ 'Content-Type': 'application/json',
82
+ 'Authorization': 'Bearer ' + token
83
+ },
84
+ body: JSON.stringify({
85
+ service_id: serviceId,
86
+ client_type: 'iframe',
87
+ protocol_version: (config.protocolVersion || Usion.config.protocolVersion || Usion.config.protocol_version || '2')
88
+ })
89
+ }).then(function(res) {
90
+ if (!res.ok) {
91
+ return res.text().then(function(text) {
92
+ throw new Error(text || ('Direct access failed: HTTP ' + res.status));
93
+ });
94
+ }
95
+ return res.json();
96
+ });
97
+ };
98
+
99
+ game._initDirectSocket = function(access) {
100
+ var self = this;
101
+ return new Promise(function(resolve, reject) {
102
+ if (!access || !access.ws_url || !access.access_token) {
103
+ reject(new Error('Invalid direct access payload'));
104
+ return;
105
+ }
106
+
107
+ var wsUrl = access.ws_url;
108
+ var separator = wsUrl.indexOf('?') === -1 ? '?' : '&';
109
+ var urlWithToken = wsUrl + separator + 'token=' + encodeURIComponent(access.access_token);
110
+ var ws = new WebSocket(urlWithToken);
111
+ self.directSocket = ws;
112
+
113
+ var opened = false;
114
+ var joinSent = false;
115
+ var timeout = setTimeout(function() {
116
+ if (!opened) {
117
+ try { ws.close(); } catch (e) {}
118
+ reject(new Error('Direct WebSocket connection timeout'));
119
+ }
120
+ }, 10000);
121
+
122
+ ws.onopen = function() {
123
+ opened = true;
124
+ clearTimeout(timeout);
125
+ if (!joinSent) {
126
+ joinSent = true;
127
+ self._sendDirect('join', {});
128
+ }
129
+ // Start heartbeat for direct mode
130
+ if (self._heartbeatInterval) clearInterval(self._heartbeatInterval);
131
+ self._heartbeatInterval = setInterval(function() {
132
+ if (self.directSocket && self.directSocket.readyState === WebSocket.OPEN) {
133
+ self._sendDirect('heartbeat', {});
134
+ }
135
+ }, 25000);
136
+ resolve();
137
+ };
138
+
139
+ ws.onerror = function() {
140
+ if (!opened) {
141
+ clearTimeout(timeout);
142
+ reject(new Error('Direct WebSocket connection error'));
143
+ }
144
+ };
145
+
146
+ ws.onclose = function(evt) {
147
+ self.connected = false;
148
+ self._joined = false;
149
+ self._joinPromise = null;
150
+ if (self._heartbeatInterval) {
151
+ clearInterval(self._heartbeatInterval);
152
+ self._heartbeatInterval = null;
153
+ }
154
+ if (self._eventHandlers.disconnect) {
155
+ self._eventHandlers.disconnect(evt && evt.reason ? evt.reason : 'direct socket closed');
156
+ }
157
+ };
158
+
159
+ ws.onmessage = function(evt) {
160
+ self._handleDirectMessage(evt && evt.data);
161
+ };
162
+ });
163
+ };
164
+
165
+ game._sendDirect = function(type, payload) {
166
+ if (!this.directSocket || this.directSocket.readyState !== WebSocket.OPEN) return;
167
+ this._directSeq = this._directSeq + 1;
168
+ this.directSocket.send(JSON.stringify({
169
+ type: type,
170
+ room_id: this.roomId,
171
+ ts: Date.now(),
172
+ seq: this._directSeq,
173
+ session_id: (this.directConfig && this.directConfig.session_id) ? this.directConfig.session_id : null,
174
+ protocol_version: (this.directConfig && this.directConfig.protocol_version) ? this.directConfig.protocol_version : '2',
175
+ payload: payload || {}
176
+ }));
177
+ };
178
+
179
+ game._handleDirectMessage = function(raw) {
180
+ var data;
181
+ try {
182
+ data = typeof raw === 'string' ? JSON.parse(raw) : raw;
183
+ } catch (e) {
184
+ return;
185
+ }
186
+ if (!data || !data.type) return;
187
+ var payload = data.payload || {};
188
+
189
+ if (data.type === 'joined') {
190
+ this._joined = true;
191
+ if (this._eventHandlers.joined) this._eventHandlers.joined(payload);
192
+ return;
193
+ }
194
+ if (data.type === 'player_joined') {
195
+ if (this._eventHandlers.playerJoined) this._eventHandlers.playerJoined(payload);
196
+ return;
197
+ }
198
+ if (data.type === 'player_left') {
199
+ if (this._eventHandlers.playerLeft) this._eventHandlers.playerLeft(payload);
200
+ return;
201
+ }
202
+ if (data.type === 'state_snapshot' || data.type === 'state_delta') {
203
+ if (this._eventHandlers.realtime) this._eventHandlers.realtime(payload);
204
+ if (this._eventHandlers.stateUpdate) this._eventHandlers.stateUpdate(payload);
205
+ return;
206
+ }
207
+ if (data.type === 'pong') {
208
+ if (this._eventHandlers.sync) this._eventHandlers.sync(payload);
209
+ return;
210
+ }
211
+ if (data.type === 'match_end') {
212
+ if (this._eventHandlers.finished) this._eventHandlers.finished(payload);
213
+ return;
214
+ }
215
+ if (data.type === 'error' && this._eventHandlers.error) {
216
+ this._eventHandlers.error(payload);
217
+ }
218
+ };
219
+ }
@@ -0,0 +1,334 @@
1
+ /**
2
+ * Usion SDK Game Methods — join, leave, action, realtime, sync, etc.
3
+ */
4
+
5
+ /**
6
+ * Add game action methods to game module
7
+ * @param {object} game - The game module object
8
+ * @param {object} Usion - Reference to the main Usion object
9
+ */
10
+ export function applyGameMethods(game, Usion) {
11
+ /**
12
+ * Join a game room
13
+ * @param {string} roomId - Game room ID (optional, uses config)
14
+ * @returns {Promise} Resolves with join data
15
+ */
16
+ game.join = function(roomId) {
17
+ const self = this;
18
+ roomId = roomId || Usion.config.roomId;
19
+
20
+ // If already joined this room, return cached promise/data
21
+ if (self._joined && self.roomId === roomId && self._joinPromise) {
22
+ return self._joinPromise;
23
+ }
24
+
25
+ self.roomId = roomId;
26
+ self.playerId = Usion.user.getId();
27
+
28
+ if (self.directMode) {
29
+ self._joined = true;
30
+ self._joinPromise = Promise.resolve({
31
+ room_id: roomId,
32
+ player_id: self.playerId
33
+ });
34
+ return self._joinPromise;
35
+ }
36
+
37
+ // Proxy mode: send join request to parent
38
+ if (self._useProxy) {
39
+ self._joinPromise = new Promise(function(resolve, reject) {
40
+ self._proxyJoinResolve = resolve;
41
+ self._proxyJoinReject = reject;
42
+ Usion._post({ type: 'GAME_JOIN', room_id: roomId });
43
+ setTimeout(function() {
44
+ if (!self._joined && self._proxyJoinReject) {
45
+ self._proxyJoinReject = null;
46
+ reject(new Error('Join timeout'));
47
+ }
48
+ }, 15000);
49
+ });
50
+ return self._joinPromise;
51
+ }
52
+
53
+ self._joinPromise = new Promise(function(resolve, reject) {
54
+ if (!self.socket || !self.connected) {
55
+ reject(new Error('Not connected'));
56
+ return;
57
+ }
58
+
59
+ if (!roomId) {
60
+ reject(new Error('No room ID provided'));
61
+ return;
62
+ }
63
+
64
+ self.socket.emit('game:join', { room_id: roomId }, function(response) {
65
+ if (response.error) {
66
+ self._joined = false;
67
+ reject(new Error(response.message || response.error));
68
+ } else {
69
+ self._joined = true;
70
+ if (response.sequence !== undefined) {
71
+ self._lastSequence = response.sequence;
72
+ }
73
+ resolve(response);
74
+ }
75
+ });
76
+ });
77
+
78
+ return self._joinPromise;
79
+ };
80
+
81
+ /**
82
+ * Leave the current game room
83
+ */
84
+ game.leave = function() {
85
+ const self = this;
86
+
87
+ if (self.directMode) {
88
+ if (self.roomId) self._sendDirect('leave', {});
89
+ self.roomId = null;
90
+ self._lastSequence = 0;
91
+ self._joined = false;
92
+ self._joinPromise = null;
93
+ return;
94
+ }
95
+
96
+ if (self._useProxy) {
97
+ if (self.roomId) Usion._post({ type: 'GAME_LEAVE', room_id: self.roomId });
98
+ self.roomId = null;
99
+ self._lastSequence = 0;
100
+ self._joined = false;
101
+ self._joinPromise = null;
102
+ return;
103
+ }
104
+
105
+ if (self.socket && self.connected && self.roomId) {
106
+ self.socket.emit('game:leave', { room_id: self.roomId });
107
+ self.roomId = null;
108
+ self._lastSequence = 0;
109
+ self._joined = false;
110
+ self._joinPromise = null;
111
+ }
112
+ };
113
+
114
+ /**
115
+ * Send a game action
116
+ * @param {string} actionType - Type of action (e.g., 'move')
117
+ * @param {object} actionData - Action data
118
+ * @returns {Promise} Resolves when action is processed
119
+ */
120
+ game.action = function(actionType, actionData) {
121
+ const self = this;
122
+
123
+ if (self.directMode) {
124
+ self._sendDirect(actionType || 'action', actionData || {});
125
+ return Promise.resolve({ success: true });
126
+ }
127
+
128
+ if (self._useProxy) {
129
+ Usion._post({ type: 'GAME_ACTION', room_id: self.roomId, action_type: actionType, action_data: actionData });
130
+ return Promise.resolve({ success: true });
131
+ }
132
+
133
+ return new Promise(function(resolve, reject) {
134
+ if (!self.socket || !self.connected) {
135
+ reject(new Error('Not connected'));
136
+ return;
137
+ }
138
+
139
+ self.socket.emit('game:action', {
140
+ room_id: self.roomId,
141
+ action_type: actionType,
142
+ action_data: actionData
143
+ }, function(response) {
144
+ if (response.error) {
145
+ reject(new Error(response.message || response.error));
146
+ } else {
147
+ if (response.sequence !== undefined) {
148
+ self._lastSequence = response.sequence;
149
+ }
150
+ resolve(response);
151
+ }
152
+ });
153
+ });
154
+ };
155
+
156
+ /**
157
+ * Send a real-time game update (fire-and-forget).
158
+ * @param {string} actionType - Type of action
159
+ * @param {object} actionData - Action data
160
+ */
161
+ game.realtime = function(actionType, actionData) {
162
+ const self = this;
163
+
164
+ if (self.directMode) {
165
+ self._sendDirect('input', {
166
+ action_type: actionType,
167
+ action_data: actionData || {}
168
+ });
169
+ return;
170
+ }
171
+
172
+ if (self._useProxy) {
173
+ Usion._post({ type: 'GAME_REALTIME', room_id: self.roomId, action_type: actionType, action_data: actionData });
174
+ return;
175
+ }
176
+
177
+ if (!self.socket || !self.connected) {
178
+ return;
179
+ }
180
+
181
+ self.socket.emit('game:realtime', {
182
+ room_id: self.roomId,
183
+ action_type: actionType,
184
+ action_data: actionData
185
+ });
186
+ };
187
+
188
+ /**
189
+ * Request game state sync (for reconnection)
190
+ * @param {number} lastSequence - Last known sequence number
191
+ */
192
+ game.requestSync = function(lastSequence) {
193
+ const self = this;
194
+ lastSequence = lastSequence !== undefined ? lastSequence : self._lastSequence;
195
+
196
+ if (self.directMode) {
197
+ self._sendDirect('ping', { last_sequence: lastSequence || 0 });
198
+ return;
199
+ }
200
+
201
+ if (self._useProxy && self.roomId) {
202
+ Usion._post({ type: 'GAME_SYNC_REQUEST', room_id: self.roomId, last_sequence: lastSequence || 0 });
203
+ return;
204
+ }
205
+
206
+ if (self.socket && self.connected && self.roomId) {
207
+ self.socket.emit('game:sync_request', {
208
+ room_id: self.roomId,
209
+ last_sequence: lastSequence || 0
210
+ });
211
+ }
212
+ };
213
+
214
+ /**
215
+ * Request a rematch
216
+ */
217
+ game.requestRematch = function() {
218
+ const self = this;
219
+
220
+ if (self.directMode) {
221
+ self._sendDirect('rematch', {});
222
+ return;
223
+ }
224
+
225
+ if (self._useProxy && self.roomId) {
226
+ Usion._post({ type: 'GAME_REMATCH', room_id: self.roomId });
227
+ return;
228
+ }
229
+
230
+ if (self.socket && self.connected && self.roomId) {
231
+ self.socket.emit('game:rematch', { room_id: self.roomId });
232
+ }
233
+ };
234
+
235
+ /**
236
+ * Forfeit the current game
237
+ * @returns {Promise}
238
+ */
239
+ game.forfeit = function() {
240
+ const self = this;
241
+
242
+ if (self.directMode) {
243
+ self._sendDirect('forfeit', {});
244
+ return Promise.resolve({ success: true });
245
+ }
246
+
247
+ if (self._useProxy) {
248
+ Usion._post({ type: 'GAME_FORFEIT', room_id: self.roomId });
249
+ return Promise.resolve({ success: true });
250
+ }
251
+
252
+ return new Promise(function(resolve, reject) {
253
+ if (!self.socket || !self.connected) {
254
+ reject(new Error('Not connected'));
255
+ return;
256
+ }
257
+
258
+ self.socket.emit('game:forfeit', { room_id: self.roomId }, function(response) {
259
+ if (response.error) {
260
+ reject(new Error(response.message || response.error));
261
+ } else {
262
+ resolve(response);
263
+ }
264
+ });
265
+ });
266
+ };
267
+
268
+ /**
269
+ * Disconnect from the game socket
270
+ */
271
+ game.disconnect = function() {
272
+ const self = this;
273
+
274
+ // Always clear heartbeat
275
+ if (self._heartbeatInterval) {
276
+ clearInterval(self._heartbeatInterval);
277
+ self._heartbeatInterval = null;
278
+ }
279
+
280
+ if (self.directMode) {
281
+ if (self.directSocket) {
282
+ try { self.directSocket.close(); } catch (e) {}
283
+ }
284
+ self.directSocket = null;
285
+ self.connected = false;
286
+ self.roomId = null;
287
+ self._lastSequence = 0;
288
+ self._connecting = false;
289
+ self._connectPromise = null;
290
+ self._joined = false;
291
+ self._joinPromise = null;
292
+ self.directMode = false;
293
+ self.directConfig = null;
294
+ self._directSeq = 0;
295
+ return;
296
+ }
297
+
298
+ if (self._useProxy) {
299
+ Usion._post({ type: 'GAME_DISCONNECT' });
300
+ self.connected = false;
301
+ self.roomId = null;
302
+ self._lastSequence = 0;
303
+ self._connecting = false;
304
+ self._connectPromise = null;
305
+ self._joined = false;
306
+ self._joinPromise = null;
307
+ self._useProxy = false;
308
+ return;
309
+ }
310
+
311
+ if (self.socket) {
312
+ self.socket.disconnect();
313
+ self.socket = null;
314
+ self.connected = false;
315
+ self.roomId = null;
316
+ self._lastSequence = 0;
317
+ self._connecting = false;
318
+ self._connectPromise = null;
319
+ self._joined = false;
320
+ self._joinPromise = null;
321
+ }
322
+ };
323
+
324
+ /**
325
+ * Get connection status
326
+ * @returns {boolean}
327
+ */
328
+ game.isConnected = function() {
329
+ if (this.directMode) {
330
+ return this.connected && this.directSocket && this.directSocket.readyState === WebSocket.OPEN;
331
+ }
332
+ return this.connected && this.socket && this.socket.connected;
333
+ };
334
+ }
@@ -0,0 +1,149 @@
1
+ /**
2
+ * Usion SDK Game Proxy — postMessage relay through parent app
3
+ */
4
+
5
+ /**
6
+ * Add proxy connection methods to game module
7
+ * @param {object} game - The game module object
8
+ * @param {object} Usion - Reference to the main Usion object
9
+ */
10
+ export function applyGameProxy(game, Usion) {
11
+ /**
12
+ * Connect via parent app proxy (postMessage relay).
13
+ * Used when mixed content prevents direct socket connection.
14
+ * @private
15
+ */
16
+ game._connectViaProxy = function() {
17
+ var self = this;
18
+
19
+ if (self._useProxy && self.connected) {
20
+ return Promise.resolve();
21
+ }
22
+
23
+ self._useProxy = true;
24
+ self._connecting = true;
25
+ self._setupProxyListener();
26
+
27
+ self._connectPromise = new Promise(function(resolve, reject) {
28
+ // Listen for GAME_CONNECTED from parent
29
+ self._proxyConnectResolve = function() {
30
+ self.connected = true;
31
+ self._connecting = false;
32
+ Usion.log('Game socket connected via parent proxy');
33
+ resolve();
34
+ };
35
+
36
+ // Send connect request to parent
37
+ Usion._post({ type: 'GAME_CONNECT' });
38
+
39
+ // Timeout after 10s
40
+ setTimeout(function() {
41
+ if (!self.connected) {
42
+ self._connecting = false;
43
+ reject(new Error('Proxy connection timeout'));
44
+ }
45
+ }, 10000);
46
+ });
47
+
48
+ return self._connectPromise;
49
+ };
50
+
51
+ /**
52
+ * Set up message listener for proxy game events from parent.
53
+ * @private
54
+ */
55
+ game._setupProxyListener = function() {
56
+ var self = this;
57
+ if (self._proxyListenerSetup) return;
58
+ self._proxyListenerSetup = true;
59
+
60
+ window.addEventListener('message', function(event) {
61
+ var data;
62
+ try {
63
+ data = typeof event.data === 'string' ? JSON.parse(event.data) : event.data;
64
+ } catch (e) {
65
+ return;
66
+ }
67
+ if (!data || typeof data !== 'object' || !self._useProxy) return;
68
+
69
+ switch (data.type) {
70
+ case 'GAME_CONNECTED':
71
+ if (self._proxyConnectResolve) {
72
+ self._proxyConnectResolve();
73
+ self._proxyConnectResolve = null;
74
+ }
75
+ break;
76
+
77
+ case 'GAME_CONNECT_ERROR':
78
+ self.connected = false;
79
+ self._connecting = false;
80
+ break;
81
+
82
+ case 'GAME_JOINED':
83
+ self._joined = true;
84
+ if (data.sequence !== undefined) self._lastSequence = data.sequence;
85
+ if (self._proxyJoinResolve) {
86
+ self._proxyJoinResolve(data);
87
+ self._proxyJoinResolve = null;
88
+ }
89
+ if (self._eventHandlers.joined) self._eventHandlers.joined(data);
90
+ break;
91
+
92
+ case 'GAME_JOIN_ERROR':
93
+ self._joined = false;
94
+ if (self._proxyJoinReject) {
95
+ self._proxyJoinReject(new Error(data.message || 'Join failed'));
96
+ self._proxyJoinReject = null;
97
+ }
98
+ break;
99
+
100
+ case 'GAME_PLAYER_JOINED':
101
+ if (self._eventHandlers.playerJoined) self._eventHandlers.playerJoined(data);
102
+ break;
103
+
104
+ case 'GAME_PLAYER_LEFT':
105
+ if (self._eventHandlers.playerLeft) self._eventHandlers.playerLeft(data);
106
+ break;
107
+
108
+ case 'GAME_STATE':
109
+ if (data.sequence !== undefined) self._lastSequence = Math.max(self._lastSequence, data.sequence);
110
+ if (self._eventHandlers.stateUpdate) self._eventHandlers.stateUpdate(data);
111
+ break;
112
+
113
+ case 'GAME_ACTION_DATA':
114
+ if (data.sequence !== undefined) self._lastSequence = Math.max(self._lastSequence, data.sequence);
115
+ if (self._eventHandlers.action) self._eventHandlers.action(data);
116
+ break;
117
+
118
+ case 'GAME_REALTIME_DATA':
119
+ if (self._eventHandlers.realtime) self._eventHandlers.realtime(data);
120
+ break;
121
+
122
+ case 'GAME_FINISHED':
123
+ if (data.sequence !== undefined) self._lastSequence = data.sequence;
124
+ if (self._eventHandlers.finished) self._eventHandlers.finished(data);
125
+ break;
126
+
127
+ case 'GAME_ERROR':
128
+ Usion.log('Game error via proxy: ' + (data.message || data.code));
129
+ if (self._eventHandlers.error) self._eventHandlers.error(data);
130
+ break;
131
+
132
+ case 'GAME_RESTARTED':
133
+ self._lastSequence = 0;
134
+ if (self._eventHandlers.restarted) self._eventHandlers.restarted(data);
135
+ break;
136
+
137
+ case 'GAME_REMATCH_REQUEST':
138
+ if (self._eventHandlers.rematchRequest) self._eventHandlers.rematchRequest(data);
139
+ break;
140
+
141
+ case 'GAME_SYNC':
142
+ if (data.sequence !== undefined) self._lastSequence = data.sequence;
143
+ if (self._eventHandlers.sync) self._eventHandlers.sync(data);
144
+ if (self._eventHandlers.stateUpdate) self._eventHandlers.stateUpdate(data);
145
+ break;
146
+ }
147
+ });
148
+ };
149
+ }