@usions/sdk 2.20.2 → 2.21.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 +316 -8
- package/src/modules/game-core.js +5 -0
- package/src/modules/game-methods.js +8 -0
- package/src/modules/game-reliability.js +278 -0
- package/src/modules/game-socket.js +24 -7
- package/types/index.d.ts +84 -0
package/package.json
CHANGED
package/src/browser.js
CHANGED
|
@@ -69,7 +69,7 @@ var Usion = (function () {
|
|
|
69
69
|
* Core Usion object with init, _post, _request
|
|
70
70
|
*/
|
|
71
71
|
const core = {
|
|
72
|
-
version: '2.
|
|
72
|
+
version: '2.21.0', // injected from package.json at build
|
|
73
73
|
config: {},
|
|
74
74
|
_initialized: false,
|
|
75
75
|
_initCallback: null,
|
|
@@ -1564,7 +1564,20 @@ var Usion = (function () {
|
|
|
1564
1564
|
// backend channel — robust to listener registration order.
|
|
1565
1565
|
if (Usion._bindBackendSocket) Usion._bindBackendSocket(self.socket);
|
|
1566
1566
|
|
|
1567
|
+
// Socket.IO v4 emits 'reconnect' on the Manager, not the Socket — a
|
|
1568
|
+
// Socket-level 'reconnect' listener never fires. Track attempts on the
|
|
1569
|
+
// Manager and treat any connect after the first (per socket) as the
|
|
1570
|
+
// reconnect signal, dispatched below AFTER rejoin gating is in place.
|
|
1571
|
+
var hadConnect = false;
|
|
1572
|
+
if (self.socket.io && typeof self.socket.io.on === 'function') {
|
|
1573
|
+
self.socket.io.on('reconnect_attempt', function(n) {
|
|
1574
|
+
self._reconnAttempts = n;
|
|
1575
|
+
});
|
|
1576
|
+
}
|
|
1577
|
+
|
|
1567
1578
|
self.socket.on('connect', function() {
|
|
1579
|
+
var isReconnect = hadConnect;
|
|
1580
|
+
hadConnect = true;
|
|
1568
1581
|
self.connected = true;
|
|
1569
1582
|
self._connecting = false;
|
|
1570
1583
|
Usion.log('Game socket connected');
|
|
@@ -1595,12 +1608,21 @@ var Usion = (function () {
|
|
|
1595
1608
|
.then(function() {
|
|
1596
1609
|
self._rejoinPromise = null;
|
|
1597
1610
|
// Send any moves queued while offline, now that membership
|
|
1598
|
-
// and sync are restored. (
|
|
1599
|
-
//
|
|
1611
|
+
// and sync are restored. (This connect handler is the reliable
|
|
1612
|
+
// hook — see the Manager-vs-Socket note above.)
|
|
1600
1613
|
if (self._flushOfflineQueue) self._flushOfflineQueue();
|
|
1601
1614
|
});
|
|
1602
1615
|
}
|
|
1603
1616
|
|
|
1617
|
+
// Dispatched after _rejoinPromise is set, so anything reacting to
|
|
1618
|
+
// 'reconnect' (offline-queue flush, connection-state machine) sees
|
|
1619
|
+
// sends gated behind the rejoin+resync.
|
|
1620
|
+
if (isReconnect) {
|
|
1621
|
+
Usion.log('Game socket reconnected');
|
|
1622
|
+
self._dispatch('reconnect', self._reconnAttempts || 1);
|
|
1623
|
+
self._reconnAttempts = 0;
|
|
1624
|
+
}
|
|
1625
|
+
|
|
1604
1626
|
resolve();
|
|
1605
1627
|
});
|
|
1606
1628
|
|
|
@@ -1623,11 +1645,6 @@ var Usion = (function () {
|
|
|
1623
1645
|
self._dispatch('disconnect', reason);
|
|
1624
1646
|
});
|
|
1625
1647
|
|
|
1626
|
-
self.socket.on('reconnect', function(attemptNumber) {
|
|
1627
|
-
Usion.log('Game socket reconnected after ' + attemptNumber + ' attempts');
|
|
1628
|
-
self._dispatch('reconnect', attemptNumber);
|
|
1629
|
-
});
|
|
1630
|
-
|
|
1631
1648
|
// Game event handlers
|
|
1632
1649
|
self.socket.on('game:joined', function(data) {
|
|
1633
1650
|
if (data.sequence !== undefined) {
|
|
@@ -2043,7 +2060,15 @@ var Usion = (function () {
|
|
|
2043
2060
|
self._joined = true;
|
|
2044
2061
|
if (response.sequence !== undefined) {
|
|
2045
2062
|
self._lastSequence = response.sequence;
|
|
2063
|
+
// The joined state reflects everything up to this sequence —
|
|
2064
|
+
// actions at or below it must not be re-delivered (same
|
|
2065
|
+
// baseline the proxy transport sets from GAME_JOINED).
|
|
2066
|
+
self._lastActionApplied = Math.max(self._lastActionApplied, response.sequence);
|
|
2046
2067
|
}
|
|
2068
|
+
// Transport parity: proxy and direct modes dispatch 'joined';
|
|
2069
|
+
// the socket transport now does too, so onJoined / recovery
|
|
2070
|
+
// logic behaves the same everywhere.
|
|
2071
|
+
self._dispatch('joined', response);
|
|
2047
2072
|
resolve(response);
|
|
2048
2073
|
}
|
|
2049
2074
|
});
|
|
@@ -4615,6 +4640,285 @@ var Usion = (function () {
|
|
|
4615
4640
|
game.getRtt = function () { return this._pingMeter ? this._pingMeter.rtt : null; };
|
|
4616
4641
|
}
|
|
4617
4642
|
|
|
4643
|
+
/**
|
|
4644
|
+
* Usion SDK Game Reliability — the reconnection-lifecycle surface promised in
|
|
4645
|
+
* v2.16: sequence getters, the unified connection-state machine, and
|
|
4646
|
+
* syncedState (reconnect-safe authoritative shared state).
|
|
4647
|
+
*
|
|
4648
|
+
* Everything here rides the events the game module already dispatches
|
|
4649
|
+
* ('disconnect' / 'reconnect' / 'joined' / 'sync' / 'action'), so it behaves
|
|
4650
|
+
* identically across the socket, embedded-proxy, and direct transports.
|
|
4651
|
+
*/
|
|
4652
|
+
|
|
4653
|
+
function applyGameReliability(game, Usion) {
|
|
4654
|
+
// ── Sequence getters ──────────────────────────────────────────────────────
|
|
4655
|
+
|
|
4656
|
+
/** Highest action sequence seen (from joins, syncs, and live actions). */
|
|
4657
|
+
game.getLastSequence = function () { return this._lastSequence || 0; };
|
|
4658
|
+
|
|
4659
|
+
/** Highest action sequence applied locally; trails while catching up. */
|
|
4660
|
+
game.getLastAppliedSequence = function () { return this._lastActionApplied || 0; };
|
|
4661
|
+
|
|
4662
|
+
// ── Connection-state machine ──────────────────────────────────────────────
|
|
4663
|
+
// connected → disconnected → rejoining → reconnected → connected.
|
|
4664
|
+
// 'reconnected' is a transient notification state: observers see it, then
|
|
4665
|
+
// the machine settles back to 'connected' in the same tick.
|
|
4666
|
+
|
|
4667
|
+
game._connState = 'disconnected';
|
|
4668
|
+
game._awaitResync = false;
|
|
4669
|
+
|
|
4670
|
+
game._setConnState = function (state) {
|
|
4671
|
+
if (this._connState === state) return;
|
|
4672
|
+
this._connState = state;
|
|
4673
|
+
this._dispatch('connectionState', state);
|
|
4674
|
+
};
|
|
4675
|
+
|
|
4676
|
+
/** Current connection state, synchronously. */
|
|
4677
|
+
game.getConnectionState = function () { return this._connState; };
|
|
4678
|
+
|
|
4679
|
+
/**
|
|
4680
|
+
* Observe connection-state transitions — one hook to drive a
|
|
4681
|
+
* "Reconnecting…" overlay and gate input, identical on every transport.
|
|
4682
|
+
* Single handler (last wins); returns an unsubscribe fn. For multiple
|
|
4683
|
+
* listeners use game.on('connectionState', cb).
|
|
4684
|
+
*/
|
|
4685
|
+
game.onConnectionState = function (callback) { return this._setHandler('connectionState', callback); };
|
|
4686
|
+
|
|
4687
|
+
/**
|
|
4688
|
+
* Fires once per reconnect, after the resync completes, with
|
|
4689
|
+
* { state, lastSequence, viaSync }. Restore local state here — it does NOT
|
|
4690
|
+
* fire for a manually-requested sync.
|
|
4691
|
+
*/
|
|
4692
|
+
game.onReconnected = function (callback) { return this._setHandler('reconnected', callback); };
|
|
4693
|
+
|
|
4694
|
+
game.on('disconnect', function () {
|
|
4695
|
+
game._awaitResync = false;
|
|
4696
|
+
game._setConnState('disconnected');
|
|
4697
|
+
});
|
|
4698
|
+
|
|
4699
|
+
game.on('reconnect', function () {
|
|
4700
|
+
if (game.roomId) {
|
|
4701
|
+
game._awaitResync = true;
|
|
4702
|
+
game._setConnState('rejoining');
|
|
4703
|
+
} else {
|
|
4704
|
+
game._setConnState('connected');
|
|
4705
|
+
}
|
|
4706
|
+
});
|
|
4707
|
+
|
|
4708
|
+
game.on('sync', function (data) {
|
|
4709
|
+
if (!game._awaitResync) return;
|
|
4710
|
+
game._awaitResync = false;
|
|
4711
|
+
game._dispatch('reconnected', {
|
|
4712
|
+
state: (data && data.game_state) || null,
|
|
4713
|
+
lastSequence: game._lastSequence || 0,
|
|
4714
|
+
viaSync: true,
|
|
4715
|
+
});
|
|
4716
|
+
game._setConnState('reconnected');
|
|
4717
|
+
game._setConnState('connected');
|
|
4718
|
+
});
|
|
4719
|
+
|
|
4720
|
+
// Transport-level connects (initial connect in every mode) resolve through
|
|
4721
|
+
// connect()/connectDirect() — reflect them in the state machine.
|
|
4722
|
+
function wrapConnect(name) {
|
|
4723
|
+
var orig = game[name];
|
|
4724
|
+
if (typeof orig !== 'function') return;
|
|
4725
|
+
game[name] = function () {
|
|
4726
|
+
var self = this;
|
|
4727
|
+
return orig.apply(self, arguments).then(function (result) {
|
|
4728
|
+
if (!self._awaitResync) self._setConnState('connected');
|
|
4729
|
+
return result;
|
|
4730
|
+
});
|
|
4731
|
+
};
|
|
4732
|
+
}
|
|
4733
|
+
wrapConnect('connect');
|
|
4734
|
+
wrapConnect('connectDirect');
|
|
4735
|
+
|
|
4736
|
+
// ── Host tracking ─────────────────────────────────────────────────────────
|
|
4737
|
+
// Kept on the game module (registered at module creation, so it never
|
|
4738
|
+
// misses an event) — a syncedState created AFTER the join still knows who
|
|
4739
|
+
// the authority is.
|
|
4740
|
+
|
|
4741
|
+
game._hostId = null;
|
|
4742
|
+
function trackHost(d) {
|
|
4743
|
+
if (!d) return;
|
|
4744
|
+
if (d.host_id) game._hostId = d.host_id;
|
|
4745
|
+
else if (d.player_ids && d.player_ids.length) game._hostId = d.player_ids[0];
|
|
4746
|
+
}
|
|
4747
|
+
game.on('joined', trackHost);
|
|
4748
|
+
game.on('playerJoined', trackHost);
|
|
4749
|
+
|
|
4750
|
+
// ── syncedState ───────────────────────────────────────────────────────────
|
|
4751
|
+
|
|
4752
|
+
/**
|
|
4753
|
+
* Reconnect-safe authoritative shared state. Commits are sequenced actions
|
|
4754
|
+
* applied through `reduce` on every client in the same order (deduped by
|
|
4755
|
+
* sequence); the authority (player_ids[0] by default) auto-checkpoints via
|
|
4756
|
+
* setState, and (re)joining clients recover automatically (checkpoint +
|
|
4757
|
+
* un-checkpointed tail). Degrades to local-apply in direct mode.
|
|
4758
|
+
* Full contract: SyncedStateOptions / SyncedState in types/index.d.ts.
|
|
4759
|
+
*/
|
|
4760
|
+
game.syncedState = function (initial, opts) {
|
|
4761
|
+
opts = opts || {};
|
|
4762
|
+
var self = this;
|
|
4763
|
+
var reduce = typeof opts.reduce === 'function'
|
|
4764
|
+
? opts.reduce
|
|
4765
|
+
: function (state, action) { return Object.assign({}, state, action.data); };
|
|
4766
|
+
var checkpointEvery = opts.checkpointEvery != null ? opts.checkpointEvery : 1;
|
|
4767
|
+
var authorityMode = opts.authority === 'all' ? 'all' : 'host';
|
|
4768
|
+
var defaultType = opts.type || 'update';
|
|
4769
|
+
|
|
4770
|
+
var state = initial === undefined ? {} : initial;
|
|
4771
|
+
var appliedSeq = 0;
|
|
4772
|
+
var sinceCheckpoint = 0;
|
|
4773
|
+
var lastGapRequest = -1;
|
|
4774
|
+
var changeCbs = [];
|
|
4775
|
+
var offs = [];
|
|
4776
|
+
|
|
4777
|
+
function notify(reason) {
|
|
4778
|
+
var copy = changeCbs.slice();
|
|
4779
|
+
for (var i = 0; i < copy.length; i++) {
|
|
4780
|
+
try { copy[i](state, reason); } catch (e) { Usion.log('syncedState onChange error: ' + e.message); }
|
|
4781
|
+
}
|
|
4782
|
+
}
|
|
4783
|
+
|
|
4784
|
+
function isAuthority() {
|
|
4785
|
+
if (authorityMode === 'all') return true;
|
|
4786
|
+
var me = self.playerId || (Usion.user && Usion.user.getId && Usion.user.getId());
|
|
4787
|
+
return !!(me && self._hostId && me === self._hostId);
|
|
4788
|
+
}
|
|
4789
|
+
|
|
4790
|
+
function doCheckpoint() {
|
|
4791
|
+
if (self.directMode) {
|
|
4792
|
+
return Promise.resolve({ success: false, error: 'not_supported_in_direct_mode' });
|
|
4793
|
+
}
|
|
4794
|
+
// The wrapper carries the sequence the checkpoint reflects, so recovery
|
|
4795
|
+
// knows exactly where the un-checkpointed tail starts.
|
|
4796
|
+
return self.setState({ __usionSyncedState: 1, seq: appliedSeq, state: state });
|
|
4797
|
+
}
|
|
4798
|
+
|
|
4799
|
+
function maybeAutoCheckpoint() {
|
|
4800
|
+
if (!(checkpointEvery > 0) || !isAuthority()) return;
|
|
4801
|
+
sinceCheckpoint += 1;
|
|
4802
|
+
if (sinceCheckpoint >= checkpointEvery) {
|
|
4803
|
+
sinceCheckpoint = 0;
|
|
4804
|
+
doCheckpoint();
|
|
4805
|
+
}
|
|
4806
|
+
}
|
|
4807
|
+
|
|
4808
|
+
function applyOne(playerId, type, data, sequence) {
|
|
4809
|
+
if (sequence !== undefined && sequence <= appliedSeq) return false; // dedupe
|
|
4810
|
+
var next;
|
|
4811
|
+
try {
|
|
4812
|
+
next = reduce(state, { playerId: playerId, type: type, data: data, sequence: sequence });
|
|
4813
|
+
} catch (e) {
|
|
4814
|
+
Usion.log('syncedState reduce error: ' + e.message);
|
|
4815
|
+
return false;
|
|
4816
|
+
}
|
|
4817
|
+
if (next !== undefined) state = next;
|
|
4818
|
+
if (sequence !== undefined) appliedSeq = sequence;
|
|
4819
|
+
return true;
|
|
4820
|
+
}
|
|
4821
|
+
|
|
4822
|
+
function onAction(d) {
|
|
4823
|
+
if (!d) return;
|
|
4824
|
+
if (applyOne(d.player_id, d.action_type, d.action_data, d.sequence)) {
|
|
4825
|
+
notify('action');
|
|
4826
|
+
maybeAutoCheckpoint();
|
|
4827
|
+
}
|
|
4828
|
+
}
|
|
4829
|
+
|
|
4830
|
+
// Recover from a join ack or sync payload: load the checkpoint (if it is
|
|
4831
|
+
// ours and newer), replay the action tail in order, and if the server's
|
|
4832
|
+
// sequence is still ahead of us, ask once for the missing range.
|
|
4833
|
+
function recover(d) {
|
|
4834
|
+
if (!d) return;
|
|
4835
|
+
var changed = false;
|
|
4836
|
+
|
|
4837
|
+
var gs = d.game_state;
|
|
4838
|
+
if (gs && gs.__usionSyncedState && typeof gs.seq === 'number'
|
|
4839
|
+
&& gs.seq >= appliedSeq && gs.state !== undefined) {
|
|
4840
|
+
state = gs.state;
|
|
4841
|
+
appliedSeq = gs.seq;
|
|
4842
|
+
changed = true;
|
|
4843
|
+
}
|
|
4844
|
+
|
|
4845
|
+
var actions = d.actions;
|
|
4846
|
+
if (actions && actions.length) {
|
|
4847
|
+
var sorted = actions.slice().sort(function (a, b) {
|
|
4848
|
+
return (a.sequence || 0) - (b.sequence || 0);
|
|
4849
|
+
});
|
|
4850
|
+
for (var i = 0; i < sorted.length; i++) {
|
|
4851
|
+
var item = sorted[i];
|
|
4852
|
+
if (item && applyOne(item.player_id, item.action_type, item.action_data, item.sequence)) {
|
|
4853
|
+
changed = true;
|
|
4854
|
+
}
|
|
4855
|
+
}
|
|
4856
|
+
}
|
|
4857
|
+
|
|
4858
|
+
if (changed) notify('recover');
|
|
4859
|
+
|
|
4860
|
+
// Gap fill: the checkpoint may lag the room sequence (checkpointEvery>1).
|
|
4861
|
+
// Request the tail from OUR applied sequence — once per position, so a
|
|
4862
|
+
// trimmed history can't loop us.
|
|
4863
|
+
if (d.sequence !== undefined && d.sequence > appliedSeq && lastGapRequest !== appliedSeq) {
|
|
4864
|
+
lastGapRequest = appliedSeq;
|
|
4865
|
+
try { self.requestSync(appliedSeq); } catch (e) { /* non-fatal */ }
|
|
4866
|
+
}
|
|
4867
|
+
}
|
|
4868
|
+
|
|
4869
|
+
offs.push(self.on('action', onAction));
|
|
4870
|
+
offs.push(self.on('sync', recover));
|
|
4871
|
+
offs.push(self.on('joined', recover));
|
|
4872
|
+
|
|
4873
|
+
return {
|
|
4874
|
+
/** Current state value. */
|
|
4875
|
+
get: function () { return state; },
|
|
4876
|
+
|
|
4877
|
+
/**
|
|
4878
|
+
* Commit a change: commit(data) with the default action type, or
|
|
4879
|
+
* commit(type, data, opts) to name it (opts forwarded to game.action,
|
|
4880
|
+
* e.g. nextTurn / queueOffline). Applied exactly once via the echo.
|
|
4881
|
+
*/
|
|
4882
|
+
commit: function (typeOrData, data, actionOpts) {
|
|
4883
|
+
var t, payload, o;
|
|
4884
|
+
if (typeof typeOrData === 'string') { t = typeOrData; payload = data; o = actionOpts; }
|
|
4885
|
+
else { t = defaultType; payload = typeOrData; o = data; }
|
|
4886
|
+
if (self.directMode) {
|
|
4887
|
+
// No platform echo in direct mode — apply locally, then send.
|
|
4888
|
+
if (applyOne(self.playerId, t, payload, undefined)) notify('action');
|
|
4889
|
+
return self.action(t, payload, o);
|
|
4890
|
+
}
|
|
4891
|
+
return self.action(t, payload, o);
|
|
4892
|
+
},
|
|
4893
|
+
|
|
4894
|
+
/** Subscribe to changes: cb(state, reason). Returns an unsubscribe fn. */
|
|
4895
|
+
onChange: function (cb) {
|
|
4896
|
+
changeCbs.push(cb);
|
|
4897
|
+
return function () {
|
|
4898
|
+
var i = changeCbs.indexOf(cb);
|
|
4899
|
+
if (i >= 0) changeCbs.splice(i, 1);
|
|
4900
|
+
};
|
|
4901
|
+
},
|
|
4902
|
+
|
|
4903
|
+
/** Whether this client is the checkpointing authority. */
|
|
4904
|
+
isAuthority: isAuthority,
|
|
4905
|
+
|
|
4906
|
+
/** Force a server checkpoint now (no-op result in direct mode). */
|
|
4907
|
+
checkpoint: function () { return doCheckpoint(); },
|
|
4908
|
+
|
|
4909
|
+
/** Sequence of the last action applied into this state. */
|
|
4910
|
+
getSequence: function () { return appliedSeq; },
|
|
4911
|
+
|
|
4912
|
+
/** Detach all listeners; the instance stops receiving updates. */
|
|
4913
|
+
destroy: function () {
|
|
4914
|
+
for (var i = 0; i < offs.length; i++) { try { offs[i](); } catch (e) { /* noop */ } }
|
|
4915
|
+
offs.length = 0;
|
|
4916
|
+
changeCbs.length = 0;
|
|
4917
|
+
},
|
|
4918
|
+
};
|
|
4919
|
+
};
|
|
4920
|
+
}
|
|
4921
|
+
|
|
4618
4922
|
/**
|
|
4619
4923
|
* Usion SDK Game Core — game module base, connect routing, event registrations
|
|
4620
4924
|
*/
|
|
@@ -4640,6 +4944,8 @@ var Usion = (function () {
|
|
|
4640
4944
|
reconnect: 'reconnect',
|
|
4641
4945
|
connection_error: 'connectionError',
|
|
4642
4946
|
room_assigned: 'roomAssigned',
|
|
4947
|
+
connection_state: 'connectionState',
|
|
4948
|
+
reconnected: 'reconnected',
|
|
4643
4949
|
};
|
|
4644
4950
|
|
|
4645
4951
|
function _normalizeEventName(event) {
|
|
@@ -4853,6 +5159,8 @@ var Usion = (function () {
|
|
|
4853
5159
|
applyGameProxy(game, Usion);
|
|
4854
5160
|
applyGameMethods(game, Usion);
|
|
4855
5161
|
applyGameNetcode(game, Usion);
|
|
5162
|
+
// Applied last: wraps connect/connectDirect and rides dispatched events.
|
|
5163
|
+
applyGameReliability(game, Usion);
|
|
4856
5164
|
|
|
4857
5165
|
// Foreground catch-up safety net (generic across every transport).
|
|
4858
5166
|
//
|
package/src/modules/game-core.js
CHANGED
|
@@ -7,6 +7,7 @@ import { applyGameSocket } from './game-socket.js';
|
|
|
7
7
|
import { applyGameProxy } from './game-proxy.js';
|
|
8
8
|
import { applyGameMethods } from './game-methods.js';
|
|
9
9
|
import { applyGameNetcode } from './game-netcode.js';
|
|
10
|
+
import { applyGameReliability } from './game-reliability.js';
|
|
10
11
|
|
|
11
12
|
// Map any reasonable spelling of a game event onto the internal handler
|
|
12
13
|
// name: 'game:player_joined' / 'player_joined' / 'playerJoined' → 'playerJoined'.
|
|
@@ -28,6 +29,8 @@ const _EVENT_ALIASES = {
|
|
|
28
29
|
reconnect: 'reconnect',
|
|
29
30
|
connection_error: 'connectionError',
|
|
30
31
|
room_assigned: 'roomAssigned',
|
|
32
|
+
connection_state: 'connectionState',
|
|
33
|
+
reconnected: 'reconnected',
|
|
31
34
|
};
|
|
32
35
|
|
|
33
36
|
function _normalizeEventName(event) {
|
|
@@ -241,6 +244,8 @@ export function createGameModule(Usion) {
|
|
|
241
244
|
applyGameProxy(game, Usion);
|
|
242
245
|
applyGameMethods(game, Usion);
|
|
243
246
|
applyGameNetcode(game, Usion);
|
|
247
|
+
// Applied last: wraps connect/connectDirect and rides dispatched events.
|
|
248
|
+
applyGameReliability(game, Usion);
|
|
244
249
|
|
|
245
250
|
// Foreground catch-up safety net (generic across every transport).
|
|
246
251
|
//
|
|
@@ -71,7 +71,15 @@ export function applyGameMethods(game, Usion) {
|
|
|
71
71
|
self._joined = true;
|
|
72
72
|
if (response.sequence !== undefined) {
|
|
73
73
|
self._lastSequence = response.sequence;
|
|
74
|
+
// The joined state reflects everything up to this sequence —
|
|
75
|
+
// actions at or below it must not be re-delivered (same
|
|
76
|
+
// baseline the proxy transport sets from GAME_JOINED).
|
|
77
|
+
self._lastActionApplied = Math.max(self._lastActionApplied, response.sequence);
|
|
74
78
|
}
|
|
79
|
+
// Transport parity: proxy and direct modes dispatch 'joined';
|
|
80
|
+
// the socket transport now does too, so onJoined / recovery
|
|
81
|
+
// logic behaves the same everywhere.
|
|
82
|
+
self._dispatch('joined', response);
|
|
75
83
|
resolve(response);
|
|
76
84
|
}
|
|
77
85
|
});
|
|
@@ -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) {
|
package/types/index.d.ts
CHANGED
|
@@ -274,6 +274,60 @@ export interface GameInviteResult {
|
|
|
274
274
|
invited: string[];
|
|
275
275
|
}
|
|
276
276
|
|
|
277
|
+
/** Unified connection-state machine, identical across all transports. */
|
|
278
|
+
export type GameConnectionState = 'connected' | 'disconnected' | 'rejoining' | 'reconnected';
|
|
279
|
+
|
|
280
|
+
/** Payload of game.onReconnected — fires once after a reconnect's resync. */
|
|
281
|
+
export interface ReconnectedInfo {
|
|
282
|
+
/** The server checkpoint from the resync (may be empty). */
|
|
283
|
+
state: Record<string, any> | null;
|
|
284
|
+
/** Highest action sequence after the resync. */
|
|
285
|
+
lastSequence: number;
|
|
286
|
+
/** True when recovery came through a game:sync payload. */
|
|
287
|
+
viaSync: boolean;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
/** An action as seen by a syncedState reducer. */
|
|
291
|
+
export interface SyncedStateAction {
|
|
292
|
+
playerId: string;
|
|
293
|
+
type: string;
|
|
294
|
+
data: any;
|
|
295
|
+
/** Authoritative sequence; undefined only for direct-mode local applies. */
|
|
296
|
+
sequence: number | undefined;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
/** Options for game.syncedState(). */
|
|
300
|
+
export interface SyncedStateOptions<S = Record<string, any>> {
|
|
301
|
+
/** Pure reducer applied to every action; default shallow-merges action.data. */
|
|
302
|
+
reduce?: (state: S, action: SyncedStateAction) => S;
|
|
303
|
+
/** Authority checkpoint cadence in actions (1 = every action; 0 disables). */
|
|
304
|
+
checkpointEvery?: number;
|
|
305
|
+
/** Who checkpoints: 'host' (player_ids[0], default) or 'all' participants. */
|
|
306
|
+
authority?: 'host' | 'all';
|
|
307
|
+
/** Default action type used by commit(data) (default 'update'). */
|
|
308
|
+
type?: string;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
/** Reconnect-safe authoritative shared state (see game.syncedState). */
|
|
312
|
+
export interface SyncedState<S = Record<string, any>> {
|
|
313
|
+
/** Current state value. */
|
|
314
|
+
get(): S;
|
|
315
|
+
/** Commit with the default action type. Applied exactly once via the echo. */
|
|
316
|
+
commit(data: Record<string, any>): Promise<ActionResult>;
|
|
317
|
+
/** Commit a named action; opts forwarded to game.action (nextTurn, …). */
|
|
318
|
+
commit(type: string, data?: Record<string, any>, opts?: GameActionOptions): Promise<ActionResult>;
|
|
319
|
+
/** Subscribe to changes: cb(state, reason). Returns an unsubscribe fn. */
|
|
320
|
+
onChange(callback: (state: S, reason: 'action' | 'recover') => void): UnsubscribeFn;
|
|
321
|
+
/** Whether this client is the checkpointing authority. */
|
|
322
|
+
isAuthority(): boolean;
|
|
323
|
+
/** Force a server checkpoint now (no-op result in direct mode). */
|
|
324
|
+
checkpoint(): Promise<{ success: boolean; error?: string; code?: UsionErrorCode }>;
|
|
325
|
+
/** Sequence of the last action applied into this state. */
|
|
326
|
+
getSequence(): number;
|
|
327
|
+
/** Detach all listeners; the instance stops receiving updates. */
|
|
328
|
+
destroy(): void;
|
|
329
|
+
}
|
|
330
|
+
|
|
277
331
|
export interface GameModule {
|
|
278
332
|
// Connection
|
|
279
333
|
connect(socketUrl?: string, token?: string): Promise<void>;
|
|
@@ -368,6 +422,36 @@ export interface GameModule {
|
|
|
368
422
|
*/
|
|
369
423
|
onRoomAssigned(callback: (data: { roomId: string }) => void): UnsubscribeFn;
|
|
370
424
|
|
|
425
|
+
// ─── Reconnection lifecycle (SDK ≥ 2.21; documented since 2.16) ──────────
|
|
426
|
+
/** Highest action sequence seen (joins, syncs, live actions). */
|
|
427
|
+
getLastSequence(): number;
|
|
428
|
+
/** Highest action sequence applied locally; trails while catching up. */
|
|
429
|
+
getLastAppliedSequence(): number;
|
|
430
|
+
/** Current connection state, synchronously. */
|
|
431
|
+
getConnectionState(): GameConnectionState;
|
|
432
|
+
/**
|
|
433
|
+
* Observe connection-state transitions ('connected' → 'disconnected' →
|
|
434
|
+
* 'rejoining' → 'reconnected' → 'connected') — identical across the socket,
|
|
435
|
+
* embedded-proxy, and direct transports. One hook to drive a
|
|
436
|
+
* "Reconnecting…" overlay and gate input.
|
|
437
|
+
*/
|
|
438
|
+
onConnectionState(callback: (state: GameConnectionState) => void): UnsubscribeFn;
|
|
439
|
+
/**
|
|
440
|
+
* Fires once per reconnect, after the resync completes. Restore local
|
|
441
|
+
* state here — it does NOT fire for a manually-requested sync.
|
|
442
|
+
*/
|
|
443
|
+
onReconnected(callback: (info: ReconnectedInfo) => void): UnsubscribeFn;
|
|
444
|
+
/**
|
|
445
|
+
* Reconnect-safe authoritative shared state. Commits are sequenced actions
|
|
446
|
+
* applied through `reduce` on every client in the same order (deduped by
|
|
447
|
+
* sequence); the authority (player_ids[0] by default) auto-checkpoints via
|
|
448
|
+
* setState, and (re)joining clients recover automatically (load checkpoint,
|
|
449
|
+
* replay the un-checkpointed tail). Use for state that must survive a
|
|
450
|
+
* disconnect — unlike replicate() (realtime, lossy). Degrades to
|
|
451
|
+
* local-apply in direct mode (no platform checkpoint there).
|
|
452
|
+
*/
|
|
453
|
+
syncedState<S = Record<string, any>>(initial: S, opts?: SyncedStateOptions<S>): SyncedState<S>;
|
|
454
|
+
|
|
371
455
|
/**
|
|
372
456
|
* Register an ADDITIONAL event listener. Unlike the onX methods this
|
|
373
457
|
* supports multiple listeners per event, can be called before connect(),
|