@usions/sdk 2.1.5 → 2.10.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 +118 -0
- package/package.json +6 -3
- package/src/browser.js +2658 -124
- package/src/modules/backend-channel.js +76 -0
- package/src/modules/core.js +61 -0
- package/src/modules/game-core.js +4 -0
- package/src/modules/game-direct.js +45 -0
- package/src/modules/game-methods.js +107 -1
- package/src/modules/game-netcode.js +346 -0
- package/src/modules/game-proxy.js +4 -0
- package/src/modules/game-socket.js +4 -0
- package/src/modules/index.js +13 -0
- package/src/modules/leaderboard.js +46 -0
- package/src/modules/lobby.js +103 -0
- package/src/modules/matchmaking.js +61 -0
- package/src/modules/misc.js +4 -0
- package/src/modules/netcode/binary.js +113 -0
- package/src/modules/netcode/delta.js +236 -0
- package/src/modules/netcode/index.js +41 -0
- package/src/modules/netcode/interpolation.js +235 -0
- package/src/modules/netcode/lagcomp.js +104 -0
- package/src/modules/netcode/lockstep.js +103 -0
- package/src/modules/netcode/mesh-network.js +103 -0
- package/src/modules/netcode/mesh.js +188 -0
- package/src/modules/netcode/netsim.js +77 -0
- package/src/modules/netcode/ping.js +69 -0
- package/src/modules/netcode/prediction.js +113 -0
- package/src/modules/netcode/sender.js +102 -0
- package/src/modules/netcode/webtransport.js +195 -0
- package/src/modules/wallet.js +5 -1
- package/types/index.d.ts +463 -0
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Usion SDK Netcode — Client-side prediction + server reconciliation.
|
|
3
|
+
*
|
|
4
|
+
* Apply local input instantly (tagged with a sequence number); when the
|
|
5
|
+
* authoritative state arrives carrying the last input the server processed,
|
|
6
|
+
* snap to it and replay every still-unacknowledged input on top. The local
|
|
7
|
+
* player sees zero input delay while staying consistent with the server.
|
|
8
|
+
*
|
|
9
|
+
* Error smoothing (Overwatch / Gabriel Gambetta): hard-snapping the render
|
|
10
|
+
* position on every correction looks jittery. Instead we keep the visible
|
|
11
|
+
* position as `corrected + error`, where `error` is the gap a correction
|
|
12
|
+
* introduced, and decay that error to zero over a few frames — so corrections
|
|
13
|
+
* are smooth and, in the common case, invisible. Enable via `opts.smooth`.
|
|
14
|
+
*
|
|
15
|
+
* The game supplies a pure `apply(state, input) -> newState` (must not mutate).
|
|
16
|
+
*/
|
|
17
|
+
function parseSmoothKeys(smooth) {
|
|
18
|
+
if (!smooth) return null;
|
|
19
|
+
let keys = smooth.keys != null ? smooth.keys : smooth;
|
|
20
|
+
if (typeof keys === 'string') keys = keys.trim().split(/\s+/).filter(Boolean);
|
|
21
|
+
return Array.isArray(keys) && keys.length ? keys : null;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export class Predictor {
|
|
25
|
+
/**
|
|
26
|
+
* @param {object} opts
|
|
27
|
+
* @param {(state:any, input:any)=>any} opts.apply Pure state transition.
|
|
28
|
+
* @param {any} [opts.initialState]
|
|
29
|
+
* @param {{keys?:string|string[], rate?:number, snapTo?:number}|string} [opts.smooth]
|
|
30
|
+
* Error-smoothing config. `keys` are numeric fields to blend; `rate` is the
|
|
31
|
+
* per-frame decay (0..1, default 0.2); `snapTo` is the residual below which
|
|
32
|
+
* the error is zeroed (default 0.001).
|
|
33
|
+
*/
|
|
34
|
+
constructor(opts = {}) {
|
|
35
|
+
if (typeof opts.apply !== 'function') throw new Error('Predictor requires an apply(state, input) function');
|
|
36
|
+
this._apply = opts.apply;
|
|
37
|
+
this._state = opts.initialState !== undefined ? opts.initialState : null;
|
|
38
|
+
this._seq = 0;
|
|
39
|
+
this._pending = [];
|
|
40
|
+
this._smoothKeys = parseSmoothKeys(opts.smooth);
|
|
41
|
+
this._smoothRate = (opts.smooth && opts.smooth.rate != null) ? opts.smooth.rate : 0.2;
|
|
42
|
+
this._snapTo = (opts.smooth && opts.smooth.snapTo != null) ? opts.smooth.snapTo : 0.001;
|
|
43
|
+
this._error = {};
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
get state() { return this._state; }
|
|
47
|
+
get pending() { return this._pending.slice(); }
|
|
48
|
+
get lastSeq() { return this._seq; }
|
|
49
|
+
|
|
50
|
+
/** Apply an input locally and record it. Attach the returned `seq` when sending. */
|
|
51
|
+
predict(input) {
|
|
52
|
+
this._seq += 1;
|
|
53
|
+
const seq = this._seq;
|
|
54
|
+
this._pending.push({ seq, input });
|
|
55
|
+
this._state = this._apply(this._state, input);
|
|
56
|
+
return { seq, state: this._state };
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Reconcile against authoritative server state.
|
|
61
|
+
* @param {any} serverState Authoritative state.
|
|
62
|
+
* @param {number} ackedSeq Highest input sequence the server has applied.
|
|
63
|
+
* @returns {any} the corrected predicted state (authoritative + replayed inputs)
|
|
64
|
+
*/
|
|
65
|
+
reconcile(serverState, ackedSeq) {
|
|
66
|
+
const before = this._state;
|
|
67
|
+
const ack = ackedSeq == null ? -1 : ackedSeq;
|
|
68
|
+
this._pending = this._pending.filter((p) => p.seq > ack);
|
|
69
|
+
let s = serverState;
|
|
70
|
+
for (let i = 0; i < this._pending.length; i++) s = this._apply(s, this._pending[i].input);
|
|
71
|
+
|
|
72
|
+
// Accumulate the correction gap into the error offset for smoothing.
|
|
73
|
+
if (this._smoothKeys && before && s) {
|
|
74
|
+
for (let i = 0; i < this._smoothKeys.length; i++) {
|
|
75
|
+
const k = this._smoothKeys[i];
|
|
76
|
+
if (typeof before[k] === 'number' && typeof s[k] === 'number') {
|
|
77
|
+
this._error[k] = (this._error[k] || 0) + (before[k] - s[k]);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
this._state = s;
|
|
82
|
+
return this._state;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* The state to render: corrected state plus the decaying error offset.
|
|
87
|
+
* Call once per rendered frame; it decays the error toward zero.
|
|
88
|
+
* @param {number} [rate] Override the per-frame decay factor.
|
|
89
|
+
* @returns {any}
|
|
90
|
+
*/
|
|
91
|
+
view(rate) {
|
|
92
|
+
if (!this._smoothKeys || !this._state || typeof this._state !== 'object') return this._state;
|
|
93
|
+
const r = rate != null ? rate : this._smoothRate;
|
|
94
|
+
const out = Array.isArray(this._state) ? this._state.slice() : Object.assign({}, this._state);
|
|
95
|
+
for (let i = 0; i < this._smoothKeys.length; i++) {
|
|
96
|
+
const k = this._smoothKeys[i];
|
|
97
|
+
let e = this._error[k] || 0;
|
|
98
|
+
if (typeof out[k] === 'number' && e !== 0) out[k] = out[k] + e;
|
|
99
|
+
e *= (1 - r);
|
|
100
|
+
if (Math.abs(e) < this._snapTo) e = 0;
|
|
101
|
+
this._error[k] = e;
|
|
102
|
+
}
|
|
103
|
+
return out;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/** Hard reset (e.g. on rematch). */
|
|
107
|
+
reset(state) {
|
|
108
|
+
this._state = state !== undefined ? state : null;
|
|
109
|
+
this._seq = 0;
|
|
110
|
+
this._pending = [];
|
|
111
|
+
this._error = {};
|
|
112
|
+
}
|
|
113
|
+
}
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Usion SDK Netcode — Outbound send coalescing at a fixed tick rate.
|
|
3
|
+
*
|
|
4
|
+
* Games tend to emit on every input event or every animation frame (often
|
|
5
|
+
* 60+/s). Each emit is a separate Socket.IO message and, in platform mode,
|
|
6
|
+
* a separate server-side broadcast (and historically a Redis write). That
|
|
7
|
+
* floods the wire and the server far beyond what gameplay needs.
|
|
8
|
+
*
|
|
9
|
+
* The Coalescer buffers outbound messages and flushes them on a fixed tick
|
|
10
|
+
* (e.g. 20 Hz). Two modes per channel:
|
|
11
|
+
* - queue(type, data): latest-wins — only the newest value per type is sent
|
|
12
|
+
* each tick. Ideal for state snapshots, cursor/position updates.
|
|
13
|
+
* - append(type, data): buffered — every value is kept and sent as a batch.
|
|
14
|
+
* Use for discrete inputs you can't afford to drop.
|
|
15
|
+
*
|
|
16
|
+
* `onFlush(entries)` receives `[{ type, data }]` in insertion order. The
|
|
17
|
+
* Coalescer is transport-agnostic; the caller decides how to send each entry
|
|
18
|
+
* (game.realtime, game.action, a WebRTC datachannel, etc.).
|
|
19
|
+
*/
|
|
20
|
+
export class Coalescer {
|
|
21
|
+
/**
|
|
22
|
+
* @param {object} opts
|
|
23
|
+
* @param {number} [opts.hz=20] Flush frequency.
|
|
24
|
+
* @param {(entries:Array<{type:string,data:any}>)=>void} opts.onFlush
|
|
25
|
+
* @param {boolean} [opts.autoStart=true]
|
|
26
|
+
* @param {Function} [opts.setInterval] Injectable for tests.
|
|
27
|
+
* @param {Function} [opts.clearInterval]
|
|
28
|
+
*/
|
|
29
|
+
constructor(opts = {}) {
|
|
30
|
+
if (typeof opts.onFlush !== 'function') {
|
|
31
|
+
throw new Error('Coalescer requires an onFlush(entries) callback');
|
|
32
|
+
}
|
|
33
|
+
this._onFlush = opts.onFlush;
|
|
34
|
+
this._hz = opts.hz || 20;
|
|
35
|
+
this._setInterval = opts.setInterval || (typeof setInterval !== 'undefined' ? setInterval : null);
|
|
36
|
+
this._clearInterval = opts.clearInterval || (typeof clearInterval !== 'undefined' ? clearInterval : null);
|
|
37
|
+
this._order = []; // slot keys in first-seen order
|
|
38
|
+
this._slots = {}; // key -> { mode:'latest'|'list', value, list }
|
|
39
|
+
this._timer = null;
|
|
40
|
+
if (opts.autoStart !== false) this.start();
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
get running() { return this._timer !== null; }
|
|
44
|
+
|
|
45
|
+
_ensure(type, mode) {
|
|
46
|
+
let slot = this._slots[type];
|
|
47
|
+
if (!slot) {
|
|
48
|
+
slot = { mode, value: undefined, list: [] };
|
|
49
|
+
this._slots[type] = slot;
|
|
50
|
+
this._order.push(type);
|
|
51
|
+
}
|
|
52
|
+
return slot;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** Latest-wins: only the newest data for `type` is sent on the next flush. */
|
|
56
|
+
queue(type, data) {
|
|
57
|
+
const slot = this._ensure(type, 'latest');
|
|
58
|
+
slot.mode = 'latest';
|
|
59
|
+
slot.value = data;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** Buffered: every value for `type` is kept and sent on the next flush. */
|
|
63
|
+
append(type, data) {
|
|
64
|
+
const slot = this._ensure(type, 'list');
|
|
65
|
+
slot.mode = 'list';
|
|
66
|
+
slot.list.push(data);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** Build pending entries (insertion order) and clear the buffer. */
|
|
70
|
+
drain() {
|
|
71
|
+
const entries = [];
|
|
72
|
+
for (let i = 0; i < this._order.length; i++) {
|
|
73
|
+
const type = this._order[i];
|
|
74
|
+
const slot = this._slots[type];
|
|
75
|
+
if (slot.mode === 'latest') {
|
|
76
|
+
if (slot.value !== undefined) entries.push({ type, data: slot.value });
|
|
77
|
+
} else {
|
|
78
|
+
for (let j = 0; j < slot.list.length; j++) entries.push({ type, data: slot.list[j] });
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
this._order = [];
|
|
82
|
+
this._slots = {};
|
|
83
|
+
return entries;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/** Flush now (also called automatically on each tick). */
|
|
87
|
+
flush() {
|
|
88
|
+
if (this._order.length === 0) return;
|
|
89
|
+
const entries = this.drain();
|
|
90
|
+
if (entries.length) this._onFlush(entries);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
start() {
|
|
94
|
+
if (this._timer || !this._setInterval) return;
|
|
95
|
+
this._timer = this._setInterval(() => this.flush(), Math.max(1, Math.round(1000 / this._hz)));
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
stop() {
|
|
99
|
+
if (this._timer && this._clearInterval) this._clearInterval(this._timer);
|
|
100
|
+
this._timer = null;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Usion SDK Netcode — WebTransport (HTTP/3 / QUIC) client transport.
|
|
3
|
+
*
|
|
4
|
+
* The lowest-latency *client-server* path for browser games (Baseline across
|
|
5
|
+
* major browsers since 2026). Unlike WebSocket (TCP → head-of-line blocking)
|
|
6
|
+
* it offers UDP-like unreliable **datagrams** — the newest snapshot always
|
|
7
|
+
* gets through — plus reliable **streams** for must-arrive events, over a
|
|
8
|
+
* single QUIC connection, without WebRTC's ICE/STUN/TURN/SDP complexity.
|
|
9
|
+
*
|
|
10
|
+
* Uses the native `WebTransport` API (zero dependency). On the server, pair
|
|
11
|
+
* with an HTTP/3 server such as the open-source `@fails-components/webtransport`
|
|
12
|
+
* (Node). Same interface shape as MeshConnection so it drops into the same
|
|
13
|
+
* snapshot sender/receiver + interpolation pipeline.
|
|
14
|
+
*
|
|
15
|
+
* Framing:
|
|
16
|
+
* - datagram = [seq:uint32 BE][type:uint8][payload] (one datagram = one msg)
|
|
17
|
+
* - stream = [len:uint32 BE][type:uint8][payload] (len = 1 + payload bytes)
|
|
18
|
+
* - type 0 = binary (Uint8Array), 1 = JSON (utf8). Sequenced datagrams drop
|
|
19
|
+
* stale/out-of-order frames (receiver only advances).
|
|
20
|
+
*/
|
|
21
|
+
const _enc = typeof TextEncoder !== 'undefined' ? new TextEncoder() : null;
|
|
22
|
+
const _dec = typeof TextDecoder !== 'undefined' ? new TextDecoder() : null;
|
|
23
|
+
|
|
24
|
+
function toBytes(s) { return _enc ? _enc.encode(s) : Uint8Array.from(Buffer.from(s, 'utf8')); }
|
|
25
|
+
function fromBytes(b) { return _dec ? _dec.decode(b) : Buffer.from(b).toString('utf8'); }
|
|
26
|
+
function isBinary(d) { return d instanceof ArrayBuffer || ArrayBuffer.isView(d); }
|
|
27
|
+
|
|
28
|
+
/** Encode a value to { type, bytes }: binary passthrough or JSON utf8. */
|
|
29
|
+
function encodePayload(data) {
|
|
30
|
+
if (isBinary(data)) return { type: 0, bytes: data instanceof Uint8Array ? data : new Uint8Array(data.buffer || data) };
|
|
31
|
+
return { type: 1, bytes: toBytes(JSON.stringify(data)) };
|
|
32
|
+
}
|
|
33
|
+
function decodePayload(type, bytes) {
|
|
34
|
+
if (type === 1) { try { return JSON.parse(fromBytes(bytes)); } catch (_) { return null; } }
|
|
35
|
+
return bytes;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** datagram frame: [seq:4][type:1][payload] */
|
|
39
|
+
export function encodeDatagram(seq, data) {
|
|
40
|
+
const { type, bytes } = encodePayload(data);
|
|
41
|
+
const out = new Uint8Array(5 + bytes.length);
|
|
42
|
+
new DataView(out.buffer).setUint32(0, seq >>> 0, false);
|
|
43
|
+
out[4] = type;
|
|
44
|
+
out.set(bytes, 5);
|
|
45
|
+
return out;
|
|
46
|
+
}
|
|
47
|
+
export function decodeDatagram(frame) {
|
|
48
|
+
const b = frame instanceof Uint8Array ? frame : new Uint8Array(frame);
|
|
49
|
+
if (b.length < 5) return null;
|
|
50
|
+
const seq = new DataView(b.buffer, b.byteOffset, b.byteLength).getUint32(0, false);
|
|
51
|
+
return { seq: seq, value: decodePayload(b[4], b.subarray(5)) };
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** stream frame: [len:4][type:1][payload], len = 1 + payload length */
|
|
55
|
+
export function encodeStreamFrame(data) {
|
|
56
|
+
const { type, bytes } = encodePayload(data);
|
|
57
|
+
const out = new Uint8Array(4 + 1 + bytes.length);
|
|
58
|
+
new DataView(out.buffer).setUint32(0, 1 + bytes.length, false);
|
|
59
|
+
out[4] = type;
|
|
60
|
+
out.set(bytes, 5);
|
|
61
|
+
return out;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** Stateful deframer for the reliable byte stream. push(bytes) → [values]. */
|
|
65
|
+
export class StreamDeframer {
|
|
66
|
+
constructor() { this._buf = new Uint8Array(0); }
|
|
67
|
+
push(chunk) {
|
|
68
|
+
const c = chunk instanceof Uint8Array ? chunk : new Uint8Array(chunk);
|
|
69
|
+
const merged = new Uint8Array(this._buf.length + c.length);
|
|
70
|
+
merged.set(this._buf, 0); merged.set(c, this._buf.length);
|
|
71
|
+
this._buf = merged;
|
|
72
|
+
const out = [];
|
|
73
|
+
while (this._buf.length >= 4) {
|
|
74
|
+
const len = new DataView(this._buf.buffer, this._buf.byteOffset, 4).getUint32(0, false);
|
|
75
|
+
if (this._buf.length < 4 + len) break;
|
|
76
|
+
const type = this._buf[4];
|
|
77
|
+
const payload = this._buf.subarray(5, 4 + len);
|
|
78
|
+
out.push(decodePayload(type, payload.slice()));
|
|
79
|
+
this._buf = this._buf.subarray(4 + len);
|
|
80
|
+
}
|
|
81
|
+
return out;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export class WebTransportConnection {
|
|
86
|
+
/**
|
|
87
|
+
* @param {object} opts
|
|
88
|
+
* @param {string} opts.url https:// HTTP/3 endpoint
|
|
89
|
+
* @param {Array} [opts.serverCertificateHashes] for self-signed dev certs
|
|
90
|
+
* @param {boolean}[opts.sequenced=true] drop stale datagrams
|
|
91
|
+
* @param {Function}[opts.WebTransport] injectable for tests
|
|
92
|
+
*/
|
|
93
|
+
constructor(opts = {}) {
|
|
94
|
+
if (!opts.url) throw new Error('WebTransportConnection requires a url');
|
|
95
|
+
this._url = opts.url;
|
|
96
|
+
this._opts = opts;
|
|
97
|
+
this._sequenced = opts.sequenced !== false;
|
|
98
|
+
this._WT = opts.WebTransport || (typeof WebTransport !== 'undefined' ? WebTransport : null);
|
|
99
|
+
|
|
100
|
+
this.connected = false;
|
|
101
|
+
this._t = null;
|
|
102
|
+
this._dgWriter = null;
|
|
103
|
+
this._streamWriter = null;
|
|
104
|
+
this._deframer = new StreamDeframer();
|
|
105
|
+
this._sendSeq = 0;
|
|
106
|
+
this._recvSeq = 0;
|
|
107
|
+
|
|
108
|
+
this.onOpen = null; // () => void
|
|
109
|
+
this.onMessage = null; // (data, channel:'datagram'|'reliable') => void
|
|
110
|
+
this.onClose = null; // () => void
|
|
111
|
+
this.onError = null; // (err) => void
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
async connect() {
|
|
115
|
+
if (!this._WT) throw new Error('WebTransport unavailable in this environment');
|
|
116
|
+
const init = {};
|
|
117
|
+
if (this._opts.serverCertificateHashes) init.serverCertificateHashes = this._opts.serverCertificateHashes;
|
|
118
|
+
const t = new this._WT(this._url, init);
|
|
119
|
+
this._t = t;
|
|
120
|
+
if (t.ready && typeof t.ready.then === 'function') await t.ready;
|
|
121
|
+
|
|
122
|
+
if (t.datagrams && t.datagrams.writable) this._dgWriter = t.datagrams.writable.getWriter();
|
|
123
|
+
if (typeof t.createBidirectionalStream === 'function') {
|
|
124
|
+
try {
|
|
125
|
+
const s = await t.createBidirectionalStream();
|
|
126
|
+
this._streamWriter = s.writable.getWriter();
|
|
127
|
+
this._pumpReadable(s.readable, 'reliable');
|
|
128
|
+
} catch (e) { /* reliable stream optional */ }
|
|
129
|
+
}
|
|
130
|
+
if (t.datagrams && t.datagrams.readable) this._pumpDatagrams(t.datagrams.readable);
|
|
131
|
+
if (t.closed && typeof t.closed.then === 'function') t.closed.then(() => this._onClosed(), () => this._onClosed());
|
|
132
|
+
|
|
133
|
+
this.connected = true;
|
|
134
|
+
if (this.onOpen) this.onOpen();
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
async _pumpDatagrams(readable) {
|
|
138
|
+
try {
|
|
139
|
+
const reader = readable.getReader();
|
|
140
|
+
for (;;) {
|
|
141
|
+
const { value, done } = await reader.read();
|
|
142
|
+
if (done) break;
|
|
143
|
+
if (value) this._onDatagramBytes(value);
|
|
144
|
+
}
|
|
145
|
+
} catch (e) { if (this.onError) this.onError(e); }
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
async _pumpReadable(readable, channel) {
|
|
149
|
+
try {
|
|
150
|
+
const reader = readable.getReader();
|
|
151
|
+
for (;;) {
|
|
152
|
+
const { value, done } = await reader.read();
|
|
153
|
+
if (done) break;
|
|
154
|
+
if (value) { const msgs = this._deframer.push(value); for (let i = 0; i < msgs.length; i++) if (this.onMessage) this.onMessage(msgs[i], channel); }
|
|
155
|
+
}
|
|
156
|
+
} catch (e) { if (this.onError) this.onError(e); }
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/** Handle one inbound datagram frame (also the test entry point). */
|
|
160
|
+
_onDatagramBytes(frame) {
|
|
161
|
+
const parsed = decodeDatagram(frame);
|
|
162
|
+
if (!parsed) return;
|
|
163
|
+
if (this._sequenced) {
|
|
164
|
+
if (parsed.seq <= this._recvSeq) return; // stale / out-of-order
|
|
165
|
+
this._recvSeq = parsed.seq;
|
|
166
|
+
}
|
|
167
|
+
if (this.onMessage) this.onMessage(parsed.value, 'datagram');
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/** Send over the unreliable datagram channel (sequenced). */
|
|
171
|
+
send(data) {
|
|
172
|
+
if (!this._dgWriter) return false;
|
|
173
|
+
this._sendSeq += 1;
|
|
174
|
+
try { this._dgWriter.write(encodeDatagram(this._sendSeq, data)); return true; }
|
|
175
|
+
catch (e) { return false; }
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/** Send over the reliable ordered stream. */
|
|
179
|
+
sendReliable(data) {
|
|
180
|
+
if (!this._streamWriter) return false;
|
|
181
|
+
try { this._streamWriter.write(encodeStreamFrame(data)); return true; }
|
|
182
|
+
catch (e) { return false; }
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
_onClosed() {
|
|
186
|
+
if (this.connected) { this.connected = false; if (this.onClose) this.onClose(); }
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
close() {
|
|
190
|
+
try { if (this._dgWriter) this._dgWriter.releaseLock && this._dgWriter.releaseLock(); } catch (_) {}
|
|
191
|
+
try { if (this._t) this._t.close(); } catch (_) {}
|
|
192
|
+
this._t = this._dgWriter = this._streamWriter = null;
|
|
193
|
+
this.connected = false;
|
|
194
|
+
}
|
|
195
|
+
}
|
package/src/modules/wallet.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* Usion SDK Wallet Module — wallet and payment operations
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
|
-
import { getNextRequestId } from './core.js';
|
|
5
|
+
import { getNextRequestId, isTrustedMessageSource } from './core.js';
|
|
6
6
|
|
|
7
7
|
/**
|
|
8
8
|
* @param {object} Usion - Reference to the main Usion object
|
|
@@ -57,6 +57,10 @@ export function createWalletModule(Usion) {
|
|
|
57
57
|
|
|
58
58
|
// Listen for response
|
|
59
59
|
function handler(event) {
|
|
60
|
+
// Only honor payment results from the trusted host shell — a forged
|
|
61
|
+
// PAYMENT_SUCCESS must never resolve this promise.
|
|
62
|
+
if (!isTrustedMessageSource(event)) return;
|
|
63
|
+
|
|
60
64
|
let response;
|
|
61
65
|
try {
|
|
62
66
|
response = typeof event.data === 'string' ? JSON.parse(event.data) : event.data;
|