@muhkoo/connect 0.10.3-alpha.0 → 0.10.5-alpha.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/dist/browser/index.js +70 -19
- package/dist/browser/index.js.map +1 -1
- package/dist/connect.d.ts +24 -6
- package/dist/server/index.js +70 -19
- package/dist/server/index.js.map +1 -1
- package/package.json +1 -1
package/dist/connect.d.ts
CHANGED
|
@@ -4379,6 +4379,10 @@ declare class SpaceNamespace {
|
|
|
4379
4379
|
private spaceCache?;
|
|
4380
4380
|
/** Spaces built this session, by id — used to route offline send replays. */
|
|
4381
4381
|
private readonly openSpaces;
|
|
4382
|
+
/** Unique id for THIS device/session — the P2P peer identity. A user's own
|
|
4383
|
+
* devices share a member id, so the mesh MUST distinguish them by this or a
|
|
4384
|
+
* device treats its siblings' signals as its own echo and never connects. */
|
|
4385
|
+
private readonly deviceId;
|
|
4382
4386
|
constructor(deps: SpaceNamespaceDeps);
|
|
4383
4387
|
/**
|
|
4384
4388
|
* Create a new space: generate its keypair (public key = id), register
|
|
@@ -5392,15 +5396,20 @@ declare class PeerExchange implements PeerBlockSource {
|
|
|
5392
5396
|
* to Space membership. The sender (`from`) is server-stamped and can't be
|
|
5393
5397
|
* spoofed; the payload is opaque to the server.
|
|
5394
5398
|
*
|
|
5395
|
-
*
|
|
5396
|
-
*
|
|
5397
|
-
*
|
|
5398
|
-
*
|
|
5399
|
+
* Peers are identified by a unique **peer id** (per device/connection), carried
|
|
5400
|
+
* in the envelope's `from`/`to` — NOT by the server-stamped member id. This is
|
|
5401
|
+
* essential: one user's own devices share a member id, so the server's `from`
|
|
5402
|
+
* can't tell them apart (and would make each treat the others' signals as its
|
|
5403
|
+
* own echo). We therefore route entirely on the envelope peer ids and broadcast
|
|
5404
|
+
* to the whole Space, filtering client-side. The server `from` still guarantees
|
|
5405
|
+
* the sender is an authenticated Space member.
|
|
5399
5406
|
*/
|
|
5400
5407
|
declare const P2P_SUBJECT = "__p2p__";
|
|
5401
5408
|
type SignalKind = "hello" | "bye" | "offer" | "answer" | "ice";
|
|
5402
5409
|
interface SignalEnvelope {
|
|
5403
|
-
/**
|
|
5410
|
+
/** Sender's unique PEER id (per device/connection). */
|
|
5411
|
+
from: string;
|
|
5412
|
+
/** Target peer id, or `"*"` to broadcast (discovery / departure). */
|
|
5404
5413
|
to: string;
|
|
5405
5414
|
kind: SignalKind;
|
|
5406
5415
|
data?: unknown;
|
|
@@ -5424,7 +5433,9 @@ declare class SpaceSignaler {
|
|
|
5424
5433
|
broadcast(kind: SignalKind, data?: unknown): void;
|
|
5425
5434
|
/**
|
|
5426
5435
|
* Subscribe to inbound signals addressed to us (or broadcast). Skips our own
|
|
5427
|
-
* echoes. Returns an unsubscribe function.
|
|
5436
|
+
* echoes. Returns an unsubscribe function. Routing is entirely by the envelope
|
|
5437
|
+
* peer id (`env.from`/`env.to`), so a user's own devices — which share the
|
|
5438
|
+
* server-stamped `e.from` — are correctly distinguished.
|
|
5428
5439
|
*/
|
|
5429
5440
|
onSignal(cb: (from: string, kind: SignalKind, data: unknown) => void): () => void;
|
|
5430
5441
|
}
|
|
@@ -5591,6 +5602,8 @@ interface WebRtcTransportOptions {
|
|
|
5591
5602
|
iceServers?: RTCIceServer[];
|
|
5592
5603
|
/** Cap on simultaneous peer connections (mesh size). Default 8. */
|
|
5593
5604
|
maxPeers?: number;
|
|
5605
|
+
/** Log ICE/connection state transitions for diagnosing peer connectivity. */
|
|
5606
|
+
debug?: boolean;
|
|
5594
5607
|
}
|
|
5595
5608
|
declare class WebRtcTransport implements PeerTransport {
|
|
5596
5609
|
private readonly signaler;
|
|
@@ -5601,8 +5614,10 @@ declare class WebRtcTransport implements PeerTransport {
|
|
|
5601
5614
|
private readonly offSignal;
|
|
5602
5615
|
private readonly iceServers;
|
|
5603
5616
|
private readonly maxPeers;
|
|
5617
|
+
private readonly debug;
|
|
5604
5618
|
private nextMsgId;
|
|
5605
5619
|
constructor(signaler: SpaceSignaler, myId: PeerId, opts?: WebRtcTransportOptions);
|
|
5620
|
+
private log;
|
|
5606
5621
|
peers(): PeerId[];
|
|
5607
5622
|
/** Dial a peer (no-op unless we're the deterministic initiator for the pair). */
|
|
5608
5623
|
connectTo(peerId: PeerId): void;
|
|
@@ -5612,6 +5627,9 @@ declare class WebRtcTransport implements PeerTransport {
|
|
|
5612
5627
|
onPeerChange(cb: (peers: PeerId[]) => void): () => void;
|
|
5613
5628
|
close(): void;
|
|
5614
5629
|
private newPc;
|
|
5630
|
+
/** Mark a peer's remote description as set and apply any ICE candidates that
|
|
5631
|
+
* arrived before it (dropping them, the old behavior, breaks LAN dials). */
|
|
5632
|
+
private markRemoteSet;
|
|
5615
5633
|
private initiate;
|
|
5616
5634
|
private bindChannel;
|
|
5617
5635
|
private onSignal;
|
package/dist/server/index.js
CHANGED
|
@@ -2976,7 +2976,7 @@ function base64UrlDecode(s) {
|
|
|
2976
2976
|
* expect (and isn't serving a stale cached bundle). Keep this in sync with
|
|
2977
2977
|
* `package.json` `version`.
|
|
2978
2978
|
*/
|
|
2979
|
-
const VERSION = "0.10.
|
|
2979
|
+
const VERSION = "0.10.5-alpha.0";
|
|
2980
2980
|
|
|
2981
2981
|
/**
|
|
2982
2982
|
* `HttpClient` — the one place the unified {@link Client} talks to the
|
|
@@ -8470,10 +8470,13 @@ class PeerExchange {
|
|
|
8470
8470
|
* to Space membership. The sender (`from`) is server-stamped and can't be
|
|
8471
8471
|
* spoofed; the payload is opaque to the server.
|
|
8472
8472
|
*
|
|
8473
|
-
*
|
|
8474
|
-
*
|
|
8475
|
-
*
|
|
8476
|
-
*
|
|
8473
|
+
* Peers are identified by a unique **peer id** (per device/connection), carried
|
|
8474
|
+
* in the envelope's `from`/`to` — NOT by the server-stamped member id. This is
|
|
8475
|
+
* essential: one user's own devices share a member id, so the server's `from`
|
|
8476
|
+
* can't tell them apart (and would make each treat the others' signals as its
|
|
8477
|
+
* own echo). We therefore route entirely on the envelope peer ids and broadcast
|
|
8478
|
+
* to the whole Space, filtering client-side. The server `from` still guarantees
|
|
8479
|
+
* the sender is an authenticated Space member.
|
|
8477
8480
|
*/
|
|
8478
8481
|
const P2P_SUBJECT = "__p2p__";
|
|
8479
8482
|
class SpaceSignaler {
|
|
@@ -8485,11 +8488,11 @@ class SpaceSignaler {
|
|
|
8485
8488
|
}
|
|
8486
8489
|
/** Send a signal to one peer (or `"*"`). */
|
|
8487
8490
|
send(to, kind, data) {
|
|
8488
|
-
const env = { to, kind, data };
|
|
8489
|
-
//
|
|
8490
|
-
//
|
|
8491
|
-
//
|
|
8492
|
-
this.space.sendEphemeral(P2P_SUBJECT, env
|
|
8491
|
+
const env = { from: this.myId, to, kind, data };
|
|
8492
|
+
// Always broadcast to the Space: the server can't unicast to a specific
|
|
8493
|
+
// DEVICE (a user's devices share a member id), so we route client-side on
|
|
8494
|
+
// the envelope peer id. Signaling frames are tiny.
|
|
8495
|
+
this.space.sendEphemeral(P2P_SUBJECT, env);
|
|
8493
8496
|
}
|
|
8494
8497
|
/** Announce/withdraw P2P availability to all Space members. */
|
|
8495
8498
|
broadcast(kind, data) {
|
|
@@ -8497,18 +8500,22 @@ class SpaceSignaler {
|
|
|
8497
8500
|
}
|
|
8498
8501
|
/**
|
|
8499
8502
|
* Subscribe to inbound signals addressed to us (or broadcast). Skips our own
|
|
8500
|
-
* echoes. Returns an unsubscribe function.
|
|
8503
|
+
* echoes. Returns an unsubscribe function. Routing is entirely by the envelope
|
|
8504
|
+
* peer id (`env.from`/`env.to`), so a user's own devices — which share the
|
|
8505
|
+
* server-stamped `e.from` — are correctly distinguished.
|
|
8501
8506
|
*/
|
|
8502
8507
|
onSignal(cb) {
|
|
8503
8508
|
return this.space.onEphemeral((e) => {
|
|
8504
|
-
if (e.subject !== P2P_SUBJECT
|
|
8509
|
+
if (e.subject !== P2P_SUBJECT)
|
|
8505
8510
|
return;
|
|
8506
8511
|
const env = e.data;
|
|
8507
|
-
if (!env || typeof env.kind !== "string")
|
|
8512
|
+
if (!env || typeof env.kind !== "string" || typeof env.from !== "string")
|
|
8508
8513
|
return;
|
|
8514
|
+
if (env.from === this.myId)
|
|
8515
|
+
return; // our own device's echo
|
|
8509
8516
|
if (env.to !== this.myId && env.to !== "*")
|
|
8510
8517
|
return;
|
|
8511
|
-
cb(
|
|
8518
|
+
cb(env.from, env.kind, env.data);
|
|
8512
8519
|
});
|
|
8513
8520
|
}
|
|
8514
8521
|
}
|
|
@@ -8543,14 +8550,20 @@ class WebRtcTransport {
|
|
|
8543
8550
|
offSignal;
|
|
8544
8551
|
iceServers;
|
|
8545
8552
|
maxPeers;
|
|
8553
|
+
debug;
|
|
8546
8554
|
nextMsgId = 1;
|
|
8547
8555
|
constructor(signaler, myId, opts = {}) {
|
|
8548
8556
|
this.signaler = signaler;
|
|
8549
8557
|
this.myId = myId;
|
|
8550
8558
|
this.iceServers = opts.iceServers ?? DEFAULT_ICE;
|
|
8551
8559
|
this.maxPeers = opts.maxPeers ?? 8;
|
|
8560
|
+
this.debug = opts.debug ?? false;
|
|
8552
8561
|
this.offSignal = this.signaler.onSignal((from, kind, data) => this.onSignal(from, kind, data));
|
|
8553
8562
|
}
|
|
8563
|
+
log(...a) {
|
|
8564
|
+
if (this.debug)
|
|
8565
|
+
console.info("[muhkoo:p2p]", ...a);
|
|
8566
|
+
}
|
|
8554
8567
|
peers() {
|
|
8555
8568
|
return [...this.conns].filter(([, c]) => c.dc?.readyState === "open").map(([id]) => id);
|
|
8556
8569
|
}
|
|
@@ -8601,19 +8614,42 @@ class WebRtcTransport {
|
|
|
8601
8614
|
// -- internals -----------------------------------------------------------
|
|
8602
8615
|
newPc(peerId) {
|
|
8603
8616
|
const pc = new RTCPeerConnection({ iceServers: this.iceServers });
|
|
8604
|
-
const conn = { pc, reasm: new Map() };
|
|
8617
|
+
const conn = { pc, reasm: new Map(), remoteSet: false, pendingIce: [] };
|
|
8605
8618
|
this.conns.set(peerId, conn);
|
|
8619
|
+
const short = String(peerId).slice(0, 8);
|
|
8606
8620
|
pc.onicecandidate = (e) => {
|
|
8607
|
-
if (e.candidate)
|
|
8621
|
+
if (e.candidate) {
|
|
8622
|
+
this.log(`${short} local candidate ${e.candidate.type ?? "?"} ${(e.candidate.address ?? e.candidate.candidate).toString().slice(0, 40)}`);
|
|
8608
8623
|
this.signaler.send(peerId, "ice", e.candidate.toJSON());
|
|
8624
|
+
}
|
|
8609
8625
|
};
|
|
8610
8626
|
pc.onconnectionstatechange = () => {
|
|
8627
|
+
this.log(`${short} connection=${pc.connectionState}`);
|
|
8611
8628
|
if (["failed", "closed", "disconnected"].includes(pc.connectionState))
|
|
8612
8629
|
this.drop(peerId);
|
|
8613
8630
|
};
|
|
8631
|
+
if (this.debug) {
|
|
8632
|
+
pc.oniceconnectionstatechange = () => this.log(`${short} ice=${pc.iceConnectionState}`);
|
|
8633
|
+
pc.onicegatheringstatechange = () => this.log(`${short} gathering=${pc.iceGatheringState}`);
|
|
8634
|
+
}
|
|
8614
8635
|
pc.ondatachannel = (e) => this.bindChannel(peerId, conn, e.channel);
|
|
8615
8636
|
return conn;
|
|
8616
8637
|
}
|
|
8638
|
+
/** Mark a peer's remote description as set and apply any ICE candidates that
|
|
8639
|
+
* arrived before it (dropping them, the old behavior, breaks LAN dials). */
|
|
8640
|
+
async markRemoteSet(conn) {
|
|
8641
|
+
conn.remoteSet = true;
|
|
8642
|
+
const pending = conn.pendingIce;
|
|
8643
|
+
conn.pendingIce = [];
|
|
8644
|
+
for (const cand of pending) {
|
|
8645
|
+
try {
|
|
8646
|
+
await conn.pc.addIceCandidate(cand);
|
|
8647
|
+
}
|
|
8648
|
+
catch { /* stale/duplicate */ }
|
|
8649
|
+
}
|
|
8650
|
+
if (pending.length)
|
|
8651
|
+
this.log(`flushed ${pending.length} queued ICE candidate(s)`);
|
|
8652
|
+
}
|
|
8617
8653
|
async initiate(peerId) {
|
|
8618
8654
|
const conn = this.newPc(peerId);
|
|
8619
8655
|
const dc = conn.pc.createDataChannel("blocks");
|
|
@@ -8635,22 +8671,32 @@ class WebRtcTransport {
|
|
|
8635
8671
|
return; // mesh cap
|
|
8636
8672
|
const conn = this.conns.get(from) ?? this.newPc(from);
|
|
8637
8673
|
await conn.pc.setRemoteDescription(data);
|
|
8674
|
+
await this.markRemoteSet(conn);
|
|
8638
8675
|
const answer = await conn.pc.createAnswer();
|
|
8639
8676
|
await conn.pc.setLocalDescription(answer);
|
|
8640
8677
|
this.signaler.send(from, "answer", answer);
|
|
8641
8678
|
}
|
|
8642
8679
|
else if (kind === "answer") {
|
|
8643
8680
|
const conn = this.conns.get(from);
|
|
8644
|
-
if (conn)
|
|
8681
|
+
if (conn) {
|
|
8645
8682
|
await conn.pc.setRemoteDescription(data);
|
|
8683
|
+
await this.markRemoteSet(conn);
|
|
8684
|
+
}
|
|
8646
8685
|
}
|
|
8647
8686
|
else if (kind === "ice") {
|
|
8648
8687
|
const conn = this.conns.get(from);
|
|
8649
8688
|
if (conn && data) {
|
|
8689
|
+
// Queue candidates that arrive before setRemoteDescription — adding
|
|
8690
|
+
// them early throws and (previously) dropped them, which killed the
|
|
8691
|
+
// connection on fast LANs where ICE beats the SDP answer.
|
|
8692
|
+
if (!conn.remoteSet) {
|
|
8693
|
+
conn.pendingIce.push(data);
|
|
8694
|
+
return;
|
|
8695
|
+
}
|
|
8650
8696
|
try {
|
|
8651
8697
|
await conn.pc.addIceCandidate(data);
|
|
8652
8698
|
}
|
|
8653
|
-
catch { /*
|
|
8699
|
+
catch { /* stale */ }
|
|
8654
8700
|
}
|
|
8655
8701
|
}
|
|
8656
8702
|
else if (kind === "bye") {
|
|
@@ -9119,6 +9165,7 @@ class PeerNetwork {
|
|
|
9119
9165
|
new WebRtcTransport(this.signaler, opts.myId, {
|
|
9120
9166
|
iceServers: opts.iceServers,
|
|
9121
9167
|
maxPeers: opts.maxPeers,
|
|
9168
|
+
debug: opts.debug,
|
|
9122
9169
|
});
|
|
9123
9170
|
this.host = buildEngineHost(opts);
|
|
9124
9171
|
// Wire transport ⇄ engine, demuxing block (engine) from gossip (app).
|
|
@@ -9245,6 +9292,10 @@ class SpaceNamespace {
|
|
|
9245
9292
|
spaceCache;
|
|
9246
9293
|
/** Spaces built this session, by id — used to route offline send replays. */
|
|
9247
9294
|
openSpaces = new Map();
|
|
9295
|
+
/** Unique id for THIS device/session — the P2P peer identity. A user's own
|
|
9296
|
+
* devices share a member id, so the mesh MUST distinguish them by this or a
|
|
9297
|
+
* device treats its siblings' signals as its own echo and never connects. */
|
|
9298
|
+
deviceId = `peer-${globalThis.crypto?.randomUUID?.() ?? Math.random().toString(36).slice(2) + Math.random().toString(36).slice(2)}`;
|
|
9248
9299
|
constructor(deps) {
|
|
9249
9300
|
this.deps = deps;
|
|
9250
9301
|
if (deps.offline?.enabled) {
|
|
@@ -9443,7 +9494,7 @@ class SpaceNamespace {
|
|
|
9443
9494
|
if (this.deps.p2p?.enabled && isP2pCapable()) {
|
|
9444
9495
|
space.attachPeerNetwork(new PeerNetwork({
|
|
9445
9496
|
space,
|
|
9446
|
-
myId:
|
|
9497
|
+
myId: this.deviceId, // per-device peer id, NOT the shared member id
|
|
9447
9498
|
workerFactory: this.deps.p2p.workerFactory,
|
|
9448
9499
|
iceServers: this.deps.p2p.iceServers,
|
|
9449
9500
|
maxPeers: this.deps.p2p.maxPeers,
|