@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 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);
Binary file
@@ -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
- const count = inputs.pending.length > redundancy ? MAX_INPUT_BACKLOG : redundancy;
127
- send({ n: label, k: 'i', i: inputs.pending.slice(-count), ...inputEpoch() });
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(encodeFrame(frame))).catch(retire);
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
- sendInputs();
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
- /** Close the stream and end every consumer. */
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
- /** Close the stream and end every consumer. */
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;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@net-mesh/browser",
3
- "version": "0.45.0",
3
+ "version": "0.45.1",
4
4
  "description": "Browser-native Net mesh leaf \u2014 idiomatic TypeScript over the net-mesh-leaf wasm node",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",