@net-mesh/browser 0.43.0 → 0.44.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,182 @@ 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.44.0 — sharded matches, compact snapshots
18
+
19
+ - **`joinShards` merges a match split across several hosts** (topology A:
20
+ every player joins every host). It runs one `joinNetcode` per shard host
21
+ and draws each entity only from the shard that owns it in the installed
22
+ `ShardAssignment` (`{ epoch, hosts, owner }`, or an `ownerOf` resolver).
23
+ Inputs go only to the shard that owns you. A shard sending snapshots
24
+ from another epoch is frozen at its last drawn state, minus anything it
25
+ no longer owns, until it catches up or `setAssignment` installs its
26
+ epoch. When ownership changes, the old owner's copy lingers for
27
+ `lingerMs`. `stats()` reports each shard (with `silentMs`) and counts
28
+ what the fencing dropped.
29
+ - **NPCs on paths.** An NPC on a known route can be published once, as an
30
+ `NpcPath` record in the lobby store, instead of in every snapshot.
31
+ - `pathAction({ assignment })` is the store action: only the NPC's
32
+ owning host may write a record.
33
+ - `npcModes().resolve(view, records, ownerNow)` is what a client draws
34
+ from. It picks whichever source, snapshot or record, carries the
35
+ higher mode version `mv`. While a due record is late it carries the
36
+ NPC forward for up to 500 ms, and it blends every switch.
37
+ - `pathAt` evaluates a path.
38
+
39
+ Measured with 50 NPCs: 47 KB/s live, 1.6 KB/s on paths, per viewer.
40
+ - **NPCs in sharded matches.** A `ShardRecord` lists the NPCs its shard
41
+ owns (`npcs`). `npcShard(id, shards)` places one deterministically
42
+ (D7), and `withNpcs` spawns a batch. The validator refuses an NPC owned
43
+ twice.
44
+ - **Recovering a lost host's players.** `NetcodeClient.latest()` returns
45
+ the newest snapshot as the host sent it: the state a surviving host
46
+ restores a lost host's entities from. `adoptPlayer(peer, { resume: true
47
+ })` takes such a player's inputs from its next frame on, dropping what
48
+ it had pending (`dropped['resumed']`).
49
+ - **Planned moves between hosts.**
50
+ - `hostNetcode`'s `releasePlayer(peer)` returns `{ lastApplied }` and
51
+ stops taking that player's inputs, unapplied and unacknowledged;
52
+ `adoptPlayer(peer, { lastApplied })` resumes above it.
53
+ - `joinNetcode({ sequencer })` with `createInputSequencer()` shares one
54
+ input sequence across clients, which `joinShards` does for every shard.
55
+
56
+ A moved player's inputs carry on, and an input the old host applied is
57
+ never applied again.
58
+ - **A backlog of unacknowledged inputs is no longer cut to the newest 8.**
59
+ When more than `inputRedundancy` inputs are waiting (a high-latency
60
+ link, or a change of host), the next input frame carries all of them,
61
+ up to 64. Before, the older ones were lost for good.
62
+ - **Sharded matches get an assignment record and leases**
63
+ (`@net-mesh/browser/netcode`). `MatchAssignment` lives in the lobby's
64
+ store and gives shard hosts, the squads each shard owns, and a lease
65
+ per host:
66
+ - `assignmentProblems` validates it: each player is owned exactly once,
67
+ and no host owns its own squad.
68
+ - `assignAction({ lobby })` is the store action only the lobby node may
69
+ call.
70
+ - `renewLeases` bumps the counters.
71
+ - `leaseWatch` judges a counter on this node's own clock.
72
+ - `shardAssignment` gives the `joinShards` view.
73
+ - `authorizeTeams` admits `team:<name>` audiences.
74
+
75
+ `hostNetcode({ live })` stops simulating and sending once its lease is
76
+ lost. `joinShards({ live })` freezes the view (`stats().frozen`).
77
+ - **`NetcodeClient.sampleAt(hostTime)`** returns the host's entities as
78
+ of a host time, from the snapshots the client holds, plus the gap
79
+ between the two snapshots around that time. It returns `null` when no
80
+ two snapshots bracket the time. This is history, never extrapolated: a
81
+ shard host uses it to check a cross-host hit claim against where the
82
+ shooter really was. `SnapshotBuffer.sample` is the same read.
83
+ - **An optional epoch on the netcode wire.** `hostNetcode({ epoch })`
84
+ stamps `ep` on every snapshot and drops input frames stamped with
85
+ another epoch (`dropped['stale-epoch']`). `joinNetcode({ epoch })` stamps
86
+ inputs, and `stats()` gains `epoch` (from the newest snapshot) and
87
+ `lastSnapshotAt`. Binary snapshots with an epoch use a new kind byte, so
88
+ an older decoder refuses them rather than misreading them. Frames
89
+ without `ep` are unchanged.
90
+
91
+ - **Compact netcode snapshots.** `hostNetcode` and `joinNetcode` take an
92
+ optional `codec` built with `entityCodec({ x: quantized(0, 512), yaw:
93
+ angle(), hp: uint(8), ... })`. When both ends have the same codec,
94
+ snapshots are binary and about 7x smaller; otherwise they stay JSON, so a
95
+ page without a codec keeps working unchanged. Two new frames ride the
96
+ JSON wire: the codec fingerprint on join and ping (`f`), and a request
97
+ for entity names (`u`). New stats: `binaryFrames`, `unnamedEntities`.
98
+ - **`joinNetcode` says why it has not joined.** `await net.ready()`
99
+ resolves at the first snapshot and rejects with a typed `NetcodeError`
100
+ after `joinTimeoutMs` (default 15 s): `no-announcement` (this node never
101
+ heard the host announce, so it cannot reach it — the failure that used
102
+ to wait silently forever), `unreachable`, `timeout`, or `closed`. The
103
+ client keeps retrying regardless. `stats()` gains `connectOutcome` and
104
+ `lastError`.
105
+ - **A pair left on the relay is no longer re-offered on every reopen.**
106
+ When `joinNetcode`'s direct attempt ended relayed (`iceTimeout`, …),
107
+ every stream reopen started a fresh ICE attempt, and one finishing late
108
+ replaced the session under the stream. That was a loop that, under
109
+ load, never delivered a snapshot. The stream now rides the relayed
110
+ session, and direct is retried at most every `directRetryMs` (default
111
+ 30 s). Send failures now show in `stats().lastError`.
112
+ - **A lossy stream no longer wedges after loss.** On a fire-and-forget
113
+ stream a lost packet's bytes were never credited back, so every loss
114
+ shrank the 64 KiB send window for good; after a window's worth the
115
+ stream refused every send (`backpressure … the peer has not granted
116
+ more yet`), and a reopen kept the drained state. Credit stalled for
117
+ 2 s with bytes outstanding is now written off as lost (`net-mesh-wire`,
118
+ `FIRE_AND_FORGET_RECLAIM_AFTER`); reliable streams are unchanged.
119
+ - **A peer pair can no longer end half-direct.** When a slow answerer's
120
+ handshake reply crossed the offerer's deadline, the offerer fell back to
121
+ the relay while the answerer reported `direct` and sent on a channel the
122
+ offerer had closed: `peerAttempt` read `failed` on one end and `open`
123
+ with `direct: true` on the other, for good. An answerer is now direct
124
+ only once the establishment is proven, and an establishment its offerer
125
+ abandons returns the pair to its relay (`net-mesh-leaf`).
126
+ - **Lobbies, joiners and region hosts refresh their announcements every
127
+ 30 s, not 2 s** (`LOBBY_ANNOUNCE_MS`, `REGION_ANNOUNCE_MS`). Every refresh
128
+ floods through the anchor to every peer, so the load grows with the square
129
+ of the players: at 64 players on one anchor a 2 s refresh pinned the
130
+ host's anchor link at its SCTP advisory, starving every relayed pair. A
131
+ change of players is still announced at once, and an announcement lives
132
+ 300 s. `joinLobby` gains `announceEveryMs`, as `createLobby` has. A node that
133
+ connects later does not wait for a refresh: the anchor replays the
134
+ announcements it holds to each session that enrolls, within its game.
135
+ - **Stream data is routed to its stream, not filtered by every stream.**
136
+ Each `LeafStream`'s listener used to receive every node event and read
137
+ it to find its own, so a host with one stream per player paid players ×
138
+ events: at 64 players that was 13% of the host's main thread. Events are
139
+ now routed by peer and stream id inside the wasm, on both the
140
+ `connect()` and the `openSession()` paths (`net-mesh-leaf`). What a
141
+ listener receives is unchanged.
142
+ - **A direct connection one side no longer recognises now recovers.** If
143
+ a host held no session for a player that was still sending to it over
144
+ an open direct channel, every packet was dropped and nothing noticed:
145
+ the player's sends never fail and the connection stayed healthy. A
146
+ 1 × 96 run lost 3 players that way for its whole 240 s. The side
147
+ without a session now tells its peer so on that channel and closes it
148
+ (and logs `closed the direct link`). The other side treats that notice
149
+ as a lost session, exactly as it already treated an ICE failure, and
150
+ connects again. Before, only ICE could report a loss, which takes its
151
+ 30 s consent timeout after a peer leaves and never fires while the
152
+ path is healthy. A channel the peer merely closes — its re-offer
153
+ replacing the link, or its tab going away — is still not a loss: the
154
+ session stays, and an open call still ends on its own deadline.
155
+ - **Fewer clock pings once the clock has settled:** `pingRate` (default
156
+ 4/s) until 8 samples, then `settledPingRate` (default 1/s). Every pong
157
+ pays a packet's fixed transport overhead; measured in real browsers
158
+ (1 host × 32 players) host upload fell from 2.19 to 1.82 Mbit/s.
159
+ - **Review fixes to this release's netcode** (sharded-match review):
160
+ - A client drops acknowledged inputs with or without `local`, so it no
161
+ longer sends 64-input frames for good after a backlog.
162
+ - Binary snapshots carry the host's instance: a host re-created under the
163
+ same label is no longer drawn under the old ids. The codec fingerprint
164
+ gains a wire version, so peers from before this change use JSON.
165
+ - `angle()` fields interpolate the short way (`lerpAngle`,
166
+ `EntityCodec.interpolate`, the default with a codec).
167
+ - `adoptPlayer`'s input boundary survives a slow first frame or an
168
+ `authorize` refusal (`dropped['adopt-unauthorized']`).
169
+ - A rejected `connectPeer` also waits `directRetryMs` before re-offering.
170
+ - Name tables forget ids unseen for 10 s (`NAME_FORGET_MS`); a `u` request
171
+ is honoured once per 100 ms per index, only for indexes that player was
172
+ shown (`dropped['name-request-throttled']`).
173
+ - Input seqs must be positive safe integers; `float32()` refuses NaN and
174
+ infinities; `sampleAt` at a snapshot's exact time includes entities that
175
+ first appear in it.
176
+ - A failed announcement (lobby, seek tag, region host) is retried after
177
+ 1 s, doubling to 8 s (`ANNOUNCE_RETRY_MS`, `ANNOUNCE_RETRY_MAX_MS`).
178
+ - `joinShards`: a rejoined shard keeps its last state until its first
179
+ snapshot; `ready()` follows rejoins.
180
+ - `leaseWatch` takes its first reading as a baseline, not a lease;
181
+ `mayReassign` takes a `maxDeliveryMs` margin.
182
+ - `assignmentProblems` validates shapes, own keys and `renewals`;
183
+ `pathAction` matches NPC ids exactly. `npcModes` prunes its tracks,
184
+ builds path poses on the freshest entry, stops drawing a live NPC no
185
+ snapshot shows, and `stats()` reports `tracks`.
186
+ - `regionHandoffs` gains `actionTiming` for forwarded actions.
187
+ - The shard host recipe uses the netcode clock throughout (cross-host hits
188
+ were refused `beyond-cap` with the old `Date.now` default), keeps path
189
+ NPCs hittable and recoverable, and validates `setAssignment`.
190
+ - `retryReport()` gains `channelClosed`: a far-end channel close is its
191
+ own re-attempt trigger, and one channel's loss counts once.
192
+
17
193
  ## 0.43.0 — one identity per origin, bounded streams
18
194
 
19
195
  - **`connect()` locks its identity.** With a custodial identity it takes