@net-mesh/browser 0.38.1 → 0.39.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.
@@ -119,16 +119,16 @@ export const proxystream_peer_node_hex: (a: number, b: number) => void;
119
119
  export const proxystream_send: (a: number, b: number) => number;
120
120
  export const proxystream_stream_id_hex: (a: number, b: number) => void;
121
121
  export const start: () => void;
122
- export const __wasm_bindgen_func_elem_6603: (a: number, b: number, c: number, d: number) => void;
123
- export const __wasm_bindgen_func_elem_6605: (a: number, b: number, c: number, d: number) => void;
124
- export const __wasm_bindgen_func_elem_2410: (a: number, b: number, c: number) => number;
125
- export const __wasm_bindgen_func_elem_2411: (a: number, b: number, c: number) => void;
126
- export const __wasm_bindgen_func_elem_2411_112: (a: number, b: number, c: number) => void;
127
- export const __wasm_bindgen_func_elem_2411_113: (a: number, b: number, c: number) => void;
128
- export const __wasm_bindgen_func_elem_2411_114: (a: number, b: number, c: number) => void;
129
- export const __wasm_bindgen_func_elem_2411_115: (a: number, b: number, c: number) => void;
130
- export const __wasm_bindgen_func_elem_2411_116: (a: number, b: number, c: number) => void;
131
- export const __wasm_bindgen_func_elem_2411_117: (a: number, b: number, c: number) => void;
122
+ export const __wasm_bindgen_func_elem_6509: (a: number, b: number, c: number, d: number) => void;
123
+ export const __wasm_bindgen_func_elem_6511: (a: number, b: number, c: number, d: number) => void;
124
+ export const __wasm_bindgen_func_elem_2365: (a: number, b: number, c: number) => number;
125
+ export const __wasm_bindgen_func_elem_2366: (a: number, b: number, c: number) => void;
126
+ export const __wasm_bindgen_func_elem_2366_112: (a: number, b: number, c: number) => void;
127
+ export const __wasm_bindgen_func_elem_2366_113: (a: number, b: number, c: number) => void;
128
+ export const __wasm_bindgen_func_elem_2366_114: (a: number, b: number, c: number) => void;
129
+ export const __wasm_bindgen_func_elem_2366_115: (a: number, b: number, c: number) => void;
130
+ export const __wasm_bindgen_func_elem_2366_116: (a: number, b: number, c: number) => void;
131
+ export const __wasm_bindgen_func_elem_2366_117: (a: number, b: number, c: number) => void;
132
132
  export const __wbindgen_export: (a: number, b: number) => number;
133
133
  export const __wbindgen_export2: (a: number, b: number, c: number, d: number) => number;
134
134
  export const __wbindgen_export3: (a: number) => void;
package/dist/node.d.ts CHANGED
@@ -294,6 +294,8 @@ export declare class BrowserNode {
294
294
  * handle.
295
295
  */
296
296
  private readonly streams;
297
+ /** One attempt per peer at a time: see {@link PeerAttempts}. */
298
+ private readonly attempts;
297
299
  /**
298
300
  * The live org handles (plan §4.5), so {@link BrowserNode.close}
299
301
  * can retire ownership: pending calls fail typed and each dropped
@@ -587,6 +589,15 @@ export declare class BrowserNode {
587
589
  * and close it under the other side. So a library may call it "to be
588
590
  * sure" before opening a stream.
589
591
  *
592
+ * **And on a pair that is still connecting.** A call while another
593
+ * `connectPeer` for the same peer is under way resolves with that
594
+ * attempt's outcome instead of offering again (a new offer would cancel
595
+ * it), and a call while an {@link acceptPeer} for the peer is under way
596
+ * waits for it and takes its outcome when that settles the pair
597
+ * (`direct`, `iceTimeout`, `udpBlocked`, or `superseded` by a newer
598
+ * attempt), offering only after an inconclusive one. See
599
+ * {@link PeerAttempts}.
600
+ *
590
601
  * Returns a {@link PeerConnectOutcome}. It **rejects** only for
591
602
  * something that is not a disposition of the attempt — a closed
592
603
  * node, a peer that answered `Reject`, a malformed peer id.
package/dist/node.js CHANGED
@@ -11,7 +11,7 @@ import { EventHub, parseEvent, parseJsonPreservingU64, toBase64, } from './event
11
11
  import { fromWasmError, IdentityError, OrgCancelledError, RtcError } from './errors.js';
12
12
  import { clientStreamOrgTrampoline, duplexOrgTrampoline, OrgDuplex, OrgHandleRegistry, OrgServe, OrgStream, OrgUpload, streamingOrgTrampoline, toWasmOrgCallOptions, toWasmOrgServeOptions, unaryOrgTrampoline, } from './org.js';
13
13
  import { LeafStream } from './stream.js';
14
- import { acceptPeer as driveAccept, connectPeer as driveConnect, handshakePeer as driveHandshake, } from './peer-driver.js';
14
+ import { acceptPeer as driveAccept, connectPeer as driveConnect, handshakePeer as driveHandshake, PeerAttempts, } from './peer-driver.js';
15
15
  import { classifyRtcFailureAll, probeBootstrapReachable, probeStunBindings, } from './udp-probe.js';
16
16
  import { idArg, loadLeafWasm, } from './wasm.js';
17
17
  /**
@@ -65,6 +65,8 @@ export class BrowserNode {
65
65
  * handle.
66
66
  */
67
67
  streams = new Set();
68
+ /** One attempt per peer at a time: see {@link PeerAttempts}. */
69
+ attempts = new PeerAttempts();
68
70
  /**
69
71
  * The live org handles (plan §4.5), so {@link BrowserNode.close}
70
72
  * can retire ownership: pending calls fail typed and each dropped
@@ -503,23 +505,38 @@ export class BrowserNode {
503
505
  * and close it under the other side. So a library may call it "to be
504
506
  * sure" before opening a stream.
505
507
  *
508
+ * **And on a pair that is still connecting.** A call while another
509
+ * `connectPeer` for the same peer is under way resolves with that
510
+ * attempt's outcome instead of offering again (a new offer would cancel
511
+ * it), and a call while an {@link acceptPeer} for the peer is under way
512
+ * waits for it and takes its outcome when that settles the pair
513
+ * (`direct`, `iceTimeout`, `udpBlocked`, or `superseded` by a newer
514
+ * attempt), offering only after an inconclusive one. See
515
+ * {@link PeerAttempts}.
516
+ *
506
517
  * Returns a {@link PeerConnectOutcome}. It **rejects** only for
507
518
  * something that is not a disposition of the attempt — a closed
508
519
  * node, a peer that answered `Reject`, a malformed peer id.
509
520
  */
510
521
  async connectPeer(nodeIdHex) {
511
- // Already direct and open: the pair it has. A new offer would
512
- // replace the working link and close it under the other side.
513
- let settled;
522
+ let peer;
514
523
  try {
515
- settled = this.inner.peer_direct_dialog(idArg(nodeIdHex, 'peer'));
524
+ peer = idArg(nodeIdHex, 'peer');
516
525
  }
517
526
  catch (error) {
518
527
  throw fromWasmError(error);
519
528
  }
520
- if (settled !== undefined && settled !== null)
521
- return { type: 'direct', peer: nodeIdHex, dialog: settled };
522
- return driveConnect(nodeIdHex, this.peerPrimitives(), parseAttemptStatus);
529
+ return this.attempts.connect(nodeIdHex,
530
+ // Already direct and open: the pair it has. A new offer would
531
+ // replace the working link and close it under the other side.
532
+ () => {
533
+ try {
534
+ return this.inner.peer_direct_dialog(peer);
535
+ }
536
+ catch (error) {
537
+ throw fromWasmError(error);
538
+ }
539
+ }, () => driveConnect(nodeIdHex, this.peerPrimitives(), parseAttemptStatus));
523
540
  }
524
541
  /**
525
542
  * The four primitives the shared drive loop calls, bound to this
@@ -577,7 +594,7 @@ export class BrowserNode {
577
594
  * {@link PeerConnectOutcome}, same meanings.
578
595
  */
579
596
  async acceptPeer(nodeIdHex) {
580
- return driveAccept(nodeIdHex, this.peerPrimitives(), parseAttemptStatus);
597
+ return this.attempts.accept(nodeIdHex, () => driveAccept(nodeIdHex, this.peerPrimitives(), parseAttemptStatus));
581
598
  }
582
599
  /**
583
600
  * Read one `peer_candidate` status for a live attempt.
@@ -77,6 +77,54 @@ export declare const PEER_TICK_MS = 50;
77
77
  * Two anchor hops and one pump, so it is generous rather than tight.
78
78
  */
79
79
  export declare const PEER_OFFER_WAIT_MS = 5000;
80
+ /**
81
+ * The pair's live dialog when it is already direct and open, `undefined`
82
+ * (or `null`) otherwise: the healthy-pair reading a surface's
83
+ * `connectPeer` must not offer over.
84
+ */
85
+ export type HealthyDialog = () => Promise<string | null | undefined> | string | null | undefined;
86
+ /**
87
+ * One attempt per peer at a time, per surface.
88
+ *
89
+ * **Every offer replaces the pair's link.** The leaf retires whatever
90
+ * attempt is live for the peer and replaces its DataChannel, so a
91
+ * second `connectPeer` while the first is still under way cancels it,
92
+ * and both callers end on the relay or in `iceTimeout`. Nothing about
93
+ * that is exotic: `joinLobby` reaches the host and then the store it
94
+ * joins reaches it again before its first stream, and netcode beside a
95
+ * store does the same. The healthy-direct check in
96
+ * `BrowserNode.connectPeer` covers a pair that has already arrived; this
97
+ * covers one that is still arriving.
98
+ *
99
+ * So, on one surface:
100
+ *
101
+ * - a `connectPeer` while another for the same peer is in flight is
102
+ * answered by that one's outcome, not by a second offer;
103
+ * - a `connectPeer` while an `acceptPeer` for the peer is in flight
104
+ * waits for it (offering over it would cancel the attempt it is
105
+ * answering), and takes its outcome when that settles the pair
106
+ * ({@link answerSettles}): it offers only after an inconclusive one;
107
+ * - concurrent `acceptPeer`s share one answer.
108
+ *
109
+ * Settled attempts are forgotten: the next call starts fresh. Another
110
+ * tab's node is outside this gate, as it is outside this surface.
111
+ */
112
+ export declare class PeerAttempts {
113
+ #private;
114
+ /**
115
+ * Run `drive` as this peer's offer, unless one is already under way,
116
+ * the pair is already direct, or an answer under way settles it.
117
+ *
118
+ * `healthy` is read HERE, inside the gate and just before the offer,
119
+ * never by the caller ahead of it: on a proxied surface the reading is
120
+ * a round trip, and an `acceptPeer` could take the pair direct and
121
+ * leave the gate between a caller's early reading and its offer, which
122
+ * would then replace the healthy link.
123
+ */
124
+ connect(peer: string, healthy: HealthyDialog, drive: () => Promise<PeerConnectOutcome>): Promise<PeerConnectOutcome>;
125
+ /** Run `drive` as this peer's answer, unless one is already under way. */
126
+ accept(peer: string, drive: () => Promise<PeerConnectOutcome>): Promise<PeerConnectOutcome>;
127
+ }
80
128
  /** Offer, drive to an open channel, then handshake (§9 steps 3-4). */
81
129
  export declare function connectPeer(peer: string, primitives: PeerPrimitives, parse: (json: string) => PeerAttemptStatus): Promise<PeerConnectOutcome>;
82
130
  /**
@@ -58,6 +58,110 @@ export const PEER_TICK_MS = 50;
58
58
  * Two anchor hops and one pump, so it is generous rather than tight.
59
59
  */
60
60
  export const PEER_OFFER_WAIT_MS = 5_000;
61
+ /**
62
+ * Whether an answer's outcome settles the pair for a `connectPeer` that
63
+ * waited on it, rather than calling for an offer of its own.
64
+ *
65
+ * - `direct`: the pair has its link.
66
+ * - `iceTimeout`, `udpBlocked`: this network could not go direct just
67
+ * now, and the routed session the answer ran over is still in place.
68
+ * An offer straight after would almost surely end the same way, a whole
69
+ * ICE deadline later, so the caller gets the answer's verdict instead.
70
+ * - `superseded` naming a successor: a newer attempt is live, and an
71
+ * offer would cancel it.
72
+ *
73
+ * Anything else (`handshakeFailed`, `noAnnouncement`, a `superseded`
74
+ * with no successor) says nothing about the pair, so the waiter offers.
75
+ */
76
+ function answerSettles(outcome) {
77
+ switch (outcome.type) {
78
+ case 'direct':
79
+ case 'iceTimeout':
80
+ case 'udpBlocked':
81
+ return true;
82
+ case 'superseded':
83
+ return outcome.liveDialog !== null;
84
+ default:
85
+ return false;
86
+ }
87
+ }
88
+ /**
89
+ * One attempt per peer at a time, per surface.
90
+ *
91
+ * **Every offer replaces the pair's link.** The leaf retires whatever
92
+ * attempt is live for the peer and replaces its DataChannel, so a
93
+ * second `connectPeer` while the first is still under way cancels it,
94
+ * and both callers end on the relay or in `iceTimeout`. Nothing about
95
+ * that is exotic: `joinLobby` reaches the host and then the store it
96
+ * joins reaches it again before its first stream, and netcode beside a
97
+ * store does the same. The healthy-direct check in
98
+ * `BrowserNode.connectPeer` covers a pair that has already arrived; this
99
+ * covers one that is still arriving.
100
+ *
101
+ * So, on one surface:
102
+ *
103
+ * - a `connectPeer` while another for the same peer is in flight is
104
+ * answered by that one's outcome, not by a second offer;
105
+ * - a `connectPeer` while an `acceptPeer` for the peer is in flight
106
+ * waits for it (offering over it would cancel the attempt it is
107
+ * answering), and takes its outcome when that settles the pair
108
+ * ({@link answerSettles}): it offers only after an inconclusive one;
109
+ * - concurrent `acceptPeer`s share one answer.
110
+ *
111
+ * Settled attempts are forgotten: the next call starts fresh. Another
112
+ * tab's node is outside this gate, as it is outside this surface.
113
+ */
114
+ export class PeerAttempts {
115
+ #connecting = new Map();
116
+ #accepting = new Map();
117
+ /**
118
+ * Run `drive` as this peer's offer, unless one is already under way,
119
+ * the pair is already direct, or an answer under way settles it.
120
+ *
121
+ * `healthy` is read HERE, inside the gate and just before the offer,
122
+ * never by the caller ahead of it: on a proxied surface the reading is
123
+ * a round trip, and an `acceptPeer` could take the pair direct and
124
+ * leave the gate between a caller's early reading and its offer, which
125
+ * would then replace the healthy link.
126
+ */
127
+ connect(peer, healthy, drive) {
128
+ const key = peer.toLowerCase();
129
+ const joining = this.#connecting.get(key);
130
+ if (joining !== undefined)
131
+ return joining;
132
+ const answering = this.#accepting.get(key);
133
+ const started = (async () => {
134
+ if (answering !== undefined) {
135
+ const answered = await answering.catch(() => null);
136
+ if (answered !== null && answerSettles(answered))
137
+ return answered;
138
+ }
139
+ const dialog = await healthy();
140
+ if (dialog !== undefined && dialog !== null)
141
+ return { type: 'direct', peer, dialog };
142
+ return drive();
143
+ })();
144
+ return PeerAttempts.#track(this.#connecting, key, started);
145
+ }
146
+ /** Run `drive` as this peer's answer, unless one is already under way. */
147
+ accept(peer, drive) {
148
+ const key = peer.toLowerCase();
149
+ const joining = this.#accepting.get(key);
150
+ if (joining !== undefined)
151
+ return joining;
152
+ return PeerAttempts.#track(this.#accepting, key, drive());
153
+ }
154
+ static #track(map, key, started) {
155
+ map.set(key, started);
156
+ // Forget it once settled, and only if no newer one took its place.
157
+ const forget = () => {
158
+ if (map.get(key) === started)
159
+ map.delete(key);
160
+ };
161
+ started.then(forget, forget);
162
+ return started;
163
+ }
164
+ }
61
165
  /** Offer, drive to an open channel, then handshake (§9 steps 3-4). */
62
166
  export async function connectPeer(peer, primitives, parse) {
63
167
  let dialog;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@net-mesh/browser",
3
- "version": "0.38.1",
3
+ "version": "0.39.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",