@net-mesh/browser 0.42.0 → 0.44.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/CHANGELOG.md +200 -0
- package/README.md +12 -0
- package/dist/index.bundle.js +2 -2
- package/dist/leader/session.d.ts +4 -2
- package/dist/leader/session.js +30 -6
- package/dist/lobby.d.ts +29 -2
- package/dist/lobby.js +80 -10
- package/dist/net_leaf.d.ts +19 -21
- package/dist/net_leaf.js +42 -44
- package/dist/net_leaf_bg.wasm +0 -0
- package/dist/net_leaf_bg.wasm.d.ts +10 -10
- package/dist/netcode/assignment.d.ts +169 -0
- package/dist/netcode/assignment.js +321 -0
- package/dist/netcode/client.d.ts +125 -3
- package/dist/netcode/client.js +291 -27
- package/dist/netcode/codec.d.ts +180 -0
- package/dist/netcode/codec.js +547 -0
- package/dist/netcode/host.d.ts +58 -1
- package/dist/netcode/host.js +254 -9
- package/dist/netcode/index.d.ts +14 -4
- package/dist/netcode/index.js +8 -3
- package/dist/netcode/interpolate.d.ts +24 -1
- package/dist/netcode/interpolate.js +52 -1
- package/dist/netcode/paths.d.ts +115 -0
- package/dist/netcode/paths.js +261 -0
- package/dist/netcode/shards.d.ts +138 -0
- package/dist/netcode/shards.js +270 -0
- package/dist/netcode/wire.d.ts +37 -3
- package/dist/netcode/wire.js +28 -1
- package/dist/node.d.ts +20 -0
- package/dist/node.js +76 -1
- package/dist/store/host.js +10 -4
- package/dist/store/join.js +38 -2
- package/dist/store/owner.d.ts +11 -0
- package/dist/store/owner.js +27 -7
- package/dist/store/types.d.ts +6 -0
- package/dist/stream.d.ts +29 -1
- package/dist/stream.js +66 -4
- package/dist/udp-probe.d.ts +21 -3
- package/dist/udp-probe.js +78 -7
- package/dist/world/link.d.ts +9 -0
- package/dist/world/link.js +4 -3
- package/dist/world/regions.d.ts +9 -2
- package/dist/world/regions.js +34 -4
- package/package.json +3 -2
package/CHANGELOG.md
CHANGED
|
@@ -14,6 +14,206 @@ 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
|
+
|
|
193
|
+
## 0.43.0 — one identity per origin, bounded streams
|
|
194
|
+
|
|
195
|
+
- **`connect()` locks its identity.** With a custodial identity it takes
|
|
196
|
+
a per-identity Web Lock, so a second tab running the same identity is
|
|
197
|
+
refused with `IdentityError` until the first closes. `openSession()`
|
|
198
|
+
elects under the same lock. A page that runs several nodes with one
|
|
199
|
+
identity on purpose passes `lockIdentity: false`.
|
|
200
|
+
- **An unread `LeafStream` is bounded.** Before a consumer attaches it
|
|
201
|
+
holds at most 1,024 frames or 1 MiB, dropping the oldest;
|
|
202
|
+
`droppedBeforeConsumer` counts them. It used to buffer every frame,
|
|
203
|
+
and the store, netcode and region links never read theirs.
|
|
204
|
+
- **Store handles are capped at 16 per peer.**
|
|
205
|
+
- **A follower tab's org calls have a deadline,** across the open and
|
|
206
|
+
every stream leg; a proxied call carries the absolute deadline.
|
|
207
|
+
- **A leader change no longer strands a store:** session streams retire
|
|
208
|
+
with the reopenable "stale stream handle" error, and
|
|
209
|
+
`joinStore().reconnect()` closes the stream it replaces.
|
|
210
|
+
- **`lobby.kick` reads node ids in decimal,** and the join key reaches
|
|
211
|
+
`authorize` on every read decision.
|
|
212
|
+
- **The STUN probe's `notRun`** now also covers gathering that completes
|
|
213
|
+
beside a public host candidate with nothing reflexive and no "no
|
|
214
|
+
response" report (an engine may suppress that reflexive candidate).
|
|
215
|
+
Anywhere else it is silence, and blocked UDP is still `udp-blocked`.
|
|
216
|
+
|
|
17
217
|
## 0.41.1 — two pages can connect to each other at once
|
|
18
218
|
|
|
19
219
|
- **`connectPeer` from both sides no longer cancels both attempts.** When
|
package/README.md
CHANGED
|
@@ -583,6 +583,18 @@ are the *same* player — and a player can't join itself. (Without it, every tab
|
|
|
583
583
|
player.) To test with two players on one machine, use two browser profiles, or
|
|
584
584
|
two different browsers.
|
|
585
585
|
|
|
586
|
+
**A second tab with the same identity is refused.** Two live nodes under one
|
|
587
|
+
identity would evict each other at the anchor, so `connect()` holds a Web Lock
|
|
588
|
+
for its identity while the node is open: a second tab's `connect()` rejects
|
|
589
|
+
with an `IdentityError` (kind `identity`) until the first node closes. Catch it
|
|
590
|
+
and tell the player the game is already open, or run the page through
|
|
591
|
+
`openSession()`, which elects one node per origin and shares it across tabs.
|
|
592
|
+
A `connect()` tab and an `openSession()` tab given the same `entitySecretHex`
|
|
593
|
+
contend for the same lock, so they cannot both run the identity either.
|
|
594
|
+
(`lockIdentity: false` opts out. Where Web Locks are unavailable `connect()`
|
|
595
|
+
takes no lock, and `openSession()` is not a fallback there: it needs Web Locks
|
|
596
|
+
to elect a leader, so it rejects with an `IdentityError`.)
|
|
597
|
+
|
|
586
598
|
**Announcements expire.** A host that announces once disappears from searches
|
|
587
599
|
a few seconds later. Keep re-announcing on a timer, as in step 3a.
|
|
588
600
|
|