@net-mesh/browser 0.37.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.
Files changed (110) hide show
  1. package/CHANGELOG.md +517 -0
  2. package/LICENSE-APACHE +190 -0
  3. package/LICENSE-MIT +21 -0
  4. package/README.md +666 -0
  5. package/dist/async-queue.d.ts +26 -0
  6. package/dist/async-queue.js +62 -0
  7. package/dist/credential.d.ts +70 -0
  8. package/dist/credential.js +95 -0
  9. package/dist/errors.d.ts +400 -0
  10. package/dist/errors.js +726 -0
  11. package/dist/events.d.ts +232 -0
  12. package/dist/events.js +298 -0
  13. package/dist/identity.d.ts +40 -0
  14. package/dist/identity.js +78 -0
  15. package/dist/index.bundle.js +2 -0
  16. package/dist/index.d.ts +38 -0
  17. package/dist/index.js +46 -0
  18. package/dist/leader/events.d.ts +111 -0
  19. package/dist/leader/events.js +116 -0
  20. package/dist/leader/index.d.ts +14 -0
  21. package/dist/leader/index.js +11 -0
  22. package/dist/leader/session.d.ts +539 -0
  23. package/dist/leader/session.js +770 -0
  24. package/dist/leader/wasm.d.ts +123 -0
  25. package/dist/leader/wasm.js +39 -0
  26. package/dist/lobby.d.ts +183 -0
  27. package/dist/lobby.js +504 -0
  28. package/dist/local.d.ts +80 -0
  29. package/dist/local.js +160 -0
  30. package/dist/net_leaf.d.ts +1494 -0
  31. package/dist/net_leaf.js +3753 -0
  32. package/dist/net_leaf_bg.wasm +0 -0
  33. package/dist/net_leaf_bg.wasm.d.ts +137 -0
  34. package/dist/netcode/client.d.ts +102 -0
  35. package/dist/netcode/client.js +296 -0
  36. package/dist/netcode/clock.d.ts +45 -0
  37. package/dist/netcode/clock.js +54 -0
  38. package/dist/netcode/host.d.ts +118 -0
  39. package/dist/netcode/host.js +276 -0
  40. package/dist/netcode/index.d.ts +19 -0
  41. package/dist/netcode/index.js +14 -0
  42. package/dist/netcode/interpolate.d.ts +50 -0
  43. package/dist/netcode/interpolate.js +100 -0
  44. package/dist/netcode/wire.d.ts +125 -0
  45. package/dist/netcode/wire.js +117 -0
  46. package/dist/node.d.ts +908 -0
  47. package/dist/node.js +1085 -0
  48. package/dist/org.d.ts +412 -0
  49. package/dist/org.js +567 -0
  50. package/dist/peer-driver.d.ts +117 -0
  51. package/dist/peer-driver.js +224 -0
  52. package/dist/store/assembly.d.ts +176 -0
  53. package/dist/store/assembly.js +325 -0
  54. package/dist/store/chunker.d.ts +83 -0
  55. package/dist/store/chunker.js +190 -0
  56. package/dist/store/core.d.ts +136 -0
  57. package/dist/store/core.js +477 -0
  58. package/dist/store/definition.d.ts +23 -0
  59. package/dist/store/definition.js +83 -0
  60. package/dist/store/errors.d.ts +77 -0
  61. package/dist/store/errors.js +94 -0
  62. package/dist/store/host.d.ts +247 -0
  63. package/dist/store/host.js +675 -0
  64. package/dist/store/index.d.ts +50 -0
  65. package/dist/store/index.js +40 -0
  66. package/dist/store/interest.d.ts +40 -0
  67. package/dist/store/interest.js +77 -0
  68. package/dist/store/inventory.d.ts +81 -0
  69. package/dist/store/inventory.js +140 -0
  70. package/dist/store/join.d.ts +118 -0
  71. package/dist/store/join.js +724 -0
  72. package/dist/store/json.d.ts +50 -0
  73. package/dist/store/json.js +283 -0
  74. package/dist/store/ledger.d.ts +187 -0
  75. package/dist/store/ledger.js +292 -0
  76. package/dist/store/owner.d.ts +616 -0
  77. package/dist/store/owner.js +1711 -0
  78. package/dist/store/patch.d.ts +70 -0
  79. package/dist/store/patch.js +166 -0
  80. package/dist/store/player.d.ts +65 -0
  81. package/dist/store/player.js +261 -0
  82. package/dist/store/replica.d.ts +187 -0
  83. package/dist/store/replica.js +740 -0
  84. package/dist/store/state.d.ts +42 -0
  85. package/dist/store/state.js +165 -0
  86. package/dist/store/types.d.ts +200 -0
  87. package/dist/store/types.js +25 -0
  88. package/dist/store/visibility.d.ts +118 -0
  89. package/dist/store/visibility.js +282 -0
  90. package/dist/store/wire.d.ts +309 -0
  91. package/dist/store/wire.js +834 -0
  92. package/dist/stream.d.ts +174 -0
  93. package/dist/stream.js +238 -0
  94. package/dist/three/index.d.ts +109 -0
  95. package/dist/three/index.js +130 -0
  96. package/dist/udp-probe.d.ts +159 -0
  97. package/dist/udp-probe.js +245 -0
  98. package/dist/wasm.d.ts +642 -0
  99. package/dist/wasm.js +98 -0
  100. package/dist/world/border.d.ts +84 -0
  101. package/dist/world/border.js +99 -0
  102. package/dist/world/handoff.d.ts +187 -0
  103. package/dist/world/handoff.js +222 -0
  104. package/dist/world/index.d.ts +13 -0
  105. package/dist/world/index.js +9 -0
  106. package/dist/world/link.d.ts +179 -0
  107. package/dist/world/link.js +476 -0
  108. package/dist/world/regions.d.ts +154 -0
  109. package/dist/world/regions.js +314 -0
  110. package/package.json +72 -0
@@ -0,0 +1,224 @@
1
+ /**
2
+ * The peer-attempt drive loop, once, for both surfaces.
3
+ *
4
+ * `BrowserNode` drives an attempt on this tab's own node;
5
+ * `MeshSession` drives one on whichever tab holds the lock. The steps
6
+ * and the classification are identical, and they must stay identical:
7
+ * the offerer and the answerer disagreeing about what supersession or
8
+ * a terminal reading looks like is a defect neither side can detect.
9
+ * So the loop lives here and each surface supplies only the four
10
+ * primitives ({@link PeerPrimitives}).
11
+ *
12
+ * **Supersession now has two sources, and that is the reason this
13
+ * file exists rather than a second copy of the loop.**
14
+ *
15
+ * 1. *Client-side*, as before: the reading names the dialog that is
16
+ * live, so a reading for a different dialog means this attempt was
17
+ * replaced.
18
+ * 2. *Server-side*, new with the proxy: a request that named a dialog
19
+ * is refused by the leaf **before** it services anything, because
20
+ * the attempt it was issued for is no longer the live one. A
21
+ * proxied request crosses a channel and is served later, so this is
22
+ * the common case for a follower rather than an edge.
23
+ *
24
+ * Both are the same disposition to a caller, and both are classified
25
+ * here. A direct caller sees (1) almost always and (2) never; a
26
+ * proxied caller sees either.
27
+ */
28
+ import { fromWasmError } from './errors.js';
29
+ /**
30
+ * The exact prefix the leaf's "peer not discovered" refusal carries
31
+ * (`wasm.rs`'s `NO_ANNOUNCEMENT_PREFIX`).
32
+ *
33
+ * Pinned because the wasm boundary carries messages, so a message is
34
+ * a contract: a reworded refusal demotes a typed disposition to a
35
+ * thrown error with nothing red.
36
+ */
37
+ export const NO_ANNOUNCEMENT_PREFIX = 'no verified announcement for';
38
+ /** `wasm.rs`'s `NO_LIVE_ATTEMPT_PREFIX`. */
39
+ export const NO_LIVE_ATTEMPT_PREFIX = 'no live attempt with';
40
+ /** `wasm.rs`'s refusal when no verified offer is waiting yet. */
41
+ export const NO_OFFER_PREFIX = 'no verified offer from';
42
+ /**
43
+ * `wasm.rs`'s `attempt_for` refusal: the named dialog is not the one
44
+ * now live.
45
+ *
46
+ * Distinct from {@link NO_LIVE_ATTEMPT_PREFIX} on purpose — "your
47
+ * attempt was replaced" and "there is no attempt" are different
48
+ * facts, and the leaf says which. Both classify as `superseded`, but
49
+ * only this one can name the replacement.
50
+ */
51
+ export const REPLACED_ATTEMPT_MARKER = 'is not the live attempt';
52
+ /** How long the drive loop waits between candidate steps. */
53
+ export const PEER_TICK_MS = 50;
54
+ /**
55
+ * How long {@link acceptPeer} waits for the offer envelope to arrive
56
+ * before it treats "no offer" as the answer.
57
+ *
58
+ * Two anchor hops and one pump, so it is generous rather than tight.
59
+ */
60
+ export const PEER_OFFER_WAIT_MS = 5_000;
61
+ /** Offer, drive to an open channel, then handshake (§9 steps 3-4). */
62
+ export async function connectPeer(peer, primitives, parse) {
63
+ let dialog;
64
+ try {
65
+ dialog = await primitives.offer(peer);
66
+ }
67
+ catch (thrown) {
68
+ const error = fromWasmError(thrown);
69
+ if (error.message.includes(NO_ANNOUNCEMENT_PREFIX)) {
70
+ return { type: 'noAnnouncement', peer, detail: error.message };
71
+ }
72
+ throw error;
73
+ }
74
+ const gathered = await driveAttempt(peer, dialog, (status) => status.state === 'open', primitives, parse);
75
+ if (gathered.type !== 'direct')
76
+ return gathered;
77
+ return handshakePeer(peer, dialog, primitives);
78
+ }
79
+ /**
80
+ * Answer an offer and drive to a direct session.
81
+ *
82
+ * The accept is retried while the leaf reports no offer yet: the
83
+ * envelope crosses A → anchor → B, so a page reacting to "the peer is
84
+ * here" a moment before it lands would be told there is no offer,
85
+ * which is true and not an answer. Every other refusal is returned or
86
+ * thrown at once, because retrying those would only hide them.
87
+ */
88
+ export async function acceptPeer(peer, primitives, parse) {
89
+ let dialog = null;
90
+ const until = Date.now() + PEER_OFFER_WAIT_MS;
91
+ for (;;) {
92
+ try {
93
+ dialog = await primitives.acceptOffer(peer);
94
+ break;
95
+ }
96
+ catch (thrown) {
97
+ const error = fromWasmError(thrown);
98
+ if (error.message.includes(NO_ANNOUNCEMENT_PREFIX)) {
99
+ return { type: 'noAnnouncement', peer, detail: error.message };
100
+ }
101
+ if (!error.message.includes(NO_OFFER_PREFIX) || Date.now() >= until) {
102
+ throw error;
103
+ }
104
+ await tick();
105
+ }
106
+ }
107
+ return driveAttempt(peer, dialog, (status) => status.direct, primitives, parse);
108
+ }
109
+ /**
110
+ * Run the offerer's handshake on an attempt whose channel is open.
111
+ *
112
+ * The dialog is **checked** rather than assumed: the handshake
113
+ * resolves to the dialog it actually ran for, and a mismatch is this
114
+ * caller's attempt having been replaced underneath it. Reporting
115
+ * `direct` with the caller's own dialog on any success is how a stale
116
+ * caller was once told its superseded attempt had connected, while
117
+ * the leaf had handshaken the attempt that replaced it.
118
+ */
119
+ export async function handshakePeer(peer, dialog, primitives) {
120
+ let handshaken;
121
+ try {
122
+ handshaken = await primitives.handshake(peer, dialog);
123
+ }
124
+ catch (thrown) {
125
+ const superseded = classifySuperseded(thrown, peer, dialog);
126
+ if (superseded !== null)
127
+ return superseded;
128
+ return { type: 'handshakeFailed', peer, dialog, detail: fromWasmError(thrown).message };
129
+ }
130
+ if (handshaken !== dialog) {
131
+ return { type: 'superseded', peer, dialog, liveDialog: handshaken };
132
+ }
133
+ return { type: 'direct', peer, dialog };
134
+ }
135
+ /**
136
+ * Pump one attempt until `done`, its terminal transition, or a newer
137
+ * attempt replaces it.
138
+ *
139
+ * `{ type: 'direct' }` here means only "`done` answered true" — the
140
+ * caller decides what remains.
141
+ *
142
+ * **Every terminal state ends the loop**, not only the two ICE ones.
143
+ * An answerer waits for `direct`, and a channel that opened with a
144
+ * session that never installed satisfies neither `direct` nor
145
+ * `iceTimeout`: the leaf reports `failed`, which is this loop's exit.
146
+ * The bound is the leaf's, which owns the attempt's deadline — a
147
+ * timeout added here would be a second clock disagreeing with it.
148
+ */
149
+ export async function driveAttempt(peer, dialog, done, primitives, parse) {
150
+ for (;;) {
151
+ let status;
152
+ try {
153
+ status = parse(await primitives.candidate(peer, dialog));
154
+ }
155
+ catch (thrown) {
156
+ const superseded = classifySuperseded(thrown, peer, dialog);
157
+ if (superseded !== null)
158
+ return superseded;
159
+ throw fromWasmError(thrown);
160
+ }
161
+ if (status.dialog !== dialog) {
162
+ return { type: 'superseded', peer, dialog, liveDialog: status.dialog };
163
+ }
164
+ if (done(status))
165
+ return { type: 'direct', peer, dialog };
166
+ if (status.state === 'iceTimeout')
167
+ return { type: 'iceTimeout', peer, dialog };
168
+ if (status.state === 'udpBlocked')
169
+ return { type: 'udpBlocked', peer, dialog };
170
+ if (status.state === 'failed') {
171
+ return {
172
+ type: 'handshakeFailed',
173
+ peer,
174
+ dialog,
175
+ detail: status.candidateError ??
176
+ 'the attempt reached its terminal transition without installing a session',
177
+ };
178
+ }
179
+ await tick();
180
+ }
181
+ }
182
+ /**
183
+ * `superseded` when `thrown` is one of the leaf's two
184
+ * attempt-ownership refusals, `null` when it is anything else.
185
+ *
186
+ * The two are kept apart in the reading, not collapsed: an ended
187
+ * attempt cannot name a successor (`liveDialog: null`), while a
188
+ * replaced one is refused by a leaf that knows which dialog took over
189
+ * — so the replacement's id is recovered from the message rather than
190
+ * reported as absent.
191
+ */
192
+ function classifySuperseded(thrown, peer, dialog) {
193
+ const { message } = fromWasmError(thrown);
194
+ if (message.includes(REPLACED_ATTEMPT_MARKER)) {
195
+ return { type: 'superseded', peer, dialog, liveDialog: liveDialogFrom(message) };
196
+ }
197
+ if (message.includes(NO_LIVE_ATTEMPT_PREFIX)) {
198
+ return { type: 'superseded', peer, dialog, liveDialog: null };
199
+ }
200
+ return null;
201
+ }
202
+ /**
203
+ * The dialog `attempt_for`'s refusal names as live, or `null` if the
204
+ * message does not carry one.
205
+ *
206
+ * Reads the parenthesised `(dialog <id> is)` the leaf writes, in the
207
+ * **16-hex spelling** every dialog on every surface uses — so a
208
+ * recovered `liveDialog` compares equal to one that came out of a
209
+ * reading. The leaf's message was decimal when this was first
210
+ * written, which would have produced a `liveDialog` matching nothing.
211
+ *
212
+ * A refusal whose wording changed yields `null` — a missing successor
213
+ * id, which is honest — rather than a wrong one.
214
+ */
215
+ function liveDialogFrom(message) {
216
+ const match = /\(dialog ([0-9a-f]{16}) is\)/.exec(message);
217
+ return match?.[1] ?? null;
218
+ }
219
+ /** One drive-loop tick. */
220
+ async function tick() {
221
+ const waited = Promise.withResolvers();
222
+ setTimeout(waited.resolve, PEER_TICK_MS);
223
+ await waited.promise;
224
+ }
@@ -0,0 +1,176 @@
1
+ /**
2
+ * Snapshot assembly, and what it refuses (brief §1.12, §2).
3
+ *
4
+ * The rules are short and every one of them is a refusal a partial
5
+ * snapshot would otherwise slip through:
6
+ *
7
+ * - `n ≥ 1`; every `i` in `0..n-1` present **exactly once**;
8
+ * - all chunks carry the same `(h, g, r, n)`;
9
+ * - the sum of decoded chunk lengths equals `bytes` **exactly**;
10
+ * - **the assembled document** — not each chunk envelope — is then
11
+ * JSON-parsed under the same depth and duplicate-key rules and
12
+ * validated by the definition's `state()` before anything is
13
+ * published.
14
+ *
15
+ * That last one is the distinction the earlier draft got wrong: the
16
+ * per-chunk checks validate the *envelopes*, not the snapshot. A
17
+ * document can be assembled from thirteen individually perfect
18
+ * envelopes and still be a duplicate-keyed depth bomb.
19
+ *
20
+ * Reclamation is equally explicit, because an assembly is the one
21
+ * place a replica holds a megabyte on a stranger's say-so: **one**
22
+ * in-flight per `(handle, generation)`, discarded on generation
23
+ * change, deadline and handle expiry, never merged into a later one,
24
+ * and bounded in total across handles so a peer cannot open many
25
+ * cheap assemblies to hold expensive memory.
26
+ */
27
+ import type { StoreErrorCode } from './errors.js';
28
+ import { type JsonValue } from './json.js';
29
+ import { type Decimal, type Hex } from './wire.js';
30
+ /** How long an assembly may stay open (brief §2). */
31
+ export declare const ASSEMBLY_DEADLINE_MS = 10000;
32
+ /** Total in-flight assembly bytes one owner or replica may hold (§2). */
33
+ export declare const MAX_ASSEMBLY_BYTES_TOTAL: number;
34
+ /** The manifest an assembly is opened against. */
35
+ export interface AssemblyManifest {
36
+ readonly h: Hex;
37
+ readonly g: Decimal;
38
+ readonly r: Decimal;
39
+ readonly n: number;
40
+ /** Declared decoded byte total. */
41
+ readonly bytes: Decimal;
42
+ }
43
+ /** One chunk, as decoded from the wire. */
44
+ export interface AssemblyChunk {
45
+ readonly h: Hex;
46
+ readonly g: Decimal;
47
+ readonly r: Decimal;
48
+ readonly n: number;
49
+ readonly i: number;
50
+ readonly d: string;
51
+ }
52
+ /** Why an assembly refused something. Stable and countable. */
53
+ export type AssemblyRefusal = 'manifest-chunk-count' | 'manifest-bytes' | 'assembly-too-large' | 'chunk-wrong-assembly' | 'chunk-index' | 'chunk-non-canonical' | 'chunk-conflict' | 'chunk-duplicate' | 'byte-total-mismatch' | 'assembled-not-json' | 'assembled-invalid' | 'deadline';
54
+ export interface AssemblyRejected {
55
+ readonly ok: false;
56
+ readonly code: StoreErrorCode;
57
+ readonly reason: AssemblyRefusal;
58
+ /**
59
+ * Whether this ends the assembly. A conflicting duplicate restarts
60
+ * it (§5.1), an out-of-assembly chunk is merely dropped.
61
+ */
62
+ readonly fatal: boolean;
63
+ }
64
+ export type ChunkOutcome = {
65
+ readonly ok: true;
66
+ readonly done: false;
67
+ readonly have: number;
68
+ } | {
69
+ readonly ok: true;
70
+ readonly done: true;
71
+ readonly document: JsonValue;
72
+ } | AssemblyRejected;
73
+ /**
74
+ * One in-flight snapshot assembly.
75
+ *
76
+ * Holds the chunks it has admitted and nothing else: no partial
77
+ * document is exposed, and there is no accessor that would let a
78
+ * caller read half a world.
79
+ */
80
+ export declare class Assembly {
81
+ readonly manifest: AssemblyManifest;
82
+ /** When this assembly was opened, for the deadline. */
83
+ private readonly openedAt;
84
+ private readonly pieces;
85
+ private readonly encoded;
86
+ private admitted;
87
+ private held;
88
+ private closed;
89
+ constructor(manifest: AssemblyManifest,
90
+ /** When this assembly was opened, for the deadline. */
91
+ openedAt: number);
92
+ /** Decoded bytes currently held. */
93
+ get bytesHeld(): number;
94
+ /**
95
+ * Bytes this assembly is COMMITTED to, for the total-bytes bound.
96
+ *
97
+ * The manifest's declared total, not what has arrived: an assembly
98
+ * that has admitted one chunk of twenty still intends to hold the
99
+ * whole snapshot, and a bound tested against arrivals lets every
100
+ * one of them in while each is still nearly empty. A reclaimed
101
+ * assembly is committed to nothing.
102
+ */
103
+ get bytesReserved(): number;
104
+ /** How many distinct `i` have been admitted. */
105
+ get have(): number;
106
+ /** Whether this assembly has been reclaimed. */
107
+ get reclaimed(): boolean;
108
+ /** Give up every chunk. A reclaimed assembly admits nothing. */
109
+ reclaim(): void;
110
+ /** Whether `now` is past this assembly's deadline. */
111
+ expired(now: number): boolean;
112
+ /**
113
+ * Admit one chunk.
114
+ *
115
+ * Returns the assembled document on the chunk that completes it, and
116
+ * **only** then: `done: false` carries a count, never a partial
117
+ * document.
118
+ */
119
+ accept(chunk: AssemblyChunk, validate: (raw: unknown) => unknown, now: number): ChunkOutcome;
120
+ }
121
+ /**
122
+ * The assemblies one side holds, with the bounds that keep them from
123
+ * being a memory lever.
124
+ *
125
+ * One in-flight per `(handle, generation)` — a second manifest for the
126
+ * same pair replaces it rather than accumulating — and a total byte
127
+ * ceiling across handles, because a count bound alone is not a memory
128
+ * bound (§2).
129
+ *
130
+ * **The deadline is the HOLDER's to drive.** Nothing inside this class
131
+ * reads a clock: a host holding its own table MUST call {@link
132
+ * AssemblyTable.sweep} from its own. The store's own replica needs no
133
+ * such call — it enforces the assembly deadline in its `tick`, where
134
+ * an expired assembly is abandoned and re-asked for rather than
135
+ * silently dropped, and clears the table through `reclaimHandle`.
136
+ */
137
+ export declare class AssemblyTable {
138
+ private readonly maxTotalBytes;
139
+ private readonly live;
140
+ constructor(maxTotalBytes?: number);
141
+ private static key;
142
+ /** Bytes currently held across every open assembly. */
143
+ get bytesHeld(): number;
144
+ /** Bytes every open assembly is committed to holding. */
145
+ get bytesReserved(): number;
146
+ get size(): number;
147
+ /** The open assembly for a pair, if any. */
148
+ get(h: Hex, g: Decimal): Assembly | undefined;
149
+ /**
150
+ * Open an assembly for a manifest.
151
+ *
152
+ * Refuses the **newest** when the total would exceed the ceiling:
153
+ * the ones already in flight have a replica waiting on them, and
154
+ * dropping those to admit an arrival is how a peer evicts another's
155
+ * progress.
156
+ */
157
+ open(manifest: AssemblyManifest, now: number): Assembly | AssemblyRejected;
158
+ /** Reclaim one pair's assembly. */
159
+ reclaim(h: Hex, g: Decimal): void;
160
+ /**
161
+ * Reclaim every assembly for a handle — a generation change, a
162
+ * handle expiry, a session loss.
163
+ */
164
+ reclaimHandle(h: Hex): void;
165
+ /**
166
+ * Reclaim everything past its deadline. Returns how many went.
167
+ *
168
+ * The one deadline enforcer for a table this module does not drive:
169
+ * a host holding its own `AssemblyTable` must call this from its own
170
+ * clock (see the class note), or its assemblies hold their
171
+ * reservations for ever.
172
+ */
173
+ sweep(now: number): number;
174
+ /** Give up everything. */
175
+ clear(): void;
176
+ }