@usions/sdk 2.20.2 → 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 +716 -163
- 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 +27 -17
- package/src/modules/game-methods.js +98 -29
- package/src/modules/game-reliability.js +278 -0
- package/src/modules/game-socket.js +24 -7
- 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 +123 -6
|
@@ -0,0 +1,278 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Usion SDK Game Reliability — the reconnection-lifecycle surface promised in
|
|
3
|
+
* v2.16: sequence getters, the unified connection-state machine, and
|
|
4
|
+
* syncedState (reconnect-safe authoritative shared state).
|
|
5
|
+
*
|
|
6
|
+
* Everything here rides the events the game module already dispatches
|
|
7
|
+
* ('disconnect' / 'reconnect' / 'joined' / 'sync' / 'action'), so it behaves
|
|
8
|
+
* identically across the socket, embedded-proxy, and direct transports.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
export function applyGameReliability(game, Usion) {
|
|
12
|
+
// ── Sequence getters ──────────────────────────────────────────────────────
|
|
13
|
+
|
|
14
|
+
/** Highest action sequence seen (from joins, syncs, and live actions). */
|
|
15
|
+
game.getLastSequence = function () { return this._lastSequence || 0; };
|
|
16
|
+
|
|
17
|
+
/** Highest action sequence applied locally; trails while catching up. */
|
|
18
|
+
game.getLastAppliedSequence = function () { return this._lastActionApplied || 0; };
|
|
19
|
+
|
|
20
|
+
// ── Connection-state machine ──────────────────────────────────────────────
|
|
21
|
+
// connected → disconnected → rejoining → reconnected → connected.
|
|
22
|
+
// 'reconnected' is a transient notification state: observers see it, then
|
|
23
|
+
// the machine settles back to 'connected' in the same tick.
|
|
24
|
+
|
|
25
|
+
game._connState = 'disconnected';
|
|
26
|
+
game._awaitResync = false;
|
|
27
|
+
|
|
28
|
+
game._setConnState = function (state) {
|
|
29
|
+
if (this._connState === state) return;
|
|
30
|
+
this._connState = state;
|
|
31
|
+
this._dispatch('connectionState', state);
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
/** Current connection state, synchronously. */
|
|
35
|
+
game.getConnectionState = function () { return this._connState; };
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Observe connection-state transitions — one hook to drive a
|
|
39
|
+
* "Reconnecting…" overlay and gate input, identical on every transport.
|
|
40
|
+
* Single handler (last wins); returns an unsubscribe fn. For multiple
|
|
41
|
+
* listeners use game.on('connectionState', cb).
|
|
42
|
+
*/
|
|
43
|
+
game.onConnectionState = function (callback) { return this._setHandler('connectionState', callback); };
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Fires once per reconnect, after the resync completes, with
|
|
47
|
+
* { state, lastSequence, viaSync }. Restore local state here — it does NOT
|
|
48
|
+
* fire for a manually-requested sync.
|
|
49
|
+
*/
|
|
50
|
+
game.onReconnected = function (callback) { return this._setHandler('reconnected', callback); };
|
|
51
|
+
|
|
52
|
+
game.on('disconnect', function () {
|
|
53
|
+
game._awaitResync = false;
|
|
54
|
+
game._setConnState('disconnected');
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
game.on('reconnect', function () {
|
|
58
|
+
if (game.roomId) {
|
|
59
|
+
game._awaitResync = true;
|
|
60
|
+
game._setConnState('rejoining');
|
|
61
|
+
} else {
|
|
62
|
+
game._setConnState('connected');
|
|
63
|
+
}
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
game.on('sync', function (data) {
|
|
67
|
+
if (!game._awaitResync) return;
|
|
68
|
+
game._awaitResync = false;
|
|
69
|
+
game._dispatch('reconnected', {
|
|
70
|
+
state: (data && data.game_state) || null,
|
|
71
|
+
lastSequence: game._lastSequence || 0,
|
|
72
|
+
viaSync: true,
|
|
73
|
+
});
|
|
74
|
+
game._setConnState('reconnected');
|
|
75
|
+
game._setConnState('connected');
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
// Transport-level connects (initial connect in every mode) resolve through
|
|
79
|
+
// connect()/connectDirect() — reflect them in the state machine.
|
|
80
|
+
function wrapConnect(name) {
|
|
81
|
+
var orig = game[name];
|
|
82
|
+
if (typeof orig !== 'function') return;
|
|
83
|
+
game[name] = function () {
|
|
84
|
+
var self = this;
|
|
85
|
+
return orig.apply(self, arguments).then(function (result) {
|
|
86
|
+
if (!self._awaitResync) self._setConnState('connected');
|
|
87
|
+
return result;
|
|
88
|
+
});
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
wrapConnect('connect');
|
|
92
|
+
wrapConnect('connectDirect');
|
|
93
|
+
|
|
94
|
+
// ── Host tracking ─────────────────────────────────────────────────────────
|
|
95
|
+
// Kept on the game module (registered at module creation, so it never
|
|
96
|
+
// misses an event) — a syncedState created AFTER the join still knows who
|
|
97
|
+
// the authority is.
|
|
98
|
+
|
|
99
|
+
game._hostId = null;
|
|
100
|
+
function trackHost(d) {
|
|
101
|
+
if (!d) return;
|
|
102
|
+
if (d.host_id) game._hostId = d.host_id;
|
|
103
|
+
else if (d.player_ids && d.player_ids.length) game._hostId = d.player_ids[0];
|
|
104
|
+
}
|
|
105
|
+
game.on('joined', trackHost);
|
|
106
|
+
game.on('playerJoined', trackHost);
|
|
107
|
+
|
|
108
|
+
// ── syncedState ───────────────────────────────────────────────────────────
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Reconnect-safe authoritative shared state. Commits are sequenced actions
|
|
112
|
+
* applied through `reduce` on every client in the same order (deduped by
|
|
113
|
+
* sequence); the authority (player_ids[0] by default) auto-checkpoints via
|
|
114
|
+
* setState, and (re)joining clients recover automatically (checkpoint +
|
|
115
|
+
* un-checkpointed tail). Degrades to local-apply in direct mode.
|
|
116
|
+
* Full contract: SyncedStateOptions / SyncedState in types/index.d.ts.
|
|
117
|
+
*/
|
|
118
|
+
game.syncedState = function (initial, opts) {
|
|
119
|
+
opts = opts || {};
|
|
120
|
+
var self = this;
|
|
121
|
+
var reduce = typeof opts.reduce === 'function'
|
|
122
|
+
? opts.reduce
|
|
123
|
+
: function (state, action) { return Object.assign({}, state, action.data); };
|
|
124
|
+
var checkpointEvery = opts.checkpointEvery != null ? opts.checkpointEvery : 1;
|
|
125
|
+
var authorityMode = opts.authority === 'all' ? 'all' : 'host';
|
|
126
|
+
var defaultType = opts.type || 'update';
|
|
127
|
+
|
|
128
|
+
var state = initial === undefined ? {} : initial;
|
|
129
|
+
var appliedSeq = 0;
|
|
130
|
+
var sinceCheckpoint = 0;
|
|
131
|
+
var lastGapRequest = -1;
|
|
132
|
+
var changeCbs = [];
|
|
133
|
+
var offs = [];
|
|
134
|
+
|
|
135
|
+
function notify(reason) {
|
|
136
|
+
var copy = changeCbs.slice();
|
|
137
|
+
for (var i = 0; i < copy.length; i++) {
|
|
138
|
+
try { copy[i](state, reason); } catch (e) { Usion.log('syncedState onChange error: ' + e.message); }
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function isAuthority() {
|
|
143
|
+
if (authorityMode === 'all') return true;
|
|
144
|
+
var me = self.playerId || (Usion.user && Usion.user.getId && Usion.user.getId());
|
|
145
|
+
return !!(me && self._hostId && me === self._hostId);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function doCheckpoint() {
|
|
149
|
+
if (self.directMode) {
|
|
150
|
+
return Promise.resolve({ success: false, error: 'not_supported_in_direct_mode' });
|
|
151
|
+
}
|
|
152
|
+
// The wrapper carries the sequence the checkpoint reflects, so recovery
|
|
153
|
+
// knows exactly where the un-checkpointed tail starts.
|
|
154
|
+
return self.setState({ __usionSyncedState: 1, seq: appliedSeq, state: state });
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function maybeAutoCheckpoint() {
|
|
158
|
+
if (!(checkpointEvery > 0) || !isAuthority()) return;
|
|
159
|
+
sinceCheckpoint += 1;
|
|
160
|
+
if (sinceCheckpoint >= checkpointEvery) {
|
|
161
|
+
sinceCheckpoint = 0;
|
|
162
|
+
doCheckpoint();
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function applyOne(playerId, type, data, sequence) {
|
|
167
|
+
if (sequence !== undefined && sequence <= appliedSeq) return false; // dedupe
|
|
168
|
+
var next;
|
|
169
|
+
try {
|
|
170
|
+
next = reduce(state, { playerId: playerId, type: type, data: data, sequence: sequence });
|
|
171
|
+
} catch (e) {
|
|
172
|
+
Usion.log('syncedState reduce error: ' + e.message);
|
|
173
|
+
return false;
|
|
174
|
+
}
|
|
175
|
+
if (next !== undefined) state = next;
|
|
176
|
+
if (sequence !== undefined) appliedSeq = sequence;
|
|
177
|
+
return true;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function onAction(d) {
|
|
181
|
+
if (!d) return;
|
|
182
|
+
if (applyOne(d.player_id, d.action_type, d.action_data, d.sequence)) {
|
|
183
|
+
notify('action');
|
|
184
|
+
maybeAutoCheckpoint();
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// Recover from a join ack or sync payload: load the checkpoint (if it is
|
|
189
|
+
// ours and newer), replay the action tail in order, and if the server's
|
|
190
|
+
// sequence is still ahead of us, ask once for the missing range.
|
|
191
|
+
function recover(d) {
|
|
192
|
+
if (!d) return;
|
|
193
|
+
var changed = false;
|
|
194
|
+
|
|
195
|
+
var gs = d.game_state;
|
|
196
|
+
if (gs && gs.__usionSyncedState && typeof gs.seq === 'number'
|
|
197
|
+
&& gs.seq >= appliedSeq && gs.state !== undefined) {
|
|
198
|
+
state = gs.state;
|
|
199
|
+
appliedSeq = gs.seq;
|
|
200
|
+
changed = true;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
var actions = d.actions;
|
|
204
|
+
if (actions && actions.length) {
|
|
205
|
+
var sorted = actions.slice().sort(function (a, b) {
|
|
206
|
+
return (a.sequence || 0) - (b.sequence || 0);
|
|
207
|
+
});
|
|
208
|
+
for (var i = 0; i < sorted.length; i++) {
|
|
209
|
+
var item = sorted[i];
|
|
210
|
+
if (item && applyOne(item.player_id, item.action_type, item.action_data, item.sequence)) {
|
|
211
|
+
changed = true;
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
if (changed) notify('recover');
|
|
217
|
+
|
|
218
|
+
// Gap fill: the checkpoint may lag the room sequence (checkpointEvery>1).
|
|
219
|
+
// Request the tail from OUR applied sequence — once per position, so a
|
|
220
|
+
// trimmed history can't loop us.
|
|
221
|
+
if (d.sequence !== undefined && d.sequence > appliedSeq && lastGapRequest !== appliedSeq) {
|
|
222
|
+
lastGapRequest = appliedSeq;
|
|
223
|
+
try { self.requestSync(appliedSeq); } catch (e) { /* non-fatal */ }
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
offs.push(self.on('action', onAction));
|
|
228
|
+
offs.push(self.on('sync', recover));
|
|
229
|
+
offs.push(self.on('joined', recover));
|
|
230
|
+
|
|
231
|
+
return {
|
|
232
|
+
/** Current state value. */
|
|
233
|
+
get: function () { return state; },
|
|
234
|
+
|
|
235
|
+
/**
|
|
236
|
+
* Commit a change: commit(data) with the default action type, or
|
|
237
|
+
* commit(type, data, opts) to name it (opts forwarded to game.action,
|
|
238
|
+
* e.g. nextTurn / queueOffline). Applied exactly once via the echo.
|
|
239
|
+
*/
|
|
240
|
+
commit: function (typeOrData, data, actionOpts) {
|
|
241
|
+
var t, payload, o;
|
|
242
|
+
if (typeof typeOrData === 'string') { t = typeOrData; payload = data; o = actionOpts; }
|
|
243
|
+
else { t = defaultType; payload = typeOrData; o = data; }
|
|
244
|
+
if (self.directMode) {
|
|
245
|
+
// No platform echo in direct mode — apply locally, then send.
|
|
246
|
+
if (applyOne(self.playerId, t, payload, undefined)) notify('action');
|
|
247
|
+
return self.action(t, payload, o);
|
|
248
|
+
}
|
|
249
|
+
return self.action(t, payload, o);
|
|
250
|
+
},
|
|
251
|
+
|
|
252
|
+
/** Subscribe to changes: cb(state, reason). Returns an unsubscribe fn. */
|
|
253
|
+
onChange: function (cb) {
|
|
254
|
+
changeCbs.push(cb);
|
|
255
|
+
return function () {
|
|
256
|
+
var i = changeCbs.indexOf(cb);
|
|
257
|
+
if (i >= 0) changeCbs.splice(i, 1);
|
|
258
|
+
};
|
|
259
|
+
},
|
|
260
|
+
|
|
261
|
+
/** Whether this client is the checkpointing authority. */
|
|
262
|
+
isAuthority: isAuthority,
|
|
263
|
+
|
|
264
|
+
/** Force a server checkpoint now (no-op result in direct mode). */
|
|
265
|
+
checkpoint: function () { return doCheckpoint(); },
|
|
266
|
+
|
|
267
|
+
/** Sequence of the last action applied into this state. */
|
|
268
|
+
getSequence: function () { return appliedSeq; },
|
|
269
|
+
|
|
270
|
+
/** Detach all listeners; the instance stops receiving updates. */
|
|
271
|
+
destroy: function () {
|
|
272
|
+
for (var i = 0; i < offs.length; i++) { try { offs[i](); } catch (e) { /* noop */ } }
|
|
273
|
+
offs.length = 0;
|
|
274
|
+
changeCbs.length = 0;
|
|
275
|
+
},
|
|
276
|
+
};
|
|
277
|
+
};
|
|
278
|
+
}
|
|
@@ -44,7 +44,20 @@ export function applyGameSocket(game, Usion) {
|
|
|
44
44
|
// backend channel — robust to listener registration order.
|
|
45
45
|
if (Usion._bindBackendSocket) Usion._bindBackendSocket(self.socket);
|
|
46
46
|
|
|
47
|
+
// Socket.IO v4 emits 'reconnect' on the Manager, not the Socket — a
|
|
48
|
+
// Socket-level 'reconnect' listener never fires. Track attempts on the
|
|
49
|
+
// Manager and treat any connect after the first (per socket) as the
|
|
50
|
+
// reconnect signal, dispatched below AFTER rejoin gating is in place.
|
|
51
|
+
var hadConnect = false;
|
|
52
|
+
if (self.socket.io && typeof self.socket.io.on === 'function') {
|
|
53
|
+
self.socket.io.on('reconnect_attempt', function(n) {
|
|
54
|
+
self._reconnAttempts = n;
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
|
|
47
58
|
self.socket.on('connect', function() {
|
|
59
|
+
var isReconnect = hadConnect;
|
|
60
|
+
hadConnect = true;
|
|
48
61
|
self.connected = true;
|
|
49
62
|
self._connecting = false;
|
|
50
63
|
Usion.log('Game socket connected');
|
|
@@ -75,12 +88,21 @@ export function applyGameSocket(game, Usion) {
|
|
|
75
88
|
.then(function() {
|
|
76
89
|
self._rejoinPromise = null;
|
|
77
90
|
// Send any moves queued while offline, now that membership
|
|
78
|
-
// and sync are restored. (
|
|
79
|
-
//
|
|
91
|
+
// and sync are restored. (This connect handler is the reliable
|
|
92
|
+
// hook — see the Manager-vs-Socket note above.)
|
|
80
93
|
if (self._flushOfflineQueue) self._flushOfflineQueue();
|
|
81
94
|
});
|
|
82
95
|
}
|
|
83
96
|
|
|
97
|
+
// Dispatched after _rejoinPromise is set, so anything reacting to
|
|
98
|
+
// 'reconnect' (offline-queue flush, connection-state machine) sees
|
|
99
|
+
// sends gated behind the rejoin+resync.
|
|
100
|
+
if (isReconnect) {
|
|
101
|
+
Usion.log('Game socket reconnected');
|
|
102
|
+
self._dispatch('reconnect', self._reconnAttempts || 1);
|
|
103
|
+
self._reconnAttempts = 0;
|
|
104
|
+
}
|
|
105
|
+
|
|
84
106
|
resolve();
|
|
85
107
|
});
|
|
86
108
|
|
|
@@ -103,11 +125,6 @@ export function applyGameSocket(game, Usion) {
|
|
|
103
125
|
self._dispatch('disconnect', reason);
|
|
104
126
|
});
|
|
105
127
|
|
|
106
|
-
self.socket.on('reconnect', function(attemptNumber) {
|
|
107
|
-
Usion.log('Game socket reconnected after ' + attemptNumber + ' attempts');
|
|
108
|
-
self._dispatch('reconnect', attemptNumber);
|
|
109
|
-
});
|
|
110
|
-
|
|
111
128
|
// Game event handlers
|
|
112
129
|
self.socket.on('game:joined', function(data) {
|
|
113
130
|
if (data.sequence !== undefined) {
|
|
@@ -7,12 +7,15 @@
|
|
|
7
7
|
*
|
|
8
8
|
* const m = await Usion.matchmaking.find(); // resolves when matched
|
|
9
9
|
* await Usion.game.connect(); await Usion.game.join(m.roomId);
|
|
10
|
-
* // ...or cancel while waiting:
|
|
10
|
+
* // ...or bound the wait / cancel while waiting:
|
|
11
|
+
* const m2 = await Usion.matchmaking.find(null, { timeout: 30000 });
|
|
11
12
|
* Usion.matchmaking.cancel();
|
|
12
13
|
* Usion.matchmaking.onMatch(({ roomId, players }) => { ... });
|
|
13
14
|
*/
|
|
15
|
+
import { UsionError, toUsionError, ERROR_CODES } from './errors.js';
|
|
16
|
+
|
|
14
17
|
export function createMatchmakingModule(Usion) {
|
|
15
|
-
let pending = null; // { resolve, reject }
|
|
18
|
+
let pending = null; // active find() entry: { resolve, reject }
|
|
16
19
|
let onMatchCb = null;
|
|
17
20
|
let bound = false;
|
|
18
21
|
|
|
@@ -36,25 +39,64 @@ export function createMatchmakingModule(Usion) {
|
|
|
36
39
|
|
|
37
40
|
/**
|
|
38
41
|
* Join the queue for `serviceId` (defaults to the current game) and resolve
|
|
39
|
-
* when matched with { roomId, players, serviceId }.
|
|
40
|
-
* match (use cancel() to stop waiting)
|
|
42
|
+
* when matched with { roomId, players, serviceId }. Without `opts.timeout`
|
|
43
|
+
* it stays pending until a match (use cancel() to stop waiting); with it,
|
|
44
|
+
* rejects MATCH_TIMEOUT (and leaves the queue) once the wait elapses.
|
|
45
|
+
* @param {string} [serviceId]
|
|
46
|
+
* @param {{size?: number, timeout?: number}} [opts]
|
|
41
47
|
*/
|
|
42
48
|
find: function (serviceId, opts) {
|
|
43
49
|
opts = opts || {};
|
|
44
50
|
const sid = serviceId || (Usion.config && Usion.config.serviceId);
|
|
45
51
|
bind();
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
+
|
|
53
|
+
if (pending) {
|
|
54
|
+
const old = pending; pending = null;
|
|
55
|
+
old.reject(new UsionError(ERROR_CODES.SUPERSEDED, 'Superseded by a newer find()'));
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
let timer = null;
|
|
59
|
+
const entry = {};
|
|
60
|
+
const promise = new Promise(function (resolve, reject) {
|
|
61
|
+
entry.resolve = function (r) {
|
|
62
|
+
if (timer) clearTimeout(timer);
|
|
63
|
+
resolve(r);
|
|
64
|
+
};
|
|
65
|
+
entry.reject = function (e) {
|
|
66
|
+
if (timer) clearTimeout(timer);
|
|
67
|
+
if (pending === entry) pending = null;
|
|
68
|
+
reject(e);
|
|
69
|
+
};
|
|
52
70
|
});
|
|
71
|
+
|
|
72
|
+
// Register BEFORE emitting mm:join: when someone is already waiting in
|
|
73
|
+
// the queue, mm:matched can arrive ahead of the join ack — registering
|
|
74
|
+
// afterwards dropped that instant match and left find() hanging forever.
|
|
75
|
+
pending = entry;
|
|
76
|
+
|
|
77
|
+
if (opts.timeout > 0) {
|
|
78
|
+
timer = setTimeout(function () {
|
|
79
|
+
timer = null;
|
|
80
|
+
Usion._backendEmit('mm:cancel', {}).catch(function () { /* best-effort dequeue */ });
|
|
81
|
+
entry.reject(new UsionError(ERROR_CODES.MATCH_TIMEOUT,
|
|
82
|
+
'No match within ' + opts.timeout + 'ms'));
|
|
83
|
+
}, opts.timeout);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
Usion._backendEmit('mm:join', { service_id: sid, size: opts.size || 2 })
|
|
87
|
+
.catch(function (err) {
|
|
88
|
+
if (pending === entry) entry.reject(toUsionError(err));
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
return promise;
|
|
53
92
|
},
|
|
54
93
|
|
|
55
94
|
/** Leave the queue / stop waiting. */
|
|
56
95
|
cancel: function () {
|
|
57
|
-
if (pending) {
|
|
96
|
+
if (pending) {
|
|
97
|
+
const old = pending; pending = null;
|
|
98
|
+
old.reject(new UsionError(ERROR_CODES.CANCELLED, 'cancelled'));
|
|
99
|
+
}
|
|
58
100
|
return Usion._backendEmit('mm:cancel', {});
|
|
59
101
|
},
|
|
60
102
|
};
|
package/src/modules/misc.js
CHANGED
|
@@ -127,9 +127,10 @@ export const miscMethods = {
|
|
|
127
127
|
* Listen for messages from parent app
|
|
128
128
|
* @param {string} type - Message type to listen for
|
|
129
129
|
* @param {function} callback - Handler function
|
|
130
|
+
* @returns {function} Unsubscribe function
|
|
130
131
|
*/
|
|
131
132
|
on: function(type, callback) {
|
|
132
|
-
|
|
133
|
+
function listener(event) {
|
|
133
134
|
if (!isTrustedMessageSource(event)) return;
|
|
134
135
|
|
|
135
136
|
let data;
|
|
@@ -142,6 +143,10 @@ export const miscMethods = {
|
|
|
142
143
|
if (data.type === type) {
|
|
143
144
|
callback(data);
|
|
144
145
|
}
|
|
145
|
-
}
|
|
146
|
+
}
|
|
147
|
+
window.addEventListener('message', listener);
|
|
148
|
+
return function() {
|
|
149
|
+
window.removeEventListener('message', listener);
|
|
150
|
+
};
|
|
146
151
|
}
|
|
147
152
|
};
|
package/src/modules/wallet.js
CHANGED
|
@@ -21,13 +21,17 @@ export function createWalletModule(Usion) {
|
|
|
21
21
|
|
|
22
22
|
/**
|
|
23
23
|
* Get current wallet balance
|
|
24
|
+
* @param {object} [opts]
|
|
25
|
+
* @param {boolean} [opts.fresh] - Bypass the cache and re-query the host.
|
|
26
|
+
* Use after out-of-band balance changes (e.g. a server-side settle).
|
|
24
27
|
* @returns {Promise<number>} Balance in credits
|
|
25
28
|
*/
|
|
26
|
-
getBalance: function() {
|
|
29
|
+
getBalance: function(opts) {
|
|
27
30
|
const self = this;
|
|
31
|
+
const fresh = !!(opts && opts.fresh);
|
|
28
32
|
|
|
29
33
|
// If we have cached balance, return it
|
|
30
|
-
if (self._balance !== null) {
|
|
34
|
+
if (!fresh && self._balance !== null) {
|
|
31
35
|
return Promise.resolve(self._balance);
|
|
32
36
|
}
|
|
33
37
|
|
|
@@ -94,11 +98,14 @@ export function createWalletModule(Usion) {
|
|
|
94
98
|
settled = true;
|
|
95
99
|
cleanup();
|
|
96
100
|
// Update cached balance from the authoritative new balance, else
|
|
97
|
-
// best-effort subtract
|
|
101
|
+
// best-effort subtract now and re-query the host so the cache
|
|
102
|
+
// converges on the real value (an estimate would otherwise stick
|
|
103
|
+
// until reload).
|
|
98
104
|
if (response && response.newBalance !== undefined) {
|
|
99
105
|
self._balance = response.newBalance;
|
|
100
|
-
} else
|
|
101
|
-
self._balance -= amount;
|
|
106
|
+
} else {
|
|
107
|
+
if (self._balance !== null) self._balance -= amount;
|
|
108
|
+
self.getBalance({ fresh: true }).catch(function() { /* keep estimate */ });
|
|
102
109
|
}
|
|
103
110
|
resolve(response);
|
|
104
111
|
}
|