@net-mesh/browser 0.43.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 +176 -0
- package/dist/index.bundle.js +2 -2
- package/dist/lobby.d.ts +29 -2
- package/dist/lobby.js +72 -6
- package/dist/net_leaf.d.ts +19 -21
- package/dist/net_leaf.js +44 -46
- 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 +5 -0
- package/dist/node.js +1 -0
- 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/dist/netcode/wire.d.ts
CHANGED
|
@@ -40,22 +40,43 @@ export interface WireInput<I> {
|
|
|
40
40
|
readonly data: I;
|
|
41
41
|
}
|
|
42
42
|
export type Frame<E, I> =
|
|
43
|
-
/**
|
|
43
|
+
/**
|
|
44
|
+
* Player → host: join (repeated until the first snapshot). `f` is the
|
|
45
|
+
* player's entity-codec fingerprint, if it has one: the host sends binary
|
|
46
|
+
* snapshots only when it matches its own (`codec.ts`).
|
|
47
|
+
*/
|
|
44
48
|
{
|
|
45
49
|
readonly n: string;
|
|
46
50
|
readonly k: 'h';
|
|
51
|
+
readonly f?: string;
|
|
47
52
|
}
|
|
48
|
-
/**
|
|
53
|
+
/**
|
|
54
|
+
* Player → host: the newest inputs, the unacknowledged ones repeated.
|
|
55
|
+
* `ep`: the assignment epoch the player sent them under (sharded
|
|
56
|
+
* matches); a host at another epoch drops them.
|
|
57
|
+
*/
|
|
49
58
|
| {
|
|
50
59
|
readonly n: string;
|
|
51
60
|
readonly k: 'i';
|
|
52
61
|
readonly i: readonly WireInput<I>[];
|
|
62
|
+
readonly ep?: number;
|
|
53
63
|
}
|
|
54
|
-
/** Player → host: clock ping. */
|
|
64
|
+
/** Player → host: clock ping, repeating the codec fingerprint (a lost join must not pin a player to JSON). */
|
|
55
65
|
| {
|
|
56
66
|
readonly n: string;
|
|
57
67
|
readonly k: 'p';
|
|
58
68
|
readonly t0: number;
|
|
69
|
+
readonly f?: string;
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Player → host: binary-snapshot indexes it has no name for; the host
|
|
73
|
+
* names them again (only indexes it showed that player, and an index at
|
|
74
|
+
* most once per 100 ms).
|
|
75
|
+
*/
|
|
76
|
+
| {
|
|
77
|
+
readonly n: string;
|
|
78
|
+
readonly k: 'u';
|
|
79
|
+
readonly u: readonly number[];
|
|
59
80
|
}
|
|
60
81
|
/**
|
|
61
82
|
* Player → host: the interest keys it wants, version `v` (repeated until a
|
|
@@ -93,10 +114,23 @@ export type Frame<E, I> =
|
|
|
93
114
|
readonly of?: number;
|
|
94
115
|
/** The interest version this snapshot was filtered by, if any. */
|
|
95
116
|
readonly iv?: number;
|
|
117
|
+
/** The assignment epoch the host sent it under (sharded matches). */
|
|
118
|
+
readonly ep?: number;
|
|
96
119
|
};
|
|
97
120
|
/** Interest keys a player may name, and the longest key, in UTF-16 units. */
|
|
98
121
|
export declare const MAX_NETCODE_INTEREST_KEYS = 256;
|
|
99
122
|
export declare const MAX_NETCODE_INTEREST_KEY_LENGTH = 64;
|
|
123
|
+
/** Indexes one `u` frame may ask to have named. */
|
|
124
|
+
export declare const MAX_NAME_REQUESTS = 256;
|
|
125
|
+
/**
|
|
126
|
+
* A binary-snapshot name not used for this long, ms, is forgotten: by the
|
|
127
|
+
* host for an id no player has been shown (it gets a fresh index if it
|
|
128
|
+
* comes back, named again), and by the host for one player once that
|
|
129
|
+
* player has not been shown it. A player keeps a name three times as long
|
|
130
|
+
* from the last frame that carried the index, so it never forgets one the
|
|
131
|
+
* host still counts as known to it; forgetting early would only cost a `u`.
|
|
132
|
+
*/
|
|
133
|
+
export declare const NAME_FORGET_MS = 10000;
|
|
100
134
|
/** Chunks a snapshot may be split into, at most. */
|
|
101
135
|
export declare const MAX_SNAPSHOT_CHUNKS = 256;
|
|
102
136
|
/**
|
package/dist/netcode/wire.js
CHANGED
|
@@ -11,6 +11,17 @@
|
|
|
11
11
|
/** Interest keys a player may name, and the longest key, in UTF-16 units. */
|
|
12
12
|
export const MAX_NETCODE_INTEREST_KEYS = 256;
|
|
13
13
|
export const MAX_NETCODE_INTEREST_KEY_LENGTH = 64;
|
|
14
|
+
/** Indexes one `u` frame may ask to have named. */
|
|
15
|
+
export const MAX_NAME_REQUESTS = 256;
|
|
16
|
+
/**
|
|
17
|
+
* A binary-snapshot name not used for this long, ms, is forgotten: by the
|
|
18
|
+
* host for an id no player has been shown (it gets a fresh index if it
|
|
19
|
+
* comes back, named again), and by the host for one player once that
|
|
20
|
+
* player has not been shown it. A player keeps a name three times as long
|
|
21
|
+
* from the last frame that carried the index, so it never forgets one the
|
|
22
|
+
* host still counts as known to it; forgetting early would only cost a `u`.
|
|
23
|
+
*/
|
|
24
|
+
export const NAME_FORGET_MS = 10_000;
|
|
14
25
|
/** Chunks a snapshot may be split into, at most. */
|
|
15
26
|
export const MAX_SNAPSHOT_CHUNKS = 256;
|
|
16
27
|
/**
|
|
@@ -25,6 +36,10 @@ export function chunkOf(id, of) {
|
|
|
25
36
|
}
|
|
26
37
|
return (hash >>> 0) % of;
|
|
27
38
|
}
|
|
39
|
+
/** An `ep` field: absent, or a non-negative safe integer. */
|
|
40
|
+
function epochField(ep) {
|
|
41
|
+
return ep === undefined || (Number.isSafeInteger(ep) && ep >= 0);
|
|
42
|
+
}
|
|
28
43
|
const encoder = new TextEncoder();
|
|
29
44
|
const decoder = new TextDecoder('utf-8', { fatal: true });
|
|
30
45
|
export function encodeFrame(frame) {
|
|
@@ -47,11 +62,22 @@ export function decodeFrame(payload, label) {
|
|
|
47
62
|
const frame = value;
|
|
48
63
|
if (frame.n !== label || typeof frame.k !== 'string')
|
|
49
64
|
return null;
|
|
65
|
+
const fingerprint = frame.f;
|
|
66
|
+
if (fingerprint !== undefined && (typeof fingerprint !== 'string' || fingerprint.length > 64))
|
|
67
|
+
return null;
|
|
50
68
|
switch (frame.k) {
|
|
51
69
|
case 'h':
|
|
52
70
|
return frame;
|
|
71
|
+
case 'u': {
|
|
72
|
+
const u = frame.u;
|
|
73
|
+
return Array.isArray(u) && u.length <= MAX_NAME_REQUESTS && u.every(index => Number.isSafeInteger(index) && index >= 0)
|
|
74
|
+
? frame
|
|
75
|
+
: null;
|
|
76
|
+
}
|
|
53
77
|
case 'i':
|
|
54
|
-
return Array.isArray(frame.i)
|
|
78
|
+
return Array.isArray(frame.i) && epochField(frame.ep)
|
|
79
|
+
? frame
|
|
80
|
+
: null;
|
|
55
81
|
case 'p':
|
|
56
82
|
return typeof frame.t0 === 'number' ? frame : null;
|
|
57
83
|
case 'w': {
|
|
@@ -73,6 +99,7 @@ export function decodeFrame(payload, label) {
|
|
|
73
99
|
case 's': {
|
|
74
100
|
const s = frame;
|
|
75
101
|
if ((s.iv !== undefined && !Number.isSafeInteger(s.iv)) ||
|
|
102
|
+
!epochField(s.ep) ||
|
|
76
103
|
typeof s.tick !== 'number' ||
|
|
77
104
|
typeof s.t !== 'number' ||
|
|
78
105
|
typeof s.ack !== 'number' ||
|
package/dist/node.d.ts
CHANGED
|
@@ -883,6 +883,11 @@ export interface RetryReport {
|
|
|
883
883
|
readonly online: string;
|
|
884
884
|
/** Triggers filed by the ICE `disconnected` → `failed` watcher. */
|
|
885
885
|
readonly iceFailed: string;
|
|
886
|
+
/**
|
|
887
|
+
* Triggers filed because the far end closed a DataChannel a session
|
|
888
|
+
* rode. One channel's loss counts once, here or under `iceFailed`.
|
|
889
|
+
*/
|
|
890
|
+
readonly channelClosed: string;
|
|
886
891
|
/** Every trigger seen, whatever the owner decided. */
|
|
887
892
|
readonly triggers: string;
|
|
888
893
|
/** Re-attempts STARTED. One per network change, per peer. */
|
package/dist/node.js
CHANGED
|
@@ -1075,6 +1075,7 @@ export function parseRetryReport(json) {
|
|
|
1075
1075
|
armed: raw.armed === true,
|
|
1076
1076
|
online: String(raw.online ?? '0'),
|
|
1077
1077
|
iceFailed: String(raw.iceFailed ?? '0'),
|
|
1078
|
+
channelClosed: String(raw.channelClosed ?? '0'),
|
|
1078
1079
|
triggers: String(raw.triggers ?? '0'),
|
|
1079
1080
|
started: String(raw.started ?? '0'),
|
|
1080
1081
|
coalesced: String(raw.coalesced ?? '0'),
|
package/dist/world/link.d.ts
CHANGED
|
@@ -109,7 +109,16 @@ export interface RegionHandoffsOptions<E> {
|
|
|
109
109
|
};
|
|
110
110
|
/** Outcomes: `moved`, `refused`, `unresolved` (source), `admitted` (target). */
|
|
111
111
|
onEvent?(event: HandoffEvent): void;
|
|
112
|
+
/** Entity handoffs: re-offer, give up, and how long a decision is remembered. */
|
|
112
113
|
readonly timing?: HandoffTiming;
|
|
114
|
+
/**
|
|
115
|
+
* Forwarded actions ({@link RegionHandoffs.forward}), when they need other
|
|
116
|
+
* timing than entity handoffs: a hit is worthless after half a second,
|
|
117
|
+
* while a moving entity must not be given up that soon (it would be live
|
|
118
|
+
* nowhere) nor forgotten while its offer may still be re-sent (it could
|
|
119
|
+
* be admitted twice). Default: `timing`.
|
|
120
|
+
*/
|
|
121
|
+
readonly actionTiming?: HandoffTiming;
|
|
113
122
|
/** How often to re-offer and prune, ms. Default 100. */
|
|
114
123
|
readonly tickMs?: number;
|
|
115
124
|
readonly now?: () => number;
|
package/dist/world/link.js
CHANGED
|
@@ -264,9 +264,10 @@ export function regionHandoffs(options) {
|
|
|
264
264
|
options.link.send({ to, body: frame });
|
|
265
265
|
}
|
|
266
266
|
};
|
|
267
|
-
const
|
|
268
|
-
const
|
|
269
|
-
const
|
|
267
|
+
const actionTiming = options.actionTiming ?? options.timing;
|
|
268
|
+
const retryMs = actionTiming?.retryMs ?? 250;
|
|
269
|
+
const giveUpMs = actionTiming?.giveUpMs ?? 60_000;
|
|
270
|
+
const retentionMs = actionTiming?.handledRetentionMs ?? 600_000;
|
|
270
271
|
const stop = options.link.onMessage(body => {
|
|
271
272
|
if (closed)
|
|
272
273
|
return;
|
package/dist/world/regions.d.ts
CHANGED
|
@@ -44,8 +44,15 @@ export declare function regionsAround(x: number, z: number, options: {
|
|
|
44
44
|
}): string[];
|
|
45
45
|
/** The directory tag a region host announces. */
|
|
46
46
|
export declare function regionTag(world: string, region: string): string;
|
|
47
|
-
/**
|
|
48
|
-
|
|
47
|
+
/**
|
|
48
|
+
* How often region hosts re-announce: an announcement is a lease, held
|
|
49
|
+
* for the leaf's 300 s TTL. 30 s, not 2 s, for the reason
|
|
50
|
+
* `LOBBY_ANNOUNCE_MS` gives: re-announcements flood through the anchor to
|
|
51
|
+
* every peer, and at 2 s they saturated a host's anchor link at 64 nodes.
|
|
52
|
+
* A failed announcement is retried sooner, as a lobby's is
|
|
53
|
+
* (`ANNOUNCE_RETRY_MS`, doubling up to `ANNOUNCE_RETRY_MAX_MS`).
|
|
54
|
+
*/
|
|
55
|
+
export declare const REGION_ANNOUNCE_MS = 30000;
|
|
49
56
|
/**
|
|
50
57
|
* Announce that this node hosts `regions` of `world`, and keep announcing
|
|
51
58
|
* (an announcement is a lease). `announce` replaces a node's tags, so pass
|
package/dist/world/regions.js
CHANGED
|
@@ -27,6 +27,7 @@
|
|
|
27
27
|
* directory (`handoffLink({ peerOf: directory.peerOf })`) needs the list:
|
|
28
28
|
* otherwise a node that announces a region could also send offers as it.
|
|
29
29
|
*/
|
|
30
|
+
import { ANNOUNCE_RETRY_MAX_MS, ANNOUNCE_RETRY_MS } from '../lobby.js';
|
|
30
31
|
import { nodeHexOf } from '../store/host.js';
|
|
31
32
|
import { joinStore } from '../store/join.js';
|
|
32
33
|
/** Region `r:<rx>:<rz>` for a position, with regions `size` units square. */
|
|
@@ -51,8 +52,15 @@ export function regionsAround(x, z, options) {
|
|
|
51
52
|
export function regionTag(world, region) {
|
|
52
53
|
return `net-world:${world}:region:${region}`;
|
|
53
54
|
}
|
|
54
|
-
/**
|
|
55
|
-
|
|
55
|
+
/**
|
|
56
|
+
* How often region hosts re-announce: an announcement is a lease, held
|
|
57
|
+
* for the leaf's 300 s TTL. 30 s, not 2 s, for the reason
|
|
58
|
+
* `LOBBY_ANNOUNCE_MS` gives: re-announcements flood through the anchor to
|
|
59
|
+
* every peer, and at 2 s they saturated a host's anchor link at 64 nodes.
|
|
60
|
+
* A failed announcement is retried sooner, as a lobby's is
|
|
61
|
+
* (`ANNOUNCE_RETRY_MS`, doubling up to `ANNOUNCE_RETRY_MAX_MS`).
|
|
62
|
+
*/
|
|
63
|
+
export const REGION_ANNOUNCE_MS = 30_000;
|
|
56
64
|
/**
|
|
57
65
|
* Announce that this node hosts `regions` of `world`, and keep announcing
|
|
58
66
|
* (an announcement is a lease). `announce` replaces a node's tags, so pass
|
|
@@ -61,9 +69,29 @@ export const REGION_ANNOUNCE_MS = 2_000;
|
|
|
61
69
|
export function announceRegions(node, world, regions, options = {}) {
|
|
62
70
|
const tags = [...(options.tags ?? []), ...regions.map(region => regionTag(world, region))];
|
|
63
71
|
let stopped = false;
|
|
72
|
+
// A failed announcement is retried on a short backoff of its own: a
|
|
73
|
+
// region nobody can find for a whole lease refresh is a hole in the map.
|
|
74
|
+
let retry = null;
|
|
75
|
+
let retryMs = ANNOUNCE_RETRY_MS;
|
|
64
76
|
const once = () => {
|
|
65
|
-
if (
|
|
66
|
-
|
|
77
|
+
if (stopped)
|
|
78
|
+
return;
|
|
79
|
+
if (retry !== null) {
|
|
80
|
+
clearTimeout(retry);
|
|
81
|
+
retry = null;
|
|
82
|
+
}
|
|
83
|
+
void node.announce(tags).then(() => {
|
|
84
|
+
retryMs = ANNOUNCE_RETRY_MS;
|
|
85
|
+
}, () => {
|
|
86
|
+
if (stopped || retry !== null)
|
|
87
|
+
return;
|
|
88
|
+
retry = setTimeout(() => {
|
|
89
|
+
retry = null;
|
|
90
|
+
once();
|
|
91
|
+
}, retryMs);
|
|
92
|
+
retry.unref?.();
|
|
93
|
+
retryMs = Math.min(retryMs * 2, ANNOUNCE_RETRY_MAX_MS);
|
|
94
|
+
});
|
|
67
95
|
};
|
|
68
96
|
once();
|
|
69
97
|
const timer = setInterval(once, options.everyMs ?? REGION_ANNOUNCE_MS);
|
|
@@ -71,6 +99,8 @@ export function announceRegions(node, world, regions, options = {}) {
|
|
|
71
99
|
return () => {
|
|
72
100
|
stopped = true;
|
|
73
101
|
clearInterval(timer);
|
|
102
|
+
if (retry !== null)
|
|
103
|
+
clearTimeout(retry);
|
|
74
104
|
};
|
|
75
105
|
}
|
|
76
106
|
/** A region directory over the mesh's signed announcements. */
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@net-mesh/browser",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.44.0",
|
|
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",
|
|
@@ -59,7 +59,8 @@
|
|
|
59
59
|
"check:package": "node scripts/check-package.mjs",
|
|
60
60
|
"test": "tsc -p tsconfig.test.json && vitest run",
|
|
61
61
|
"test:watch": "vitest",
|
|
62
|
-
"bench:world": "node scripts/bench-world.mjs"
|
|
62
|
+
"bench:world": "node scripts/bench-world.mjs",
|
|
63
|
+
"bench:netcode": "node scripts/bench-netcode.mjs"
|
|
63
64
|
},
|
|
64
65
|
"devDependencies": {
|
|
65
66
|
"esbuild": "^0.28.0",
|