@net-mesh/browser 0.45.0 → 0.45.1
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/CHANGELOG.md +20 -0
- package/dist/net_leaf.js +3 -3
- package/dist/net_leaf_bg.wasm +0 -0
- package/dist/netcode/client.js +41 -6
- package/dist/stream.d.ts +8 -1
- package/dist/stream.js +8 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -14,6 +14,26 @@ missing method at the call site rather than at install time. Unlike
|
|
|
14
14
|
`@net-mesh/sdk`, this package never depends on `@net-mesh/core` — see
|
|
15
15
|
the README on why it is a sibling package rather than a sub-path.
|
|
16
16
|
|
|
17
|
+
## 0.45.1 — a closed stream keeps hearing its peer
|
|
18
|
+
|
|
19
|
+
- **Closing a stream no longer silences its peer.** After `close()`, frames
|
|
20
|
+
the peer still sends on that label reach `node.onEvent` as `stream_data`,
|
|
21
|
+
exactly as for a stream the page never opened. Before, the node dropped
|
|
22
|
+
them (counted `drops.stream_closed`) until the page reopened the stream,
|
|
23
|
+
so closing a stream was worse than never opening it. `stream_closed` is
|
|
24
|
+
no longer counted and reads 0.
|
|
25
|
+
- **A timed-out netcode player can rejoin.** `hostNetcode` closes a
|
|
26
|
+
player's stream when it times the player out, and only learns about a
|
|
27
|
+
player from its frames, so the change above was what locked a timed-out
|
|
28
|
+
player out for good. It is now re-admitted when heard again and carries
|
|
29
|
+
on from where it was applied.
|
|
30
|
+
- **A player cut off from its host stays quiet.** `joinNetcode` keeps at
|
|
31
|
+
most the newest 64 unacknowledged inputs (older ones were never sent
|
|
32
|
+
again anyway), keeps every input frame under one event (8,000 bytes) so
|
|
33
|
+
it never rides as fragments, and while the host has said nothing for a
|
|
34
|
+
second repeats its inputs only at the ping rate instead of with every
|
|
35
|
+
input. A cut-off player used to upload about 700 kB/s.
|
|
36
|
+
|
|
17
37
|
## 0.44.0 — sharded matches, compact snapshots
|
|
18
38
|
|
|
19
39
|
- **`joinShards` merges a match split across several hosts** (topology A:
|
package/dist/net_leaf.js
CHANGED
|
@@ -2740,6 +2740,9 @@ function __wbg_get_imports() {
|
|
|
2740
2740
|
const ret = getObject(arg0).errorCode;
|
|
2741
2741
|
return ret;
|
|
2742
2742
|
},
|
|
2743
|
+
__wbg_error_0b48a92af341cfbc: function(arg0, arg1) {
|
|
2744
|
+
console.error(getStringFromWasm0(arg0, arg1));
|
|
2745
|
+
},
|
|
2743
2746
|
__wbg_error_3da0c81bd4184d52: function(arg0) {
|
|
2744
2747
|
const ret = getObject(arg0).error;
|
|
2745
2748
|
return isLikeNone(ret) ? 0 : addHeapObject(ret);
|
|
@@ -2748,9 +2751,6 @@ function __wbg_get_imports() {
|
|
|
2748
2751
|
const ret = getObject(arg0).error;
|
|
2749
2752
|
return isLikeNone(ret) ? 0 : addHeapObject(ret);
|
|
2750
2753
|
}, arguments); },
|
|
2751
|
-
__wbg_error_6bdb7b2cf1241109: function(arg0, arg1) {
|
|
2752
|
-
console.error(getStringFromWasm0(arg0, arg1));
|
|
2753
|
-
},
|
|
2754
2754
|
__wbg_fetch_4178962c570137ca: function(arg0, arg1, arg2) {
|
|
2755
2755
|
const ret = getObject(arg0).fetch(getStringFromWasm0(arg1, arg2));
|
|
2756
2756
|
return addHeapObject(ret);
|
package/dist/net_leaf_bg.wasm
CHANGED
|
Binary file
|
package/dist/netcode/client.js
CHANGED
|
@@ -34,6 +34,20 @@ export class NetcodeError extends Error {
|
|
|
34
34
|
* ticks, and a host skips the gap the earlier frame would have filled.
|
|
35
35
|
*/
|
|
36
36
|
const MAX_INPUT_BACKLOG = 64;
|
|
37
|
+
/**
|
|
38
|
+
* Largest input frame, bytes. Under one event, as the host's snapshot
|
|
39
|
+
* default is, so an input frame never rides as fragments: on the lossy
|
|
40
|
+
* carrier one lost fragment loses the whole frame. A backlog that does not
|
|
41
|
+
* fit sends its newest inputs.
|
|
42
|
+
*/
|
|
43
|
+
const MAX_INPUT_FRAME_BYTES = 8000;
|
|
44
|
+
/**
|
|
45
|
+
* How long the host may say nothing before input frames stop going out at
|
|
46
|
+
* the input rate, ms. While it is silent, unacknowledged inputs are repeated
|
|
47
|
+
* only on the ping timer: a player cut off from its host used to send its
|
|
48
|
+
* whole backlog, as fragments, with every input (~700 kB/s, net-fps report).
|
|
49
|
+
*/
|
|
50
|
+
const HOST_SILENT_MS = 1000;
|
|
37
51
|
/** A fresh {@link InputSequencer}: `seq` 0, nothing pending. */
|
|
38
52
|
export function createInputSequencer() {
|
|
39
53
|
return { seq: 0, pending: [] };
|
|
@@ -121,11 +135,23 @@ export function joinNetcode(options) {
|
|
|
121
135
|
let lastSnapshotAt = null;
|
|
122
136
|
const inputEpoch = () => (options.epoch === undefined ? {} : { ep: options.epoch() });
|
|
123
137
|
// The newest `redundancy` inputs — or, past that, the whole backlog up to
|
|
124
|
-
// MAX_INPUT_BACKLOG, in one frame.
|
|
138
|
+
// MAX_INPUT_BACKLOG, in one frame of at most MAX_INPUT_FRAME_BYTES. A
|
|
139
|
+
// backlog too big for one frame sends its newest inputs: a host skips
|
|
140
|
+
// whatever an earlier frame would have filled anyway.
|
|
125
141
|
const sendInputs = () => {
|
|
126
|
-
|
|
127
|
-
|
|
142
|
+
let count = Math.min(inputs.pending.length, inputs.pending.length > redundancy ? MAX_INPUT_BACKLOG : redundancy);
|
|
143
|
+
for (;;) {
|
|
144
|
+
const bytes = encodeFrame({ n: label, k: 'i', i: inputs.pending.slice(-count), ...inputEpoch() });
|
|
145
|
+
if (bytes.byteLength <= MAX_INPUT_FRAME_BYTES || count <= 1) {
|
|
146
|
+
sendBytes(bytes);
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
count = Math.max(1, Math.min(count - 1, Math.floor((count * MAX_INPUT_FRAME_BYTES) / bytes.byteLength)));
|
|
150
|
+
}
|
|
128
151
|
};
|
|
152
|
+
// When the host was last heard from: any frame of this label, or binary.
|
|
153
|
+
let hostHeardAt = now();
|
|
154
|
+
const hostSilent = () => now() - hostHeardAt > HOST_SILENT_MS;
|
|
129
155
|
const offset = () => clock.estimate()?.offsetMs ?? null;
|
|
130
156
|
const hostNow = () => {
|
|
131
157
|
const o = offset();
|
|
@@ -191,7 +217,8 @@ export function joinNetcode(options) {
|
|
|
191
217
|
// replaced (a reconnect, a relayed → direct upgrade) and a stream handle
|
|
192
218
|
// is fenced to the session it was opened on. The protocol repeats what
|
|
193
219
|
// was lost, so reopening is all recovery needs.
|
|
194
|
-
const send = (frame) =>
|
|
220
|
+
const send = (frame) => sendBytes(encodeFrame(frame));
|
|
221
|
+
const sendBytes = (payload) => {
|
|
195
222
|
const current = stream;
|
|
196
223
|
if (current === null) {
|
|
197
224
|
void open();
|
|
@@ -211,7 +238,7 @@ export function joinNetcode(options) {
|
|
|
211
238
|
}
|
|
212
239
|
};
|
|
213
240
|
try {
|
|
214
|
-
void Promise.resolve(current.send(
|
|
241
|
+
void Promise.resolve(current.send(payload)).catch(retire);
|
|
215
242
|
}
|
|
216
243
|
catch (error) {
|
|
217
244
|
retire(error);
|
|
@@ -223,12 +250,14 @@ export function joinNetcode(options) {
|
|
|
223
250
|
if (eventPeer(event.peerNode) !== host)
|
|
224
251
|
return;
|
|
225
252
|
if (isBinaryFrame(event.payload)) {
|
|
253
|
+
hostHeardAt = now();
|
|
226
254
|
onBinary(event.payload);
|
|
227
255
|
return;
|
|
228
256
|
}
|
|
229
257
|
const frame = decodeFrame(event.payload, label);
|
|
230
258
|
if (frame === null)
|
|
231
259
|
return;
|
|
260
|
+
hostHeardAt = now();
|
|
232
261
|
if (frame.k === 'q') {
|
|
233
262
|
clock.add(frame.t0, frame.t1, frame.t2, now());
|
|
234
263
|
return;
|
|
@@ -492,12 +521,18 @@ export function joinNetcode(options) {
|
|
|
492
521
|
const seq = inputs.seq;
|
|
493
522
|
const seen = (hostNow() ?? now()) - delay;
|
|
494
523
|
inputs.pending.push({ seq, seen, data });
|
|
524
|
+
// Only the newest MAX_INPUT_BACKLOG are ever sent again, so older
|
|
525
|
+
// ones are dropped rather than held (and replayed) until an ack.
|
|
526
|
+
if (inputs.pending.length > MAX_INPUT_BACKLOG)
|
|
527
|
+
inputs.pending.splice(0, inputs.pending.length - MAX_INPUT_BACKLOG);
|
|
495
528
|
if (options.local && predicted !== null) {
|
|
496
529
|
predicted = options.local.predict(predicted, data);
|
|
497
530
|
if (correcting !== null)
|
|
498
531
|
correcting = { ...correcting, from: options.local.predict(correcting.from, data) };
|
|
499
532
|
}
|
|
500
|
-
|
|
533
|
+
// A silent host gets the backlog from the ping timer, not per input.
|
|
534
|
+
if (!hostSilent())
|
|
535
|
+
sendInputs();
|
|
501
536
|
},
|
|
502
537
|
view() {
|
|
503
538
|
const at = hostNow();
|
package/dist/stream.d.ts
CHANGED
|
@@ -179,7 +179,14 @@ export declare class LeafStream implements AsyncIterable<Uint8Array> {
|
|
|
179
179
|
/** Listen for payloads. Returns a cancel handle. */
|
|
180
180
|
onMessage(listener: (payload: Uint8Array) => void): () => void;
|
|
181
181
|
[Symbol.asyncIterator](): AsyncIterableIterator<Uint8Array>;
|
|
182
|
-
/**
|
|
182
|
+
/**
|
|
183
|
+
* Close the stream and end every consumer.
|
|
184
|
+
*
|
|
185
|
+
* Close is local: the peer's half is its own. Frames the peer still
|
|
186
|
+
* sends on this label reach `node.onEvent` as `stream_data`, exactly as
|
|
187
|
+
* for a stream this page never opened. Before 0.45.1 the node dropped
|
|
188
|
+
* them (`drops.stream_closed`) until the stream was reopened.
|
|
189
|
+
*/
|
|
183
190
|
close(): void;
|
|
184
191
|
/**
|
|
185
192
|
* One callback from the wasm boundary.
|
package/dist/stream.js
CHANGED
|
@@ -176,7 +176,14 @@ export class LeafStream {
|
|
|
176
176
|
this.retiredBy = reason;
|
|
177
177
|
this.close();
|
|
178
178
|
}
|
|
179
|
-
/**
|
|
179
|
+
/**
|
|
180
|
+
* Close the stream and end every consumer.
|
|
181
|
+
*
|
|
182
|
+
* Close is local: the peer's half is its own. Frames the peer still
|
|
183
|
+
* sends on this label reach `node.onEvent` as `stream_data`, exactly as
|
|
184
|
+
* for a stream this page never opened. Before 0.45.1 the node dropped
|
|
185
|
+
* them (`drops.stream_closed`) until the stream was reopened.
|
|
186
|
+
*/
|
|
180
187
|
close() {
|
|
181
188
|
if (this.closed)
|
|
182
189
|
return;
|